Commit 1846f0cc authored by Chris Scott's avatar Chris Scott

Merge pull request #83 from christocracy/edge

Edge
parents 2a6ef1c0 e79d55a3
...@@ -20,8 +20,18 @@ The plugin creates the object `window.plugins.backgroundGeoLocation` with the me ...@@ -20,8 +20,18 @@ The plugin creates the object `window.plugins.backgroundGeoLocation` with the me
`changePace(true) // engages aggressive monitoring immediately` `changePace(true) // engages aggressive monitoring immediately`
`onStationary(callback, fail)` `getCurrentPosition(success, fail)`
`onMotionChange(callback, fail)`
`addGeofence(config, callback, fail)`
`removeGeofence(identifier, callback, fail)`
`getGeofences(callback, fail)`
`onGeofence(callback, fail)`
`getLocations(callback, fail)` `getLocations(callback, fail)`
`sync(callback, fail)` `sync(callback, fail)`
...@@ -30,6 +40,8 @@ The plugin creates the object `window.plugins.backgroundGeoLocation` with the me ...@@ -30,6 +40,8 @@ The plugin creates the object `window.plugins.backgroundGeoLocation` with the me
`resetOdometer(callback, fail)` `resetOdometer(callback, fail)`
`playSound(int soundId)`
## Installing the plugin ## ## Installing the plugin ##
``` ```
...@@ -49,27 +61,27 @@ The plugin creates the object `window.plugins.backgroundGeoLocation` with the me ...@@ -49,27 +61,27 @@ The plugin creates the object `window.plugins.backgroundGeoLocation` with the me
// As with all Cordova plugins, you must configure within an #deviceready callback. // As with all Cordova plugins, you must configure within an #deviceready callback.
// //
function onDeviceReady() { function onDeviceReady() {
/**
* This would be your own callback for Ajax-requests after POSTing background geolocation to your server.
*/
var yourAjaxCallback = function(response) {
////
// IMPORTANT: You must execute the #finish method here 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();
};
/** /**
* This callback will be executed every time a geolocation is recorded in the background. * 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: ' + location.latitude + ',' + location.longitude); console.log('[js] BackgroundGeoLocation callback: ' + location.latitude + ',' + location.longitude);
// Do your HTTP request here to POST location to your server.
// /**
// * 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, 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(taskId);
};
yourAjaxCallback.call(this); yourAjaxCallback.call(this);
}; };
...@@ -79,7 +91,7 @@ function onDeviceReady() { ...@@ -79,7 +91,7 @@ function onDeviceReady() {
// BackgroundGeoLocation is highly configurable. // BackgroundGeoLocation is highly configurable.
bgGeo.configure(callbackFn, failureFn, { bgGeo.configure(callbackFn, failureFn, {
debug: true, // <-- enable this hear sounds for background-geolocation life-cycle. // Geolocation config
desiredAccuracy: 0, desiredAccuracy: 0,
stationaryRadius: 50, stationaryRadius: 50,
distanceFilter: 50, distanceFilter: 50,
...@@ -89,16 +101,20 @@ function onDeviceReady() { ...@@ -89,16 +101,20 @@ function onDeviceReady() {
fastestLocationUpdateInterval: 5000, fastestLocationUpdateInterval: 5000,
activityRecognitionInterval: 10000, activityRecognitionInterval: 10000,
stopTimeout: 0, 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.
activityType: 'AutomotiveNavigation', activityType: 'AutomotiveNavigation',
/**
* HTTP Feature: set an url to allow the native background service to POST locations to your server // Application config
*/ debug: true, // <-- enable this hear sounds for background-geolocation life-cycle.
forceReloadOnLocationChange: false, // <-- [Android] If the user closes the app **while location-tracking is started** , reboot app when a new location is recorded (WARNING: possibly distruptive to user)
forceReloadOnMotionChange: false, // <-- [Android] If the user closes the app **while location-tracking is started** , reboot app when device changes stationary-state (stationary->moving or vice-versa) --WARNING: possibly distruptive to user)
forceReloadOnGeofence: false, // <-- [Android] If the user closes the app **while location-tracking is started** , reboot app when a geofence crossing occurs --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.
// HTTP / SQLite config
url: 'http://posttestserver.com/post.php?dir=cordova-background-geolocation', url: 'http://posttestserver.com/post.php?dir=cordova-background-geolocation',
batchSync: false, // <-- [Default: false] Set true to sync locations to server in 1 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. 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 maxDaysToPersist: 1, // <-- Maximum days to persist a location in plugin's SQLite database when HTTP fails
headers: { headers: {
"X-FOO": "bar" "X-FOO": "bar"
...@@ -118,11 +134,22 @@ function onDeviceReady() { ...@@ -118,11 +134,22 @@ function onDeviceReady() {
``` ```
## Example Application ## Advanced Sample Application for Field-testing
A fully-featured [SampleApp](https://github.com/christocracy/cordova-background-geolocation-SampleApp) is available in its own public repo. After first cloning that repo, follow the installation instructions in the **README** there. This SampleApp includes a settings-screen allowing you to quickly experiment with all the different settings available for each platform.
![Home](/assets/images/iphone/screenshot-iphone5-geofences-framed.png "Home")
![Settings](/assets/images/iphone/screenshot-iphone5-settings-framed.png "Settings")
## Simple Sample Application
This plugin itself hosts a more rudimentary SampleApp in ```example/SampleApp``` folder. This SampleApp should be used for reference only -- for actual field-testing, please use the **more advanced** [SampleApp](https://github.com/christocracy/cordova-background-geolocation-SampleApp) above.
This SampleApp contains no plugins so you must first start by adding its required plugins (most importantly, this one). **NOTE** In order to use the SampleApp, it's important to make a copy of it **outside** of the plugin itself.
![SampleApp](/android-sample-app.png "SampleApp") ![SampleApp](/android-sample-app.png "SampleApp")
This plugin hosts a SampleApp in ```example/SampleApp``` folder. This SampleApp contains no plugins so you must first start by adding its required plugins (most importantly, this one). **NOTE** In order to use the SampleApp, it's important to make a copy of it outside of the plugin itself.
``` ```
$ git clone git@github.com:christocracy/cordova-background-geolocation.git $ git clone git@github.com:christocracy/cordova-background-geolocation.git
...@@ -191,11 +218,38 @@ Keep in mind that it is **not** possible to use ```start()``` at the ```pause``` ...@@ -191,11 +218,38 @@ Keep in mind that it is **not** possible to use ```start()``` at the ```pause```
## Methods ## Methods
#####`configure(locationCallback, failureCallback, config)` ####`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 and provided with the following parameters:
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. ######@param {Object} location The Location data
######@param {Integer} taskId The taskId used to send to bgGeo.finish(taskId) in order to signal completion of your callbackFn
#####`setConfig(successFn, failureFn, config)` ```
bgGeo.configure(function(location, taskId) {
try {
var coords = location.coords,
timestamp = location.timestamp
latitude = coords.latitude,
longitude = coords.longitude,
speed = coords.speed;
console.log("A location has arrived:", timestamp, latitude, longitude, speed);
} catch(e) {
console.error("An error occurred in my application code", e);
}
// 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);
}, failureFn, {
distanceFilter: 50,
desiredAccuracy: 0,
stationaryRadius: 25
});
```
####`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). 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).
``` ```
...@@ -205,7 +259,7 @@ bgGeo.setConfig(function(){}, function(){}, { ...@@ -205,7 +259,7 @@ bgGeo.setConfig(function(){}, function(){}, {
}); });
``` ```
#####`start(successFn, failureFn)` ####`start(successFn, failureFn)`
Enable background geolocation tracking. Enable background geolocation tracking.
...@@ -213,7 +267,7 @@ Enable background geolocation tracking. ...@@ -213,7 +267,7 @@ Enable background geolocation tracking.
bgGeo.start() bgGeo.start()
``` ```
#####`stop(successFn, failureFn)` ####`stop(successFn, failureFn)`
Disable background geolocation tracking. Disable background geolocation tracking.
...@@ -221,7 +275,24 @@ Disable background geolocation tracking. ...@@ -221,7 +275,24 @@ Disable background geolocation tracking.
bgGeo.stop(); bgGeo.stop();
``` ```
#####`changePace(enabled, successFn, failureFn)` ####`getCurrentPosition(successFn, failureFn)`
Retrieves the current position. This method instructs the native code to fetch exactly one location using maximum power & accuracy. **NOTE:** The plugin **MUST** be enabled via `#start` to use this method (otherwise the plugin will call your `failureFn` with a status-code `401` (UNAUTHORIZED). The native code will persist the fetched location to its SQLite database just as any other location in addition to POSTing to your configured `#url` (if you've enabled the HTTP features). In addition to your supplied `callbackFn`, the plugin will also execute the `callback` provided to `#configure`. Your provided `successFn` will be executed with the same signature as that provided to `#configure`:
######@param {Object} location The Location data
######@param {Integer} taskId The taskId used to send to bgGeo.finish(taskId) in order to signal completion of your callbackFn
```
bgGeo.getCurrentPosition(function(location, taskId) {
// This location is already persisted to plugin’s SQLite db.
// If you’ve configured #autoSync: true, the HTTP POST has already started.
console.log(“- Current position received: “, location);
bgGeo.finish(taskId);
});
```
####`changePace(enabled, successFn, failureFn)`
Initiate or cancel immediate background tracking. When set to ```true```, the plugin will begin aggressively tracking the devices Geolocation, bypassing stationary monitoring. If you were making a "Jogging" application, this would be your [Start Workout] button to immediately begin GPS tracking. Send ```false``` to disable aggressive GPS monitoring and return to stationary-monitoring mode. Initiate or cancel immediate background tracking. When set to ```true```, the plugin will begin aggressively tracking the devices Geolocation, bypassing stationary monitoring. If you were making a "Jogging" application, this would be your [Start Workout] button to immediately begin GPS tracking. Send ```false``` to disable aggressive GPS monitoring and return to stationary-monitoring mode.
``` ```
...@@ -229,39 +300,170 @@ bgGeo.changePace(true); // <-- Aggressive GPS monitoring immediately engaged. ...@@ -229,39 +300,170 @@ bgGeo.changePace(true); // <-- Aggressive GPS monitoring immediately engaged.
bgGeo.changePace(false); // <-- Disable aggressive GPS monitoring. Engages stationary-mode. bgGeo.changePace(false); // <-- Disable aggressive GPS monitoring. Engages stationary-mode.
``` ```
#####`onStationary(callbackFn, failureFn)` ####`onMotionChange(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 changed-state between **MOVING** or **STATIONARY**. 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.
######@param {Boolean} isMoving `false` if entered **STATIONARY** mode; `true` if entered **MOVING** mode.
######@param {Object} location The location at the state-change.
######@param {Integer} taskId The taskId used to send to bgGeo.finish(taskId) in order to signal completion of your callbackFn
```
bgGeo.onMotionChange(function(isMoving, location, taskId) {
if (isMoving) {
console.log('Device has just started MOVING', location);
} else {
console.log('Device has just STOPPED', location);
}
bgGeo.finish(taskId);
})
```
####`onStationary(callbackFn, failureFn)`
**DEPRECATED** &mdash; Use [onMotionChange](https://github.com/christocracy/cordova-background-geolocation/tree/trigger-activities#onmotionchangecallbackfn-failurefn) instead.
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.
######@param {Object} location The Location data
######@param {Integer} taskId The taskId used to send to bgGeo.finish(taskId) in order to signal completion of your callbackFn
```
bgGeo.onStationary(function(location, taskId) {
try {
console.log('- Device is stopped: ', location.latitude, location.longitude);
} catch(e) {
console.error('An error occurred in my application code', e);
}
// 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.
######@config {Boolean} notifyOnExit Whether to listen to EXIT events
######@config {Boolean} notifyOnEntry Whether to listen to ENTER events
```
bgGeo.addGeofence({
identifier: "Home",
radius: 150,
latitude: 45.51921926,
longitude: -73.61678581,
notifyOnEntry: true,
notifyOnExit: false
}, function() {
console.log("Successfully added geofence");
}, function(error) {
console.warn("Failed to add geofence", error);
});
```
####`removeGeofence(identifier, callbackFn, failureFn)`
Removes a geofence having the given `{String} identifier`.
######@config {String} identifier The name of your geofence, eg: "Home", "Office"
######@config {Function} callbackFn successfully removed geofence.
######@config {Function} failureFn failed to remove geofence
```
bgGeo.removeGeofence("Home", function() {
console.log("Successfully removed geofence");
}, function(error) {
console.warn("Failed to remove geofence", error);
});
```
####`getGeofences(callbackFn, failureFn)`
Fetch the list of monitored geofences. Your `callbackFn` will be provided with an `Array` of geofences. If there are no geofences being monitored, you'll receive an empty Array `[]`.
``` ```
bgGeo.onStationary(function(location) { bgGeo.getGeofences(function(geofences) {
console.log('- Device is stopped: ', location.latitude, location.longitude); for (var n=0,len=geofences.length;n<len;n++) {
console.log("Geofence: ", geofence.identifier, geofence.radius, geofence.latitude, geofence.longitude);
}
}, function(error) {
console.warn("Failed to fetch geofences from server");
});
```
####`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 {Object} params. This object contains 2 keys: `@param {String} identifier` and `@param {String} action [ENTER|EXIT]`.
######@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(params, taskId) {
try {
console.log('A geofence has been crossed: ', params.identifier);
console.log('ENTER or EXIT?: ', params.action);
} catch(e) {
console.error('An error occurred in my application code', e);
}
// 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);
}); });
``` ```
#####`getLocations(callbackFn, failureFn)` ####`getLocations(callbackFn, failureFn)`
Fetch all the locations currently stored in native plugin's SQLite database. Your ```callbackFn`` will receive an ```Array``` of locations in the 1st parameter. Eg: Fetch all the locations currently stored in native plugin's SQLite database. Your ```callbackFn`` will receive an ```Array``` of locations in the 1st parameter. Eg:
The `callbackFn` will be executed with following params:
######@param {Array} locations. The list of locations stored in SQLite database.
######@param {Integer} taskId The background taskId which you must send back to the native plugin via `bgGeo.finish(taskId)` in order to signal the end of your background thread.
``` ```
bgGeo.getLocations(function(locations) { bgGeo.getLocations(function(locations, taskId) {
console.log("locations: ", locations); try {
console.log("locations: ", locations);
} catch(e) {
console.error("An error occurred in my application code");
}
bgGeo.finish(taskId);
}); });
``` ```
#####`sync(callbackFn, failureFn)` ####`sync(callbackFn, failureFn)`
If the plugin is configured for HTTP with an ```#url``` and ```#autoSync: false```, this method will initiate POSTing the locations currently stored in the native SQLite database to your configured ```#url```. All records in the database will be DELETED. If you configured ```batchSync: true```, all the locations will be sent to your server in a single HTTP POST request, otherwise the plugin will create execute an HTTP post for **each** location in the database (REST-style). Your ```callbackFn``` will be executed and provided with an Array of all the locations from the SQLite database. If the plugin failed to sync to your server (possibly because of no network connection), the ```failureFn``` will be called with an ```errorMessage```. If you are **not** using the HTTP features, ```sync``` is the only way to clear the native SQLite datbase. Eg: If the plugin is configured for HTTP with an ```#url``` and ```#autoSync: false```, this method will initiate POSTing the locations currently stored in the native SQLite database to your configured ```#url```. All records in the database will be DELETED. If you configured ```batchSync: true```, all the locations will be sent to your server in a single HTTP POST request, otherwise the plugin will create execute an HTTP post for **each** location in the database (REST-style). Your ```callbackFn``` will be executed and provided with an Array of all the locations from the SQLite database. If you configured the plugin for HTTP (by configuring an `#url`, your `callbackFn` will be executed after the HTTP request(s) have completed. If the plugin failed to sync to your server (possibly because of no network connection), the ```failureFn``` will be called with an ```errorMessage```. If you are **not** using the HTTP features, ```sync``` is the only way to clear the native SQLite datbase. Eg:
Your callback will be provided with the following params
######@param {Array} locations. The list of locations stored in SQLite database.
######@param {Integer} taskId The background taskId which you must send back to the native plugin via `bgGeo.finish(taskId)` in order to signal the end of your background thread.
``` ```
bgGeo.sync(function(locations) { bgGeo.sync(function(locations, taskId) {
// Here are all the locations from the database. The database is now EMPTY. try {
console.log('synced locations: ', locations); // Here are all the locations from the database. The database is now EMPTY.
console.log('synced locations: ', locations);
} catch(e) {
console.error('An error occurred in my application code', e);
}
// Be sure to call finish(taskId) in order to signal the end of the background-thread.
bgGeo.finish(taskId);
}, function(errorMessage) { }, function(errorMessage) {
console.warn('Sync FAILURE: ', errorMessage); console.warn('Sync FAILURE: ', errorMessage);
}); });
``` ```
#####`getOdometer(callbackFn, failureFn)` ####`getOdometer(callbackFn, failureFn)`
The plugin constantly tracks distance travelled. To fetch the current **odometer** reading: The plugin constantly tracks distance travelled. To fetch the current **odometer** reading:
...@@ -271,15 +473,28 @@ The plugin constantly tracks distance travelled. To fetch the current **odomete ...@@ -271,15 +473,28 @@ The plugin constantly tracks distance travelled. To fetch the current **odomete
}); });
``` ```
#####`resetOdometer(callbackFn, failureFn)` ####`resetOdometer(callbackFn, failureFn)`
Reset the **odometer** to zero. The plugin never automatically resets the odometer so it's up to you to reset it as desired. Reset the **odometer** to zero. The plugin never automatically resets the odometer so it's up to you to reset it as desired.
####`playSound(soundId)`
Here's a fun one. The plugin can play a number of OS system sounds for each platform. For [IOS](http://iphonedevwiki.net/index.php/AudioServices) and [Android](http://developer.android.com/reference/android/media/ToneGenerator.html). I offer this API as-is, it's up to you to figure out how this works.
```
// A soundId iOS recognizes
bgGeo.playSound(1303);
// An Android soundId
bgGeo.playSound(90);
```
## Config ## Config
Use the following config-parameters with the #configure method: Use the following config-parameters with the #configure method:
#####`@param {Boolean} debug` ####`@param {Boolean} debug`
When enabled, the plugin will emit sounds for life-cycle events of background-geolocation! **NOTE iOS**: In addition, you must manually enable the *Audio and Airplay* background mode in *Background Capabilities* to hear these debugging sounds. When enabled, the plugin will emit sounds for life-cycle events of background-geolocation! **NOTE iOS**: In addition, you must manually enable the *Audio and Airplay* background mode in *Background Capabilities* to hear these debugging sounds.
...@@ -291,18 +506,18 @@ When enabled, the plugin will emit sounds for life-cycle events of background-ge ...@@ -291,18 +506,18 @@ When enabled, the plugin will emit sounds for life-cycle events of background-ge
![Enable Background Audio](/enable-background-audio.png "Enable Background Audio") ![Enable Background Audio](/enable-background-audio.png "Enable Background Audio")
#####`@param {Integer} desiredAccuracy [0, 10, 100, 1000] in meters` ####`@param {Integer} desiredAccuracy [0, 10, 100, 1000] in meters`
Specify the desired-accuracy of the geolocation system with 1 of 4 values, ```0, 10, 100, 1000``` where ```0``` means HIGHEST POWER, HIGHEST ACCURACY and ```1000``` means LOWEST POWER, LOWEST ACCURACY Specify the desired-accuracy of the geolocation system with 1 of 4 values, ```0, 10, 100, 1000``` where ```0``` means HIGHEST POWER, HIGHEST ACCURACY and ```1000``` means LOWEST POWER, LOWEST ACCURACY
- [Android](https://developer.android.com/reference/com/google/android/gms/location/LocationRequest.html#PRIORITY_BALANCED_POWER_ACCURACY) - [Android](https://developer.android.com/reference/com/google/android/gms/location/LocationRequest.html#PRIORITY_BALANCED_POWER_ACCURACY)
- [iOS](https://developer.apple.com/library/ios/documentation/CoreLocation/Reference/CLLocationManager_Class/index.html#//apple_ref/occ/instp/CLLocationManager/desiredAccuracy) - [iOS](https://developer.apple.com/library/ios/documentation/CoreLocation/Reference/CLLocationManager_Class/index.html#//apple_ref/occ/instp/CLLocationManager/desiredAccuracy)
#####`@param {Integer} stationaryRadius (meters)` ####`@param {Integer} stationaryRadius (meters)`
When stopped, the minimum distance the device must move beyond the stationary location for aggressive background-tracking to engage. Note, since the plugin uses iOS significant-changes API, the plugin cannot detect the exact moment the device moves out of the stationary-radius. In normal conditions, it can take as much as 3 city-blocks to 1/2 km before staionary-region exit is detected. When stopped, the minimum distance the device must move beyond the stationary location for aggressive background-tracking to engage. Note, since the plugin uses iOS significant-changes API, the plugin cannot detect the exact moment the device moves out of the stationary-radius. In normal conditions, it can take as much as 3 city-blocks to 1/2 km before staionary-region exit is detected.
#####`@param {Integer} distanceFilter` ####`@param {Integer} distanceFilter`
The minimum distance (measured in meters) a device must move horizontally before an update event is generated. @see [Apple docs](https://developer.apple.com/library/ios/documentation/CoreLocation/Reference/CLLocationManager_Class/CLLocationManager/CLLocationManager.html#//apple_ref/occ/instp/CLLocationManager/distanceFilter). However, #distanceFilter is elastically auto-calculated by the plugin: When speed increases, #distanceFilter increases; when speed decreases, so does distanceFilter. The minimum distance (measured in meters) a device must move horizontally before an update event is generated. @see [Apple docs](https://developer.apple.com/library/ios/documentation/CoreLocation/Reference/CLLocationManager_Class/CLLocationManager/CLLocationManager.html#//apple_ref/occ/instp/CLLocationManager/distanceFilter). However, #distanceFilter is elastically auto-calculated by the plugin: When speed increases, #distanceFilter increases; when speed decreases, so does distanceFilter.
...@@ -336,14 +551,14 @@ Compare now background-geolocation in the scope of a city. In this image, the l ...@@ -336,14 +551,14 @@ Compare now background-geolocation in the scope of a city. In this image, the l
![distanceFilter at city scale](/distance-filter-city.png "distanceFilter at city scale") ![distanceFilter at city scale](/distance-filter-city.png "distanceFilter at city scale")
#####`@param {Boolean} stopOnTerminate` ####`@param {Boolean} stopOnTerminate`
Enable this in order to force a stop() when the application terminated (e.g. on iOS, double-tap home button, swipe away the app). On Android, ```stopOnTerminate: false``` will cause the plugin to operate as a headless background-service (in this case, you should configure an #url in order for the background-service to send the location to your server) Enable this in order to force a stop() when the application terminated (e.g. on iOS, double-tap home button, swipe away the app). On Android, ```stopOnTerminate: false``` will cause the plugin to operate as a headless background-service (in this case, you should configure an #url in order for the background-service to send the location to your server)
#####`@param {Boolean} stopAfterElapsedMinutes` ####`@param {Boolean} stopAfterElapsedMinutes`
The plugin can optionally auto-stop monitoring location when some number of minutes elapse after being the #start method was called. The plugin can optionally auto-stop monitoring location when some number of minutes elapse after being the #start method was called.
#### In-Plugin SQLite Storage ### In-Plugin SQLite Storage
The plugin will cache **every** recorded geolocation to its internal SQLite database -- when you sync the locations and your server responds with HTTP ```200, 201 or 204```, the plugin will **DELETE** the stored location from cache. The plugin has a cache-pruning feature with ```@config {Integer} maxDaysToPersist``` -- If the plugin hasn't successfully synced these these records in the database before ```maxDaysToPersist``` expires, the plugin will give up and those geolocation records will be pruned from the database. The plugin will cache **every** recorded geolocation to its internal SQLite database -- when you sync the locations and your server responds with HTTP ```200, 201 or 204```, the plugin will **DELETE** the stored location from cache. The plugin has a cache-pruning feature with ```@config {Integer} maxDaysToPersist``` -- If the plugin hasn't successfully synced these these records in the database before ```maxDaysToPersist``` expires, the plugin will give up and those geolocation records will be pruned from the database.
...@@ -356,34 +571,36 @@ If you **don't** configure the optional HTTP feature, the only way to delete the ...@@ -356,34 +571,36 @@ If you **don't** configure the optional HTTP feature, the only way to delete the
}); });
``` ```
#### HTTP Features ### HTTP Features
#####`@param {String} url` ####`@param {String} url`
Your server url where you wish to HTTP POST location data to. Your server url where you wish to HTTP POST location data to.
#####`@param {String} batchSync [false]` ####`@param {String} batchSync [false]`
Default is ```false```. If you've enabled HTTP feature by configuring an ```#url```, ```batchSync: true``` will POST all the locations currently stored in native SQLite datbase to your server in a single HTTP POST request. With ```batchSync: false```, an HTTP POST request will be initiated for **each** location in database. Default is ```false```. If you've enabled HTTP feature by configuring an ```#url```, ```batchSync: true``` will POST all the locations currently stored in native SQLite datbase to your server in a single HTTP POST request. With ```batchSync: false```, an HTTP POST request will be initiated for **each** location in database.
#####`@param {String} autoSync [true]` ####`@param {String} autoSync [true]`
Default is ```true```. If you've enabeld HTTP feature by configuring an ```#url```, the plugin will attempt to HTTP POST each location to your server **as it is recorded**. If you set ```autoSync: false```, it's up to you to **manually** execute the ```#sync``` method to initate the HTTP POST (**NOTE** The plugin will continue to persist **every** recorded location in the SQLite database until you execute ```#sync```). Default is ```true```. If you've enabeld HTTP feature by configuring an ```#url```, the plugin will attempt to HTTP POST each location to your server **as it is recorded**. If you set ```autoSync: false```, it's up to you to **manually** execute the ```#sync``` method to initate the HTTP POST (**NOTE** The plugin will continue to persist **every** recorded location in the SQLite database until you execute ```#sync```).
#####`@param {Object} params` ####`@param {Object} params`
Optional HTTP params sent along in HTTP request to above ```#url```. Optional HTTP params sent along in HTTP request to above ```#url```.
#####`@param {Object} headers` ####`@param {Object} headers`
Optional HTTP params sent along in HTTP request to above ```#url```. Optional HTTP params sent along in HTTP request to above ```#url```.
#####`@param {Integer} maxDaysToPersist` ####`@param {Integer} maxDaysToPersist`
Maximum number of days to store a geolocation in plugin's SQLite database when your server fails to respond with ```HTTP 200 OK```. The plugin will continue attempting to sync with your server until ```maxDaysToPersist``` when it will give up and remove the location from the database. Maximum number of days to store a geolocation in plugin's SQLite database when your server fails to respond with ```HTTP 200 OK```. The plugin will continue attempting to sync with your server until ```maxDaysToPersist``` when it will give up and remove the location from the database.
Both iOS and Android can send the Geolocation to your server simply by configuring an ```#url``` in addition to optional ```#headers``` and ```#params```. This is the preferred way to send the Geolocation to your server, rather than doing it yourself with Ajax in your javascript. Both iOS and Android can send the Geolocation to your server simply by configuring an ```#url``` in addition to optional ```#headers``` and ```#params```. This is the preferred way to send the Geolocation to your server, rather than doing it yourself with Ajax in your javascript.
#### Sample HTTP Request arriving at your server
``` ```
bgGeo.configure(callbackFn, failureFn, { bgGeo.configure(callbackFn, failureFn, {
. .
...@@ -447,7 +664,7 @@ No Post Params. ...@@ -447,7 +664,7 @@ No Post Params.
### Android Config ### Android Config
#####`@param {Integer millis} locationUpdateInterval` ####`@param {Integer millis} locationUpdateInterval`
Set the desired interval for active location updates, in milliseconds. Set the desired interval for active location updates, in milliseconds.
...@@ -457,7 +674,7 @@ This interval is inexact. You may not receive updates at all (if no location sou ...@@ -457,7 +674,7 @@ This interval is inexact. You may not receive updates at all (if no location sou
Applications with only the coarse location permission may have their interval silently throttled. Applications with only the coarse location permission may have their interval silently throttled.
#####`@param {Integer millis} fastestLocationUpdateInterval` ####`@param {Integer millis} fastestLocationUpdateInterval`
Explicitly set the fastest interval for location updates, in milliseconds. Explicitly set the fastest interval for location updates, in milliseconds.
...@@ -476,19 +693,35 @@ If ```#fastestLocationUpdateInterval``` is set slower than ```#locationUpdateInt ...@@ -476,19 +693,35 @@ If ```#fastestLocationUpdateInterval``` is set slower than ```#locationUpdateInt
======== ========
An interval of 0 is allowed, but not recommended, since location updates may be extremely fast on future implementations. An interval of 0 is allowed, but not recommended, since location updates may be extremely fast on future implementations.
#####`@param {Integer millis} activityRecognitionInterval` ####`@param {Integer millis} activityRecognitionInterval`
the desired time between activity detections. Larger values will result in fewer activity detections while improving battery life. A value of 0 will result in activity detections at the fastest possible rate. the desired time between activity detections. Larger values will result in fewer activity detections while improving battery life. A value of 0 will result in activity detections at the fastest possible rate.
#####`@param {Integer minutes} stopTimeout` ####`@param {Integer millis} minimumActivityRecognitionConfidence`
Each activity-recognition-result returned by the API is tagged with a "confidence" level expressed as a %. You can set your desired confidence to trigger a state-change. Defaults to `80`.
####`@param {String} triggerActivities`
These are the comma-delimited list of [activity-names](https://developers.google.com/android/reference/com/google/android/gms/location/DetectedActivity) returned by the `ActivityRecognition` API which will trigger a state-change from **stationary** to **moving**. By default, this list is set to all five **moving-states**: `"in_vehicle, on_bicycle, on_foot, running, walking"`. If you wish, you could configure the plugin to only engage **moving-mode** for vehicles by providing only `"in_vehicle"`.
####`@param {Integer minutes} stopTimeout`
The number of miutes to wait before turning off the GPS after the ActivityRecognition System (ARS) detects the device is ```STILL``` (defaults to 0, no timeout). If you don't set a value, the plugin is eager to turn off the GPS ASAP. An example use-case for this configuration is to delay GPS OFF while in a car waiting at a traffic light. The number of miutes to wait before turning off the GPS after the ActivityRecognition System (ARS) detects the device is ```STILL``` (defaults to 0, no timeout). If you don't set a value, the plugin is eager to turn off the GPS ASAP. An example use-case for this configuration is to delay GPS OFF while in a car waiting at a traffic light.
#####`@param {Boolean} forceReload` ####`@param {Boolean} forceReloadOnMotionChange`
If the user closes the application while the background-tracking has been started, location-tracking will continue on if ```stopOnTerminate: false```. You may choose to force the foreground application to reload (since this is where your Javascript runs). `forceReloadOnMotionChange: true` will reload the app only when a state-change occurs from **stationary -> moving** or vice-versa. (**WARNING** possibly disruptive to user).
If the user closes the application while the background-tracking has been started, location-tracking will continue on if ```stopOnTerminate: false```. You may choose to force the foreground application to reload (since this is where your Javascript runs) by setting ```foreceReload: true```. This will guarantee that locations are always sent to your Javascript callback (**WARNING** possibly disruptive to user). ####`@param {Boolean} forceReloadOnLocationChange`
#####`@param {Boolean} startOnBoot` If the user closes the application while the background-tracking has been started, location-tracking will continue on if ```stopOnTerminate: false```. You may choose to force the foreground application to reload (since this is where your Javascript runs). `forceReloadOnLocationChange: true` will reload the app when a new location is recorded.
####`@param {Boolean} forceReloadOnGeofence`
If the user closes the application while the background-tracking has been started, location-tracking will continue on if ```stopOnTerminate: false```. You may choose to force the foreground application to reload (since this is where your Javascript runs). `forceReloadOnGeolocation: true` will reload the app only when a geofence crossing event has occurred.
####`@param {Boolean} startOnBoot`
Set to ```true``` to start the background-service whenever the device boots. Unless you configure the plugin to ```forceReload``` (ie: boot your app), you should configure the plugin's HTTP features so it can POST to your server in "headless" mode. Set to ```true``` to start the background-service whenever the device boots. Unless you configure the plugin to ```forceReload``` (ie: boot your app), you should configure the plugin's HTTP features so it can POST to your server in "headless" mode.
...@@ -496,21 +729,99 @@ Set to ```true``` to start the background-service whenever the device boots. Un ...@@ -496,21 +729,99 @@ Set to ```true``` to start the background-service whenever the device boots. Un
### iOS Config ### iOS Config
#####`@param {Boolean} disableElasticity [false]` ####`@param {Boolean} disableElasticity [false]`
Defaults to ```false```. Set ```true``` to disable automatic speed-based ```#distanceFilter``` elasticity. eg: When device is moving at highway speeds, locations are returned at ~ 1 / km. Defaults to ```false```. Set ```true``` to disable automatic speed-based ```#distanceFilter``` elasticity. eg: When device is moving at highway speeds, locations are returned at ~ 1 / km.
#####`@param {String} activityType [AutomotiveNavigation, OtherNavigation, Fitness, Other]` ####`@param {String} activityType [AutomotiveNavigation, OtherNavigation, Fitness, Other]`
Presumably, this affects ios GPS algorithm. See [Apple docs](https://developer.apple.com/library/ios/documentation/CoreLocation/Reference/CLLocationManager_Class/CLLocationManager/CLLocationManager.html#//apple_ref/occ/instp/CLLocationManager/activityType) for more information Presumably, this affects ios GPS algorithm. See [Apple docs](https://developer.apple.com/library/ios/documentation/CoreLocation/Reference/CLLocationManager_Class/CLLocationManager/CLLocationManager.html#//apple_ref/occ/instp/CLLocationManager/activityType) for more information
### WP8 Config ### WP8 Config
#####`{Integer [0, 10, 100, 1000]} desiredAccuracy` ####`{Integer [0, 10, 100, 1000]} desiredAccuracy`
###### Windows Phone ##### Windows Phone
The underlying GeoLocator you can choose to use 'DesiredAccuracy' or 'DesiredAccuracyInMeters'. Since this plugins default configuration accepts meters, the default desiredAccuracy is mapped to the Windows Phone DesiredAccuracyInMeters leaving the DesiredAccuracy enum empty. For more info see the [MS docs](http://msdn.microsoft.com/en-us/library/windows/apps/windows.devices.geolocation.geolocator.desiredaccuracyinmeters) for more information. The underlying GeoLocator you can choose to use 'DesiredAccuracy' or 'DesiredAccuracyInMeters'. Since this plugins default configuration accepts meters, the default desiredAccuracy is mapped to the Windows Phone DesiredAccuracyInMeters leaving the DesiredAccuracy enum empty. For more info see the [MS docs](http://msdn.microsoft.com/en-us/library/windows/apps/windows.devices.geolocation.geolocator.desiredaccuracyinmeters) for more information.
# Geofence Features
![Geofence Features](https://www.dropbox.com/s/609iibr6ofzoq7p/Screenshot%202015-06-06%2017.05.33.png?dl=1)
The plugin includes native **geofencing** features. You may add, remove and query the list of monitored geofences from the native plugin. The native plugin will persist monitored geofences and re-initiate them when the app boots or the device is restarted.
A monitored geofence **will remain active** until you explicity remove it via `bgGeo.removeGeofence(identifier)`.
#### Geofence Model
#####`@param {String} identifier`
A unique `String` to identify your Geofence, eg: "Home", "Office".
#####`@param {Integer} radius`
The radius of the circular geofence. A radius of >100 meters works best.
#####`@param {Boolean} notifyOnEntry`
Transitioning **into** the geofence will generate an event.
#####`@param {Boolean} notifyOnExit`
Transitioning **out of** the geofence will generate an event.
#### Listening to Geofence Events
Listen to geofence transition events using the method `#onGeofence`. You may set up any number of `#onGeofence` event-listeners throughout your code -- they will all be executed.
```
bgGeo.addGeofence({
identifier: "Home",
radius: 200,
latitude: 47.2342323,
longitude: -57.342342,
notifyOnEntry: true
});
bgGeo.onGeofence(function(geofence, taskId) {
try {
console.log("- A Geofence transition occurred");
console.log(" identifier: ", geofence.identifier);
console.log(" action: ", geofence.action);
} catch(e) {
console.error("An error occurred in my code!", e);
}
// Be sure to call #finish!!
bgGeo.finish(taskId);
});
```
#### Removing Geofences
The native plugin will continue to monitor geofences and fire transition-events until you explicity tell the plugin to remove a geofence via `#removeGeofence(identifier)`.
```
bgGeo.removeGeofence("Home");
```
#### Querying Geofences
The native plugin persists monitored geofences between application boots and device restarts. When your app boots, you can fetch the currently monitored geofences from the native plugin and, for example, re-draw markers on your map.
```
bgGeo.getGeofences(function(geofences) {
for (var n=0,len=geofences.length;n<len;n++) {
var geofence = geofences[n];
var marker = new google.maps.Circle({
radius: parseInt(geofence.radius, 10),
center: new google.maps.LatLng(geofence.latitude, geofence.longitude),
map: myMapInstance
});
}
});
```
## Licence ## ## Licence ##
``` ```
cordova-background-geolocation cordova-background-geolocation
......
...@@ -138,26 +138,28 @@ var app = { ...@@ -138,26 +138,28 @@ var app = {
app.onClickHome(); 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. * 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)); console.log('[js] BackgroundGeoLocation callback: ' + JSON.stringify(location));
// Update our current-position marker. // Update our current-position marker.
app.setCurrentLocation(location); 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. // 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) { var failureFn = function(error) {
...@@ -165,7 +167,7 @@ var app = { ...@@ -165,7 +167,7 @@ var app = {
}; };
// Only ios emits this stationary event // Only ios emits this stationary event
bgGeo.onStationary(function(location) { bgGeo.onStationary(function(location, taskId) {
console.log('[js] BackgroundGeoLocation onStationary ' + JSON.stringify(location)); console.log('[js] BackgroundGeoLocation onStationary ' + JSON.stringify(location));
app.setCurrentLocation(location); app.setCurrentLocation(location);
...@@ -187,6 +189,8 @@ var app = { ...@@ -187,6 +189,8 @@ var app = {
var center = new google.maps.LatLng(coords.latitude, coords.longitude); var center = new google.maps.LatLng(coords.latitude, coords.longitude);
app.stationaryRadius.setRadius(radius); app.stationaryRadius.setRadius(radius);
app.stationaryRadius.setCenter(center); app.stationaryRadius.setCenter(center);
bgGeo.finish(taskId);
}); });
...@@ -195,22 +199,22 @@ var app = { ...@@ -195,22 +199,22 @@ var app = {
debug: true, // <-- enable this hear sounds for background-geolocation life-cycle. debug: true, // <-- enable this hear sounds for background-geolocation life-cycle.
desiredAccuracy: 0, desiredAccuracy: 0,
stationaryRadius: 50, stationaryRadius: 50,
distanceFilter: 25, distanceFilter: 50,
disableElasticity: false, // <-- [iOS] Default is 'false'. Set true to disable speed-based distanceFilter elasticity disableElasticity: false, // <-- [iOS] Default is 'false'. Set true to disable speed-based distanceFilter elasticity
locationUpdateInterval: 5000, locationUpdateInterval: 5000,
minimumActivityRecognitionConfidence: 80, // 0-100%. Minimum activity-confidence for a state-change minimumActivityRecognitionConfidence: 80, // 0-100%. Minimum activity-confidence for a state-change
fastestLocationUpdateInterval: 5000, fastestLocationUpdateInterval: 5000,
activityRecognitionInterval: 10000, activityRecognitionInterval: 10000,
stopTimeout: 0, stopTimeout: 0,
forceReload: true, // <-- [Android] If the user closes the app **while location-tracking is started** , reboot app (WARNING: possibly distruptive to user) forceReload: false, // <-- [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. stopOnTerminate: true, // <-- [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. startOnBoot: false, // <-- [Android] Auto start background-service in headless mode when device is powered-up.
activityType: 'AutomotiveNavigation', activityType: 'AutomotiveNavigation',
/** /**
* HTTP Feature: set an url to allow the native background service to POST locations to your server * 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', 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. 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 maxDaysToPersist: 1, // <-- Maximum days to persist a location in plugin's SQLite database when HTTP fails
headers: { headers: {
...@@ -221,9 +225,10 @@ var app = { ...@@ -221,9 +225,10 @@ var app = {
} }
}); });
bgGeo.onGeofence(function(identifier) { bgGeo.onGeofence(function(identifier, taskId) {
alert('Enter Geofence: ' + identifier); 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. // Add longpress event for adding GeoFence of hard-coded radius 200m.
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<plugin xmlns="http://www.phonegap.com/ns/plugins/1.0" <plugin xmlns="http://www.phonegap.com/ns/plugins/1.0"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
id="com.transistorsoft.cordova.background-geolocation" id="com.transistorsoft.cordova.background-geolocation"
version="0.4.1"> version="0.5.1">
<name>BackgroundGeolocation</name> <name>BackgroundGeolocation</name>
<description>Sophisticated, battery-efficient background-geolocation plugin for Cordova</description> <description>Sophisticated, battery-efficient background-geolocation plugin for Cordova</description>
<license>MIT</license> <license>MIT</license>
...@@ -30,6 +30,7 @@ ...@@ -30,6 +30,7 @@
<service android:name="com.transistorsoft.locationmanager.BackgroundGeolocationService" /> <service android:name="com.transistorsoft.locationmanager.BackgroundGeolocationService" />
<service android:name="com.transistorsoft.locationmanager.LocationService" /> <service android:name="com.transistorsoft.locationmanager.LocationService" />
<service android:name="com.transistorsoft.locationmanager.ActivityRecognitionService" /> <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"> <receiver android:name="com.transistorsoft.locationmanager.BootReceiver" android:enabled="true" android:exported="false">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/> <action android:name="android.intent.action.BOOT_COMPLETED"/>
......
package com.transistorsoft.cordova.bggeo; package com.transistorsoft.cordova.bggeo;
import java.util.List;
import java.util.ArrayList;
import org.apache.cordova.CallbackContext; import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin; import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView; import org.apache.cordova.CordovaWebView;
...@@ -8,12 +10,14 @@ import org.json.JSONArray; ...@@ -8,12 +10,14 @@ import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import org.json.JSONException; import org.json.JSONException;
import android.os.Bundle; import android.os.Bundle;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.location.ActivityRecognitionResult; import com.google.android.gms.location.ActivityRecognitionResult;
import com.google.android.gms.location.DetectedActivity; import com.google.android.gms.location.DetectedActivity;
import com.transistorsoft.locationmanager.BackgroundGeolocationService; import com.transistorsoft.locationmanager.BackgroundGeolocationService;
import com.transistorsoft.locationmanager.BackgroundGeolocationService.PaceChangeEvent; import com.google.android.gms.location.Geofence;
import com.transistorsoft.locationmanager.BackgroundGeolocationService.PausedEvent; import com.google.android.gms.location.GeofencingEvent;
//import com.transistorsoft.locationmanager.BackgroundGeolocationService.StationaryLocation; import android.app.AlertDialog;
import android.content.DialogInterface;
import de.greenrobot.event.EventBus; import de.greenrobot.event.EventBus;
import android.app.Activity; import android.app.Activity;
...@@ -21,43 +25,65 @@ import android.content.Intent; ...@@ -21,43 +25,65 @@ import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.location.Location; import android.location.Location;
import android.util.Log; import android.util.Log;
import android.media.AudioManager;
import android.media.ToneGenerator;
public class CDVBackgroundGeolocation extends CordovaPlugin { public class CDVBackgroundGeolocation extends CordovaPlugin {
private static final String TAG = "BackgroundGeolocation"; private static final String TAG = "TSLocationManager";
private static CordovaWebView gWebView; private static CordovaWebView gWebView;
public static Boolean forceReload = false; public static Boolean forceReload = false;
/**
* Timeout in millis for a getCurrentPosition request to give up.
* TODO make configurable.
*/
private static final long GET_CURRENT_POSITION_TIMEOUT = 30000;
public static final String ACTION_START = "start"; public static final String ACTION_START = "start";
public static final String ACTION_STOP = "stop"; public static final String ACTION_STOP = "stop";
public static final String ACTION_ON_PACE_CHANGE = "onPaceChange"; public static final String ACTION_FINISH = "finish";
public static final String ACTION_CONFIGURE = "configure"; public static final String ACTION_ERROR = "error";
public static final String ACTION_SET_CONFIG = "setConfig"; public static final String ACTION_CHANGE_PACE = "changePace";
public static final String ACTION_ON_STATIONARY = "addStationaryRegionListener"; public static final String ACTION_CONFIGURE = "configure";
public static final String ACTION_GET_LOCATIONS = "getLocations"; public static final String ACTION_SET_CONFIG = "setConfig";
public static final String ACTION_SYNC = "sync"; public static final String ACTION_ON_STATIONARY = "addStationaryRegionListener";
public static final String ACTION_GET_ODOMETER = "getOdometer"; public static final String ACTION_ADD_MOTION_CHANGE_LISTENER = "addMotionChangeListener";
public static final String ACTION_RESET_ODOMETER = "resetOdometer"; public static final String ACTION_ON_MOTION_CHANGE = "onMotionChange";
public static final String ACTION_GET_LOCATIONS = "getLocations";
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_REMOVE_GEOFENCE = "removeGeofence";
public static final String ACTION_GET_GEOFENCES = "getGeofences";
public static final String ACTION_ON_GEOFENCE = "onGeofence";
public static final String ACTION_PLAY_SOUND = "playSound";
public static final String ACTION_ACTIVITY_RELOAD = "activityReload";
private Boolean isEnabled = false; private Boolean isEnabled = false;
private Boolean stopOnTerminate = false; private Boolean stopOnTerminate = false;
private Boolean isMoving = false; private Boolean isMoving = false;
private Boolean isAcquiringCurrentPosition = false;
private long isAcquiringCurrentPositionSince;
private Intent backgroundServiceIntent; private Intent backgroundServiceIntent;
private DetectedActivity currentActivity; private DetectedActivity currentActivity;
// Geolocation callback // Geolocation callback
private CallbackContext locationCallback; private CallbackContext locationCallback;
// Called when DetectedActivity is STILL
private CallbackContext stationaryCallback; private CallbackContext stationaryCallback;
private CallbackContext getLocationsCallback; private CallbackContext getLocationsCallback;
private CallbackContext syncCallback; private CallbackContext syncCallback;
private CallbackContext getOdometerCallback; private CallbackContext getOdometerCallback;
private CallbackContext resetOdometerCallback; private CallbackContext resetOdometerCallback;
private CallbackContext paceChangeCallback;
private CallbackContext getGeofencesCallback;
private ToneGenerator toneGenerator;
private List<CallbackContext> motionChangeCallbacks = new ArrayList<CallbackContext>();
private List<CallbackContext> geofenceCallbacks = new ArrayList<CallbackContext>();
private List<CallbackContext> currentPositionCallbacks = new ArrayList<CallbackContext>();
public static boolean isActive() { public static boolean isActive() {
return gWebView != null; return gWebView != null;
...@@ -69,8 +95,8 @@ public class CDVBackgroundGeolocation extends CordovaPlugin { ...@@ -69,8 +95,8 @@ public class CDVBackgroundGeolocation extends CordovaPlugin {
backgroundServiceIntent = new Intent(this.cordova.getActivity(), BackgroundGeolocationService.class); backgroundServiceIntent = new Intent(this.cordova.getActivity(), BackgroundGeolocationService.class);
toneGenerator = new ToneGenerator(AudioManager.STREAM_NOTIFICATION, 100);
// Register for events fired by our IntentService "LocationService" // Register for events fired by our IntentService "LocationService"
EventBus.getDefault().register(this);
} }
public boolean execute(String action, JSONArray data, CallbackContext callbackContext) throws JSONException { public boolean execute(String action, JSONArray data, CallbackContext callbackContext) throws JSONException {
...@@ -78,13 +104,21 @@ public class CDVBackgroundGeolocation extends CordovaPlugin { ...@@ -78,13 +104,21 @@ public class CDVBackgroundGeolocation extends CordovaPlugin {
Boolean result = false; Boolean result = false;
if (ACTION_START.equalsIgnoreCase(action) && !isEnabled) { if (ACTION_START.equalsIgnoreCase(action)) {
result = true; result = true;
this.setEnabled(true); this.setEnabled(true);
callbackContext.success(); callbackContext.success(1);
} else if (ACTION_STOP.equalsIgnoreCase(action)) { } else if (ACTION_STOP.equalsIgnoreCase(action)) {
// No implementation to stop background-tasks with Android. Just say "success"
result = true; result = true;
this.setEnabled(false); this.setEnabled(false);
callbackContext.success(0);
} else if (ACTION_FINISH.equalsIgnoreCase(action)) {
result = true;
callbackContext.success();
} else if (ACTION_ERROR.equalsIgnoreCase(action)) {
result = true;
this.onError(data.getString(1));
callbackContext.success(); callbackContext.success();
} else if (ACTION_CONFIGURE.equalsIgnoreCase(action)) { } else if (ACTION_CONFIGURE.equalsIgnoreCase(action)) {
result = applyConfig(data); result = applyConfig(data);
...@@ -93,22 +127,28 @@ public class CDVBackgroundGeolocation extends CordovaPlugin { ...@@ -93,22 +127,28 @@ public class CDVBackgroundGeolocation extends CordovaPlugin {
} else { } else {
callbackContext.error("- Configuration error!"); callbackContext.error("- Configuration error!");
} }
} else if (ACTION_ON_PACE_CHANGE.equalsIgnoreCase(action)) { } else if (BackgroundGeolocationService.ACTION_CHANGE_PACE.equalsIgnoreCase(action)) {
if (!isEnabled) { if (!isEnabled) {
Log.w(TAG, "- Cannot change pace while disabled"); Log.w(TAG, "- Cannot change pace while disabled");
result = false; result = false;
callbackContext.error("Cannot #changePace while disabled"); callbackContext.error("Cannot #changePace while disabled");
} else { } else {
PaceChangeEvent event = new PaceChangeEvent(data.getBoolean(0));
EventBus.getDefault().post(event);
result = true; result = true;
callbackContext.success(); isMoving = data.getBoolean(0);
paceChangeCallback = callbackContext;
Bundle event = new Bundle();
event.putString("name", action);
event.putBoolean("request", true);
event.putBoolean("isMoving", isMoving);
EventBus.getDefault().post(event);
} }
} else if (ACTION_SET_CONFIG.equalsIgnoreCase(action)) { } else if (ACTION_SET_CONFIG.equalsIgnoreCase(action)) {
result = applyConfig(data); result = applyConfig(data);
// TODO reconfigure Service
if (result) { if (result) {
Bundle event = new Bundle();
event.putString("name", action);
event.putBoolean("request", true);
EventBus.getDefault().post(event);
callbackContext.success(); callbackContext.success();
} else { } else {
callbackContext.error("- Configuration error!"); callbackContext.error("- Configuration error!");
...@@ -116,6 +156,9 @@ public class CDVBackgroundGeolocation extends CordovaPlugin { ...@@ -116,6 +156,9 @@ public class CDVBackgroundGeolocation extends CordovaPlugin {
} else if (ACTION_ON_STATIONARY.equalsIgnoreCase(action)) { } else if (ACTION_ON_STATIONARY.equalsIgnoreCase(action)) {
result = true; result = true;
this.stationaryCallback = callbackContext; this.stationaryCallback = callbackContext;
} else if (ACTION_ADD_MOTION_CHANGE_LISTENER.equalsIgnoreCase(action)) {
result = true;
this.addMotionChangeListener(callbackContext);
} else if (ACTION_GET_LOCATIONS.equalsIgnoreCase(action)) { } else if (ACTION_GET_LOCATIONS.equalsIgnoreCase(action)) {
result = true; result = true;
Bundle event = new Bundle(); Bundle event = new Bundle();
...@@ -144,25 +187,148 @@ public class CDVBackgroundGeolocation extends CordovaPlugin { ...@@ -144,25 +187,148 @@ public class CDVBackgroundGeolocation extends CordovaPlugin {
event.putBoolean("request", true); event.putBoolean("request", true);
resetOdometerCallback = callbackContext; resetOdometerCallback = callbackContext;
EventBus.getDefault().post(event); EventBus.getDefault().post(event);
} else if (ACTION_ADD_GEOFENCE.equalsIgnoreCase(action)) {
result = onAddGeofence(data.getJSONObject(0));
if (result) {
callbackContext.success();
} else {
callbackContext.error("Failed to add geofence");
}
} else if (ACTION_REMOVE_GEOFENCE.equalsIgnoreCase(action)) {
result = onRemoveGeofence(data.getString(0));
if (result) {
callbackContext.success();
} else {
callbackContext.error("Failed to add geofence");
}
} else if (ACTION_ON_GEOFENCE.equalsIgnoreCase(action)) {
result = true;
addGeofenceListener(callbackContext);
} else if (ACTION_GET_GEOFENCES.equalsIgnoreCase(action)) {
result = true;
getGeofencesCallback = callbackContext;
Bundle event = new Bundle();
event.putString("name", action);
event.putBoolean("request", true);
EventBus.getDefault().post(event);
} else if (ACTION_PLAY_SOUND.equalsIgnoreCase(action)) {
result = true;
playSound(data.getInt(0));
callbackContext.success();
} else if (BackgroundGeolocationService.ACTION_GET_CURRENT_POSITION.equalsIgnoreCase(action)) {
result = true;
if (!isEnabled) {
callbackContext.error(401); // aka HTTP UNAUTHORIZED
} else {
onGetCurrentPosition(callbackContext);
}
} }
return result; return result;
} }
private void onGetCurrentPosition(CallbackContext callbackContext) {
isAcquiringCurrentPosition = true;
isAcquiringCurrentPositionSince = System.nanoTime();
addCurrentPositionListener(callbackContext);
Bundle event = new Bundle();
event.putString("name", BackgroundGeolocationService.ACTION_GET_CURRENT_POSITION);
event.putBoolean("request", true);
EventBus.getDefault().post(event);
}
private Boolean onAddGeofence(JSONObject config) {
try {
Bundle event = new Bundle();
event.putString("name", ACTION_ADD_GEOFENCE);
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"));
if (config.has("notifyOnEntry")) {
event.putBoolean("notifyOnEntry", config.getBoolean("notifyOnEntry"));
}
if (config.has("notifyOnExit")) {
event.putBoolean("notifyOnExit", config.getBoolean("notifyOnExit"));
}
EventBus.getDefault().post(event);
return true;
} catch (JSONException e) {
Log.w(TAG, e);
return false;
}
}
private void addGeofenceListener(CallbackContext callbackContext) {
geofenceCallbacks.add(callbackContext);
Activity activity = this.cordova.getActivity();
Intent launchIntent = activity.getIntent();
if (launchIntent.hasExtra("forceReload") && launchIntent.hasExtra("geofencingEvent")) {
try {
JSONObject geofencingEvent = new JSONObject(launchIntent.getStringExtra("geofencingEvent"));
handleGeofencingEvent(geofencingEvent);
} catch (JSONException e) {
Log.w(TAG, e);
}
}
}
private void addCurrentPositionListener(CallbackContext callbackContext) {
currentPositionCallbacks.add(callbackContext);
}
private void addMotionChangeListener(CallbackContext callbackContext) {
motionChangeCallbacks.add(callbackContext);
Activity activity = this.cordova.getActivity();
Intent launchIntent = activity.getIntent();
if (launchIntent.hasExtra("forceReload")) {
if (launchIntent.getStringExtra("name").equalsIgnoreCase(ACTION_ON_MOTION_CHANGE)) {
Bundle event = launchIntent.getExtras();
this.onEventMainThread(event);
}
}
}
private Boolean onRemoveGeofence(String identifier) {
Bundle event = new Bundle();
event.putString("name", ACTION_REMOVE_GEOFENCE);
event.putBoolean("request", true);
event.putString("identifier", identifier);
EventBus.getDefault().post(event);
return true;
}
private void setEnabled(boolean value) { private void setEnabled(boolean value) {
// Don't set a state that we're already in.
if (value == isEnabled) {
return;
}
isEnabled = value; isEnabled = value;
Activity activity = this.cordova.getActivity(); Intent launchIntent = this.cordova.getActivity().getIntent();
if (launchIntent.hasExtra("forceReload") && launchIntent.hasExtra("location")) {
try {
JSONObject location = new JSONObject(launchIntent.getStringExtra("location"));
onLocationChange(location);
} catch (JSONException e) {
Log.w(TAG, e);
}
}
Activity activity = this.cordova.getActivity();
SharedPreferences settings = activity.getSharedPreferences("TSLocationManager", 0); SharedPreferences settings = activity.getSharedPreferences("TSLocationManager", 0);
SharedPreferences.Editor editor = settings.edit(); SharedPreferences.Editor editor = settings.edit();
editor.putBoolean("enabled", isEnabled); editor.putBoolean("enabled", isEnabled);
editor.commit(); editor.commit();
if (isEnabled) { if (isEnabled) {
EventBus.getDefault().register(this);
if (!BackgroundGeolocationService.isInstanceCreated()) { if (!BackgroundGeolocationService.isInstanceCreated()) {
activity.startService(backgroundServiceIntent); activity.startService(backgroundServiceIntent);
} }
} else { } else {
EventBus.getDefault().unregister(this);
activity.stopService(backgroundServiceIntent); activity.stopService(backgroundServiceIntent);
} }
} }
...@@ -178,7 +344,6 @@ public class CDVBackgroundGeolocation extends CordovaPlugin { ...@@ -178,7 +344,6 @@ public class CDVBackgroundGeolocation extends CordovaPlugin {
SharedPreferences.Editor editor = settings.edit(); SharedPreferences.Editor editor = settings.edit();
editor.putBoolean("activityIsActive", true); editor.putBoolean("activityIsActive", true);
editor.putBoolean("isMoving", isMoving);
if (config.has("distanceFilter")) { if (config.has("distanceFilter")) {
editor.putFloat("distanceFilter", config.getInt("distanceFilter")); editor.putFloat("distanceFilter", config.getInt("distanceFilter"));
...@@ -189,12 +354,18 @@ public class CDVBackgroundGeolocation extends CordovaPlugin { ...@@ -189,12 +354,18 @@ public class CDVBackgroundGeolocation extends CordovaPlugin {
if (config.has("locationUpdateInterval")) { if (config.has("locationUpdateInterval")) {
editor.putInt("locationUpdateInterval", config.getInt("locationUpdateInterval")); editor.putInt("locationUpdateInterval", config.getInt("locationUpdateInterval"));
} }
if (config.has("fastestLocationUpdateInterval")) {
editor.putInt("fastestLocationUpdateInterval", config.getInt("fastestLocationUpdateInterval"));
}
if (config.has("activityRecognitionInterval")) { if (config.has("activityRecognitionInterval")) {
editor.putLong("activityRecognitionInterval", config.getLong("activityRecognitionInterval")); editor.putLong("activityRecognitionInterval", config.getLong("activityRecognitionInterval"));
} }
if (config.has("minimumActivityRecognitionConfidence")) { if (config.has("minimumActivityRecognitionConfidence")) {
editor.putInt("minimumActivityRecognitionConfidence", config.getInt("minimumActivityRecognitionConfidence")); editor.putInt("minimumActivityRecognitionConfidence", config.getInt("minimumActivityRecognitionConfidence"));
} }
if (config.has("triggerActivities")) {
editor.putString("triggerActivities", config.getString("triggerActivities"));
}
if (config.has("stopTimeout")) { if (config.has("stopTimeout")) {
editor.putLong("stopTimeout", config.getLong("stopTimeout")); editor.putLong("stopTimeout", config.getLong("stopTimeout"));
} }
...@@ -205,13 +376,23 @@ public class CDVBackgroundGeolocation extends CordovaPlugin { ...@@ -205,13 +376,23 @@ public class CDVBackgroundGeolocation extends CordovaPlugin {
editor.putInt("stopAfterElapsedMinutes", config.getInt("stopAfterElapsedMinutes")); editor.putInt("stopAfterElapsedMinutes", config.getInt("stopAfterElapsedMinutes"));
} }
if (config.has("stopOnTerminate")) { if (config.has("stopOnTerminate")) {
editor.putBoolean("stopOnTerminate", config.getBoolean("stopOnTerminate")); stopOnTerminate = config.getBoolean("stopOnTerminate");
editor.putBoolean("stopOnTerminate", stopOnTerminate);
} }
if (config.has("startOnBoot")) { if (config.has("startOnBoot")) {
editor.putBoolean("startOnBoot", config.getBoolean("startOnBoot")); editor.putBoolean("startOnBoot", config.getBoolean("startOnBoot"));
} }
if (config.has("forceReload")) { if (config.has("forceReloadOnLocationChange")) {
editor.putBoolean("forceReload", config.getBoolean("forceReload")); editor.putBoolean("forceReloadOnLocationChange", config.getBoolean("forceReloadOnLocationChange"));
}
if (config.has("forceReload")) { // @deprecated, alias to #forceReloadOnLocationChange
editor.putBoolean("forceReloadOnLocationChange", config.getBoolean("forceReload"));
}
if (config.has("forceReloadOnMotionChange")) {
editor.putBoolean("forceReloadOnMotionChange", config.getBoolean("forceReloadOnMotionChange"));
}
if (config.has("forceReloadOnGeofence")) {
editor.putBoolean("forceReloadOnGeofence", config.getBoolean("forceReloadOnGeofence"));
} }
if (config.has("url")) { if (config.has("url")) {
editor.putString("url", config.getString("url")); editor.putString("url", config.getString("url"));
...@@ -269,19 +450,25 @@ public class CDVBackgroundGeolocation extends CordovaPlugin { ...@@ -269,19 +450,25 @@ public class CDVBackgroundGeolocation extends CordovaPlugin {
if (ACTION_GET_LOCATIONS.equalsIgnoreCase(name)) { if (ACTION_GET_LOCATIONS.equalsIgnoreCase(name)) {
try { try {
JSONArray json = new JSONArray(event.getString("data")); JSONObject params = new JSONObject();
PluginResult result = new PluginResult(PluginResult.Status.OK, json); params.put("locations", new JSONArray(event.getString("data")));
params.put("taskId", "android-bg-task-id");
PluginResult result = new PluginResult(PluginResult.Status.OK, params);
runInBackground(getLocationsCallback, result); runInBackground(getLocationsCallback, result);
} catch (JSONException e) { } catch (JSONException e) {
// TODO Auto-generated catch block // TODO Auto-generated catch block
e.printStackTrace(); e.printStackTrace();
PluginResult result = new PluginResult(PluginResult.Status.JSON_EXCEPTION, e.getMessage());
runInBackground(getLocationsCallback, result);
} }
} else if (ACTION_SYNC.equalsIgnoreCase(name)) { } else if (ACTION_SYNC.equalsIgnoreCase(name)) {
Boolean success = event.getBoolean("success"); Boolean success = event.getBoolean("success");
if (success) { if (success) {
try { try {
JSONArray json = new JSONArray(event.getString("data")); JSONObject params = new JSONObject();
PluginResult result = new PluginResult(PluginResult.Status.OK, json); params.put("locations", new JSONArray(event.getString("data")));
params.put("taskId", "android-bg-task-id");
PluginResult result = new PluginResult(PluginResult.Status.OK, params);
runInBackground(syncCallback, result); runInBackground(syncCallback, result);
} catch (JSONException e) { } catch (JSONException e) {
// TODO Auto-generated catch block // TODO Auto-generated catch block
...@@ -296,40 +483,120 @@ public class CDVBackgroundGeolocation extends CordovaPlugin { ...@@ -296,40 +483,120 @@ public class CDVBackgroundGeolocation extends CordovaPlugin {
runInBackground(getOdometerCallback, result); runInBackground(getOdometerCallback, result);
} else if (ACTION_RESET_ODOMETER.equalsIgnoreCase(name)) { } else if (ACTION_RESET_ODOMETER.equalsIgnoreCase(name)) {
PluginResult result = new PluginResult(PluginResult.Status.OK); PluginResult result = new PluginResult(PluginResult.Status.OK);
runInBackground(resetOdometerCallback, result); resetOdometerCallback.sendPluginResult(result);
} else if (BackgroundGeolocationService.ACTION_CHANGE_PACE.equalsIgnoreCase(name)) {
//PluginResult result = new PluginResult(PluginResult.Status.OK);
//paceChangeCallback.sendPluginResult(result);
int state = event.getBoolean("isMoving") ? 1 : 0;
paceChangeCallback.success(state);
} else if (ACTION_GET_GEOFENCES.equalsIgnoreCase(name)) {
try {
JSONArray json = new JSONArray(event.getString("data"));
PluginResult result = new PluginResult(PluginResult.Status.OK, json);
runInBackground(getGeofencesCallback, result);
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
PluginResult result = new PluginResult(PluginResult.Status.JSON_EXCEPTION, e.getMessage());
runInBackground(getGeofencesCallback, result);
}
} else if (ACTION_ON_MOTION_CHANGE.equalsIgnoreCase(name)) {
this.onMotionChange(event);
} else if (name.equalsIgnoreCase(BackgroundGeolocationService.ACTION_GOOGLE_PLAY_SERVICES_CONNECT_ERROR)) {
GooglePlayServicesUtil.getErrorDialog(event.getInt("errorCode"), this.cordova.getActivity(), 1001).show();
} }
} }
private void onMotionChange(Bundle event) {
PluginResult result;
isMoving = event.getBoolean("isMoving");
try {
JSONObject params = new JSONObject();
params.put("location", new JSONObject(event.getString("location")));
params.put("isMoving", isMoving);
params.put("taskId", "android-bg-task-id");
result = new PluginResult(PluginResult.Status.OK, params);
} catch (JSONException e) {
e.printStackTrace();
result = new PluginResult(PluginResult.Status.JSON_EXCEPTION, e.getMessage());
}
result.setKeepCallback(true);
for (CallbackContext callback : motionChangeCallbacks) {
runInBackground(callback, result);
}
}
/** /**
* EventBus listener for ARS * EventBus listener for ARS
* @param {ActivityRecognitionResult} result * @param {ActivityRecognitionResult} result
*/ */
public void onEventMainThread(ActivityRecognitionResult result) { public void onEventMainThread(ActivityRecognitionResult result) {
currentActivity = result.getMostProbableActivity(); currentActivity = result.getMostProbableActivity();
String activityName = BackgroundGeolocationService.getActivityName(currentActivity.getType());
int confidence = currentActivity.getConfidence(); if (isAcquiringCurrentPosition) {
long elapsedMillis = (System.nanoTime() - isAcquiringCurrentPositionSince) / 1000000;
if (elapsedMillis > GET_CURRENT_POSITION_TIMEOUT) {
isAcquiringCurrentPosition = false;
Log.i(TAG, "- getCurrentPosition timeout, giving up");
for (CallbackContext callback : currentPositionCallbacks) {
callback.error(408); // aka HTTP 408 Request Timeout
}
currentPositionCallbacks.clear();
}
}
} }
/** /**
* EventBus listener * EventBus listener
* @param {Location} location * @param {Location} location
*/ */
public void onEventMainThread(Location location) { public void onEventMainThread(Location location) {
PluginResult result; JSONObject locationData = BackgroundGeolocationService.locationToJson(location, currentActivity, isMoving);
result = new PluginResult(PluginResult.Status.OK, BackgroundGeolocationService.locationToJson(location, currentActivity)); this.onLocationChange(locationData);
}
private void onLocationChange(JSONObject location) {
PluginResult result = new PluginResult(PluginResult.Status.OK, location);
result.setKeepCallback(true);
result.setKeepCallback(true); result.setKeepCallback(true);
runInBackground(locationCallback, result);
if (location instanceof com.transistorsoft.locationmanager.BackgroundGeolocationService.StationaryLocation) { if (isAcquiringCurrentPosition) {
isMoving = false; // Current position has arrived: release the hounds.
if (stationaryCallback != null) { isAcquiringCurrentPosition = false;
runInBackground(stationaryCallback, result); for (CallbackContext callback : currentPositionCallbacks) {
result = new PluginResult(PluginResult.Status.OK, location);
result.setKeepCallback(false);
runInBackground(callback, result);
} }
} else { currentPositionCallbacks.clear();
isMoving = true;
result.setKeepCallback(true);
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 = BackgroundGeolocationService.geofencingEventToJson(geofenceEvent, geofence);
handleGeofencingEvent(params);
}
}
}
private void handleGeofencingEvent(JSONObject params) {
PluginResult result = new PluginResult(PluginResult.Status.OK, params);
result.setKeepCallback(true);
for (CallbackContext callback : geofenceCallbacks) {
runInBackground(callback, result);
}
}
private void playSound(int soundId) {
int duration = 1000;
toneGenerator = new ToneGenerator(AudioManager.STREAM_NOTIFICATION, 100);
toneGenerator.startTone(soundId, duration);
}
/** /**
* Run a javascript callback in Background * Run a javascript callback in Background
* @param cb * @param cb
...@@ -345,6 +612,27 @@ public class CDVBackgroundGeolocation extends CordovaPlugin { ...@@ -345,6 +612,27 @@ public class CDVBackgroundGeolocation extends CordovaPlugin {
} }
} }
private void onError(String error) {
String message = "BG Geolocation caught a Javascript exception while running in background-thread:\n".concat(error);
Log.e(TAG, message);
SharedPreferences settings = this.cordova.getActivity().getSharedPreferences("TSLocationManager", 0);
// Show alert popup with js error
if (settings.contains("debug") && settings.getBoolean("debug", false)) {
playSound(68);
AlertDialog.Builder builder = new AlertDialog.Builder(this.cordova.getActivity());
builder.setMessage(message)
.setCancelable(false)
.setNegativeButton("OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
//do things
}
});
AlertDialog alert = builder.create();
alert.show();
}
}
/** /**
* Override method in CordovaPlugin. * Override method in CordovaPlugin.
* Checks to see if it should turn off * Checks to see if it should turn off
...@@ -356,6 +644,8 @@ public class CDVBackgroundGeolocation extends CordovaPlugin { ...@@ -356,6 +644,8 @@ public class CDVBackgroundGeolocation extends CordovaPlugin {
Activity activity = this.cordova.getActivity(); Activity activity = this.cordova.getActivity();
EventBus.getDefault().unregister(this);
SharedPreferences settings = activity.getSharedPreferences("TSLocationManager", 0); SharedPreferences settings = activity.getSharedPreferences("TSLocationManager", 0);
SharedPreferences.Editor editor = settings.edit(); SharedPreferences.Editor editor = settings.edit();
editor.putBoolean("activityIsActive", false); editor.putBoolean("activityIsActive", false);
......
...@@ -10,22 +10,32 @@ ...@@ -10,22 +10,32 @@
@interface CDVBackgroundGeolocation : CDVPlugin @interface CDVBackgroundGeolocation : CDVPlugin
@property (nonatomic, strong) NSString* syncCallbackId; @property (nonatomic, strong) NSString* syncCallbackId;
@property (nonatomic, strong) NSString* geofenceCallbackId; @property (nonatomic) UIBackgroundTaskIdentifier syncTaskId;
@property (nonatomic, strong) NSString* locationCallbackId;
@property (nonatomic, strong) NSMutableArray* currentPositionListeners;
@property (nonatomic, strong) NSMutableArray* geofenceListeners;
@property (nonatomic, strong) NSMutableArray* stationaryRegionListeners; @property (nonatomic, strong) NSMutableArray* stationaryRegionListeners;
@property (nonatomic, strong) NSMutableArray* motionChangeListeners;
- (void) configure:(CDVInvokedUrlCommand*)command; - (void) configure:(CDVInvokedUrlCommand*)command;
- (void) start:(CDVInvokedUrlCommand*)command; - (void) start:(CDVInvokedUrlCommand*)command;
- (void) stop:(CDVInvokedUrlCommand*)command; - (void) stop:(CDVInvokedUrlCommand*)command;
- (void) finish:(CDVInvokedUrlCommand*)command; - (void) finish:(CDVInvokedUrlCommand*)command;
- (void) onPaceChange:(CDVInvokedUrlCommand*)command; - (void) error:(CDVInvokedUrlCommand*)command;
- (void) changePace:(CDVInvokedUrlCommand*)command;
- (void) setConfig:(CDVInvokedUrlCommand*)command; - (void) setConfig:(CDVInvokedUrlCommand*)command;
- (void) addStationaryRegionListener:(CDVInvokedUrlCommand*)command; - (void) addStationaryRegionListener:(CDVInvokedUrlCommand*)command;
- (void) addMotionChangeListener:(CDVInvokedUrlCommand*)command;
- (void) getStationaryLocation:(CDVInvokedUrlCommand *)command; - (void) getStationaryLocation:(CDVInvokedUrlCommand *)command;
- (void) getLocations:(CDVInvokedUrlCommand *)command; - (void) getLocations:(CDVInvokedUrlCommand *)command;
- (void) sync:(CDVInvokedUrlCommand *)command; - (void) sync:(CDVInvokedUrlCommand *)command;
- (void) getOdometer:(CDVInvokedUrlCommand *)command; - (void) getOdometer:(CDVInvokedUrlCommand *)command;
- (void) resetOdometer:(CDVInvokedUrlCommand *)command; - (void) resetOdometer:(CDVInvokedUrlCommand *)command;
- (void) addGeofence:(CDVInvokedUrlCommand *)command; - (void) addGeofence:(CDVInvokedUrlCommand *)command;
- (void) removeGeofence:(CDVInvokedUrlCommand *)command;
- (void) getGeofences:(CDVInvokedUrlCommand *)command;
- (void) onGeofence:(CDVInvokedUrlCommand *)command; - (void) onGeofence:(CDVInvokedUrlCommand *)command;
- (void) getCurrentPosition:(CDVInvokedUrlCommand *)command;
- (void) playSound:(CDVInvokedUrlCommand *)command;
@end @end
//// ////
// CDVBackgroundGeoLocation // CDVBackgroundGeolocation
// //
// Created by Chris Scott <chris@transistorsoft.com> on 2013-06-15 // Created by Chris Scott <chris@transistorsoft.com> on 2013-06-15
// //
#import "CDVBackgroundGeoLocation.h" #import "CDVBackgroundGeolocation.h"
@implementation CDVBackgroundGeolocation { @implementation CDVBackgroundGeolocation {
TSLocationManager *bgGeo; TSLocationManager *bgGeo;
NSDictionary *config; NSDictionary *config;
} }
@synthesize syncCallbackId, geofenceCallbackId, stationaryRegionListeners; @synthesize syncCallbackId, syncTaskId, locationCallbackId, geofenceListeners, stationaryRegionListeners, motionChangeListeners, currentPositionListeners;
- (void)pluginInitialize - (void)pluginInitialize
{ {
bgGeo = [[TSLocationManager alloc] init]; bgGeo = [[TSLocationManager alloc] init];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onLocationChanged:) name:@"TSLocationManager.location" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onLocationChanged:) name:@"TSLocationManager.location" object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onStationaryLocation:) name:@"TSLocationManager.stationary" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMotionChange:) name:@"TSLocationManager.motionchange" object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onEnterGeofence:) name:@"TSLocationManager.geofence" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onEnterGeofence:) name:@"TSLocationManager.geofence" object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onSyncComplete:) name:@"TSLocationManager.sync" object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onLocationManagerError:) name:@"TSLocationManager.error" object:nil];
} }
/** /**
...@@ -30,7 +32,7 @@ ...@@ -30,7 +32,7 @@
*/ */
- (void) configure:(CDVInvokedUrlCommand*)command - (void) configure:(CDVInvokedUrlCommand*)command
{ {
self.syncCallbackId = command.callbackId; self.locationCallbackId = command.callbackId;
config = [command.arguments objectAtIndex:0]; config = [command.arguments objectAtIndex:0];
...@@ -53,6 +55,9 @@ ...@@ -53,6 +55,9 @@
- (void) start:(CDVInvokedUrlCommand*)command - (void) start:(CDVInvokedUrlCommand*)command
{ {
[bgGeo start]; [bgGeo start];
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsBool: true];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
} }
/** /**
* Turn it off * Turn it off
...@@ -60,6 +65,8 @@ ...@@ -60,6 +65,8 @@
- (void) stop:(CDVInvokedUrlCommand*)command - (void) stop:(CDVInvokedUrlCommand*)command
{ {
[bgGeo stop]; [bgGeo stop];
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsBool: false];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
} }
- (void) getOdometer:(CDVInvokedUrlCommand*)command - (void) getOdometer:(CDVInvokedUrlCommand*)command
{ {
...@@ -77,55 +84,128 @@ ...@@ -77,55 +84,128 @@
* Change pace to moving/stopped * Change pace to moving/stopped
* @param {Boolean} isMoving * @param {Boolean} isMoving
*/ */
- (void) onPaceChange:(CDVInvokedUrlCommand *)command - (void) changePace:(CDVInvokedUrlCommand *)command
{ {
BOOL moving = [[command.arguments objectAtIndex: 0] boolValue]; BOOL moving = [[command.arguments objectAtIndex: 0] boolValue];
[bgGeo onPaceChange:moving]; [bgGeo changePace:moving];
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsBool: moving];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
} }
/** /**
* location handler from BackgroundGeolocation * location handler from BackgroundGeolocation
*/ */
- (void)onLocationChanged:(NSNotification*)notification { - (void)onLocationChanged:(NSNotification*)notification {
CLLocation *location = notification.object; CLLocation *location = [notification.userInfo objectForKey:@"location"];
NSDictionary *locationData = [bgGeo locationToDictionary:location];
CDVPluginResult* result = nil; NSDictionary *locationData = [bgGeo locationToDictionary:location];
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:locationData]; NSDictionary *params = @{
@"location": locationData,
@"taskId": @([bgGeo createBackgroundTask])
};
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:params];
[result setKeepCallbackAsBool:YES]; [result setKeepCallbackAsBool:YES];
[self.commandDelegate sendPluginResult:result callbackId:self.syncCallbackId];
[self.commandDelegate runInBackground:^{
[self.commandDelegate sendPluginResult:result callbackId:self.locationCallbackId];
}];
if ([self.currentPositionListeners count]) {
for (NSString *callbackId in self.currentPositionListeners) {
NSDictionary *params = @{
@"location": locationData,
@"taskId": @([bgGeo createBackgroundTask])
};
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:params];
[result setKeepCallbackAsBool:NO];
[self.commandDelegate runInBackground:^{
[self.commandDelegate sendPluginResult:result callbackId:callbackId];
}];
}
[self.currentPositionListeners removeAllObjects];
}
} }
- (void) onStationaryLocation:(NSNotification*)notification
{
CLLocation *location = notification.object;
NSDictionary *locationData = [bgGeo locationToDictionary:location];
if (![self.stationaryRegionListeners count]) { - (void) onMotionChange:(NSNotification*)notification
[bgGeo stopBackgroundTask]; {
if (![self.stationaryRegionListeners count] && ![self.motionChangeListeners count]) {
return; return;
} }
BOOL isMoving = [[notification.userInfo objectForKey:@"isMoving"] boolValue];
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:locationData]; CLLocation *location = [notification.userInfo objectForKey:@"location"];
[result setKeepCallbackAsBool:YES]; NSDictionary *locationData = [bgGeo locationToDictionary:location];
for (NSString *callbackId in self.stationaryRegionListeners)
{ // @deprecated stationaryRegionListeners in favour of dual-function motionChangeListeners
[self.commandDelegate sendPluginResult:result callbackId:callbackId]; if (!isMoving) {
for (NSString *callbackId in self.stationaryRegionListeners) {
NSLog(@"- CALLBACK: %@", callbackId);
NSDictionary *params = @{
@"isMoving": @(isMoving),
@"location": locationData,
@"taskId": @([bgGeo createBackgroundTask])
};
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:params];
[result setKeepCallbackAsBool:YES];
[self.commandDelegate runInBackground:^{
[self.commandDelegate sendPluginResult:result callbackId:callbackId];
}];
}
}
for (NSString *callbackId in self.motionChangeListeners) {
NSDictionary *params = @{
@"isMoving": @(isMoving),
@"location": locationData,
@"taskId": @([bgGeo createBackgroundTask])
};
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:params];
[result setKeepCallbackAsBool:YES];
[self.commandDelegate runInBackground:^{
[self.commandDelegate sendPluginResult:result callbackId:callbackId];
}];
} }
[bgGeo stopBackgroundTask];
} }
- (void) onEnterGeofence:(NSNotification*)notification - (void) onEnterGeofence:(NSNotification*)notification
{ {
if (self.geofenceCallbackId == nil) { if (![self.geofenceListeners count]) {
return; return;
} }
CLCircularRegion *region = notification.object; NSLog(@"- onEnterGeofence: %@", notification.userInfo);
CLCircularRegion *region = [notification.userInfo objectForKey:@"geofence"];
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:region.identifier]; for (NSString *callbackId in self.geofenceListeners) {
[result setKeepCallbackAsBool:YES]; NSDictionary *params = @{
[self.commandDelegate sendPluginResult:result callbackId:self.geofenceCallbackId]; @"identifier": region.identifier,
@"action": [notification.userInfo objectForKey:@"action"],
@"taskId": @([bgGeo createBackgroundTask])
};
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:params];
[result setKeepCallbackAsBool:YES];
[self.commandDelegate runInBackground:^{
[self.commandDelegate sendPluginResult:result callbackId:callbackId];
}];
}
} }
- (void) onSyncComplete:(NSNotification*)notification
{
if (syncCallbackId == nil) {
return;
}
NSDictionary *params = @{
@"locations": [notification.userInfo objectForKey:@"locations"],
@"taskId": @(syncTaskId)
};
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:params];
[self.commandDelegate sendPluginResult:result callbackId:syncCallbackId];
// Ready for another sync task.
syncCallbackId = nil;
syncTaskId = UIBackgroundTaskInvalid;
}
/** /**
* Fetches current stationaryLocation * Fetches current stationaryLocation
*/ */
...@@ -141,10 +221,12 @@ ...@@ -141,10 +221,12 @@
* Fetches current stationaryLocation * Fetches current stationaryLocation
*/ */
- (void) getLocations:(CDVInvokedUrlCommand *)command - (void) getLocations:(CDVInvokedUrlCommand *)command
{ {
NSArray* locations = [bgGeo getLocations]; NSDictionary *params = @{
@"locations": [bgGeo getLocations],
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:locations]; @"taskId": @([bgGeo createBackgroundTask])
};
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:params];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
} }
...@@ -153,13 +235,21 @@ ...@@ -153,13 +235,21 @@
*/ */
- (void) sync:(CDVInvokedUrlCommand *)command - (void) sync:(CDVInvokedUrlCommand *)command
{ {
NSArray* locations = [bgGeo sync]; if (syncCallbackId != nil) {
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"A sync action is already in progress."];
if (locations != nil) {
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:locations];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
} else { return;
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Failed to sync to server. Is there a network connection?"]; }
// Important to set these before we execute #sync since this fires a *very fast* async NSNotification event!
syncCallbackId = command.callbackId;
syncTaskId = [bgGeo createBackgroundTask];
NSArray* locations = [bgGeo sync];
if (locations == nil) {
syncCallbackId = nil;
syncTaskId = UIBackgroundTaskInvalid;
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Sync failed. Is there a network connection?"];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
} }
} }
...@@ -172,30 +262,127 @@ ...@@ -172,30 +262,127 @@
[self.stationaryRegionListeners addObject:command.callbackId]; [self.stationaryRegionListeners addObject:command.callbackId];
} }
- (void) addMotionChangeListener:(CDVInvokedUrlCommand*)command
{
if (self.motionChangeListeners == nil) {
self.motionChangeListeners = [[NSMutableArray alloc] init];
}
[self.motionChangeListeners addObject:command.callbackId];
}
- (void) addGeofence:(CDVInvokedUrlCommand*)command - (void) addGeofence:(CDVInvokedUrlCommand*)command
{ {
NSDictionary *cfg = [command.arguments objectAtIndex:0]; NSDictionary *cfg = [command.arguments objectAtIndex:0];
NSString *notifyOnExit = [cfg objectForKey:@"notifyOnExit"];
NSString *notifyOnEntry = [cfg objectForKey:@"notifyOnEntry"];
[bgGeo addGeofence:[cfg objectForKey:@"identifier"] [bgGeo addGeofence:[cfg objectForKey:@"identifier"]
radius:[[cfg objectForKey:@"radius"] doubleValue] radius:[[cfg objectForKey:@"radius"] doubleValue]
latitude:[[cfg objectForKey:@"latitude"] doubleValue] latitude:[[cfg objectForKey:@"latitude"] doubleValue]
longitude:[[cfg objectForKey:@"longitude"] doubleValue] longitude:[[cfg objectForKey:@"longitude"] doubleValue]
notifyOnEntry: (notifyOnEntry) ? [notifyOnEntry boolValue] : NO
notifyOnExit: (notifyOnExit) ? [notifyOnExit boolValue] : NO
]; ];
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
} }
- (void) removeGeofence:(CDVInvokedUrlCommand*)command
{
NSString *identifier = [command.arguments objectAtIndex:0];
CDVPluginResult *result;
if ([bgGeo removeGeofence:identifier]) {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
} else {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Failed to locate geofence"];
}
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
}
- (void) getGeofences:(CDVInvokedUrlCommand*)command
{
NSMutableArray *rs = [[NSMutableArray alloc] init];
for (CLRegion *geofence in [bgGeo getGeofences]) {
[rs addObject:@{
@"identifier":geofence.identifier,
@"radius": @(geofence.radius),
@"latitude": @(geofence.center.latitude),
@"longitude": @(geofence.center.longitude)
}];
}
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:rs];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
}
- (void) onGeofence:(CDVInvokedUrlCommand*)command - (void) onGeofence:(CDVInvokedUrlCommand*)command
{ {
self.geofenceCallbackId = command.callbackId; if (self.geofenceListeners == nil) {
self.geofenceListeners = [[NSMutableArray alloc] init];
}
[self.geofenceListeners addObject:command.callbackId];
}
- (void) getCurrentPosition:(CDVInvokedUrlCommand*)command
{
if (![bgGeo isEnabled]) {
NSLog(@"- CDVBackgroundGeolocation#getCurrentPosition cannot be used when plugin is disabled");
// If plugin isn't enabled, return 401 Unauthorized
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:401];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
return;
}
if (self.currentPositionListeners == nil) {
self.currentPositionListeners = [[NSMutableArray alloc] init];
}
[self.currentPositionListeners addObject:command.callbackId];
[bgGeo updateCurrentPosition];
}
- (void) playSound:(CDVInvokedUrlCommand*)command
{
int soundId = [[command.arguments objectAtIndex:0] integerValue];
[bgGeo playSound: soundId];
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
} }
/** /**
* Called by js to signify the end of a background-geolocation event * Called by js to signify the end of a background-geolocation event
*/ */
-(void) finish:(CDVInvokedUrlCommand*)command -(void) finish:(CDVInvokedUrlCommand*)command
{ {
[bgGeo finish]; UIBackgroundTaskIdentifier taskId = [[command.arguments objectAtIndex: 0] integerValue];
[bgGeo stopBackgroundTask:taskId];
} }
/**
* Called by js to signal a caught exception from application code.
*/
-(void) error:(CDVInvokedUrlCommand*)command
{
UIBackgroundTaskIdentifier taskId = [[command.arguments objectAtIndex: 0] integerValue];
NSString *error = [command.arguments objectAtIndex:1];
[bgGeo error:taskId message:error];
}
- (void) onLocationManagerError:(NSNotification*)notification
{
NSLog(@" - onLocationManagerError: %@", notification.userInfo);
NSString *errorType = [notification.userInfo objectForKey:@"type"];
if ([errorType isEqualToString:@"location"]) {
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:[[notification.userInfo objectForKey:@"code"] intValue]];
if ([self.currentPositionListeners count]) {
[result setKeepCallbackAsBool:NO];
for (NSString *callbackId in self.currentPositionListeners) {
[self.commandDelegate sendPluginResult:result callbackId:callbackId];
}
[self.currentPositionListeners removeAllObjects];
}
[self.commandDelegate sendPluginResult:result callbackId:locationCallbackId];
}
}
/** /**
* If you don't stopMonitoring when application terminates, the app will be awoken still when a * If you don't stopMonitoring when application terminates, the app will be awoken still when a
* new location arrives, essentially monitoring the user's location even when they've killed the app. * new location arrives, essentially monitoring the user's location even when they've killed the app.
......
...@@ -5,15 +5,17 @@ ...@@ -5,15 +5,17 @@
@interface TSLocationManager : NSObject <CLLocationManagerDelegate> @interface TSLocationManager : NSObject <CLLocationManagerDelegate>
@property (nonatomic) CLLocationDistance odometer; @property (nonatomic) CLLocationDistance odometer;
@property (nonatomic, strong) CLLocationManager* locationManager;
- (void) configure:(NSDictionary*)config; - (void) configure:(NSDictionary*)config;
- (void) start; - (void) start;
- (void) stop; - (void) stop;
- (void) finish;
- (NSArray*) sync; - (NSArray*) sync;
- (NSArray*) getLocations; - (NSArray*) getLocations;
- (void) stopBackgroundTask; - (UIBackgroundTaskIdentifier) createBackgroundTask;
- (void) onPaceChange:(BOOL)value; - (void) stopBackgroundTask:(UIBackgroundTaskIdentifier)taskId;
- (void) error:(UIBackgroundTaskIdentifier)taskId message:(NSString*)message;
- (void) changePace:(BOOL)value;
- (void) setConfig:(NSDictionary*)command; - (void) setConfig:(NSDictionary*)command;
- (NSDictionary*) getStationaryLocation; - (NSDictionary*) getStationaryLocation;
- (void) onSuspend:(NSNotification *)notification; - (void) onSuspend:(NSNotification *)notification;
...@@ -21,7 +23,10 @@ ...@@ -21,7 +23,10 @@
- (void) onAppTerminate; - (void) onAppTerminate;
- (BOOL) isEnabled; - (BOOL) isEnabled;
- (NSDictionary*) locationToDictionary:(CLLocation*)location; - (NSDictionary*) locationToDictionary:(CLLocation*)location;
- (void) addGeofence:(NSString*)identifier radius:(CLLocationDistance)radius latitude:(CLLocationDegrees)latitude longitude:(CLLocationDegrees)longitude; - (void) addGeofence:(NSString*)identifier radius:(CLLocationDistance)radius latitude:(CLLocationDegrees)latitude longitude:(CLLocationDegrees)longitude notifyOnEntry:(BOOL)notifyOnEntry notifyOnExit:(BOOL)notifyOnExit;
- (BOOL) removeGeofence:(NSString*)identifier;
- (NSArray*) getGeofences;
- (void) updateCurrentPosition;
- (void) playSound:(SystemSoundID)soundId;
@end @end
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
<dict> <dict>
<key>Headers/TSLocationManager.h</key> <key>Headers/TSLocationManager.h</key>
<data> <data>
93eNJDqyN5vuBJn9oYeT7lb2x1w= dTpEJTYyFANcuJNr9upQ8PrgD3Q=
</data> </data>
<key>Info.plist</key> <key>Info.plist</key>
<data> <data>
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
<dict> <dict>
<key>Headers/TSLocationManager.h</key> <key>Headers/TSLocationManager.h</key>
<data> <data>
93eNJDqyN5vuBJn9oYeT7lb2x1w= dTpEJTYyFANcuJNr9upQ8PrgD3Q=
</data> </data>
<key>Modules/module.modulemap</key> <key>Modules/module.modulemap</key>
<data> <data>
......
...@@ -9,24 +9,34 @@ ...@@ -9,24 +9,34 @@
var exec = require("cordova/exec"); var exec = require("cordova/exec");
module.exports = { module.exports = {
/** /**
* @property {Object} stationaryRegion * @property {Object} stationaryLocation
*/ */
stationaryRegion: null, stationaryLocation: null,
/** /**
* @property {Object} config * @property {Object} config
*/ */
config: {}, config: {},
/**
* @private {Error} error
*/
configure: function(success, failure, config) { configure: function(success, failure, config) {
var me = this;
config = config || {}; config = config || {};
this.config = config; this.config = config;
success = success || function(location) {}; success = success || function(location, taskId) {
var mySuccess = function(location) { me.finish(taskId);
};
var mySuccess = function(params) {
var location = params.location || params;
var taskId = params.taskId || 'task-id-undefined';
// Transform timestamp to Date instance. // Transform timestamp to Date instance.
if (location.timestamp) { if (location.timestamp) {
location.timestamp = new Date(location.timestamp); location.timestamp = new Date(location.timestamp);
} }
success.call(this, location); me._runBackgroundTask(taskId, function() {
success.call(this, location, taskId);
});
} }
exec(mySuccess, exec(mySuccess,
failure || function() {}, failure || function() {},
...@@ -49,18 +59,31 @@ module.exports = { ...@@ -49,18 +59,31 @@ module.exports = {
'stop', '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() {}, exec(success || function() {},
failure || function() {}, failure || function() {},
'BackgroundGeoLocation', 'BackgroundGeoLocation',
'finish', 'finish',
[]); [taskId]);
},
error: function(taskId, message) {
if (!taskId) {
throw "BackgroundGeolocation#error must now be provided with a taskId as 1st param, eg: bgGeo.finish(taskId). taskId is provided by 2nd param in callback";
}
exec(function() {},
function() {},
'BackgroundGeoLocation',
'error',
[taskId, message]);
}, },
changePace: function(isMoving, success, failure) { changePace: function(isMoving, success, failure) {
exec(success || function() {}, exec(success || function() {},
failure || function() {}, failure || function() {},
'BackgroundGeoLocation', 'BackgroundGeoLocation',
'onPaceChange', 'changePace',
[isMoving]); [isMoving]);
}, },
/** /**
...@@ -70,7 +93,7 @@ module.exports = { ...@@ -70,7 +93,7 @@ module.exports = {
* @param {Integer} timeout * @param {Integer} timeout
*/ */
setConfig: function(success, failure, config) { setConfig: function(success, failure, config) {
this.apply(this.config, config); this._apply(this.config, config);
exec(success || function() {}, exec(success || function() {},
failure || function() {}, failure || function() {},
'BackgroundGeoLocation', 'BackgroundGeoLocation',
...@@ -89,15 +112,24 @@ module.exports = { ...@@ -89,15 +112,24 @@ module.exports = {
}, },
/** /**
* Add a stationary-region listener. Whenever the devices enters "stationary-mode", your #success callback will be executed with #location param containing #radius of region * Add a stationary-region listener. Whenever the devices enters "stationary-mode", your #success callback will be executed with #location param containing #radius of region
* @deprecated in favour of dual-function #onMotionChange
* @param {Function} success * @param {Function} success
* @param {Function} failure [optional] NOT IMPLEMENTED * @param {Function} failure [optional] NOT IMPLEMENTED
*/ */
onStationary: function(success, failure) { onStationary: function(success, failure) {
var me = this; var me = this;
success = success || function() {}; success = success || function(location, taskId) {
var callback = function(region) { me.finish(taskId);
me.stationaryRegion = region; };
success.apply(me, arguments); var callback = function(params) {
var location = params.location || params,
taskId = params.taskId || 'task-id-undefined';
me.stationaryLocation = location;
me._runBackgroundTask(taskId, function() {
success.call(me, location, taskId);
}, failure);
}; };
exec(callback, exec(callback,
failure || function() {}, failure || function() {},
...@@ -105,13 +137,46 @@ module.exports = { ...@@ -105,13 +137,46 @@ module.exports = {
'addStationaryRegionListener', 'addStationaryRegionListener',
[]); []);
}, },
/**
* Add a movement-state-change listener. Whenever the devices enters "stationary" or "moving" mode, your #success callback will be executed with #location param containing #radius of region
* @param {Function} success
* @param {Function} failure [optional] NOT IMPLEMENTED
*/
onMotionChange: function(success, failure) {
var me = this;
success = success || function(isMoving, location, taskId) {
me.finish(taskId);
};
var callback = function(params) {
var isMoving = params.isMoving;
var location = params.location;
var taskId = params.taskId || 'task-id-undefined';
if (!isMoving) {
me.stationaryLocation = location;
}
me._runBackgroundTask(taskId, function() {
success.call(me, isMoving, location, taskId);
}, failure);
};
exec(callback,
failure || function() {},
'BackgroundGeoLocation',
'addMotionChangeListener',
[]);
},
getLocations: function(success, failure) { getLocations: function(success, failure) {
if (typeof(success) !== 'function') { if (typeof(success) !== 'function') {
throw "BackgroundGeolocation#getLocations requires a success callback"; throw "BackgroundGeolocation#getLocations requires a success callback";
} }
var me = this; var me = this;
var mySuccess = function(locations) { var mySuccess = function(params) {
success.call(this, me._setTimestamp(locations)); var taskId = params.taskId;
var locations = me._setTimestamp(params.locations);
me._runBackgroundTask(taskId, function() {
success.call(me, locations, taskId);
});
} }
exec(mySuccess, exec(mySuccess,
failure || function() {}, failure || function() {},
...@@ -127,8 +192,13 @@ module.exports = { ...@@ -127,8 +192,13 @@ module.exports = {
throw "BackgroundGeolocation#sync requires a success callback"; throw "BackgroundGeolocation#sync requires a success callback";
} }
var me = this; var me = this;
var mySuccess = function(locations) { var mySuccess = function(params) {
success.call(this, me._setTimestamp(locations)); var locations = me._setTimestamp(params.locations);
var taskId = params.taskId;
me._runBackgroundTask(taskId, function() {
success.call(me, locations, taskId);
});
} }
exec(mySuccess, exec(mySuccess,
failure || function() {}, failure || function() {},
...@@ -170,22 +240,98 @@ module.exports = { ...@@ -170,22 +240,98 @@ module.exports = {
if (!config.radius) { if (!config.radius) {
throw "#addGeofence requires a #radius"; throw "#addGeofence requires a #radius";
} }
if ( (typeof(config.notifyOnEnter) === 'undefined') && (typeof(config.notifyOnExit) === 'undefined') ) {
throw "#addGeofence requires at least notifyOnEnter {Boolean} and/or #notifyOnExit {Boolean}";
}
exec(success || function() {}, exec(success || function() {},
failure || function() {}, failure || function() {},
'BackgroundGeoLocation', 'BackgroundGeoLocation',
'addGeofence', 'addGeofence',
[config]); [config]);
}, },
/**
* remove a geofence
* @param {String} identifier
*/
removeGeofence: function(identifier, success, failure) {
if (!identifier) {
throw "#removeGeofence requires an 'identifier'";
}
exec(success || function() {},
failure || function() {},
'BackgroundGeoLocation',
'removeGeofence',
[identifier]);
},
onGeofence: function(success, failure) { onGeofence: function(success, failure) {
if (!typeof(success) === 'function') { if (!typeof(success) === 'function') {
throw "#onGeofence requires a success callback"; throw "#onGeofence requires a success callback";
} }
exec(success, var me = this;
var mySuccess = function(params) {
var taskId = params.taskId || 'task-id-undefined';
delete(params.taskId);
me._runBackgroundTask(taskId, function() {
success.call(me, params, taskId);
}, failure);
};
exec(mySuccess,
failure || function() {}, failure || function() {},
'BackgroundGeoLocation', 'BackgroundGeoLocation',
'onGeofence', 'onGeofence',
[]); []);
}, },
/**
* Fetch a list of all monitored geofences
*/
getGeofences: function(success, failure) {
exec(success || function() {},
failure || function() {},
'BackgroundGeoLocation',
'getGeofences',
[]);
},
/**
* Fetch the current position
*/
getCurrentPosition: function(success, failure) {
var me = this;
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);
}
me._runBackgroundTask(taskId, function() {
success.call(this, location, taskId);
});
}
exec(mySuccess || function() {},
failure || function() {},
'BackgroundGeoLocation',
'getCurrentPosition',
[]);
},
/**
* Play a system sound. This is totally experimental.
* iOS http://iphonedevwiki.net/index.php/AudioServices
* Android:
*/
playSound: function(soundId) {
var success = function() {};
var failure = function() {};
exec(success,
failure,
'BackgroundGeoLocation',
'playSound',
[soundId]);
},
_setTimestamp: function(rs) { _setTimestamp: function(rs) {
// Transform timestamp to Date instance. // Transform timestamp to Date instance.
if (typeof(rs) === 'object') { if (typeof(rs) === 'object') {
...@@ -195,7 +341,24 @@ module.exports = { ...@@ -195,7 +341,24 @@ module.exports = {
} }
return rs; return rs;
}, },
apply: function(destination, source) { _runBackgroundTask: function(taskId, callback) {
var me = this;
try {
callback.call(this);
} catch(e) {
console.log("*************************************************************************************");
console.error("BackgroundGeolocation caught a Javascript Exception in your application code");
console.log(" while running in a background thread. Auto-finishing background-task:", taskId);
console.log(" to prevent application crash");
console.log("*************************************************************************************");
console.log("STACK:\n", e.stack);
console.error(e);
// And finally, here's our raison d'etre: catching the error in order to ensure background-task is completed.
this.error(taskId, e.message);
}
},
_apply: function(destination, source) {
source = source || {}; source = source || {};
for (var property in source) { for (var property in source) {
if (source.hasOwnProperty(property)) { if (source.hasOwnProperty(property)) {
......
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