Commit fbaf7950 authored by Chris Scott's avatar Chris Scott

Implementing geofencing. Refactoring background-task handling

parent b8b92c56
......@@ -22,6 +22,10 @@ The plugin creates the object `window.plugins.backgroundGeoLocation` with the me
`onStationary(callback, fail)`
`addGeofence(callback, fail)`
`onGeofence(callback, fail)`
`getLocations(callback, fail)`
`sync(callback, fail)`
......@@ -49,27 +53,27 @@ The plugin creates the object `window.plugins.backgroundGeoLocation` with the me
// As with all Cordova plugins, you must configure within an #deviceready callback.
//
function onDeviceReady() {
/**
* This callback will be executed every time a geolocation is recorded in the background.
*/
var callbackFn = function(location, taskId) {
console.log('[js] BackgroundGeoLocation callback: ' + location.latitude + ',' + location.longitude);
/**
* This would be your own callback for Ajax-requests after POSTing background geolocation to your server.
* eg:
* $.post({url: url, success: yourAjaxCallback});
*/
var yourAjaxCallback = function(response) {
////
// IMPORTANT: You must execute the #finish method here to inform the native plugin that you're finished,
// IMPORTANT: You must execute the #finish, providing the taskId provided to callbackFn above in order to inform the native plugin that you're finished,
// and the background-task may be completed. You must do this regardless if your HTTP request is successful or not.
// IF YOU DON'T, ios will CRASH YOUR APP for spending too much time in the background.
//
//
bgGeo.finish();
bgGeo.finish(taskId);
};
/**
* This callback will be executed every time a geolocation is recorded in the background.
*/
var callbackFn = function(location) {
console.log('[js] BackgroundGeoLocation callback: ' + location.latitude + ',' + location.longitude);
// Do your HTTP request here to POST location to your server.
//
//
yourAjaxCallback.call(this);
};
......@@ -193,7 +197,11 @@ Keep in mind that it is **not** possible to use ```start()``` at the ```pause```
#####`configure(locationCallback, failureCallback, config)`
Configures the plugin's parameters (@see following [Config](https://github.com/christocracy/cordova-background-geolocation/blob/edge/README.md#config) section for accepted ```config``` params. The ```locationCallback``` will be executed each time a new Geolocation is recorded.
Configures the plugin's parameters (@see following [Config](https://github.com/christocracy/cordova-background-geolocation/blob/edge/README.md#config) section for accepted ```config``` params. The ```locationCallback``` will be executed each time a new Geolocation is recorded and provided with the following parameters:
######`@param {Object} location` The Location data
`{"coords":{"latitude":45.5192735,"longitude":-73.6168883,"accuracy":10,"speed":0,"heading":0,"altitude":0},"timestamp":"2015-05-27T18:26:44Z"}`
######`@param {Integer} taskId` The taskId used to send to bgGeo.finish(taskId) in order to signal completion of your callbackFn
#####`setConfig(successFn, failureFn, config)`
Reconfigure plugin's configuration (@see followign ##Config## section for accepted ```config``` params. **NOTE** The plugin will continue to send recorded Geolocation to the ```locationCallback``` you provided to ```configure``` method -- use this method only to change configuration params (eg: ```distanceFilter```, ```stationaryRadius```, etc).
......@@ -230,11 +238,54 @@ bgGeo.changePace(false); // <-- Disable aggressive GPS monitoring. Engages stat
```
#####`onStationary(callbackFn, failureFn)`
Your ```callbackFn``` will be executed each time the device has entered stationary-monitoring mode. The ```callbackFn``` will be provided with a ```Geolocation``` object as the 1st param, with the usual params (```latitude, longitude, accuracy, speed, bearing, altitude```).
Your ```callbackFn``` will be executed each time the device has entered stationary-monitoring mode. The ```callbackFn``` will be provided with a ```Location``` object as the 1st param, with the usual params (```latitude, longitude, accuracy, speed, bearing, altitude```), in addition to a ```taskId``` used to signal that your callback is finished.
```
bgGeo.onStationary(function(location) {
bgGeo.onStationary(function(location, taskId) {
console.log('- Device is stopped: ', location.latitude, location.longitude);
// The plugin runs your callback in a background-thread:
// you MUST signal to the native plugin when your callback is finished so it can halt the thread.
// IF YOU DON'T, iOS WILL KILL YOUR APP
bgGeo.finish(taskId);
});
```
#####`addGeofence(config, callbackFn, failureFn)`
Adds a geofence to be monitored by the native plugin. Monitoring of a geofence is halted after a crossing occurs. The `config` object accepts the following params.
######`@config {String} identifier The name of your geofence, eg: "Home", "Office"
######`@config {Float} radius The radius (meters) of the geofence. In practice, you should make this >= 100 meters.
######`@config {Float} latitude Latitude of the center-point of the circular geofence.
######`@config {Float} longitude Longitude of the center-point of the circular geofence.
```
bgGeo.addGeofence({
identifier: "Home",
radius: 150,
latitude: 45.51921926,
longitude: -73.61678581
}, function() {
console.log("Successfully added geofence");
}, function(error) {
console.warn("Failed to add geofence", error);
});
```
#####`onGeofence(callbackFn)`
Adds a geofence event-listener. Your supplied callback will be called when any monitored geofence crossing occurs. The `callbackFn` will be provided the following parameters:
######`@param {String} identifier The name of the geofence which generated the crossing event, eg: "Home"
######`@param {Integer} taskId The background taskId which you must send back to the native plugin via `bgGeo.finish(taskId)` in order to signal that your callback is complete.
```
bgGeo.onGeofence(function(identifier, taskId) {
console.log('A geofence has been entered: ' + identifier);
// The plugin runs your callback in a background-thread:
// you MUST signal to the native plugin when your callback is finished so it can halt the thread.
// IF YOU DON'T, iOS WILL KILL YOUR APP
bgGeo.finish(taskId);
});
```
......
......@@ -138,26 +138,28 @@ var app = {
app.onClickHome();
/**
* This would be your own callback for Ajax-requests after POSTing background geolocation to your server.
*/
var yourAjaxCallback = function(response) {
// Very important to call #finish -- it signals to the native plugin that it can destroy the background thread, which your callbackFn is running in.
// IF YOU DON'T, THE OS CAN KILL YOUR APP FOR RUNNING TOO LONG IN THE BACKGROUND
bgGeo.finish();
};
/**
* This callback will be executed every time a geolocation is recorded in the background.
*/
var callbackFn = function(location) {
var callbackFn = function(location, taskId) {
console.log('[js] BackgroundGeoLocation callback: ' + JSON.stringify(location));
// Update our current-position marker.
app.setCurrentLocation(location);
// After you Ajax callback is complete, you MUST signal to the native code, which is running a background-thread, that you're done and it can gracefully kill that thread.
yourAjaxCallback.call(this);
/**
* This would be your own callback for Ajax-requests after POSTing background geolocation to your server.
*/
var yourAjaxCallback = function(response) {
// Very important to call #finish -- it signals to the native plugin that it can destroy the background thread, which your callbackFn is running in.
// IF YOU DON'T, THE OS CAN KILL YOUR APP FOR RUNNING TOO LONG IN THE BACKGROUND
bgGeo.finish(taskId);
};
yourAjaxCallback.call(this, {status: 200});
};
var failureFn = function(error) {
......@@ -165,7 +167,7 @@ var app = {
};
// Only ios emits this stationary event
bgGeo.onStationary(function(location) {
bgGeo.onStationary(function(location, taskId) {
console.log('[js] BackgroundGeoLocation onStationary ' + JSON.stringify(location));
app.setCurrentLocation(location);
......@@ -188,6 +190,8 @@ var app = {
app.stationaryRadius.setRadius(radius);
app.stationaryRadius.setCenter(center);
bgGeo.finish(taskId);
});
// BackgroundGeoLocation is highly configurable.
......@@ -195,22 +199,22 @@ var app = {
debug: true, // <-- enable this hear sounds for background-geolocation life-cycle.
desiredAccuracy: 0,
stationaryRadius: 50,
distanceFilter: 25,
distanceFilter: 50,
disableElasticity: false, // <-- [iOS] Default is 'false'. Set true to disable speed-based distanceFilter elasticity
locationUpdateInterval: 5000,
minimumActivityRecognitionConfidence: 80, // 0-100%. Minimum activity-confidence for a state-change
fastestLocationUpdateInterval: 5000,
activityRecognitionInterval: 10000,
stopTimeout: 0,
forceReload: true, // <-- [Android] If the user closes the app **while location-tracking is started** , reboot app (WARNING: possibly distruptive to user)
stopOnTerminate: false, // <-- [Android] Allow the background-service to run headless when user closes the app.
startOnBoot: true, // <-- [Android] Auto start background-service in headless mode when device is powered-up.
forceReload: false, // <-- [Android] If the user closes the app **while location-tracking is started** , reboot app (WARNING: possibly distruptive to user)
stopOnTerminate: true, // <-- [Android] Allow the background-service to run headless when user closes the app.
startOnBoot: false, // <-- [Android] Auto start background-service in headless mode when device is powered-up.
activityType: 'AutomotiveNavigation',
/**
* HTTP Feature: set an url to allow the native background service to POST locations to your server
*/
url: 'http://posttestserver.com/post.php?dir=cordova-background-geolocation',
batchSync: false, // <-- [Default: false] Set true to sync locations to server in a single HTTP request.
batchSync: true, // <-- [Default: false] Set true to sync locations to server in a single HTTP request.
autoSync: true, // <-- [Default: true] Set true to sync each location to server as it arrives.
maxDaysToPersist: 1, // <-- Maximum days to persist a location in plugin's SQLite database when HTTP fails
headers: {
......@@ -221,9 +225,10 @@ var app = {
}
});
bgGeo.onGeofence(function(identifier) {
bgGeo.onGeofence(function(identifier, taskId) {
alert('Enter Geofence: ' + identifier);
console.log('[js] Geofence ENTER: ', identifier);
console.log('[js] Geofence ENTER: ', identifier, taskId);
bgGeo.finish(taskId);
});
// Add longpress event for adding GeoFence of hard-coded radius 200m.
......
......@@ -30,6 +30,7 @@
<service android:name="com.transistorsoft.locationmanager.BackgroundGeolocationService" />
<service android:name="com.transistorsoft.locationmanager.LocationService" />
<service android:name="com.transistorsoft.locationmanager.ActivityRecognitionService" />
<service android:name="com.transistorsoft.locationmanager.GeofenceService" />
<receiver android:name="com.transistorsoft.locationmanager.BootReceiver" android:enabled="true" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
......
package com.transistorsoft.cordova.bggeo;
import java.util.List;
import java.util.ArrayList;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView;
......@@ -11,8 +13,8 @@ import android.os.Bundle;
import com.google.android.gms.location.ActivityRecognitionResult;
import com.google.android.gms.location.DetectedActivity;
import com.transistorsoft.locationmanager.BackgroundGeolocationService;
import com.transistorsoft.locationmanager.BackgroundGeolocationService.PaceChangeEvent;
import com.transistorsoft.locationmanager.BackgroundGeolocationService.PausedEvent;
import com.google.android.gms.location.Geofence;
import com.google.android.gms.location.GeofencingEvent;
//import com.transistorsoft.locationmanager.BackgroundGeolocationService.StationaryLocation;
import de.greenrobot.event.EventBus;
......@@ -37,6 +39,8 @@ public class CDVBackgroundGeolocation extends CordovaPlugin {
public static final String ACTION_SYNC = "sync";
public static final String ACTION_GET_ODOMETER = "getOdometer";
public static final String ACTION_RESET_ODOMETER = "resetOdometer";
public static final String ACTION_ADD_GEOFENCE = "addGeofence";
public static final String ACTION_ON_GEOFENCE = "onGeofence";
private Boolean isEnabled = false;
private Boolean stopOnTerminate = false;
......@@ -48,16 +52,13 @@ public class CDVBackgroundGeolocation extends CordovaPlugin {
// Geolocation callback
private CallbackContext locationCallback;
// Called when DetectedActivity is STILL
private CallbackContext stationaryCallback;
private CallbackContext getLocationsCallback;
private CallbackContext syncCallback;
private CallbackContext getOdometerCallback;
private CallbackContext resetOdometerCallback;
private CallbackContext paceChangeCallback;
private List<CallbackContext> geofenceCallbacks = new ArrayList<CallbackContext>();
public static boolean isActive() {
return gWebView != null;
......@@ -99,11 +100,13 @@ public class CDVBackgroundGeolocation extends CordovaPlugin {
result = false;
callbackContext.error("Cannot #changePace while disabled");
} else {
PaceChangeEvent event = new PaceChangeEvent(data.getBoolean(0));
EventBus.getDefault().post(event);
result = true;
callbackContext.success();
paceChangeCallback = callbackContext;
Bundle event = new Bundle();
event.putString("name", action);
event.putBoolean("request", true);
event.putBoolean("isMoving", data.getBoolean(0));
EventBus.getDefault().post(event);
}
} else if (ACTION_SET_CONFIG.equalsIgnoreCase(action)) {
result = applyConfig(data);
......@@ -144,6 +147,29 @@ public class CDVBackgroundGeolocation extends CordovaPlugin {
event.putBoolean("request", true);
resetOdometerCallback = callbackContext;
EventBus.getDefault().post(event);
} else if (ACTION_ADD_GEOFENCE.equalsIgnoreCase(action)) {
result = true;
JSONObject config = data.getJSONObject(0);
try {
Bundle event = new Bundle();
event.putString("name", action);
event.putBoolean("request", true);
event.putFloat("radius", (float) config.getLong("radius"));
event.putDouble("latitude", config.getDouble("latitude"));
event.putDouble("longitude", config.getDouble("longitude"));
event.putString("identifier", config.getString("identifier"));
EventBus.getDefault().post(event);
callbackContext.success();
} catch (JSONException e) {
Log.w(TAG, e);
callbackContext.error("Failed to add geofence");
result = false;
}
} else if (ACTION_ON_GEOFENCE.equalsIgnoreCase(action)) {
result = true;
geofenceCallbacks.add(callbackContext);
}
return result;
}
......@@ -296,7 +322,10 @@ public class CDVBackgroundGeolocation extends CordovaPlugin {
runInBackground(getOdometerCallback, result);
} else if (ACTION_RESET_ODOMETER.equalsIgnoreCase(name)) {
PluginResult result = new PluginResult(PluginResult.Status.OK);
runInBackground(resetOdometerCallback, result);
resetOdometerCallback.sendPluginResult(result);
} else if (ACTION_ON_PACE_CHANGE.equalsIgnoreCase(name)) {
PluginResult result = new PluginResult(PluginResult.Status.OK);
paceChangeCallback.sendPluginResult(result);
}
}
......@@ -329,6 +358,29 @@ public class CDVBackgroundGeolocation extends CordovaPlugin {
runInBackground(locationCallback, result);
}
}
/**
* EventBus handler for Geofencing events
*/
public void onEventMainThread(GeofencingEvent geofenceEvent) {
Log.i(TAG, "- Rx GeofencingEvent: " + geofenceEvent);
if (!geofenceCallbacks.isEmpty()) {
for (Geofence geofence : geofenceEvent.getTriggeringGeofences()) {
JSONObject params = new JSONObject();
try {
params.put("identifier", geofence.getRequestId());
} catch (JSONException e) {
e.printStackTrace();
}
PluginResult result = new PluginResult(PluginResult.Status.OK, params);
result.setKeepCallback(true);
for (CallbackContext callback : geofenceCallbacks) {
runInBackground(callback, result);
}
}
}
}
/**
* Run a javascript callback in Background
......
......@@ -10,7 +10,7 @@
@interface CDVBackgroundGeolocation : CDVPlugin
@property (nonatomic, strong) NSString* syncCallbackId;
@property (nonatomic, strong) NSString* geofenceCallbackId;
@property (nonatomic, strong) NSMutableArray* geofenceListeners;
@property (nonatomic, strong) NSMutableArray* stationaryRegionListeners;
- (void) configure:(CDVInvokedUrlCommand*)command;
......
......@@ -10,7 +10,7 @@
NSDictionary *config;
}
@synthesize syncCallbackId, geofenceCallbackId, stationaryRegionListeners;
@synthesize syncCallbackId, geofenceListeners, stationaryRegionListeners;
- (void)pluginInitialize
{
......@@ -87,44 +87,61 @@
* location handler from BackgroundGeolocation
*/
- (void)onLocationChanged:(NSNotification*)notification {
CLLocation *location = notification.object;
NSDictionary *locationData = [bgGeo locationToDictionary:location];
CLLocation *location = [notification.userInfo objectForKey:@"location"];
CDVPluginResult* result = nil;
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:locationData];
NSDictionary *params = @{
@"location": [bgGeo locationToDictionary:location],
@"taskId": @([bgGeo createBackgroundTask])
};
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:params];
[result setKeepCallbackAsBool:YES];
[self.commandDelegate runInBackground:^{
[self.commandDelegate sendPluginResult:result callbackId:self.syncCallbackId];
}];
}
- (void) onStationaryLocation:(NSNotification*)notification
{
CLLocation *location = notification.object;
NSDictionary *locationData = [bgGeo locationToDictionary:location];
if (![self.stationaryRegionListeners count]) {
[bgGeo stopBackgroundTask];
return;
}
CLLocation *location = [notification.userInfo objectForKey:@"location"];
NSDictionary *locationData = [bgGeo locationToDictionary:location];
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:locationData];
for (NSString *callbackId in self.stationaryRegionListeners) {
NSDictionary *params = @{
@"location": locationData,
@"taskId": @([bgGeo createBackgroundTask])
};
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:params];
[result setKeepCallbackAsBool:YES];
for (NSString *callbackId in self.stationaryRegionListeners)
{
[self.commandDelegate runInBackground:^{
[self.commandDelegate sendPluginResult:result callbackId:callbackId];
}];
}
[bgGeo stopBackgroundTask];
}
- (void) onEnterGeofence:(NSNotification*)notification
{
if (self.geofenceCallbackId == nil) {
if (![self.geofenceListeners count]) {
return;
}
CLCircularRegion *region = notification.object;
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:region.identifier];
CLCircularRegion *region = [notification.userInfo objectForKey:@"geofence"];
for (NSString *callbackId in self.stationaryRegionListeners) {
NSDictionary *params = @{
@"identifier": region.identifier,
@"taskId": @([bgGeo createBackgroundTask])
};
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:params];
[result setKeepCallbackAsBool:YES];
[self.commandDelegate sendPluginResult:result callbackId:self.geofenceCallbackId];
[self.commandDelegate runInBackground:^{
[self.commandDelegate sendPluginResult:result callbackId:callbackId];
}];
}
}
/**
* Fetches current stationaryLocation
......@@ -187,14 +204,18 @@
- (void) onGeofence:(CDVInvokedUrlCommand*)command
{
self.geofenceCallbackId = command.callbackId;
if (self.geofenceListeners == nil) {
self.geofenceListeners = [[NSMutableArray alloc] init];
}
[self.geofenceListeners addObject:command.callbackId];
}
/**
* Called by js to signify the end of a background-geolocation event
*/
-(void) finish:(CDVInvokedUrlCommand*)command
{
[bgGeo finish];
UIBackgroundTaskIdentifier taskId = [[command.arguments objectAtIndex: 0] integerValue];
[bgGeo stopBackgroundTask:taskId];
}
/**
* If you don't stopMonitoring when application terminates, the app will be awoken still when a
......
......@@ -9,10 +9,10 @@
- (void) configure:(NSDictionary*)config;
- (void) start;
- (void) stop;
- (void) finish;
- (NSArray*) sync;
- (NSArray*) getLocations;
- (void) stopBackgroundTask;
- (UIBackgroundTaskIdentifier) createBackgroundTask;
- (void) stopBackgroundTask:(UIBackgroundTaskIdentifier)taskId;
- (void) onPaceChange:(BOOL)value;
- (void) setConfig:(NSDictionary*)command;
- (NSDictionary*) getStationaryLocation;
......
......@@ -6,7 +6,7 @@
<dict>
<key>Headers/TSLocationManager.h</key>
<data>
93eNJDqyN5vuBJn9oYeT7lb2x1w=
w7pffjKj41qqtvUN0J6fOruESPM=
</data>
<key>Info.plist</key>
<data>
......@@ -21,7 +21,7 @@
<dict>
<key>Headers/TSLocationManager.h</key>
<data>
93eNJDqyN5vuBJn9oYeT7lb2x1w=
w7pffjKj41qqtvUN0J6fOruESPM=
</data>
<key>Modules/module.modulemap</key>
<data>
......
......@@ -9,24 +9,30 @@
var exec = require("cordova/exec");
module.exports = {
/**
* @property {Object} stationaryRegion
* @property {Object} stationaryLocation
*/
stationaryRegion: null,
stationaryLocation: null,
/**
* @property {Object} config
*/
config: {},
configure: function(success, failure, config) {
var me = this;
config = config || {};
this.config = config;
success = success || function(location) {};
var mySuccess = function(location) {
success = success || function(location, taskId) {
me.finish(taskId);
};
var mySuccess = function(params) {
var location = params.location || params;
var taskId = params.taskId || 'task-id-undefined';
// Transform timestamp to Date instance.
if (location.timestamp) {
location.timestamp = new Date(location.timestamp);
}
success.call(this, location);
success.call(this, location, taskId);
}
exec(mySuccess,
failure || function() {},
......@@ -49,12 +55,15 @@ module.exports = {
'stop',
[]);
},
finish: function(success, failure) {
finish: function(taskId, success, failure) {
if (!taskId) {
throw "BackgroundGeolocation#finish must now be provided with a taskId as 1st param, eg: bgGeo.finish(taskId). taskId is provided by 2nd param in callback";
}
exec(success || function() {},
failure || function() {},
'BackgroundGeoLocation',
'finish',
[]);
[taskId]);
},
changePace: function(isMoving, success, failure) {
exec(success || function() {},
......@@ -94,10 +103,15 @@ module.exports = {
*/
onStationary: function(success, failure) {
var me = this;
success = success || function() {};
var callback = function(region) {
me.stationaryRegion = region;
success.apply(me, arguments);
success = success || function(location, taskId) {
me.finish(taskId);
};
var callback = function(params) {
var location = params.location || params,
taskId = params.taskId || 'task-id-undefined';
me.stationaryLocation = location;
success.call(me, location, taskId);
};
exec(callback,
failure || function() {},
......@@ -180,7 +194,13 @@ module.exports = {
if (!typeof(success) === 'function') {
throw "#onGeofence requires a success callback";
}
exec(success,
var me = this;
var mySuccess = function(params) {
var identifier = params.identifier,
taskId = params.taskId || 'task-id-undefined';
success.call(me, identifier, taskId);
}
exec(mySuccess,
failure || function() {},
'BackgroundGeoLocation',
'onGeofence',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment