Commit 2a6ef1c0 authored by Chris Scott's avatar Chris Scott

Merge pull request #48 from christocracy/edge

Merging edge to master finally
parents 6bef7fed b8b92c56
.DS_Store
BackgroundGeoLocation
Background Geolocation
==============================
Cross-platform background geolocation for Cordova with battery-saving "circular region monitoring" and "stop detection".
......@@ -22,7 +22,14 @@ The plugin creates the object `window.plugins.backgroundGeoLocation` with the me
`onStationary(callback, fail)`
`getLocations(callback, fail)`
`sync(callback, fail)`
`getOdometer(callback, fail)`
`resetOdometer(callback, fail)`
## Installing the plugin ##
```
......@@ -30,23 +37,18 @@ The plugin creates the object `window.plugins.backgroundGeoLocation` with the me
cordova plugin add https://github.com/christocracy/cordova-background-geolocation.git
```
## Help
[See the Wiki](https://github.com/christocracy/cordova-background-geolocation/wiki)
## Example
```
//
//
// after deviceready
//
//
// Your app must execute AT LEAST ONE call for the current position via standard Cordova geolocation,
// in order to prompt the user for Location permission.
window.navigator.geolocation.getCurrentPosition(function(location) {
console.log('Location from Phonegap');
});
var bgGeo = window.plugins.backgroundGeoLocation;
////
// As with all Cordova plugins, you must configure within an #deviceready callback.
//
function onDeviceReady() {
/**
* This would be your own callback for Ajax-requests after POSTing background geolocation to your server.
*/
......@@ -77,14 +79,33 @@ The plugin creates the object `window.plugins.backgroundGeoLocation` with the me
// BackgroundGeoLocation is highly configurable.
bgGeo.configure(callbackFn, failureFn, {
desiredAccuracy: 0, // <-- 0: highest power, highest accuracy; 1000: lowest power, lowest accuracy.
debug: true, // <-- enable this hear sounds for background-geolocation life-cycle.
desiredAccuracy: 0,
stationaryRadius: 50,
distanceFilter: 50, // <-- minimum distance between location events
activityType: 'AutomotiveNavigation', // <-- [ios]
locationUpdateInterval: 30000, // <-- [android] minimum time between location updates, used in conjunction with #distanceFilter
activityRecognitionInterval: 10000, // <-- [android] sampling-rate activity-recognition system for movement/stationary detection
debug: true, // <-- enable this hear sounds, see notifications during life-cycle events.
stopOnTerminate: false // <-- enable this to clear background location settings when the app terminates
distanceFilter: 50,
disableElasticity: false, // <-- [iOS] Default is 'false'. Set true to disable speed-based distanceFilter elasticity
locationUpdateInterval: 5000,
minimumActivityRecognitionConfidence: 80, // 0-100%. Minimum activity-confidence for a state-change
fastestLocationUpdateInterval: 5000,
activityRecognitionInterval: 10000,
stopTimeout: 0,
forceReload: true, // <-- [Android] If the user closes the app **while location-tracking is started** , reboot app (WARNING: possibly distruptive to user)
stopOnTerminate: false, // <-- [Android] Allow the background-service to run headless when user closes the app.
startOnBoot: true, // <-- [Android] Auto start background-service in headless mode when device is powered-up.
activityType: 'AutomotiveNavigation',
/**
* HTTP Feature: set an url to allow the native background service to POST locations to your server
*/
url: 'http://posttestserver.com/post.php?dir=cordova-background-geolocation',
batchSync: false, // <-- [Default: false] Set true to sync locations to server in 1 HTTP request.
autoSync: true, // <-- [Default: true] Set true to sync each location to server as it arrives.
maxDaysToPersist: 1, // <-- Maximum days to persist a location in plugin's SQLite database when HTTP fails
headers: {
"X-FOO": "bar"
},
params: {
"auth_token": "maybe_your_server_authenticates_via_token_YES?"
}
});
// Turn ON the background-geolocation system. The user will be tracked whenever they suspend the app.
......@@ -92,23 +113,24 @@ The plugin creates the object `window.plugins.backgroundGeoLocation` with the me
// If you wish to turn OFF background-tracking, call the #stop method.
// bgGeo.stop()
}
```
NOTE: The plugin includes `org.apache.cordova.geolocation` as a dependency. You must enable Cordova's GeoLocation in the foreground and have the user accept Location services by executing `#watchPosition` or `#getCurrentPosition`.
## Example Application
![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 this plugin. **NOTE** In order to use the SampleApp, it's important to make a copy of it outside of the plugin itself.
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
$ mkdir tmp
$ cp -R cordova-background-geolocation/example/SampleApp tmp
$ cd tmp/SampleApp
$ cordova plugin add cordova-plugin-whitelist
$ cordova plugin add cordova-plugin-geolocation
$ cordova plugin add git@github.com:christocracy/cordova-background-geolocation.git
$ cordova platform add ios
$ cordova platform add android
......@@ -119,12 +141,33 @@ $ cordova build android
If you're using XCode, boot the SampleApp in the iOS Simulator and enable ```Debug->Location->City Drive```.
## Help! It doesn't work!
Yes it does. [See the Wiki](https://github.com/christocracy/cordova-background-geolocation/wiki)
- on iOS, background tracking won't be engaged until you travel about **2-3 city blocks**, so go for a walk or car-ride (or use the Simulator with ```Debug->Location->City Drive```)
- Android is much quicker detecting movements; typically several meters of walking will do it.
- When in doubt, **nuke everything**: First delete the app from your device (or simulator)
```
$ cordova plugin remove com.transistorsoft.cordova.background-geolocation
$ cordova plugin add git@github.com:christocracy/cordova-background-geolocation.git
$ cordova platform remove ios
$ cordova platform add ios
$ cordova build ios
```
## Behaviour
The plugin has features allowing you to control the behaviour of background-tracking, striking a balance between accuracy and battery-usage. In stationary-mode, the plugin attempts to descrease its power usage and accuracy by setting up a circular stationary-region of configurable #stationaryRadius. iOS has a nice system [Significant Changes API](https://developer.apple.com/library/ios/documentation/CoreLocation/Reference/CLLocationManager_Class/CLLocationManager/CLLocationManager.html#//apple_ref/occ/instm/CLLocationManager/startMonitoringSignificantLocationChanges), which allows the os to suspend your app until a cell-tower change is detected (typically 2-3 city-block change) Android uses the Google Play Services APIs [FusedLocationProvider API](https://developer.android.com/reference/com/google/android/gms/location/FusedLocationProviderApi.html) as well as the [ActivityRecognition API](https://developer.android.com/reference/com/google/android/gms/location/ActivityRecognitionApi.html) (for movement/stationary detection). Windows Phone does not have such a API.
The plugin has features allowing you to control the behaviour of background-tracking, striking a balance between accuracy and battery-usage. In stationary-mode, the plugin attempts to descrease its power usage and accuracy by setting up a circular stationary-region of configurable #stationaryRadius.
The plugin will execute your configured ```callback``` provided to the ```#configure(callback, config)``` method. You must manually POST the received ```GeoLocation``` to your server using standard XHR, as well as manually cache a recieved location into ```localStorage``` if no network connection is available.
iOS has a nice system [Significant Changes API](https://developer.apple.com/library/ios/documentation/CoreLocation/Reference/CLLocationManager_Class/CLLocationManager/CLLocationManager.html#//apple_ref/occ/instm/CLLocationManager/startMonitoringSignificantLocationChanges), which allows the os to suspend your app until a cell-tower change is detected (typically 2-3 city-block change)
Android uses the Google Play Services APIs [FusedLocationProvider API](https://developer.android.com/reference/com/google/android/gms/location/FusedLocationProviderApi.html) as well as the [ActivityRecognition API](https://developer.android.com/reference/com/google/android/gms/location/ActivityRecognitionApi.html) (for movement/stationary detection). Windows Phone does not have such a API.
The plugin will execute your configured ```callback``` provided to the ```#configure(callback, config)``` method. Both iOS & Android use a SQLite database to persist **every** recorded geolocation so you don't have to worry about persistence when no network is detected. The plugin provides a Javascript API to fetch and destroy the records in the database. In addition, the plugin has an optional HTTP layer allowing allowing you to automatically HTTP POST recorded geolocations to your server.
The function ```changePace(isMoving, success, failure)``` is provided to force the plugin to enter "moving" or "stationary" state.
......@@ -195,10 +238,59 @@ bgGeo.onStationary(function(location) {
});
```
#####`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:
```
bgGeo.getLocations(function(locations) {
console.log("locations: ", locations);
});
```
#####`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:
```
bgGeo.sync(function(locations) {
// Here are all the locations from the database. The database is now EMPTY.
console.log('synced locations: ', locations);
}, function(errorMessage) {
console.warn('Sync FAILURE: ', errorMessage);
});
```
#####`getOdometer(callbackFn, failureFn)`
The plugin constantly tracks distance travelled. To fetch the current **odometer** reading:
```
bgGeo.getOdometer(function(distance) {
console.log("Distance travelled: ", distance);
});
```
#####`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.
## Config
Use the following config-parameters with the #configure method:
#####`@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.
- Exit stationary region: **[ios]** Calendar event notification sound
- GeoLocation recorded: **[ios]** SMS-sent sound, **[android]** "blip", *[WP8]* High beep, 1 sec.
- Aggressive geolocation engaged: **[ios]** SIRI listening sound, **[android]** "Doodly-doo"
- Acquiring stationary location sound: **[ios]** "tick,tick,tick" sound, *[android]* none
- Stationary location acquired sound: **[ios]** "bloom" sound, **[android]** long "beeeeeep"
![Enable Background Audio](/enable-background-audio.png "Enable Background Audio")
#####`@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
......@@ -210,19 +302,6 @@ Specify the desired-accuracy of the geolocation system with 1 of 4 values, ```0,
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 {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.
- Exit stationary region: *[ios]* Calendar event notification sound *[android]* dialtone beep-beep-beep
- GeoLocation recorded: *[ios]* SMS sent sound, *[android]* tt short beep, *[WP8]* High beep, 1 sec.
- Aggressive geolocation engaged: *[ios]* SIRI listening sound, *[android]* none
- Passive geolocation engaged: *[ios]* SIRI stop listening sound, *[android]* none
- Acquiring stationary location sound: *[ios]* "tick,tick,tick" sound, *[android]* none
- Stationary location acquired sound: *[ios]* "bloom" sound, *[android]* long tt beep.
![Enable Background Audio](/enable-background-audio.png "Enable Background Audio")
#####`@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.
......@@ -260,38 +339,50 @@ Compare now background-geolocation in the scope of a city. In this image, the l
#####`@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)
### Android Config
#####`@param {Boolean} stopAfterElapsedMinutes`
#####`@param {Integer millis} locationUpdateInterval`
The plugin can optionally auto-stop monitoring location when some number of minutes elapse after being the #start method was called.
Set the desired interval for active location updates, in milliseconds.
#### In-Plugin SQLite Storage
The location client will actively try to obtain location updates for your application at this interval, so it has a direct influence on the amount of power used by your application. Choose your interval wisely.
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.
This interval is inexact. You may not receive updates at all (if no location sources are available), or you may receive them slower than requested. You may also receive them faster than requested (if other applications are requesting location at a faster interval).
If you **don't** configure the optional HTTP feature, the only way to delete the SQLite database is by executing the ```#sync``` method.
Applications with only the coarse location permission may have their interval silently throttled.
```
bgGeo.sync(function(locations) {
// The SQLite database is now EMPTY.
console.log('locations: ', locations);
});
```
An interval of 0 is allowed, but not recommended, since location updates may be extremely fast on future implementations.
#### HTTP Features
#####`@param {Integer millis} activityRecognitionInterval`
#####`@param {String} url`
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.
Your server url where you wish to HTTP POST location data to.
#####`@param {Integer minutes} stopTimeout`
#####`@param {String} batchSync [false]`
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.
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 {Boolean} forceReload`
#####`@param {String} autoSync [true]`
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).
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 {Boolean} startOnBoot`
#####`@param {Object} params`
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.
Optional HTTP params sent along in HTTP request to above ```#url```.
#####`@param {Object} headers`
#### HTTP Feature
The Android plugin can run as a "headless" background service, sending the user's location to your server even after the user close the application (by configuring ```stopOnTerminate: false```). The plugin's HTTP request will arrive at your server as follows:
Optional HTTP params sent along in HTTP request to above ```#url```.
#####`@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.
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.
```
bgGeo.configure(callbackFn, failureFn, {
......@@ -299,6 +390,9 @@ bgGeo.configure(callbackFn, failureFn, {
.
.
url: 'http://posttestserver.com/post.php?dir=cordova-background-geolocation',
autoSync: true,
batchSync: false,
maxDaysToPersist: 1,
headers: {
"X-FOO": "bar"
},
......@@ -330,24 +424,75 @@ REQUEST_TIME = 1429198883
No Post Params.
== Begin post body ==
{"auth_token":"maybe_your_server_authenticates_via_token_YES?","location":{"latitude":45.5192875,"longitude":-73.6169281,"accuracy":25.42799949645996,"speed":0,"bearing":0,"altitude":0,"timestamp":1429198882716},"android_id":"39dbac67e2c9d80"}
{
"location":{
"timestamp":"2015-05-05T04:31:54Z", // <-- ISO-8601, UTC
"coords":{
"latitude":45.519282,
"longitude":-73.6169562,
"accuracy":12.850000381469727,
"speed":0,
"heading":0,
"altitude":0
},
"activity":{ // <-- Android-only currently
"type":"still",
"confidence":48
}
},
"android_id":"39dbac67e2c9d80"
}
== End post body ==
Upload contains PUT data:
{"auth_token":"maybe_your_server_authenticates_via_token_YES?","location":{"latitude":45.5192875,"longitude":-73.6169281,"accuracy":25.42799949645996,"speed":0,"bearing":0,"altitude":0,"timestamp":1429198882716},"android_id":"39dbac67e2c9d80"}
```
#####`@param {String} url`
### Android Config
By configuring an ```#url```, the plugin will always attempt to HTTP POST the location to your server.
#####`@param {Integer millis} locationUpdateInterval`
#####`@param {Object} params`
Set the desired interval for active location updates, in milliseconds.
Optional HTTP params sent along in HTTP request to above ```#url```.
The location client will actively try to obtain location updates for your application at this interval, so it has a direct influence on the amount of power used by your application. Choose your interval wisely.
This interval is inexact. You may not receive updates at all (if no location sources are available), or you may receive them slower than requested. You may also receive them faster than requested (if other applications are requesting location at a faster interval).
Applications with only the coarse location permission may have their interval silently throttled.
#####`@param {Integer millis} fastestLocationUpdateInterval`
Explicitly set the fastest interval for location updates, in milliseconds.
This controls the fastest rate at which your application will receive location updates, which might be faster than ```#locationUpdateInterval``` in some situations (for example, if other applications are triggering location updates).
This allows your application to passively acquire locations at a rate faster than it actively acquires locations, saving power.
Unlike ```#locationUpdateInterval```, this parameter is exact. Your application will never receive updates faster than this value.
If you don't call this method, a fastest interval will be set to **30000 (30s)**.
An interval of 0 is allowed, but not recommended, since location updates may be extremely fast on future implementations.
If ```#fastestLocationUpdateInterval``` is set slower than ```#locationUpdateInterval```, then your effective fastest interval is ```#locationUpdateInterval```.
========
An interval of 0 is allowed, but not recommended, since location updates may be extremely fast on future implementations.
#####`@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.
#####`@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.
#####`@param {Boolean} forceReload`
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} 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.
#####`@param {Object} headers`
Optional HTTP params sent along in HTTP request to above ```#url```.
### iOS Config
......
......@@ -9,4 +9,19 @@
</author>
<content src="index.html" />
<access origin="*" />
<allow-intent href="http://*/*" />
<allow-intent href="https://*/*" />
<allow-intent href="tel:*" />
<allow-intent href="sms:*" />
<allow-intent href="mailto:*" />
<allow-intent href="geo:*" />
<platform name="android">
<allow-intent href="market:*" />
</platform>
<platform name="ios">
<allow-intent href="itms:*" />
<allow-intent href="itms-apps:*" />
</platform>
</widget>
......@@ -24,12 +24,11 @@
<meta name="msapplication-tap-highlight" content="no" />
<!-- WARNING: for iOS 7, remove the width=device-width and height=device-height attributes. See https://issues.apache.org/jira/browse/CB-4323 -->
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height, target-densitydpi=device-dpi" />
<meta http-equiv="Content-Security-Policy" content="default-src *; style-src * 'unsafe-inline' data:; img-src *; script-src * 'unsafe-inline' 'unsafe-eval'">
<link rel="stylesheet" type="text/css" href="css/bootstrap-min.css" />
<link rel="stylesheet" type="text/css" href="css/index.css" />
<title>BG GeoLocation</title>
<script type="text/javascript"
src="http://maps.google.com/maps/api/js?sensor=true&libraries=geometry">
</script>
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=true&libraries=geometry"></script>
</head>
<body>
......@@ -51,6 +50,7 @@
<script type="text/javascript" src="cordova.js"></script>
<script type="text/javascript" src="js/jquery-2.1.1.js"></script>
<script type="text/javascript" src="js/LongPress.js"></script>
<script type="text/javascript" src="js/index.js"></script>
</body>
</html>
/**
* Custom google-maps @event LongClick hack.
*/
function LongPress(map, maxTime) {
this.maxTime = maxTime;
this.isDragging = false;
var me = this;
me.map = map;
google.maps.event.addListener(map, 'mousedown', function(e) {
me.onMouseDown_(e);
});
google.maps.event.addListener(map, 'mouseup', function(e) {
me.onMouseUp_(e);
});
google.maps.event.addListener(map, 'drag', function(e) {
me.onMapDrag_(e);
});
}
LongPress.prototype.onMouseUp_ = function(e) {
var now = +new Date;
if (now - this.downTime > this.maxTime && this.isDragging === false) {
google.maps.event.trigger(this.map, 'longpress', e);
}
}
LongPress.prototype.onMouseDown_ = function() {
this.downTime = +new Date;
this.isDragging = false;
}
LongPress.prototype.onMapDrag_ = function(e) {
this.isDragging = true;
};
......@@ -36,7 +36,7 @@ var app = {
/**
* @property {google.maps.Marker} location The current location
*/
location: undefined,
currentLocationMarker: undefined,
/**
* @property {google.map.PolyLine} path The list of background geolocations
*/
......@@ -49,6 +49,14 @@ var app = {
* @property {Array} locations List of rendered map markers of prev locations
*/
locations: [],
/**
* @property currentLocation {Location}
*/
currentLocation: null,
/**
* @property geofence {google.maps.Cirlce}
*/
geofence: undefined,
/**
* @private
*/
......@@ -79,6 +87,9 @@ var app = {
canvas.width(window.clientWidth);
app.map = new google.maps.Map(canvas[0], mapOptions);
// Add custom LongPress event to google map so we can add Geofences with longpress event!
new LongPress(app.map, 500);
},
// Bind Event Listeners
//
......@@ -120,11 +131,10 @@ var app = {
onDeviceReady: function() {
app.receivedEvent('deviceready');
app.configureBackgroundGeoLocation();
app.watchPosition();
app.watchForegroundPosition();
},
configureBackgroundGeoLocation: function() {
var fgGeo = window.navigator.geolocation,
bgGeo = window.plugins.backgroundGeoLocation;
var bgGeo = window.plugins.backgroundGeoLocation;
app.onClickHome();
......@@ -132,6 +142,8 @@ var app = {
* 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();
};
......@@ -155,6 +167,14 @@ var app = {
// Only ios emits this stationary event
bgGeo.onStationary(function(location) {
console.log('[js] BackgroundGeoLocation onStationary ' + JSON.stringify(location));
app.setCurrentLocation(location);
var coords = location.coords;
// Center ourself on map
app.onClickHome();
if (!app.stationaryRadius) {
app.stationaryRadius = new google.maps.Circle({
fillColor: '#cc0000',
......@@ -163,8 +183,8 @@ var app = {
map: app.map
});
}
var radius = (location.accuracy < location.radius) ? location.radius : location.accuracy;
var center = new google.maps.LatLng(location.latitude, location.longitude);
var radius = 50;
var center = new google.maps.LatLng(coords.latitude, coords.longitude);
app.stationaryRadius.setRadius(radius);
app.stationaryRadius.setCenter(center);
......@@ -175,29 +195,59 @@ var app = {
debug: true, // <-- enable this hear sounds for background-geolocation life-cycle.
desiredAccuracy: 0,
stationaryRadius: 50,
distanceFilter: 50,
distanceFilter: 25,
disableElasticity: false, // <-- [iOS] Default is 'false'. Set true to disable speed-based distanceFilter elasticity
locationUpdateInterval: 5000,
minimumActivityRecognitionConfidence: 80, // 0-100%. Minimum activity-confidence for a state-change
fastestLocationUpdateInterval: 5000,
activityRecognitionInterval: 10000,
stopTimeout: 0,
forceReload: true, // <-- [Android] If the user closes the app **while location-tracking is started** , reboot app (WARNING: possibly distruptive to user)
stopOnTerminate: false, // <-- [Android] Allow the background-service to run headless when user closes the app.
startOnBoot: true, // <-- [Android] Auto start background-service in headless mode when device is powered-up.
activityType: 'AutomotiveNavigation'
activityType: 'AutomotiveNavigation',
/**
* HTTP Feature: set an url to allow the native background service to POST locations to your server
*
,url: 'http://posttestserver.com/post.php?dir=cordova-background-geolocation',
*/
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.
autoSync: true, // <-- [Default: true] Set true to sync each location to server as it arrives.
maxDaysToPersist: 1, // <-- Maximum days to persist a location in plugin's SQLite database when HTTP fails
headers: {
"X-FOO": "bar"
},
params: {
"auth_token": "maybe_your_server_authenticates_via_token_YES?"
}
*
*/
});
bgGeo.onGeofence(function(identifier) {
alert('Enter Geofence: ' + identifier);
console.log('[js] Geofence ENTER: ', identifier);
});
// Add longpress event for adding GeoFence of hard-coded radius 200m.
google.maps.event.addListener(app.map, 'longpress', function(e) {
if (app.geofence) {
app.geofence.setMap(null);
}
bgGeo.addGeofence({
identifier: 'MyGeofence',
radius: 200,
latitude: e.latLng.lat(),
longitude: e.latLng.lng()
}, function() {
app.geofence = new google.maps.Circle({
fillColor: '#00cc00',
fillOpacity: 0.4,
strokeOpacity: 0,
radius: 200,
center: e.latLng,
map: app.map
});
})
});
// Turn ON the background-geolocation system. The user will be tracked whenever they suspend the app.
var settings = ENV.settings;
......@@ -210,22 +260,20 @@ var app = {
}
},
onClickHome: function() {
var fgGeo = window.navigator.geolocation;
// Your app must execute AT LEAST ONE call for the current position via standard Cordova geolocation,
// in order to prompt the user for Location permission.
fgGeo.getCurrentPosition(function(location) {
var map = app.map,
coords = location.coords,
ll = new google.maps.LatLng(coords.latitude, coords.longitude),
zoom = map.getZoom();
var location = app.currentLocation;
if (!location) {
// No location recorded yet; bail out.
return;
}
var map = app.map,
coords = location.coords,
ll = new google.maps.LatLng(coords.latitude, coords.longitude),
zoom = map.getZoom();
map.setCenter(ll);
if (zoom < 15) {
map.setZoom(15);
}
app.setCurrentLocation(coords);
});
map.setCenter(ll);
if (zoom < 15) {
map.setZoom(15);
}
},
onClickChangePace: function(value) {
var bgGeo = window.plugins.backgroundGeoLocation,
......@@ -273,26 +321,27 @@ var app = {
bgGeo.stop();
}
},
watchPosition: function() {
/**
* We use standard cordova-plugin-geolocation to watch position in foreground.
*/
watchForegroundPosition: function() {
var fgGeo = window.navigator.geolocation;
if (app.watchId) {
if (app.foregroundWatchId) {
app.stopPositionWatch();
}
// Watch foreground location
app.watchId = fgGeo.watchPosition(function(location) {
app.setCurrentLocation(location.coords);
}, function() {}, {
enableHighAccuracy: true,
maximumAge: 5000,
frequency: 10000,
timeout: 10000
app.foregroundWatchId = fgGeo.watchPosition(function(location) {
app.setCurrentLocation(location);
});
},
stopPositionWatch: function() {
/**
* Stop watching position in foreground when we pause: THIS IS VERY IMPORTANT
*/
stopWatchingForegroundPosition: function() {
var fgGeo = window.navigator.geolocation;
if (app.watchId) {
fgGeo.clearWatch(app.watchId);
app.watchId = undefined;
if (app.foregroundWatchId) {
fgGeo.clearWatch(app.foregroundWatchId);
app.foregroundWatchId = undefined;
}
},
/**
......@@ -301,23 +350,28 @@ var app = {
* determine start/stop of device.
*/
onPause: function() {
console.log('- onPause');
app.stopPositionWatch();
console.log('[js] onPause');
app.stopWatchingForegroundPosition();
},
/**
* Once in foreground, re-engage foreground geolocation watch with standard Cordova GeoLocation api
*/
onResume: function() {
console.log('- onResume');
app.watchPosition();
console.log('[js] onResume');
app.watchForegroundPosition();
},
// Update DOM on a Received Event
receivedEvent: function(id) {
console.log('Received Event: ' + id);
},
setCurrentLocation: function(location) {
if (!app.location) {
app.location = new google.maps.Marker({
// Set currentLocation @property
app.currentLocation = location;
var coords = location.coords;
if (!app.currentLocationMarker) {
app.currentLocationMarker = new google.maps.Marker({
map: app.map,
icon: {
path: google.maps.SymbolPath.CIRCLE,
......@@ -327,12 +381,13 @@ var app = {
strokeWeight: 5
}
});
app.locationAccuracy = new google.maps.Circle({
app.locationAccuracyMarker = new google.maps.Circle({
fillColor: '#3366cc',
fillOpacity: 0.4,
strokeOpacity: 0,
map: app.map
});
app.onClickHome();
}
if (!app.path) {
app.path = new google.maps.Polyline({
......@@ -341,7 +396,8 @@ var app = {
fillOpacity: 0.4
});
}
var latlng = new google.maps.LatLng(location.latitude, location.longitude);
var latlng = new google.maps.LatLng(coords.latitude, coords.longitude);
if (app.previousLocation) {
var prevLocation = app.previousLocation;
......@@ -355,14 +411,14 @@ var app = {
strokeWeight: 5
},
map: app.map,
position: new google.maps.LatLng(prevLocation.latitude, prevLocation.longitude)
position: new google.maps.LatLng(prevLocation.coords.latitude, prevLocation.coords.longitude)
}));
}
// Update our current position marker and accuracy bubble.
app.location.setPosition(latlng);
app.locationAccuracy.setCenter(latlng);
app.locationAccuracy.setRadius(location.accuracy);
app.currentLocationMarker.setPosition(latlng);
app.locationAccuracyMarker.setCenter(latlng);
app.locationAccuracyMarker.setRadius(location.coords.accuracy);
// Add breadcrumb to current Polyline path.
app.path.getPath().push(latlng);
......
......@@ -3,47 +3,34 @@
<plugin xmlns="http://www.phonegap.com/ns/plugins/1.0"
xmlns:android="http://schemas.android.com/apk/res/android"
id="com.transistorsoft.cordova.background-geolocation"
version="0.3.6">
version="0.4.1">
<name>BackgroundGeolocation</name>
<description>Sophisticated, battery-efficient background-geolocation plugin for Cordova</description>
<license>MIT</license>
<keywords>phonegap,background geolocation</keywords>
<keywords>cordova, phonegap, background geolocation</keywords>
<engines>
<engine name="cordova" version=">=3.0.0" />
</engines>
<dependency id="org.apache.cordova.geolocation" />
<dependency id="org.apache.cordova.dialogs" />
<dependency id="com.google.playservices" url="https://github.com/MobileChromeApps/google-play-services.git" />
<js-module src="www/BackgroundGeoLocation.js" name="BackgroundGeoLocation">
<clobbers target="plugins.backgroundGeoLocation" />
</js-module>
<!-- android -->
<platform name="android">
<framework src="com.google.android.gms:play-services-location:7.3.0" />
<source-file src="src/android/libs/eventbus-2.4.0.jar" target-dir="libs" />
<source-file src="src/android/BackgroundGeolocationPlugin.java" target-dir="src/com/transistorsoft/cordova/bggeo" />
<source-file src="src/android/BackgroundGeolocationService.java" target-dir="src/com/transistorsoft/cordova/bggeo" />
<source-file src="src/android/ActivityRecognitionService.java" target-dir="src/com/transistorsoft/cordova/bggeo" />
<source-file src="src/android/LocationService.java" target-dir="src/com/transistorsoft/cordova/bggeo" />
<source-file src="src/android/BootReceiver.java" target-dir="src/com/transistorsoft/cordova/bggeo" />
<!-- For SQLite persistence NOT YET IMPLEMENTED
<source-file src="src/android/data/LocationDAO.java" target-dir="src/com/tenforwardconsulting/cordova/bgloc/data" />
<source-file src="src/android/data/sqlite/LocationOpenHelper.java" target-dir="src/com/tenforwardconsulting/cordova/bgloc/data/sqlite" />
<source-file src="src/android/data/sqlite/SQLiteLocationDAO.java" target-dir="src/com/tenforwardconsulting/cordova/bgloc/data/sqlite" />
-->
<source-file src="src/android/libs/transistor-locationmanager.jar" target-dir="libs" />
<source-file src="src/android/CDVBackgroundGeolocation.java" target-dir="src/com/transistorsoft/cordova/bggeo" />
<config-file target="AndroidManifest.xml" parent="/manifest/application">
<service android:name="com.transistorsoft.cordova.bggeo.BackgroundGeolocationService" />
<service android:name="com.transistorsoft.cordova.bggeo.LocationService" />
<service android:name="com.transistorsoft.cordova.bggeo.ActivityRecognitionService" />
<!-- autorun on boot receiver -->
<receiver android:name="com.transistorsoft.cordova.bggeo.BootReceiver" android:enabled="true" android:exported="false">
<service android:name="com.transistorsoft.locationmanager.BackgroundGeolocationService" />
<service android:name="com.transistorsoft.locationmanager.LocationService" />
<service android:name="com.transistorsoft.locationmanager.ActivityRecognitionService" />
<receiver android:name="com.transistorsoft.locationmanager.BootReceiver" android:enabled="true" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
......@@ -63,7 +50,7 @@
</config-file>
<config-file target="res/xml/config.xml" parent="/*">
<feature name="BackgroundGeoLocation">
<param name="android-package" value="com.transistorsoft.cordova.bggeo.BackgroundGeolocationPlugin"/>
<param name="android-package" value="com.transistorsoft.cordova.bggeo.CDVBackgroundGeolocation"/>
</feature>
</config-file>
</platform>
......@@ -83,13 +70,15 @@
<config-file target="config.xml" parent="/*">
<feature name="BackgroundGeoLocation">
<param name="ios-package" value="CDVBackgroundGeoLocation"/>
<param name="ios-package" value="CDVBackgroundGeolocation"/>
</feature>
</config-file>
<framework src="AudioToolbox.framework" weak="true" />
<framework src="AVFoundation.framework" weak="true" />
<source-file src="src/ios/CDVBackgroundGeoLocation.m" />
<header-file src="src/ios/CDVBackgroundGeoLocation.h" />
<framework src="libsqlite3.dylib" weak="true" />
<framework src="src/ios/TSLocationManager.framework" custom="true" />
<source-file src="src/ios/CDVBackgroundGeolocation.m" />
<header-file src="src/ios/CDVBackgroundGeolocation.h" />
</platform>
<!-- wp8 -->
......
package com.transistorsoft.cordova.bggeo;
import de.greenrobot.event.EventBus;
import com.google.android.gms.location.DetectedActivity;
import com.google.android.gms.location.ActivityRecognitionResult;
import android.app.IntentService;
import android.content.Intent;
import android.util.Log;
public class ActivityRecognitionService extends IntentService {
private static final String TAG = "BackgroundGeolocation";
public ActivityRecognitionService() {
super("com.transistorsoft.cordova.bggeo.ActivityRecognitionService");
}
@Override
protected void onHandleIntent(Intent intent) {
// Determine whether the fore-ground Activity is running. If it's not, we'll reboot it.
if (ActivityRecognitionResult.hasResult(intent)) {
ActivityRecognitionResult result = ActivityRecognitionResult.extractResult(intent);
DetectedActivity probableActivity = result.getMostProbableActivity();
Log.w(TAG, "Activity detected:" + getActivityName(probableActivity.getType()) + ", confidence:" + probableActivity.getConfidence());
if (probableActivity.getConfidence() < 80) {
return;
}
switch (probableActivity.getType()) {
case DetectedActivity.IN_VEHICLE:
case DetectedActivity.ON_BICYCLE:
case DetectedActivity.ON_FOOT:
case DetectedActivity.WALKING:
case DetectedActivity.RUNNING:
case DetectedActivity.STILL:
EventBus.getDefault().post(result);
break;
case DetectedActivity.UNKNOWN:
return;
case DetectedActivity.TILTING:
return;
}
}
}
/**
* This method has no other purpose than formatting the Activity for log-messages
*/
private String getActivityName(int activityType) {
switch (activityType) {
case DetectedActivity.IN_VEHICLE:
return "in_vehicle";
case DetectedActivity.ON_BICYCLE:
return "on_bicycle";
case DetectedActivity.ON_FOOT:
return "on_foot";
case DetectedActivity.RUNNING:
return "running";
case DetectedActivity.WALKING:
return "walking";
case DetectedActivity.STILL:
return "still";
case DetectedActivity.UNKNOWN:
return "unknown";
case DetectedActivity.TILTING:
return "tilting";
}
return "unknown";
}
}
\ No newline at end of file
package com.transistorsoft.cordova.bggeo;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONException;
import org.json.JSONObject;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.location.ActivityRecognition;
import com.google.android.gms.location.ActivityRecognitionResult;
import com.google.android.gms.location.DetectedActivity;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationServices;
import de.greenrobot.event.EventBus;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.location.Location;
import android.media.AudioManager;
import android.media.ToneGenerator;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.provider.Settings.Secure;
import android.util.Log;
public class BackgroundGeolocationService extends Service implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {
private static final String TAG = "BackgroundGeolocation";
private static BackgroundGeolocationService instance = null;
public static boolean isInstanceCreated() {
return instance != null;
}
private GoogleApiClient googleApiClient;
private ToneGenerator toneGenerator;
private PendingIntent activityRecognitionPI;
private PendingIntent locationUpdatePI;
private LocationRequest locationRequest;
// Common config
/**
* @config {Integer} desiredAccuracy
*/
private Integer desiredAccuracy = 10;
/**
* @config {Float} distanceFilter
*/
private Float distanceFilter = 50f;
/**
* @config {Boolean} isDebugging
*/
private Boolean isDebugging = false;
/**
* @config {Boolean} stopOnTerminate
*/
private Boolean stopOnTerminate = false;
// Android-only config
/**
* @config {Integer} locationUpdateInterval (ms)
*/
private Integer locationUpdateInterval = 60000;
/**
* @config {Integer} fastestLocationUpdateInterval (ms)
*/
private Integer fastestLocationUpdateInterval = 30000;
/**
* @config {Integer{ activityRecognitionInterval (ms)
*/
private Integer activityRecognitionInterval = 60000;
/*
* @config {Boolean} forceReload Whether to reboot the Android Activity when detected to have closed
*/
private Boolean forceReload = false;
/**
* @config {Integer} stopTimeout The time to wait after ARS STILL to turn of GPS
*/
private long stopTimeout = 0;
// HTTP config
/**
* @config {String} url For sending location to your server
*/
private String url = null;
/**
* @config {JSONObject} params For sending location to your server
*/
private JSONObject params = new JSONObject();
/**
* @config {JSONObject} headers For sending location to your server
*/
private JSONObject headers = new JSONObject();
// Flags
private Boolean isEnabled = false;
private Boolean isMoving = false;
private Boolean isPaused = true;
private long stoppedAt = 0;
private Location stationaryLocation;
private DetectedActivity currentActivity;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
instance = this;
EventBus eventBus = EventBus.getDefault();
if (!eventBus.isRegistered(this)) {
eventBus.register(this);
}
// Load config settings
SharedPreferences settings = getSharedPreferences(TAG, 0);
isEnabled = true;
isDebugging = settings.getBoolean("debug", false);
distanceFilter = settings.getFloat("distanceFilter", 50);
desiredAccuracy = settings.getInt("desiredAccuracy", 10);
locationUpdateInterval = settings.getInt("locationUpdateInterval", 30000);
activityRecognitionInterval = settings.getInt("activityRecognitionInterval", 60000);
stopTimeout = settings.getLong("stopTimeout", 0);
stopOnTerminate = settings.getBoolean("stopOnTerminate", true);
forceReload = settings.getBoolean("forceReload", false);
isMoving = settings.getBoolean("isMoving", false);
// HTTP Configuration
url = settings.getString("url", null);
if (settings.contains("params")) {
try {
params = new JSONObject(settings.getString("params", "{}"));
} catch (JSONException e) {
Log.w(TAG, "- Faile to parse #params to JSONObject");
}
}
if (settings.contains("headers")) {
try {
headers = new JSONObject(settings.getString("headers", "{}"));
} catch (JSONException e) {
Log.w(TAG, "- Failed to parse #headers to JSONObject");
}
}
Log.i(TAG, "----------------------------------------");
Log.i(TAG, "- Start BackgroundGeolocationService");
Log.i(TAG, " debug: " + isDebugging);
Log.i(TAG, " distanceFilter: " + distanceFilter);
Log.i(TAG, " desiredAccuracy: " + desiredAccuracy);
Log.i(TAG, " locationUpdateInterval: " + locationUpdateInterval);
Log.i(TAG, " activityRecognitionInterval: " + activityRecognitionInterval);
Log.i(TAG, " stopTimeout: " + stopTimeout);
Log.i(TAG, " stopOnTerminate: " + stopOnTerminate);
Log.i(TAG, " forceReload: " + forceReload);
Log.i(TAG, " isMoving: " + isMoving);
Log.i(TAG, "----------------------------------------");
// For debug sounds, turn on ToneGenerator.
if (isDebugging) {
toneGenerator = new ToneGenerator(AudioManager.STREAM_NOTIFICATION, 100);
}
// Connect to google-play services.
if (ConnectionResult.SUCCESS == GooglePlayServicesUtil.isGooglePlayServicesAvailable(this)) {
Log.i(TAG, "- Connecting to GooglePlayServices...");
googleApiClient = new GoogleApiClient.Builder(this)
.addApi(LocationServices.API)
.addApi(ActivityRecognition.API)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
googleApiClient.connect();
} else {
Log.e(TAG, "- GooglePlayServices unavailable");
}
return Service.START_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
// TODO Auto-generated method stub
return null;
}
@Override
public void onConnectionFailed(ConnectionResult arg0) {
// TODO Auto-generated method stub
}
@Override
public void onConnected(Bundle arg0) {
Log.i(TAG, "- GooglePlayServices connected");
Intent arsIntent = new Intent(this, ActivityRecognitionService.class);
activityRecognitionPI = PendingIntent.getService(this, 0, arsIntent, PendingIntent.FLAG_UPDATE_CURRENT);
Intent locationIntent = new Intent(this, LocationService.class);
locationUpdatePI = PendingIntent.getService(this, 0, locationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
setPace(isMoving);
// Start monitoring ARS
if (googleApiClient.isConnected()) {
requestActivityUpdates();
}
}
/**
* EventBus listener
* Fired from Plugin
* @param {PausedEvent} event
*/
public void onEventMainThread(PausedEvent event) {
isPaused = event.isPaused;
if (isPaused) {
setPace(isMoving);
} else {
removeLocationUpdates();
}
}
/**
* EventBus listener
* Fired from Plugin
* @param {PaceChangeEvent} event
*/
public void onEventMainThread(PaceChangeEvent event) {
setPace(event.isMoving);
}
/**
* EventBus listener for ARS
* @param {ActivityRecognitionResult} result
*/
public void onEventMainThread(ActivityRecognitionResult result) {
currentActivity = result.getMostProbableActivity();
String probableActivityName = getActivityName(currentActivity.getType());
Log.i(TAG, "- Activity received: " + probableActivityName + ", confidence: " + currentActivity.getConfidence());
// If configured to stop when user closes app, kill this service.
if (!BackgroundGeolocationPlugin.isActive() && stopOnTerminate) {
stopSelf();
return;
}
boolean wasMoving = isMoving;
boolean nowMoving = false;
switch (currentActivity.getType()) {
case DetectedActivity.IN_VEHICLE:
case DetectedActivity.ON_BICYCLE:
case DetectedActivity.ON_FOOT:
case DetectedActivity.RUNNING:
case DetectedActivity.WALKING:
nowMoving = true;
break;
case DetectedActivity.STILL:
nowMoving = false;
break;
case DetectedActivity.UNKNOWN:
case DetectedActivity.TILTING:
// We're not interested in these modes.
return;
}
boolean startedMoving = !wasMoving && nowMoving;
boolean justStopped = wasMoving && !nowMoving;
boolean initialState = !nowMoving && (stationaryLocation == null);
// If we're using a stopTimeout, record the current activity's timestamp.
if (justStopped && stopTimeout > 0 && stoppedAt == 0) {
stoppedAt = result.getElapsedRealtimeMillis();
return;
}
// If we're using a stopTimeout, compare the current activity's timestamp with the 1st recorded STILL event.
if (!nowMoving && stoppedAt > 0) {
long elapsedMillis = result.getElapsedRealtimeMillis() - stoppedAt;
long elapsedMinutes = TimeUnit.MILLISECONDS.toMinutes(elapsedMillis);
Log.i(TAG, "- Waiting for stopTimeout (" + stopTimeout + " min): elapsed min: " + elapsedMinutes);
if (elapsedMinutes >= stopTimeout) {
justStopped = true;
} else {
return;
}
}
stoppedAt = 0;
if ( startedMoving || justStopped || initialState ) {
setPace(nowMoving);
}
}
public void onEventMainThread(Location location) {
if (location instanceof StationaryLocation) {
return;
}
Log.i(TAG, "BUS Rx:" + location.toString());
startTone("beep");
// Force main-activity reload (if not running) if we're detected to be moving.
boolean isPluginActive = BackgroundGeolocationPlugin.isActive();
if (!isPluginActive && forceReload) {
forceMainActivityReload();
}
if (url != null) {
if (isNetworkAvailable()) {
schedulePostLocation(location);
} else {
Log.i(TAG, "- No network detected");
// TODO no in-plugin persistence
}
}
}
private void setPace(Boolean moving) {
Log.i(TAG, "- setPace: " + moving);
boolean wasMoving = isMoving;
isMoving = moving;
if (moving && isEnabled) {
if (!wasMoving) {
startTone("doodly_doo");
}
stationaryLocation = null;
requestLocationUpdates();
} else {
removeLocationUpdates();
if (stationaryLocation == null) {
startTone("long_beep");
// set our stationaryLocation
stationaryLocation = LocationServices.FusedLocationApi.getLastLocation(googleApiClient);
if (stationaryLocation != null) {
EventBus.getDefault().post(new StationaryLocation(stationaryLocation));
}
}
}
}
/**
* Translates a number representing desired accuracy of GeoLocation system from set [0, 10, 100, 1000].
* 0: most aggressive, most accurate, worst battery drain
* 1000: least aggressive, least accurate, best for battery.
*/
private Integer translateDesiredAccuracy(Integer accuracy) {
switch (accuracy) {
case 1000:
accuracy = LocationRequest.PRIORITY_NO_POWER;
break;
case 100:
accuracy = LocationRequest.PRIORITY_LOW_POWER;
break;
case 10:
accuracy = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY;
break;
case 0:
accuracy = LocationRequest.PRIORITY_HIGH_ACCURACY;
break;
default:
accuracy = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY;
}
return accuracy;
}
private Integer getLocationUpdateInterval() {
// TODO Can add intelligence here based upon currentActivity.
SharedPreferences settings = getSharedPreferences(TAG, 0);
return settings.getInt("locationUpdateInterval", locationUpdateInterval);
}
private Integer getFastestLocationUpdateInterval() {
/* TODO Add intelligent calculation of fastestLocationUpdateInterval based upon currentActivity here
* switch (currentActivity.getType()) {
case DetectedActivity.IN_VEHICLE:
fastestLocationUpdateInterval = 30000;
break;
case DetectedActivity.ON_BICYCLE:
fastestLocationUpdateInterval = 30000;
break;
case DetectedActivity.ON_FOOT:
fastestLocationUpdateInterval = 30000;
break;
case DetectedActivity.RUNNING:
fastestLocationUpdateInterval = 30000;
break;
case DetectedActivity.WALKING:
fastestLocationUpdateInterval = 30000;
break;
}
*/
return fastestLocationUpdateInterval;
}
private String getActivityName(int activityType) {
switch (activityType) {
case DetectedActivity.IN_VEHICLE:
return "in_vehicle";
case DetectedActivity.ON_BICYCLE:
return "on_bicycle";
case DetectedActivity.ON_FOOT:
return "on_foot";
case DetectedActivity.RUNNING:
return "running";
case DetectedActivity.WALKING:
return "walking";
case DetectedActivity.STILL:
return "still";
case DetectedActivity.UNKNOWN:
return "unknown";
case DetectedActivity.TILTING:
return "tilting";
}
return "unknown";
}
private boolean isNetworkAvailable() {
ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();
return activeNetworkInfo != null && activeNetworkInfo.isConnected();
}
private void schedulePostLocation(Location location) {
PostLocationTask task = new BackgroundGeolocationService.PostLocationTask();
task.setLocation(location);
Log.d(TAG, "beforeexecute " + task.getStatus());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
else
task.execute();
Log.d(TAG, "afterexecute " + task.getStatus());
}
private boolean postLocation(Location location) {
try {
DefaultHttpClient httpClient = new DefaultHttpClient();
HttpPost request = new HttpPost(url);
JSONObject data = locationToJson(location);
params.put("location", data);
// Append android UUID to params so that server can map the UUID to some user in your database on server.
// If you've configured the plugin to execute on BOOT, there's no way to append your user's auth-token to the params
// since this BackgroundGeolocationService will be running in "headless" mode.
//
// It's up to you to register this UUID with your system. You can fetch this UUID using the
// Cordova Device plugin org.apache.cordova.device http://plugins.cordova.io/#/package/org.apache.cordova.device
params.put("android_id", Secure.getString(this.getContentResolver(), Secure.ANDROID_ID));
Log.i(TAG, "data: " + params.toString());
StringEntity se = new StringEntity(params.toString());
request.setEntity(se);
request.setHeader("Accept", "application/json");
request.setHeader("Content-type", "application/json");
Iterator<String> keys = headers.keys();
while( keys.hasNext() ){
String key = keys.next();
if(key != null) {
request.setHeader(key, (String)headers.getString(key));
}
}
Log.d(TAG, "Posting to " + request.getURI().toString());
HttpResponse response = httpClient.execute(request);
Log.i(TAG, "Response received: " + response.getStatusLine());
if (response.getStatusLine().getStatusCode() == 200) {
return true;
} else {
return false;
}
} catch (Throwable e) {
Log.w(TAG, "Exception posting location: " + e);
e.printStackTrace();
return false;
}
}
/**
* Forces the main activity to re-launch if it's unloaded. This is how we're able to rely upon Javascript
* running always, since we for the app to boot.
*/
private void forceMainActivityReload() {
Log.w(TAG, "- Forcing main-activity reload");
PackageManager pm = getPackageManager();
Intent launchIntent = pm.getLaunchIntentForPackage(getApplicationContext().getPackageName());
launchIntent.addFlags(Intent.FLAG_FROM_BACKGROUND);
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION);
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(launchIntent);
}
private void requestActivityUpdates() {
SharedPreferences settings = getSharedPreferences(TAG, 0);
ActivityRecognition.ActivityRecognitionApi.requestActivityUpdates(googleApiClient, settings.getInt("activityRecognitionInterval", activityRecognitionInterval), activityRecognitionPI);
}
private void removeActivityUpdates() {
ActivityRecognition.ActivityRecognitionApi.removeActivityUpdates(googleApiClient, activityRecognitionPI);
}
private void requestLocationUpdates() {
if (!isPaused || !isEnabled) { return; } // <-- Don't engage GPS when app is in foreground
SharedPreferences settings = getSharedPreferences(TAG, 0);
// Configure LocationRequest
locationRequest = LocationRequest.create()
.setPriority(translateDesiredAccuracy(settings.getInt("desiredAccuracy", desiredAccuracy)))
.setInterval(getLocationUpdateInterval())
.setFastestInterval(getFastestLocationUpdateInterval())
.setSmallestDisplacement(settings.getFloat("distanceFilter", distanceFilter));
LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, locationRequest, locationUpdatePI);
}
private void removeLocationUpdates() {
LocationServices.FusedLocationApi.removeLocationUpdates(googleApiClient, locationUpdatePI);
}
/**
* Plays debug sound
* @param name
*/
private void startTone(String name) {
int tone = 0;
int duration = 1000;
if (name.equals("beep")) {
tone = ToneGenerator.TONE_PROP_BEEP;
} else if (name.equals("beep_beep_beep")) {
tone = ToneGenerator.TONE_CDMA_CONFIRM;
} else if (name.equals("long_beep")) {
tone = ToneGenerator.TONE_CDMA_ABBR_ALERT;
} else if (name.equals("doodly_doo")) {
tone = ToneGenerator.TONE_CDMA_ALERT_NETWORK_LITE;
} else if (name.equals("chirp_chirp_chirp")) {
tone = ToneGenerator.TONE_CDMA_ALERT_CALL_GUARD;
} else if (name.equals("dialtone")) {
tone = ToneGenerator.TONE_SUP_RINGTONE;
}
if (isDebugging) {
toneGenerator.startTone(tone, duration);
}
}
@Override
public void onConnectionSuspended(int arg0) {
// TODO Auto-generated method stub
}
@Override
public void onDestroy() {
Log.w(TAG, "- Destroy service");
cleanUp();
super.onDestroy();
}
private void cleanUp() {
instance = null;
EventBus.getDefault().unregister(this);
if (googleApiClient != null && googleApiClient.isConnected()) {
removeActivityUpdates();
removeLocationUpdates();
googleApiClient.disconnect();
}
}
/**
* Convert a Location instance to JSONObject
* @param Location
* @return JSONObject
*/
public static JSONObject locationToJson(Location l) {
try {
JSONObject data = new JSONObject();
data.put("latitude", l.getLatitude());
data.put("longitude", l.getLongitude());
data.put("accuracy", l.getAccuracy());
data.put("speed", l.getSpeed());
data.put("bearing", l.getBearing());
data.put("altitude", l.getAltitude());
data.put("timestamp", l.getTime());
return data;
} catch (JSONException e) {
Log.e(TAG, "could not parse location");
return null;
}
}
public static class PausedEvent {
public boolean isPaused;
public PausedEvent(boolean paused) {
isPaused = paused;
}
}
public static class PaceChangeEvent {
public boolean isMoving;
public PaceChangeEvent(boolean moving) {
isMoving = moving;
}
}
class StationaryLocation extends Location {
public StationaryLocation(Location l) {
super(l);
}
}
private class PostLocationTask extends AsyncTask<Object, Integer, Boolean> {
private Location location;
public void setLocation(Location l) {
location = l;
}
@Override
protected Boolean doInBackground(Object...objects) {
Log.d(TAG, "Executing PostLocationTask#doInBackground");
if (postLocation(location)) {
location = null;
}
return true;
}
@Override
protected void onPostExecute(Boolean result) {
Log.d(TAG, "PostLocationTask#onPostExecture");
}
}
}
package com.transistorsoft.cordova.bggeo;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.util.Log;
/**
* This boot receiver is meant to handle the case where device is first booted after power up.
* This boot the headless BackgroundGeolocationService as configured by this class.
* @author chris scott
*
*/
public class BootReceiver extends BroadcastReceiver {
private static final String TAG = "BackgroundGeolocation";
@Override
public void onReceive(Context context, Intent intent) {
SharedPreferences settings = context.getSharedPreferences(TAG, 0);
boolean startOnBoot = settings.getBoolean("startOnBoot", false);
boolean enabled = settings.getBoolean("enabled", false);
if (!startOnBoot || !enabled) {
return;
}
Log.i(TAG, "- BootReceiver booting service");
// Start the service.
context.startService(new Intent(context, BackgroundGeolocationService.class));
}
}
......@@ -7,10 +7,13 @@ import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONException;
import com.transistorsoft.cordova.bggeo.BackgroundGeolocationService.PaceChangeEvent;
import com.transistorsoft.cordova.bggeo.BackgroundGeolocationService.PausedEvent;
import com.transistorsoft.cordova.bggeo.BackgroundGeolocationService.StationaryLocation;
import android.os.Bundle;
import com.google.android.gms.location.ActivityRecognitionResult;
import com.google.android.gms.location.DetectedActivity;
import com.transistorsoft.locationmanager.BackgroundGeolocationService;
import com.transistorsoft.locationmanager.BackgroundGeolocationService.PaceChangeEvent;
import com.transistorsoft.locationmanager.BackgroundGeolocationService.PausedEvent;
//import com.transistorsoft.locationmanager.BackgroundGeolocationService.StationaryLocation;
import de.greenrobot.event.EventBus;
import android.app.Activity;
......@@ -19,18 +22,21 @@ import android.content.SharedPreferences;
import android.location.Location;
import android.util.Log;
public class BackgroundGeolocationPlugin extends CordovaPlugin {
public class CDVBackgroundGeolocation extends CordovaPlugin {
private static final String TAG = "BackgroundGeolocation";
private static CordovaWebView gWebView;
public static Boolean forceReload = false;
public static final String ACTION_START = "start";
public static final String ACTION_STOP = "stop";
public static final String ACTION_START = "start";
public static final String ACTION_STOP = "stop";
public static final String ACTION_ON_PACE_CHANGE = "onPaceChange";
public static final String ACTION_CONFIGURE = "configure";
public static final String ACTION_SET_CONFIG = "setConfig";
public static final String ACTION_CONFIGURE = "configure";
public static final String ACTION_SET_CONFIG = "setConfig";
public static final String ACTION_ON_STATIONARY = "addStationaryRegionListener";
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";
private Boolean isEnabled = false;
private Boolean stopOnTerminate = false;
......@@ -38,11 +44,21 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin {
private Intent backgroundServiceIntent;
private DetectedActivity currentActivity;
// Geolocation callback
private CallbackContext locationCallback;
// Called when DetectedActivity is STILL
private CallbackContext stationaryCallback;
private CallbackContext getLocationsCallback;
private CallbackContext syncCallback;
private CallbackContext getOdometerCallback;
private CallbackContext resetOdometerCallback;
public static boolean isActive() {
return gWebView != null;
}
......@@ -100,6 +116,34 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin {
} else if (ACTION_ON_STATIONARY.equalsIgnoreCase(action)) {
result = true;
this.stationaryCallback = callbackContext;
} else if (ACTION_GET_LOCATIONS.equalsIgnoreCase(action)) {
result = true;
Bundle event = new Bundle();
event.putString("name", action);
event.putBoolean("request", true);
getLocationsCallback = callbackContext;
EventBus.getDefault().post(event);
} else if (ACTION_SYNC.equalsIgnoreCase(action)) {
result = true;
Bundle event = new Bundle();
event.putString("name", action);
event.putBoolean("request", true);
syncCallback = callbackContext;
EventBus.getDefault().post(event);
} else if (ACTION_GET_ODOMETER.equalsIgnoreCase(action)) {
result = true;
Bundle event = new Bundle();
event.putString("name", action);
event.putBoolean("request", true);
getOdometerCallback = callbackContext;
EventBus.getDefault().post(event);
} else if (ACTION_RESET_ODOMETER.equalsIgnoreCase(action)) {
result = true;
Bundle event = new Bundle();
event.putString("name", action);
event.putBoolean("request", true);
resetOdometerCallback = callbackContext;
EventBus.getDefault().post(event);
}
return result;
}
......@@ -109,7 +153,7 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin {
Activity activity = this.cordova.getActivity();
SharedPreferences settings = activity.getSharedPreferences(TAG, 0);
SharedPreferences settings = activity.getSharedPreferences("TSLocationManager", 0);
SharedPreferences.Editor editor = settings.edit();
editor.putBoolean("enabled", isEnabled);
editor.commit();
......@@ -130,9 +174,10 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin {
JSONObject config = data.getJSONObject(0);
Log.i(TAG, "- configure: " + config.toString());
SharedPreferences settings = activity.getSharedPreferences(TAG, 0);
SharedPreferences settings = activity.getSharedPreferences("TSLocationManager", 0);
SharedPreferences.Editor editor = settings.edit();
editor.putBoolean("activityIsActive", true);
editor.putBoolean("isMoving", isMoving);
if (config.has("distanceFilter")) {
......@@ -145,7 +190,10 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin {
editor.putInt("locationUpdateInterval", config.getInt("locationUpdateInterval"));
}
if (config.has("activityRecognitionInterval")) {
editor.putInt("activityRecognitionInterval", config.getInt("activityRecognitionInterval"));
editor.putLong("activityRecognitionInterval", config.getLong("activityRecognitionInterval"));
}
if (config.has("minimumActivityRecognitionConfidence")) {
editor.putInt("minimumActivityRecognitionConfidence", config.getInt("minimumActivityRecognitionConfidence"));
}
if (config.has("stopTimeout")) {
editor.putLong("stopTimeout", config.getLong("stopTimeout"));
......@@ -153,6 +201,9 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin {
if (config.has("debug")) {
editor.putBoolean("debug", config.getBoolean("debug"));
}
if (config.has("stopAfterElapsedMinutes")) {
editor.putInt("stopAfterElapsedMinutes", config.getInt("stopAfterElapsedMinutes"));
}
if (config.has("stopOnTerminate")) {
editor.putBoolean("stopOnTerminate", config.getBoolean("stopOnTerminate"));
}
......@@ -165,6 +216,12 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin {
if (config.has("url")) {
editor.putString("url", config.getString("url"));
}
if (config.has("autoSync")) {
editor.putBoolean("autoSync", config.getBoolean("autoSync"));
}
if (config.has("batchSync")) {
editor.putBoolean("batchSync", config.getBoolean("batchSync"));
}
if (config.has("params")) {
try {
editor.putString("params", config.getJSONObject("params").toString());
......@@ -190,25 +247,78 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin {
public void onPause(boolean multitasking) {
Log.i(TAG, "- onPause");
if (isEnabled) {
EventBus.getDefault().post(new PausedEvent(true));
}
}
public void onResume(boolean multitasking) {
Log.i(TAG, "- onResume");
if (isEnabled) {
EventBus.getDefault().post(new PausedEvent(false));
}
}
/**
* EventBus listener for Event Bundle
* @param {Bundle} event
*/
public void onEventMainThread(Bundle event) {
if (event.containsKey("request")) {
return;
}
String name = event.getString("name");
if (ACTION_GET_LOCATIONS.equalsIgnoreCase(name)) {
try {
JSONArray json = new JSONArray(event.getString("data"));
PluginResult result = new PluginResult(PluginResult.Status.OK, json);
runInBackground(getLocationsCallback, result);
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} else if (ACTION_SYNC.equalsIgnoreCase(name)) {
Boolean success = event.getBoolean("success");
if (success) {
try {
JSONArray json = new JSONArray(event.getString("data"));
PluginResult result = new PluginResult(PluginResult.Status.OK, json);
runInBackground(syncCallback, result);
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} else {
PluginResult result = new PluginResult(PluginResult.Status.IO_EXCEPTION, event.getString("message"));
runInBackground(syncCallback, result);
}
} else if (ACTION_GET_ODOMETER.equalsIgnoreCase(name)) {
PluginResult result = new PluginResult(PluginResult.Status.OK, event.getFloat("data"));
runInBackground(getOdometerCallback, result);
} else if (ACTION_RESET_ODOMETER.equalsIgnoreCase(name)) {
PluginResult result = new PluginResult(PluginResult.Status.OK);
runInBackground(resetOdometerCallback, result);
}
}
/**
* EventBus listener for ARS
* @param {ActivityRecognitionResult} result
*/
public void onEventMainThread(ActivityRecognitionResult result) {
currentActivity = result.getMostProbableActivity();
String activityName = BackgroundGeolocationService.getActivityName(currentActivity.getType());
int confidence = currentActivity.getConfidence();
}
/**
* EventBus listener
* @param {Location} location
*/
public void onEventMainThread(Location location) {
PluginResult result = new PluginResult(PluginResult.Status.OK, BackgroundGeolocationService.locationToJson(location));
PluginResult result;
result = new PluginResult(PluginResult.Status.OK, BackgroundGeolocationService.locationToJson(location, currentActivity));
result.setKeepCallback(true);
if (location instanceof StationaryLocation) {
if (location instanceof com.transistorsoft.locationmanager.BackgroundGeolocationService.StationaryLocation) {
isMoving = false;
if (stationaryCallback != null) {
runInBackground(stationaryCallback, result);
......@@ -234,7 +344,7 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin {
});
}
}
/**
* Override method in CordovaPlugin.
* Checks to see if it should turn off
......@@ -244,6 +354,13 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin {
Log.i(TAG, " stopOnTerminate: " + stopOnTerminate);
Log.i(TAG, " isEnabled: " + isEnabled);
Activity activity = this.cordova.getActivity();
SharedPreferences settings = activity.getSharedPreferences("TSLocationManager", 0);
SharedPreferences.Editor editor = settings.edit();
editor.putBoolean("activityIsActive", false);
editor.commit();
if(isEnabled && stopOnTerminate) {
this.cordova.getActivity().stopService(backgroundServiceIntent);
}
......
package com.transistorsoft.cordova.bggeo;
import de.greenrobot.event.EventBus;
import com.google.android.gms.location.FusedLocationProviderApi;
import android.app.IntentService;
import android.content.Intent;
import android.location.Location;
import android.util.Log;
public class LocationService extends IntentService {
private static final String TAG = "BackgroundGeolocation";
public LocationService() {
super("com.transistorsoft.cordova.bggeo.LocationUpdateService");
}
@Override
protected void onHandleIntent(Intent intent) {
final Location location = intent.getParcelableExtra(FusedLocationProviderApi.KEY_LOCATION_CHANGED);
if (location != null) {
Log.i(TAG, "Location received: " + location.toString());
EventBus.getDefault().post(location);
}
}
}
\ No newline at end of file
package com.transistorsoft.cordova.bggeo.data;
import org.json.JSONObject;
import org.json.JSONException;
public interface LocationDAO {
public JSONObject[] getAllLocations();
public boolean persistLocation(JSONObject l);
public void deleteLocation(JSONObject l);
}
\ No newline at end of file
package com.transistorsoft.cordova.bggeo.data.sqlite;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
public class LocationOpenHelper extends SQLiteOpenHelper {
private static final String SQLITE_DATABASE_NAME = "cordova_background_geolocation";
private static final int DATABASE_VERSION = 1;
public static final String LOCATION_TABLE_NAME = "locations";
private static final String LOCATION_TABLE_COLUMNS = "timestamp INTEGER PRIMARY KEY, json TEXT";
private static final String LOCATION_TABLE_CREATE = "CREATE TABLE " + LOCATION_TABLE_NAME + " (" + LOCATION_TABLE_COLUMNS + ");";
LocationOpenHelper(Context context) {
super(context, SQLITE_DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(LOCATION_TABLE_CREATE);
Log.d(this.getClass().getName(), LOCATION_TABLE_CREATE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// TODO Auto-generated method stub
}
}
package com.transistorsoft.cordova.bggeo.data.sqlite;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.TimeZone;
import java.util.List;
import org.json.JSONObject;
import org.json.JSONException;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import com.transistorsoft.cordova.bggeo.data.LocationDAO;
public class SQLiteLocationDAO implements LocationDAO {
private static final String TAG = "SQLiteLocationDAO";
private Context context;
public SQLiteLocationDAO(Context context) {
this.context = context;
}
/*
public JSONObject[] getAllLocations() {
SQLiteDatabase db = null;
Cursor c = null;
List<JSONObject> all = new ArrayList<JSONObject>();
try {
db = new LocationOpenHelper(context).getReadableDatabase();
c = db.query(LocationOpenHelper.LOCATION_TABLE_NAME, null);
while (c.moveToNext()) {
all.add(hydrate(c));
}
} finally {
if (c != null) {
c.close();
}
if (db != null) {
db.close();
}
}
return all.toArray(new JSONObject[all.size()]);
}
public boolean persistLocation(JSONObject location) {
SQLiteDatabase db = new LocationOpenHelper(context).getWritableDatabase();
db.beginTransaction();
ContentValues values = getContentValues(location);
long rowId = db.insert(LocationOpenHelper.LOCATION_TABLE_NAME, null, values);
Log.d(TAG, "After insert, rowId = " + rowId);
db.setTransactionSuccessful();
db.endTransaction();
db.close();
if (rowId > -1) {
return true;
} else {
return false;
}
}
public void deleteLocation(JSONObject location) {
SQLiteDatabase db = new LocationOpenHelper(context).getWritableDatabase();
db.beginTransaction();
db.delete(LocationOpenHelper.LOCATION_TABLE_NAME, "timestamp = ?", location.getString("timestamp"));
db.setTransactionSuccessful();
db.endTransaction();
db.close();
}
private JSONObject hydrate(Cursor c) {
JSONObject l = new JSONObject(c);
return l;
}
private ContentValues getContentValues(JSONObject location) {
ContentValues values = new ContentValues();
values.put("latitude", location.get("latitude"));
values.put("longitude", location.get("longitude"));
values.put("timestamp", location.get("timestamp"));
values.put("accuracy", location.get("accuracy"));
values.put("altitude", location.get("altitude"));
values.put("bearing", location.get("bearing"));
values.put("speed", location.get("speed"));
return values;
}
*/
}
\ No newline at end of file
......@@ -5,12 +5,12 @@
//
#import <Cordova/CDVPlugin.h>
#import "CDVLocation.h"
#import <AudioToolbox/AudioToolbox.h>
#import <TSLocationManager/TSLocationManager.h>
@interface CDVBackgroundGeoLocation : CDVPlugin <CLLocationManagerDelegate>
@interface CDVBackgroundGeolocation : CDVPlugin
@property (nonatomic, strong) NSString* syncCallbackId;
@property (nonatomic, strong) NSString* geofenceCallbackId;
@property (nonatomic, strong) NSMutableArray* stationaryRegionListeners;
- (void) configure:(CDVInvokedUrlCommand*)command;
......@@ -21,9 +21,11 @@
- (void) setConfig:(CDVInvokedUrlCommand*)command;
- (void) addStationaryRegionListener:(CDVInvokedUrlCommand*)command;
- (void) getStationaryLocation:(CDVInvokedUrlCommand *)command;
- (void) onSuspend:(NSNotification *)notification;
- (void) onResume:(NSNotification *)notification;
- (void) onAppTerminate;
- (void) getLocations:(CDVInvokedUrlCommand *)command;
- (void) sync:(CDVInvokedUrlCommand *)command;
- (void) getOdometer:(CDVInvokedUrlCommand *)command;
- (void) resetOdometer:(CDVInvokedUrlCommand *)command;
- (void) addGeofence:(CDVInvokedUrlCommand *)command;
- (void) onGeofence:(CDVInvokedUrlCommand *)command;
@end
......@@ -3,100 +3,24 @@
//
// Created by Chris Scott <chris@transistorsoft.com> on 2013-06-15
//
#import "CDVLocation.h"
#import "CDVBackgroundGeoLocation.h"
#import <Cordova/CDVJSON.h>
// Debug sounds for bg-geolocation life-cycle events.
// http://iphonedevwiki.net/index.php/AudioServices
#define exitRegionSound 1005
#define locationSyncSound 1004
#define paceChangeYesSound 1110
#define paceChangeNoSound 1112
#define acquiringLocationSound 1103
#define acquiredLocationSound 1052
#define locationErrorSound 1073
@implementation CDVBackgroundGeoLocation {
BOOL isDebugging;
BOOL enabled;
BOOL isUpdatingLocation;
BOOL stopOnTerminate;
NSString *token;
NSString *url;
UIBackgroundTaskIdentifier bgTask;
NSDate *lastBgTaskAt;
NSError *locationError;
BOOL isMoving;
NSNumber *maxBackgroundHours;
CLLocationManager *locationManager;
UILocalNotification *localNotification;
CDVLocationData *locationData;
CLLocation *lastLocation;
NSMutableArray *locationQueue;
NSDate *suspendedAt;
CLLocation *stationaryLocation;
CLCircularRegion *stationaryRegion;
NSInteger locationAcquisitionAttempts;
BOOL isAcquiringStationaryLocation;
NSInteger maxStationaryLocationAttempts;
BOOL isAcquiringSpeed;
NSInteger maxSpeedAcquistionAttempts;
// @config params
NSInteger stationaryRadius;
NSInteger distanceFilter;
CLLocationAccuracy desiredAccuracy;
CLActivityType activityType;
BOOL disableElasticity;
@implementation CDVBackgroundGeolocation {
TSLocationManager *bgGeo;
NSDictionary *config;
}
@synthesize syncCallbackId;
@synthesize stationaryRegionListeners;
@synthesize syncCallbackId, geofenceCallbackId, stationaryRegionListeners;
- (void)pluginInitialize
{
// @config params
isDebugging = NO;
stopOnTerminate = NO;
stationaryRadius = 50;
distanceFilter = 50;
desiredAccuracy = kCLLocationAccuracyBest;
disableElasticity = NO;
activityType = CLActivityTypeOther;
bgGeo = [[TSLocationManager alloc] init];
// background location cache, for when no network is detected.
locationManager = [[CLLocationManager alloc] init];
locationManager.delegate = self;
localNotification = [[UILocalNotification alloc] init];
localNotification.timeZone = [NSTimeZone defaultTimeZone];
locationQueue = [[NSMutableArray alloc] init];
// Flags
isMoving = NO;
isUpdatingLocation = NO;
stationaryLocation = nil;
stationaryRegion = nil;
maxStationaryLocationAttempts = 4;
maxSpeedAcquistionAttempts = 3;
bgTask = UIBackgroundTaskInvalid;
// Listen to suspend/resume events
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onSuspend:) name:UIApplicationDidEnterBackgroundNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onResume:) name:UIApplicationWillEnterForegroundNotification 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(onEnterGeofence:) name:@"TSLocationManager.geofence" object:nil];
}
/**
* configure plugin
* @param {String} token
......@@ -106,664 +30,171 @@
*/
- (void) configure:(CDVInvokedUrlCommand*)command
{
NSDictionary *config = [command.arguments objectAtIndex:0];
[self applyConfig:config];
self.syncCallbackId = command.callbackId;
locationManager.activityType = activityType;
locationManager.pausesLocationUpdatesAutomatically = YES;
locationManager.distanceFilter = distanceFilter; // meters
locationManager.desiredAccuracy = desiredAccuracy;
// ios 8 requires permissions to send local-notifications
if (isDebugging) {
UIApplication *app = [UIApplication sharedApplication];
if ([app respondsToSelector:@selector(registerUserNotificationSettings:)]) {
[app registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert|UIUserNotificationTypeBadge|UIUserNotificationTypeSound categories:nil]];
}
}
config = [command.arguments objectAtIndex:0];
[bgGeo configure:config];
}
- (void) setConfig:(CDVInvokedUrlCommand*)command
{
NSDictionary *config = [command.arguments objectAtIndex:0];
[self applyConfig:config];
NSDictionary *cfg = [command.arguments objectAtIndex:0];
[bgGeo setConfig:cfg];
CDVPluginResult* result = nil;
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
}
- (void) applyConfig:(NSDictionary*)config
{
if (config[@"debug"]) {
isDebugging = [[config objectForKey:@"debug"] boolValue];
}
if (config[@"stationaryRadius"]) {
stationaryRadius = [[config objectForKey:@"stationaryRadius"] intValue];
}
if (config[@"distanceFilter"]) {
distanceFilter = [[config objectForKey:@"distanceFilter"] intValue];
}
if (config[@"desiredAccuracy"]) {
desiredAccuracy = [self decodeDesiredAccuracy:[[config objectForKey:@"desiredAccuracy"] intValue]];
}
if (config[@"activityType"]) {
activityType = [self decodeActivityType:[config objectForKey:@"activityType"]];
}
if (config[@"stopOnTerminate"]) {
stopOnTerminate = [[config objectForKey:@"stopOnTerminate"] boolValue];
}
if (config[@"disableElasticity"]) {
disableElasticity = [[config objectForKey:@"disableElasticity"] boolValue];
}
[self log: @"- CDVBackgroundGeoLocation config %@", config];
}
/**
* Log a message. Only outputs when @config debug: true
*/
- (void) log:(NSString *)format, ...
{
if (isDebugging) {
va_list args;
va_start(args, format);
va_end(args);
NSLogv(format, args);
}
}
- (void) addStationaryRegionListener:(CDVInvokedUrlCommand*)command
{
if (self.stationaryRegionListeners == nil) {
self.stationaryRegionListeners = [[NSMutableArray alloc] init];
}
[self.stationaryRegionListeners addObject:command.callbackId];
if (stationaryRegion) {
[self queue:stationaryLocation type:@"stationary"];
}
}
- (void) flushQueue
{
// Sanity-check the duration of last bgTask: If greater than 30s, kill it.
if (bgTask != UIBackgroundTaskInvalid) {
if (-[lastBgTaskAt timeIntervalSinceNow] > 30.0) {
[self log: @"- CDVBackgroundGeoLocation#flushQueue has to kill an out-standing background-task!"];
[self stopBackgroundTask];
}
return;
}
if ([locationQueue count] > 0) {
NSMutableDictionary *data = [locationQueue lastObject];
[locationQueue removeObject:data];
// Create a background-task and delegate to Javascript for syncing location
bgTask = [self createBackgroundTask];
[self.commandDelegate runInBackground:^{
[self sync:data];
}];
}
}
-(NSInteger)decodeDesiredAccuracy:(NSInteger)accuracy
{
switch (accuracy) {
case 1000:
accuracy = kCLLocationAccuracyKilometer;
break;
case 100:
accuracy = kCLLocationAccuracyHundredMeters;
break;
case 10:
accuracy = kCLLocationAccuracyNearestTenMeters;
break;
case 0:
accuracy = kCLLocationAccuracyBest;
break;
case -1:
accuracy = kCLLocationAccuracyBestForNavigation;
break;
default:
accuracy = kCLLocationAccuracyBest;
}
return accuracy;
}
-(CLActivityType)decodeActivityType:(NSString*)name
{
if ([name caseInsensitiveCompare:@"AutomotiveNavigation"]) {
return CLActivityTypeAutomotiveNavigation;
} else if ([name caseInsensitiveCompare:@"OtherNavigation"]) {
return CLActivityTypeOtherNavigation;
} else if ([name caseInsensitiveCompare:@"Fitness"]) {
return CLActivityTypeFitness;
} else {
return CLActivityTypeOther;
}
}
/**
* Turn on background geolocation
*/
- (void) start:(CDVInvokedUrlCommand*)command
{
enabled = YES;
UIApplicationState state = [[UIApplication sharedApplication] applicationState];
[self log: @"- CDVBackgroundGeoLocation start (background? %d)", state];
[locationManager startMonitoringSignificantLocationChanges];
if (state == UIApplicationStateBackground) {
[self setPace:isMoving];
}
CDVPluginResult* result = nil;
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
[bgGeo start];
}
/**
* Turn it off
*/
- (void) stop:(CDVInvokedUrlCommand*)command
{
[self log: @"- CDVBackgroundGeoLocation stop"];
enabled = NO;
isMoving = NO;
[self stopUpdatingLocation];
[locationManager stopMonitoringSignificantLocationChanges];
if (stationaryRegion != nil) {
[locationManager stopMonitoringForRegion:stationaryRegion];
stationaryRegion = nil;
}
CDVPluginResult* result = nil;
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
}
/**
* Change pace to moving/stopped
* @param {Boolean} isMoving
*/
- (void) onPaceChange:(CDVInvokedUrlCommand *)command
{
isMoving = [[command.arguments objectAtIndex: 0] boolValue];
[self log: @"- CDVBackgroundGeoLocation onPaceChange %d", isMoving];
UIApplicationState state = [[UIApplication sharedApplication] applicationState];
if (state == UIApplicationStateBackground) {
[self setPace:isMoving];
}
}
/**
* toggle passive or aggressive location services
*/
- (void)setPace:(BOOL)value
{
[self log: @"- CDVBackgroundGeoLocation setPace %d, stationaryRegion? %d", value, stationaryRegion!=nil];
isMoving = value;
isAcquiringStationaryLocation = NO;
isAcquiringSpeed = NO;
locationAcquisitionAttempts = 0;
stationaryLocation = nil;
if (isDebugging) {
AudioServicesPlaySystemSound (isMoving ? paceChangeYesSound : paceChangeNoSound);
}
if (isMoving) {
if (stationaryRegion) {
[locationManager stopMonitoringForRegion:stationaryRegion];
stationaryRegion = nil;
}
isAcquiringSpeed = YES;
} else {
isAcquiringStationaryLocation = YES;
}
if (isAcquiringSpeed || isAcquiringStationaryLocation) {
// Crank up the GPS power temporarily to get a good fix on our current location
[self stopUpdatingLocation];
locationManager.distanceFilter = kCLDistanceFilterNone;
locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation;
[self startUpdatingLocation];
}
[bgGeo stop];
}
/**
* Fetches current stationaryLocation
*/
- (void) getStationaryLocation:(CDVInvokedUrlCommand *)command
- (void) getOdometer:(CDVInvokedUrlCommand*)command
{
[self log: @"- CDVBackgroundGeoLocation getStationaryLocation"];
// Build a resultset for javascript callback.
CDVPluginResult* result = nil;
if (stationaryLocation) {
NSMutableDictionary *returnInfo = [self locationToHash:stationaryLocation];
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:returnInfo];
} else {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsBool:NO];
}
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDouble: bgGeo.odometer];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
}
-(NSMutableDictionary*) locationToHash:(CLLocation*)location
{
NSMutableDictionary *returnInfo;
returnInfo = [NSMutableDictionary dictionaryWithCapacity:10];
NSNumber* timestamp = [NSNumber numberWithDouble:([location.timestamp timeIntervalSince1970] * 1000)];
[returnInfo setObject:timestamp forKey:@"timestamp"];
[returnInfo setObject:[NSNumber numberWithDouble:location.speed] forKey:@"speed"];
[returnInfo setObject:[NSNumber numberWithDouble:location.verticalAccuracy] forKey:@"altitudeAccuracy"];
[returnInfo setObject:[NSNumber numberWithDouble:location.horizontalAccuracy] forKey:@"accuracy"];
[returnInfo setObject:[NSNumber numberWithDouble:location.course] forKey:@"heading"];
[returnInfo setObject:[NSNumber numberWithDouble:location.altitude] forKey:@"altitude"];
[returnInfo setObject:[NSNumber numberWithDouble:location.coordinate.latitude] forKey:@"latitude"];
[returnInfo setObject:[NSNumber numberWithDouble:location.coordinate.longitude] forKey:@"longitude"];
return returnInfo;
}
/**
* Called by js to signify the end of a background-geolocation event
*/
-(void) finish:(CDVInvokedUrlCommand*)command
- (void) resetOdometer:(CDVInvokedUrlCommand*)command
{
[self log: @"- CDVBackgroundGeoLocation finish"];
[self stopBackgroundTask];
bgGeo.odometer = 0;
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
}
/**
* Suspend. Turn on passive location services
*/
-(void) onSuspend:(NSNotification *) notification
{
[self log: @"- CDVBackgroundGeoLocation suspend (enabled? %d)", enabled];
suspendedAt = [NSDate date];
if (enabled) {
// Sample incoming stationary-location candidate: Is it within the current stationary-region? If not, I guess we're moving.
if (!isMoving && stationaryRegion) {
if ([self locationAge:stationaryLocation] < (5 * 60.0)) {
if (isDebugging) {
AudioServicesPlaySystemSound (acquiredLocationSound);
[self notify:[NSString stringWithFormat:@"Continue stationary\n%f,%f", [stationaryLocation coordinate].latitude, [stationaryLocation coordinate].longitude]];
}
[self queue:stationaryLocation type:@"stationary"];
return;
}
}
[self setPace: isMoving];
}
}
/**@
* Resume. Turn background off
*/
-(void) onResume:(NSNotification *) notification
{
[self log: @"- CDVBackgroundGeoLocation resume"];
if (enabled) {
[self stopUpdatingLocation];
}
}
/**@
* Termination. Checks to see if it should turn off
* Change pace to moving/stopped
* @param {Boolean} isMoving
*/
-(void) onAppTerminate
- (void) onPaceChange:(CDVInvokedUrlCommand *)command
{
[self log: @"- CDVBackgroundGeoLocation appTerminate"];
if (enabled && stopOnTerminate) {
[self log: @"- CDVBackgroundGeoLocation stoping on terminate"];
enabled = NO;
isMoving = NO;
[self stopUpdatingLocation];
[locationManager stopMonitoringSignificantLocationChanges];
if (stationaryRegion != nil) {
[locationManager stopMonitoringForRegion:stationaryRegion];
stationaryRegion = nil;
}
}
BOOL moving = [[command.arguments objectAtIndex: 0] boolValue];
[bgGeo onPaceChange:moving];
}
-(void) locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
[self log: @"- CDVBackgroundGeoLocation didUpdateLocations (isMoving: %d)", isMoving];
locationError = nil;
if (isMoving && !isUpdatingLocation) {
[self startUpdatingLocation];
}
CLLocation *location = [locations lastObject];
if (!isMoving && !isAcquiringStationaryLocation && !stationaryLocation) {
// Perhaps our GPS signal was interupted, re-acquire a stationaryLocation now.
[self setPace: NO];
}
// test the age of the location measurement to determine if the measurement is cached
// in most cases you will not want to rely on cached measurements
if ([self locationAge:location] > 5.0) return;
// test that the horizontal accuracy does not indicate an invalid measurement
if (location.horizontalAccuracy < 0) return;
lastLocation = location;
// test the measurement to see if it is more accurate than the previous measurement
if (isAcquiringStationaryLocation) {
[self log: @"- Acquiring stationary location, accuracy: %f", location.horizontalAccuracy];
if (isDebugging) {
AudioServicesPlaySystemSound (acquiringLocationSound);
}
if (stationaryLocation == nil || stationaryLocation.horizontalAccuracy > location.horizontalAccuracy) {
stationaryLocation = location;
}
if (++locationAcquisitionAttempts == maxStationaryLocationAttempts) {
isAcquiringStationaryLocation = NO;
[self startMonitoringStationaryRegion:stationaryLocation];
} else {
// Unacceptable stationary-location: bail-out and wait for another.
return;
}
} else if (isAcquiringSpeed) {
if (isDebugging) {
AudioServicesPlaySystemSound (acquiringLocationSound);
}
if (++locationAcquisitionAttempts == maxSpeedAcquistionAttempts) {
if (isDebugging) {
[self notify:@"Aggressive monitoring engaged"];
}
// We should have a good sample for speed now, power down our GPS as configured by user.
isAcquiringSpeed = NO;
[locationManager setDesiredAccuracy:desiredAccuracy];
[locationManager setDistanceFilter:[self calculateDistanceFilter:location.speed]];
[self startUpdatingLocation];
} else {
return;
}
} else if (isMoving) {
// Adjust distanceFilter incrementally based upon current speed
float newDistanceFilter = [self calculateDistanceFilter:location.speed];
if (newDistanceFilter != locationManager.distanceFilter) {
[self log: @"- CDVBackgroundGeoLocation updated distanceFilter, new: %f, old: %f", newDistanceFilter, locationManager.distanceFilter];
[locationManager setDistanceFilter:newDistanceFilter];
[self startUpdatingLocation];
}
} else if ([self locationIsBeyondStationaryRegion:location]) {
if (isDebugging) {
[self notify:@"Manual stationary exit-detection"];
}
[self setPace:YES];
}
[self queue:location type:@"current"];
}
/**
* Manual stationary location his-testing. This seems to help stationary-exit detection in some places where the automatic geo-fencing soesn't
*/
-(bool)locationIsBeyondStationaryRegion:(CLLocation*)location
{
[self log: @"- CDVBackgroundGeoLocation locationIsBeyondStationaryRegion"];
if (![stationaryRegion containsCoordinate:[location coordinate]]) {
double pointDistance = [stationaryLocation distanceFromLocation:location];
return (pointDistance - stationaryLocation.horizontalAccuracy - location.horizontalAccuracy) > stationaryRadius;
} else {
return NO;
}
}
/**
* Calculates distanceFilter by rounding speed to nearest 5 and multiplying by 10.
* - Clamped at 1km max.
* - Disabled by #disableElasticity
* location handler from BackgroundGeolocation
*/
-(float) calculateDistanceFilter:(float)speed
{
if (disableElasticity == YES) {
return distanceFilter;
}
float newDistanceFilter = distanceFilter;
if (speed < 100) {
// (rounded-speed-to-nearest-5) / 2)^2
// eg 5.2 becomes (5/2)^2
newDistanceFilter = pow((5.0 * floorf(fabsf(speed) / 5.0 + 0.5f)), 2) + distanceFilter;
}
return (newDistanceFilter < 1000) ? newDistanceFilter : 1000;
}
-(void) queue:(CLLocation*)location type:(id)type
{
[self log: @"- CDVBackgroundGeoLocation queue %@", type];
NSMutableDictionary *data = [self locationToHash:location];
[data setObject:type forKey:@"location_type"];
[locationQueue addObject:data];
[self flushQueue];
}
-(UIBackgroundTaskIdentifier) createBackgroundTask
{
lastBgTaskAt = [NSDate date];
return [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[self stopBackgroundTask];
}];
- (void)onLocationChanged:(NSNotification*)notification {
CLLocation *location = notification.object;
NSDictionary *locationData = [bgGeo locationToDictionary:location];
CDVPluginResult* result = nil;
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:locationData];
[result setKeepCallbackAsBool:YES];
[self.commandDelegate sendPluginResult:result callbackId:self.syncCallbackId];
}
/**
* We are running in the background if this is being executed.
* We can't assume normal network access.
* bgTask is defined as an instance variable of type UIBackgroundTaskIdentifier
*/
-(void) sync:(NSMutableDictionary*)data
- (void) onStationaryLocation:(NSNotification*)notification
{
[self log: @"- CDVBackgroundGeoLocation#sync"];
[self log: @" type: %@, position: %@,%@ speed: %@", [data objectForKey:@"location_type"], [data objectForKey:@"latitude"], [data objectForKey:@"longitude"], [data objectForKey:@"speed"]];
if (isDebugging) {
[self notify:[NSString stringWithFormat:@"Location update: %s\nSPD: %0.0f | DF: %ld | ACY: %0.0f",
((isMoving) ? "MOVING" : "STATIONARY"),
[[data objectForKey:@"speed"] doubleValue],
(long) locationManager.distanceFilter,
[[data objectForKey:@"accuracy"] doubleValue]]];
AudioServicesPlaySystemSound (locationSyncSound);
}
// Build a resultset for javascript callback.
NSString *locationType = [data objectForKey:@"location_type"];
if ([locationType isEqualToString:@"stationary"]) {
[self fireStationaryRegionListeners:data];
} else if ([locationType isEqualToString:@"current"]) {
CDVPluginResult* result = nil;
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:data];
[result setKeepCallbackAsBool:YES];
[self.commandDelegate sendPluginResult:result callbackId:self.syncCallbackId];
} else {
[self log: @"- CDVBackgroundGeoLocation#sync could not determine location_type."];
[self stopBackgroundTask];
}
}
CLLocation *location = notification.object;
NSDictionary *locationData = [bgGeo locationToDictionary:location];
- (void) fireStationaryRegionListeners:(NSMutableDictionary*)data
{
[self log: @"- CDVBackgroundGeoLocation#fireStationaryRegionListener"];
if (![self.stationaryRegionListeners count]) {
[self stopBackgroundTask];
[bgGeo stopBackgroundTask];
return;
}
// Any javascript stationaryRegion event-listeners?
[data setObject:[NSNumber numberWithDouble:stationaryRadius] forKey:@"radius"];
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:data];
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:locationData];
[result setKeepCallbackAsBool:YES];
for (NSString *callbackId in self.stationaryRegionListeners)
{
[self.commandDelegate sendPluginResult:result callbackId:callbackId];
}
[bgGeo stopBackgroundTask];
}
/**
* Creates a new circle around user and region-monitors it for exit
*/
- (void) startMonitoringStationaryRegion:(CLLocation*)location {
stationaryLocation = location;
// fire onStationary @event for Javascript.
[self queue:location type:@"stationary"];
CLLocationCoordinate2D coord = [location coordinate];
[self log: @"- CDVBackgroundGeoLocation createStationaryRegion (%f,%f)", coord.latitude, coord.longitude];
if (isDebugging) {
AudioServicesPlaySystemSound (acquiredLocationSound);
[self notify:[NSString stringWithFormat:@"Acquired stationary location\n%f, %f", location.coordinate.latitude,location.coordinate.longitude]];
}
if (stationaryRegion != nil) {
[locationManager stopMonitoringForRegion:stationaryRegion];
}
isAcquiringStationaryLocation = NO;
stationaryRegion = [[CLCircularRegion alloc] initWithCenter: coord radius:stationaryRadius identifier:@"BackgroundGeoLocation stationary region"];
stationaryRegion.notifyOnExit = YES;
[locationManager startMonitoringForRegion:stationaryRegion];
[self stopUpdatingLocation];
locationManager.distanceFilter = distanceFilter;
locationManager.desiredAccuracy = desiredAccuracy;
}
- (bool) stationaryRegionContainsLocation:(CLLocation*)location {
CLCircularRegion *region = [locationManager.monitoredRegions member:stationaryRegion];
return ([region containsCoordinate:location.coordinate]) ? YES : NO;
}
- (void) stopBackgroundTask
- (void) onEnterGeofence:(NSNotification*)notification
{
UIApplication *app = [UIApplication sharedApplication];
[self log: @"- CDVBackgroundGeoLocation stopBackgroundTask (remaining t: %f)", app.backgroundTimeRemaining];
if (bgTask != UIBackgroundTaskInvalid)
{
[app endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
if (self.geofenceCallbackId == nil) {
return;
}
[self flushQueue];
CLCircularRegion *region = notification.object;
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:region.identifier];
[result setKeepCallbackAsBool:YES];
[self.commandDelegate sendPluginResult:result callbackId:self.geofenceCallbackId];
}
/**
* Called when user exits their stationary radius (ie: they walked ~50m away from their last recorded location.
* - turn on more aggressive location monitoring.
* Fetches current stationaryLocation
*/
- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region
- (void) getStationaryLocation:(CDVInvokedUrlCommand *)command
{
[self log: @"- CDVBackgroundGeoLocation exit region"];
if (isDebugging) {
AudioServicesPlaySystemSound (exitRegionSound);
[self notify:@"Exit stationary region"];
}
[self setPace:YES];
NSDictionary* location = [bgGeo getStationaryLocation];
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:location];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
}
/**
* 1. turn off std location services
* 2. turn on significantChanges API
* 3. create a region and start monitoring exits.
* Fetches current stationaryLocation
*/
- (void)locationManagerDidPauseLocationUpdates:(CLLocationManager *)manager
- (void) getLocations:(CDVInvokedUrlCommand *)command
{
[self log: @"- CDVBackgroundGeoLocation paused location updates"];
if (isDebugging) {
[self notify:@"Stop detected"];
}
if (locationError) {
isMoving = NO;
[self startMonitoringStationaryRegion:lastLocation];
[self stopUpdatingLocation];
} else {
[self setPace:NO];
}
NSArray* locations = [bgGeo getLocations];
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:locations];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
}
/**
* 1. Turn off significantChanges ApI
* 2. turn on std. location services
* 3. nullify stationaryRegion
* Fetches current stationaryLocation
*/
- (void)locationManagerDidResumeLocationUpdates:(CLLocationManager *)manager
- (void) sync:(CDVInvokedUrlCommand *)command
{
[self log: @"- CDVBackgroundGeoLocation resume location updates"];
if (isDebugging) {
[self notify:@"Resume location updates"];
NSArray* locations = [bgGeo sync];
if (locations != nil) {
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:locations];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
} else {
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Failed to sync to server. Is there a network connection?"];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
}
[self setPace:YES];
}
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
- (void) addStationaryRegionListener:(CDVInvokedUrlCommand*)command
{
[self log: @"- CDVBackgroundGeoLocation locationManager failed: %@", error];
if (isDebugging) {
AudioServicesPlaySystemSound (locationErrorSound);
[self notify:[NSString stringWithFormat:@"Location error: %@", error.localizedDescription]];
}
locationError = error;
switch(error.code) {
case kCLErrorLocationUnknown:
case kCLErrorNetwork:
case kCLErrorRegionMonitoringDenied:
case kCLErrorRegionMonitoringSetupDelayed:
case kCLErrorRegionMonitoringResponseDelayed:
case kCLErrorGeocodeFoundNoResult:
case kCLErrorGeocodeFoundPartialResult:
case kCLErrorGeocodeCanceled:
break;
case kCLErrorDenied:
[self stopUpdatingLocation];
break;
default:
[self stopUpdatingLocation];
if (self.stationaryRegionListeners == nil) {
self.stationaryRegionListeners = [[NSMutableArray alloc] init];
}
[self.stationaryRegionListeners addObject:command.callbackId];
}
- (void) stopUpdatingLocation
- (void) addGeofence:(CDVInvokedUrlCommand*)command
{
[locationManager stopUpdatingLocation];
isUpdatingLocation = NO;
}
NSDictionary *cfg = [command.arguments objectAtIndex:0];
- (void) startUpdatingLocation
{
SEL requestSelector = NSSelectorFromString(@"requestAlwaysAuthorization");
if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined && [locationManager respondsToSelector:requestSelector]) {
((void (*)(id, SEL))[locationManager methodForSelector:requestSelector])(locationManager, requestSelector);
[locationManager startUpdatingLocation];
isUpdatingLocation = YES;
} else {
[locationManager startUpdatingLocation];
isUpdatingLocation = YES;
}
}
- (void) locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status
{
[self log: @"- CDVBackgroundGeoLocation didChangeAuthorizationStatus %u", status];
if (isDebugging) {
[self notify:[NSString stringWithFormat:@"Authorization status changed %u", status]];
}
[bgGeo addGeofence:[cfg objectForKey:@"identifier"]
radius:[[cfg objectForKey:@"radius"] doubleValue]
latitude:[[cfg objectForKey:@"latitude"] doubleValue]
longitude:[[cfg objectForKey:@"longitude"] doubleValue]
];
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
}
- (NSTimeInterval) locationAge:(CLLocation*)location
- (void) onGeofence:(CDVInvokedUrlCommand*)command
{
return -[location.timestamp timeIntervalSinceNow];
self.geofenceCallbackId = command.callbackId;
}
- (void) notify:(NSString*)message
/**
* Called by js to signify the end of a background-geolocation event
*/
-(void) finish:(CDVInvokedUrlCommand*)command
{
localNotification.fireDate = [NSDate date];
localNotification.alertBody = message;
[[UIApplication sharedApplication] scheduleLocalNotification:localNotification];
[bgGeo finish];
}
/**
* If you don't stopMonitoring when application terminates, the app will be awoken still when a
......@@ -771,12 +202,6 @@
* Might be desirable in certain apps.
*/
- (void)applicationWillTerminate:(UIApplication *)application {
}
- (void)dealloc
{
locationManager.delegate = nil;
}
@end
Versions/Current/Headers
\ No newline at end of file
Versions/Current/Resources
\ No newline at end of file
Versions/Current/TSLocationManager
\ No newline at end of file
#import <CoreLocation/CoreLocation.h>
#import <AudioToolbox/AudioToolbox.h>
#import <sqlite3.h>
@interface TSLocationManager : NSObject <CLLocationManagerDelegate>
@property (nonatomic) CLLocationDistance odometer;
- (void) configure:(NSDictionary*)config;
- (void) start;
- (void) stop;
- (void) finish;
- (NSArray*) sync;
- (NSArray*) getLocations;
- (void) stopBackgroundTask;
- (void) onPaceChange:(BOOL)value;
- (void) setConfig:(NSDictionary*)command;
- (NSDictionary*) getStationaryLocation;
- (void) onSuspend:(NSNotification *)notification;
- (void) onResume:(NSNotification *)notification;
- (void) onAppTerminate;
- (BOOL) isEnabled;
- (NSDictionary*) locationToDictionary:(CLLocation*)location;
- (void) addGeofence:(NSString*)identifier radius:(CLLocationDistance)radius latitude:(CLLocationDegrees)latitude longitude:(CLLocationDegrees)longitude;
@end
framework module TSLocationManager {
umbrella header "TSLocationManager.h"
export *
module * { export * }
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>files</key>
<dict>
<key>Headers/TSLocationManager.h</key>
<data>
93eNJDqyN5vuBJn9oYeT7lb2x1w=
</data>
<key>Info.plist</key>
<data>
1G+AqP61j6Sq6MmsyYP98Uqa1/Y=
</data>
<key>Modules/module.modulemap</key>
<data>
ZlH8rkma2jB7yWJU5dFqHfgbHZc=
</data>
</dict>
<key>files2</key>
<dict>
<key>Headers/TSLocationManager.h</key>
<data>
93eNJDqyN5vuBJn9oYeT7lb2x1w=
</data>
<key>Modules/module.modulemap</key>
<data>
ZlH8rkma2jB7yWJU5dFqHfgbHZc=
</data>
</dict>
<key>rules</key>
<dict>
<key>^</key>
<true/>
<key>^.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^version.plist$</key>
<true/>
</dict>
<key>rules2</key>
<dict>
<key>.*\.dSYM($|/)</key>
<dict>
<key>weight</key>
<real>11</real>
</dict>
<key>^</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^(.*/)?\.DS_Store$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>2000</real>
</dict>
<key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
<dict>
<key>nested</key>
<true/>
<key>weight</key>
<real>10</real>
</dict>
<key>^.*</key>
<true/>
<key>^.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Info\.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^PkgInfo$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^[^/]+$</key>
<dict>
<key>nested</key>
<true/>
<key>weight</key>
<real>10</real>
</dict>
<key>^embedded\.provisionprofile$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^version\.plist$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
</dict>
</dict>
</plist>
......@@ -20,8 +20,15 @@ module.exports = {
configure: function(success, failure, config) {
config = config || {};
this.config = config;
exec(success || function() {},
success = success || function(location) {};
var mySuccess = function(location) {
// Transform timestamp to Date instance.
if (location.timestamp) {
location.timestamp = new Date(location.timestamp);
}
success.call(this, location);
}
exec(mySuccess,
failure || function() {},
'BackgroundGeoLocation',
'configure',
......@@ -98,6 +105,96 @@ module.exports = {
'addStationaryRegionListener',
[]);
},
getLocations: function(success, failure) {
if (typeof(success) !== 'function') {
throw "BackgroundGeolocation#getLocations requires a success callback";
}
var me = this;
var mySuccess = function(locations) {
success.call(this, me._setTimestamp(locations));
}
exec(mySuccess,
failure || function() {},
'BackgroundGeoLocation',
'getLocations',
[]);
},
/**
* Signal native plugin to sync locations queue to HTTP
*/
sync: function(success, failure) {
if (typeof(success) !== 'function') {
throw "BackgroundGeolocation#sync requires a success callback";
}
var me = this;
var mySuccess = function(locations) {
success.call(this, me._setTimestamp(locations));
}
exec(mySuccess,
failure || function() {},
'BackgroundGeoLocation',
'sync',
[]);
},
/**
* Fetch current odometer value
*/
getOdometer: function(success, failure) {
exec(success || function() {},
failure || function() {},
'BackgroundGeoLocation',
'getOdometer',
[]);
},
/**
* Reset Odometer to 0
*/
resetOdometer: function(success, failure) {
exec(success || function() {},
failure || function() {},
'BackgroundGeoLocation',
'resetOdometer',
[]);
},
/**
* add geofence
*/
addGeofence: function(config, success, failure) {
config = config || {};
if (!config.identifier) {
throw "#addGeofence requires an 'identifier'";
}
if (!(config.latitude && config.longitude)) {
throw "#addGeofence requires a #latitude and #longitude";
}
if (!config.radius) {
throw "#addGeofence requires a #radius";
}
exec(success || function() {},
failure || function() {},
'BackgroundGeoLocation',
'addGeofence',
[config]);
},
onGeofence: function(success, failure) {
if (!typeof(success) === 'function') {
throw "#onGeofence requires a success callback";
}
exec(success,
failure || function() {},
'BackgroundGeoLocation',
'onGeofence',
[]);
},
_setTimestamp: function(rs) {
// Transform timestamp to Date instance.
if (typeof(rs) === 'object') {
for (var n=0,len=rs.length;n<len;n++) {
rs[n].timestamp = new Date(rs[n].timestamp);
}
}
return rs;
},
apply: function(destination, source) {
source = source || {};
for (var property in source) {
......
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