Commit b1b6b2f7 authored by Chris Scott's avatar Chris Scott

Merge pull request #18 from christocracy/edge

Edge
parents 5710fba8 8ef8ff1d
......@@ -16,7 +16,7 @@ Cordova Background Geolocation - Terms and conditions
- Allowing 3rd Parties to run Software on Licensee's Website[s] and Server[s];
- Publishing Software’s output to Licensee and 3rd Parties;
- Distribute verbatim copies of Software's output (including compiled binaries);
- Modify Software to suit Licensee’s needs and specifications.
- Modify Software to suit Licensee's needs and specifications.
2.2 Binary Restricted: Licensee may sublicense Software as a part of a larger work containing more than Software, distributed solely in Object or Binary form under a personal, non-sublicensable, limited license. Such redistribution shall be limited to unlimited codebases.</li><li>
......
......@@ -11,12 +11,17 @@ This plugin leverages Cordova/PhoneGap's [require/define functionality used for
The plugin creates the object `window.plugins.backgroundGeoLocation` with the methods
`configure(success, fail, option)`,
`setConfig(success, fail, config) // reconfigure`,
`start(success, fail)`
`stop(success, fail)`.
`changePace(true) // engages aggressive monitoring immediately`
`onStationary(callback, fail)`
## Installing the plugin ##
......@@ -141,7 +146,56 @@ Using the [ActivityRecognition API](https://developer.android.com/reference/com/
WP8 uses ```callbackFn``` the way iOS do. On WP8, however, the plugin does not support the Stationary location and does not implement ```getStationaryLocation()``` and ```onPaceChange()```.
Keep in mind that it is **not** possible to use ```start()``` at the ```pause``` event of Cordova/PhoneGap. WP8 suspend your app immediately and ```start()``` will not be executed. So make sure you fire ```start()``` before the app is closed/minimized.
### Config
## Methods
#####`configure(locationCallback, failureCallback, config)`
Configures the plugin's parameters (@see following [Config](https://github.com/christocracy/cordova-background-geolocation/blob/edge/README.md#config) section for accepted ```config``` params. The ```locationCallback``` will be executed each time a new Geolocation is recorded.
#####`setConfig(successFn, failureFn, config)`
Reconfigure plugin's configuration (@see followign ##Config## section for accepted ```config``` params. **NOTE** The plugin will continue to send recorded Geolocation to the ```locationCallback``` you provided to ```configure``` method -- use this method only to change configuration params (eg: ```distanceFilter```, ```stationaryRadius```, etc).
```
bgGeo.setConfig(function(){}, function(){}, {
desiredAccuracy: 10,
distanceFilter: 100
});
```
#####`start(successFn, failureFn)`
Enable background geolocation tracking.
```
bgGeo.start()
```
#####`stop(successFn, failureFn)`
Disable background geolocation tracking.
```
bgGeo.stop();
```
#####`changePace(enabled, successFn, failureFn)`
Initiate or cancel immediate background tracking. When set to ```true```, the plugin will begin aggressively tracking the devices Geolocation, bypassing stationary monitoring. If you were making a "Jogging" application, this would be your [Start Workout] button to immediately begin GPS tracking. Send ```false``` to disable aggressive GPS monitoring and return to stationary-monitoring mode.
```
bgGeo.changePace(true); // <-- Aggressive GPS monitoring immediately engaged.
bgGeo.changePace(false); // <-- Disable aggressive GPS monitoring. Engages stationary-mode.
```
#####`onStationary(callbackFn, failureFn)`
Your ```callbackFn``` will be executed each time the device has entered stationary-monitoring mode. The ```callbackFn``` will be provided with a ```Geolocation``` object as the 1st param, with the usual params (```latitude, longitude, accuracy, speed, bearing, altitude```).
```
bgGeo.onStationary(function(location) {
console.log('- Device is stopped: ', location.latitude, location.longitude);
});
```
## Config
Use the following config-parameters with the #configure method:
......@@ -204,8 +258,7 @@ Compare now background-geolocation in the scope of a city. In this image, the l
![distanceFilter at city scale](/distance-filter-city.png "distanceFilter at city scale")
#####`@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)
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
......@@ -225,6 +278,40 @@ An interval of 0 is allowed, but not recommended, since location updates may be
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).
#### 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```).
#####`@param {String} url`
By configuring an ```#url```, the plugin will always attempt to HTTP POST the location to your server.
#####`@param {Object} params`
Optional HTTP params sent along in HTTP request to above ```#url```.
#####`@param {Object} headers`
Optional HTTP params sent along in HTTP request to above ```#url```.
#### Autorun on Boot Feature
The Android background-service can be configured to autorun whenever the device is booted. In ```plugin.xml```, you must first un-comment the Android permission ```android.permission.RECEIVE_BOOT_COMPLETED```
```
<!-- Autorun your app on device BOOT. UNCOMMENT TO ENABLE AUTO-BOOT -->
<!-- <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> -->
```
Next, since the plugin will have no access to your presumably logged-in user at boot-time (eg authentication_token, password, etc), you must manually configure the plugin's parameters within the Java file [src/android/BootReceiver.java](https://github.com/christocracy/cordova-background-geolocation/blob/edge/src/android/BootReceiver.java). Since the plugin will be running in "headless" mode at boot-time (ie: no foreground application, thus no javascript) you should configure an ```#url``` so the plugin can automatically POST location to your server. Since the plugin has no access to any user-identifying information, the Android plugin will send along the device's UUID in the HTTP request params as ```#android_id```. It's up to you to map this Android UUID to a user on your server (at login time, for example). You may fetch the device UUID using standard cordova plugin [org.apache.cordova.device](http://plugins.cordova.io/#/package/org.apache.cordova.device).
### iOS Config
#####`@param {String} activityType [AutomotiveNavigation, OtherNavigation, Fitness, Other]`
......@@ -239,25 +326,74 @@ Presumably, this affects ios GPS algorithm. See [Apple docs](https://developer.
The underlying GeoLocator you can choose to use 'DesiredAccuracy' or 'DesiredAccuracyInMeters'. Since this plugins default configuration accepts meters, the default desiredAccuracy is mapped to the Windows Phone DesiredAccuracyInMeters leaving the DesiredAccuracy enum empty. For more info see the [MS docs](http://msdn.microsoft.com/en-us/library/windows/apps/windows.devices.geolocation.geolocator.desiredaccuracyinmeters) for more information.
## Licence ##
```
cordova-background-geolocation
Copyright (c) 2015, Transistor Software (9224-2932 Quebec Inc)
All rights reserved.
sales@transistorsoft.com
http://transistorsoft.com
```
1. Preamble: This Agreement governs the relationship between YOU OR THE ORGANIZATION ON WHOSE BEHALF YOU ARE ENTERING INTO THIS AGREEMENT (hereinafter: Licensee) and Transistor Software, a LICENSOR AFFILIATION whose principal place of business is Montreal, Quebec, Canada (Hereinafter: Licensor). This Agreement sets the terms, rights, restrictions and obligations on using [{software}] (hereinafter: The Software) created and owned by Licensor, as detailed herein
2. License Grant: Licensor hereby grants Licensee a Personal, Non-assignable &amp; non-transferable, Commercial, Royalty free, Including the rights to create but not distribute derivative works, Non-exclusive license, all with accordance with the terms set forth and other legal restrictions set forth in 3rd party software used while running Software.
2.1 Limited: Licensee may use Software for the purpose of:
- Running Software on Licensee's Website[s] and Server[s];
- Allowing 3rd Parties to run Software on Licensee's Website[s] and Server[s];
- Publishing Software&rsquo;s output to Licensee and 3rd Parties;
- Distribute verbatim copies of Software's output (including compiled binaries);
- Modify Software to suit Licensee&rsquo;s needs and specifications.
2.2 Binary Restricted: Licensee may sublicense Software as a part of a larger work containing more than Software, distributed solely in Object or Binary form under a personal, non-sublicensable, limited license. Such redistribution shall be limited to unlimited codebases.</li><li>
2.3 Non Assignable &amp; Non-Transferable: Licensee may not assign or transfer his rights and duties under this license.
2.4 Commercial, Royalty Free: Licensee may use Software for any purpose, including paid-services, without any royalties
2.5 Including the Right to Create Derivative Works: </strong>Licensee may create derivative works based on Software, including amending Software&rsquo;s source code, modifying it, integrating it into a larger work or removing portions of Software, as long as no distribution of the derivative works is made.
3. Term & Termination: The Term of this license shall be until terminated. Licensor may terminate this Agreement, including Licensee's license in the case where Licensee :
3.1 became insolvent or otherwise entered into any liquidation process; or
3.2 exported The Software to any jurisdiction where licensor may not enforce his rights under this agreements in; or
3.3 Licensee was in breach of any of this license's terms and conditions and such breach was not cured, immediately upon notification; or
3.4 Licensee in breach of any of the terms of clause 2 to this license; or
3.5 Licensee otherwise entered into any arrangement which caused Licensor to be unable to enforce his rights under this License.
4. Payment: In consideration of the License granted under clause 2, Licensee shall pay Licensor a FEE, via Credit-Card, PayPal or any other mean which Licensor may deem adequate. Failure to perform payment shall construe as material breach of this Agreement.
5. Upgrades, Updates and Fixes: Licensor may provide Licensee, from time to time, with Upgrades, Updates or Fixes, as detailed herein and according to his sole discretion. Licensee hereby warrants to keep The Software up-to-date and install all relevant updates and fixes, and may, at his sole discretion, purchase upgrades, according to the rates set by Licensor. Licensor shall provide any update or Fix free of charge; however, nothing in this Agreement shall require Licensor to provide Updates or Fixes.
5.1 Upgrades: for the purpose of this license, an Upgrade shall be a material amendment in The Software, which contains new features and or major performance improvements and shall be marked as a new version number. For example, should Licensee purchase The Software under version 1.X.X, an upgrade shall commence under number 2.0.0.
5.2 Updates: for the purpose of this license, an update shall be a minor amendment in The Software, which may contain new features or minor improvements and shall be marked as a new sub-version number. For example, should Licensee purchase The Software under version 1.1.X, an upgrade shall commence under number 1.2.0.
5.3 Fix: for the purpose of this license, a fix shall be a minor amendment in The Software, intended to remove bugs or alter minor features which impair the The Software's functionality. A fix shall be marked as a new sub-sub-version number. For example, should Licensee purchase Software under version 1.1.1, an upgrade shall commence under number 1.1.2.
6. Support: Software is provided under an AS-IS basis and without any support, updates or maintenance. Nothing in this Agreement shall require Licensor to provide Licensee with support or fixes to any bug, failure, mis-performance or other defect in The Software.
6.1 Bug Notification: Licensee may provide Licensor of details regarding any bug, defect or failure in The Software promptly and with no delay from such event; Licensee shall comply with Licensor's request for information regarding bugs, defects or failures and furnish him with information, screenshots and try to reproduce such bugs, defects or failures.
6.2 Feature Request: Licensee may request additional features in Software, provided, however, that (i) Licensee shall waive any claim or right in such feature should feature be developed by Licensor; (ii) Licensee shall be prohibited from developing the feature, or disclose such feature request, or feature, to any 3rd party directly competing with Licensor or any 3rd party which may be, following the development of such feature, in direct competition with Licensor; (iii) Licensee warrants that feature does not infringe any 3rd party patent, trademark, trade-secret or any other intellectual property right; and (iv) Licensee developed, envisioned or created the feature solely by himself.
7. Liability: To the extent permitted under Law, The Software is provided under an AS-IS basis. Licensor shall never, and without any limit, be liable for any damage, cost, expense or any other payment incurred by Licensee as a result of Software&rsquo;s actions, failure, bugs and/or any other interaction between The Software &nbsp;and Licensee&rsquo;s end-equipment, computers, other software or any 3rd party, end-equipment, computer or services. Moreover, Licensor shall never be liable for any defect in source code written by Licensee when relying on The Software or using The Software&rsquo;s source code.
8. Warranty:
8.1 Intellectual Property: Licensor hereby warrants that The Software does not violate or infringe any 3rd party claims in regards to intellectual property, patents and/or trademarks and that to the best of its knowledge no legal action has been taken against it for any infringement or violation of any 3rd party intellectual property rights.
8.2 No-Warranty: The Software is provided without any warranty; Licensor hereby disclaims any warranty that The Software shall be error free, without defects or code which may cause damage to Licensee&rsquo;s computers or to Licensee, and that Software shall be functional. Licensee shall be solely liable to any damage, defect or loss incurred as a result of operating software and undertake the risks contained in running The Software on License&rsquo;s Server[s] and Website[s].
The MIT License
8.3 Prior Inspection: Licensee hereby states that he inspected The Software thoroughly and found it satisfactory and adequate to his needs, that it does not interfere with his regular operation and that it does meet the standards and scope of his computer systems and architecture. Licensee found that The Software interacts with his development, website and server environment and that it does not infringe any of End User License Agreement of any software Licensee may use in performing his services. Licensee hereby waives any claims regarding The Software's incompatibility, performance, results and features, and warrants that he inspected the The Software.</p>
Copyright (c) 2013 Chris Scott, [Transistor Software](http://transistorsoft.com)
9. No Refunds: Licensee warrants that he inspected The Software according to clause 7(c) and that it is adequate to his needs. Accordingly, as The Software is intangible goods, Licensee shall not be, ever, entitled to any refund, rebate, compensation or restitution for any reason whatsoever, even if The Software contains material flaws.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
10. Indemnification: Licensee hereby warrants to hold Licensor harmless and indemnify Licensor for any lawsuit brought against it in regards to Licensee&rsquo;s use of The Software in means that violate, breach or otherwise circumvent this license, Licensor's intellectual property rights or Licensor's title in The Software. Licensor shall promptly notify Licensee in case of such legal action and request Licensee's consent prior to any settlement in relation to such lawsuit or claim.
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
11. Governing Law, Jurisdiction: Licensee hereby agrees not to initiate class-action lawsuits against Licensor in relation to this license and to compensate Licensor for any legal fees, cost or attorney fees should any claim brought by Licensee against Licensor be denied, in part or in full.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
/**
* cordova-background-geolocation
* Copyright (c) 2015, Transistor Software (9224-2932 Quebec Inc)
* All rights reserved.
* sales@transistorsoft.com
* http://transistorsoft.com
* @see LICENSE
*/
var ENV = (function() {
var localStorage = window.localStorage;
......@@ -124,8 +132,6 @@ var app = {
* This would be your own callback for Ajax-requests after POSTing background geolocation to your server.
*/
var yourAjaxCallback = function(response) {
// NB: It's important to inform BackgroundGeolocation when your callback is complete so it can terminate the native background-process which is currently running your callback.
// If you fail to execute #finish, the OS might kill your app for leaving a background-process running.
bgGeo.finish();
};
......@@ -133,7 +139,7 @@ var app = {
* This callback will be executed every time a geolocation is recorded in the background.
*/
var callbackFn = function(location) {
console.log('[js] BackgroundGeoLocation callback: ' + location.latitude + ',' + location.longitude);
console.log('[js] BackgroundGeoLocation callback: ' + JSON.stringify(location));
// Update our current-position marker.
app.setCurrentLocation(location);
......@@ -148,6 +154,7 @@ var app = {
// Only ios emits this stationary event
bgGeo.onStationary(function(location) {
console.log('[js] BackgroundGeoLocation onStationary ' + JSON.stringify(location));
if (!app.stationaryRadius) {
app.stationaryRadius = new google.maps.Circle({
fillColor: '#cc0000',
......@@ -165,14 +172,28 @@ var app = {
// 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,
locationUpdateInterval: 5000,
activityRecognitionInterval: 10000,
stopTimeout: 0,
forceReload: true, // <-- If the user closes the app **while location-tracking is started** , reboot app (WARNING: possibly distruptive to user)
stopOnTerminate: false, // <-- Allow the background-service to run headless when user closes the app.
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',
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.
......@@ -347,4 +368,4 @@ var app = {
}
};
app.initialize();
\ No newline at end of file
app.initialize();
......@@ -27,9 +27,27 @@
<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" />
-->
<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">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
</config-file>
<config-file target="AndroidManifest.xml" parent="/manifest">
......@@ -40,6 +58,10 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="com.google.android.gms.permission.ACTIVITY_RECOGNITION" />
<!-- Autorun your app on device BOOT. UNCOMMENT TO ENABLE AUTO-BOOT -->
<!-- <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> -->
</config-file>
<config-file target="res/xml/config.xml" parent="/*">
<feature name="BackgroundGeoLocation">
......
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
......@@ -8,358 +8,212 @@ import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONException;
import de.greenrobot.event.EventBus;
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.DetectedActivity;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationServices;
import com.google.android.gms.location.LocationListener;
import com.transistorsoft.cordova.bggeo.BackgroundGeolocationService.PaceChangeEvent;
import com.transistorsoft.cordova.bggeo.BackgroundGeolocationService.PausedEvent;
import com.transistorsoft.cordova.bggeo.BackgroundGeolocationService.StationaryLocation;
import de.greenrobot.event.EventBus;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.location.Location;
import android.os.Bundle;
import android.util.Log;
import android.media.AudioManager;
import android.media.ToneGenerator;
public class BackgroundGeolocationPlugin extends CordovaPlugin implements LocationListener, GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {
public class BackgroundGeolocationPlugin 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_ON_PACE_CHANGE = "onPaceChange";
public static final String ACTION_CONFIGURE = "configure";
public static final String ACTION_SET_CONFIG = "setConfig";
private PendingIntent locationUpdateService;
private Boolean isEnabled = false;
private Boolean isMoving = false;
public static final String ACTION_ON_STATIONARY = "addStationaryRegionListener";
// Common config
private Integer desiredAccuracy = 10;
private Integer stationaryRadius = 50;
private Float distanceFilter = (float) 50;
private Boolean isDebugging = false;
private Boolean stopOnTerminate = false;
// Android-only config
private Integer locationUpdateInterval = 60000;
private Integer activityRecognitionInterval = 60000;
private Boolean isEnabled = false;
private Boolean stopOnTerminate = false;
private Boolean isMoving = false;
private Intent backgroundServiceIntent;
private CallbackContext callback;
private GoogleApiClient googleApiClient;
private DetectedActivity currentActivity;
private static CordovaWebView gWebView;
private ToneGenerator toneGenerator;
// Geolocation callback
private CallbackContext locationCallback;
// Called when DetectedActivity is STILL
private CallbackContext stationaryCallback;
public static boolean isActive() {
return gWebView != null;
}
@Override
protected void pluginInitialize() {
Log.d("BUS","registering");
protected void pluginInitialize() {
gWebView = this.webView;
Activity activity = this.cordova.getActivity();
// Connect to google-play services.
if (ConnectionResult.SUCCESS == GooglePlayServicesUtil.isGooglePlayServicesAvailable(activity)) {
Log.i(TAG, "- Connecting to GooglePlayServices...");
googleApiClient = new GoogleApiClient.Builder(activity)
.addApi(LocationServices.API)
.addApi(ActivityRecognition.API)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
backgroundServiceIntent = new Intent(this.cordova.getActivity(), BackgroundGeolocationService.class);
googleApiClient.connect();
} else {
Log.e(TAG, "- GooglePlayServices unavailable");
}
// This is the IntentService we'll provide to google-play API.
locationUpdateService = PendingIntent.getService(activity, 0, new Intent(activity, BackgroundGeolocationService.class), PendingIntent.FLAG_UPDATE_CURRENT);
// Register for events fired by our IntentService "LocationService"
EventBus.getDefault().register(this);
}
public boolean execute(String action, JSONArray data, CallbackContext callbackContext) throws JSONException {
Log.d(TAG, "execute / action : " + action);
Boolean result = false;
Activity activity = this.cordova.getActivity();
Boolean result = false;
if (ACTION_START.equalsIgnoreCase(action) && !isEnabled) {
result = true;
isEnabled = true;
isMoving = false;
requestActivityUpdates();
if (!BackgroundGeolocationService.isInstanceCreated()) {
activity.startService(backgroundServiceIntent);
}
} else if (ACTION_STOP.equalsIgnoreCase(action)) {
result = true;
isEnabled = false;
isMoving = false;
removeLocationUpdates();
removeActivityUpdates();
isEnabled = false;
activity.stopService(backgroundServiceIntent);
callbackContext.success();
} else if (ACTION_CONFIGURE.equalsIgnoreCase(action)) {
result = true;
try {
JSONObject config = data.getJSONObject(0);
Log.i(TAG, "- configure: " + config.toString());
stationaryRadius = config.getInt("stationaryRadius");
distanceFilter = (float) config.getInt("distanceFilter");
desiredAccuracy = config.getInt("desiredAccuracy");
locationUpdateInterval = config.getInt("locationUpdateInterval");
activityRecognitionInterval = config.getInt("activityRecognitionInterval");
isDebugging = config.getBoolean("debug");
stopOnTerminate = config.getBoolean("stopOnTerminate");
if (isDebugging) {
toneGenerator = new ToneGenerator(AudioManager.STREAM_NOTIFICATION, 100);
}
this.callback = callbackContext;
} catch (JSONException e) {
callbackContext.error("Configuration error " + e.getMessage());
result = applyConfig(data);
if (result) {
this.locationCallback = callbackContext;
} else {
callbackContext.error("- Configuration error!");
}
} else if (ACTION_ON_PACE_CHANGE.equalsIgnoreCase(action)) {
if (!isEnabled) {
Log.w(TAG, "- Cannot change pace while in #stop mode");
Log.w(TAG, "- Cannot change pace while disabled");
result = false;
callbackContext.error("Cannot #changePace while in #stop mode");
} else {
callbackContext.error("Cannot #changePace while disabled");
} else {
PaceChangeEvent event = new PaceChangeEvent(data.getBoolean(0));
EventBus.getDefault().post(event);
result = true;
isMoving = data.getBoolean(0);
callbackContext.success();
}
} else if (ACTION_SET_CONFIG.equalsIgnoreCase(action)) {
result = true;
activity.stopService(backgroundServiceIntent);
result = applyConfig(data);
// TODO reconfigure Service
callbackContext.success();
if (result) {
activity.startService(backgroundServiceIntent);
callbackContext.success();
} else {
callbackContext.error("- Configuration error!");
}
} else if (ACTION_ON_STATIONARY.equalsIgnoreCase(action)) {
result = true;
this.stationaryCallback = callbackContext;
}
return result;
}
/**
* 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;
private boolean applyConfig(JSONArray data) {
try {
JSONObject config = data.getJSONObject(0);
Log.i(TAG, "- configure: " + config.toString());
backgroundServiceIntent.putExtra("isMoving", isMoving);
if (config.has("distanceFilter")) {
backgroundServiceIntent.putExtra("distanceFilter", (float) config.getInt("distanceFilter"));
}
if (config.has("desiredAccuracy")) {
backgroundServiceIntent.putExtra("desiredAccuracy", config.getInt("desiredAccuracy"));
}
if (config.has("locationUpdateInterval")) {
backgroundServiceIntent.putExtra("locationUpdateInterval", config.getInt("locationUpdateInterval"));
}
if (config.has("activityRecognitionInterval")) {
backgroundServiceIntent.putExtra("activityRecognitionInterval", config.getInt("activityRecognitionInterval"));
}
if (config.has("stopTimeout")) {
backgroundServiceIntent.putExtra("stopTimeout", config.getLong("stopTimeout"));
}
if (config.has("debug")) {
backgroundServiceIntent.putExtra("debug", config.getBoolean("debug"));
}
if (config.has("stopOnTerminate")) {
stopOnTerminate = config.getBoolean("stopOnTerminate");
backgroundServiceIntent.putExtra("stopOnTerminate", config.getBoolean("stopOnTerminate"));
}
if (config.has("forceReload")) {
backgroundServiceIntent.putExtra("forceReload", config.getBoolean("forceReload"));
}
if (config.has("url")) {
backgroundServiceIntent.putExtra("url", config.getString("url"));
}
if (config.has("params")) {
backgroundServiceIntent.putExtra("params", config.getString("params"));
}
if (config.has("headers")) {
backgroundServiceIntent.putExtra("headers", config.getString("headers"));
}
return true;
} catch (JSONException e) {
return false;
}
return accuracy;
}
public static boolean isActive() {
return gWebView != null;
}
}
public void onPause(boolean multitasking) {
Log.i(TAG, "- onPause");
if (isEnabled) {
setPace(isMoving);
EventBus.getDefault().post(new PausedEvent(true));
}
}
public void onResume(boolean multitasking) {
Log.i(TAG, "- onResume");
if (isEnabled) {
removeLocationUpdates();
EventBus.getDefault().post(new PausedEvent(false));
}
}
private void setPace(Boolean moving) {
Log.i(TAG, "- setPace: " + moving);
isMoving = moving;
if (moving && isEnabled) {
LocationRequest request = LocationRequest.create()
.setPriority(translateDesiredAccuracy(desiredAccuracy))
.setInterval(this.locationUpdateInterval)
.setFastestInterval(30000)
.setSmallestDisplacement(distanceFilter);
requestLocationUpdates(request);
} else {
removeLocationUpdates();
}
}
public void onEventMainThread(DetectedActivity probableActivity) {
currentActivity = probableActivity;
Boolean wasMoving = isMoving;
switch (probableActivity.getType()) {
case DetectedActivity.IN_VEHICLE:
case DetectedActivity.ON_BICYCLE:
case DetectedActivity.ON_FOOT:
isMoving = true;
break;
case DetectedActivity.STILL:
isMoving = false;
break;
case DetectedActivity.UNKNOWN:
break;
case DetectedActivity.TILTING:
break;
}
if (!wasMoving && isMoving) {
startTone("doodly_doo");
setPace(isMoving);
} else if (wasMoving && !isMoving) {
startTone("long_beep");
setPace(isMoving);
}
String probableActivityName = getActivityName(probableActivity.getType());
Log.w(TAG, "- DetectedActivity: " + probableActivityName + ", confidence: " + probableActivity.getConfidence());
}
/**
* EventBus listener
* @param {Location} location
*/
public void onEventMainThread(Location location) {
Log.i(TAG, "BUS Rx:" + location.toString());
startTone("beep");
try {
JSONObject loc = new JSONObject();
loc.put("latitude", location.getLatitude());
loc.put("longitude", location.getLongitude());
loc.put("accuracy", location.getAccuracy());
loc.put("speed", location.getSpeed());
loc.put("bearing", location.getBearing());
loc.put("altitude", location.getAltitude());
loc.put("timestamp", location.getTime());
PluginResult result = new PluginResult(PluginResult.Status.OK, loc);
result.setKeepCallback(true);
if(callback != null){
callback.sendPluginResult(result);
PluginResult result = new PluginResult(PluginResult.Status.OK, BackgroundGeolocationService.locationToJson(location));
result.setKeepCallback(true);
if (location instanceof StationaryLocation) {
isMoving = false;
if (stationaryCallback != null) {
runInBackground(stationaryCallback, result);
}
} catch (JSONException e) {
Log.e(TAG, "could not parse location");
}
}
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.STILL:
return "still";
case DetectedActivity.UNKNOWN:
return "unknown";
case DetectedActivity.TILTING:
return "tilting";
} else {
isMoving = true;
result.setKeepCallback(true);
runInBackground(locationCallback, result);
}
return "unknown";
}
private void requestActivityUpdates() {
ActivityRecognition.ActivityRecognitionApi.requestActivityUpdates(googleApiClient, activityRecognitionInterval, locationUpdateService);
}
private void removeActivityUpdates() {
ActivityRecognition.ActivityRecognitionApi.removeActivityUpdates(googleApiClient, locationUpdateService);
}
private void requestLocationUpdates(LocationRequest request) {
LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, request, locationUpdateService);
}
private void removeLocationUpdates() {
LocationServices.FusedLocationApi.removeLocationUpdates(googleApiClient, locationUpdateService);
}
/**
* Plays debug sound
* @param name
* Run a javascript callback in Background
* @param cb
* @param result
*/
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);
private void runInBackground(final CallbackContext cb, final PluginResult result) {
if(cb != null){
cordova.getThreadPool().execute(new Runnable() {
public void run() {
cb.sendPluginResult(result);
}
});
}
}
@Override
public void onLocationChanged(Location arg0) {
// TODO Auto-generated method stub
}
public void onConnectionFailed(ConnectionResult arg0) {
// TODO Auto-generated method stub
}
public void onConnected(Bundle arg0) {
// TODO Auto-generated method stub
Log.i(TAG, "- onConnected");
}
public void onConnectionSuspended(int arg0) {
// TODO Auto-generated method stub
}
/**
* Override method in CordovaPlugin.
* Checks to see if it should turn off
*/
public void onDestroy() {
if(isEnabled && stopOnTerminate || !isEnabled) {
removeActivityUpdates();
removeLocationUpdates();
Log.i(TAG, "- onDestroy");
Log.i(TAG, " stopOnTerminate: " + stopOnTerminate);
Log.i(TAG, " isEnabled: " + isEnabled);
if(isEnabled && stopOnTerminate) {
this.cordova.getActivity().stopService(backgroundServiceIntent);
}
}
}
package com.transistorsoft.cordova.bggeo;
import de.greenrobot.event.EventBus;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
import com.google.android.gms.location.DetectedActivity;
import com.google.android.gms.location.FusedLocationProviderApi;
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 android.app.IntentService;
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.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 IntentService {
private static final String TAG = "BackgroundGeolocationService";
public BackgroundGeolocationService() {
super("com.transistorsoft.cordova.bggeo.BackgroundGeolocationService");
}
@Override
protected void onHandleIntent(Intent intent) {
if (ActivityRecognitionResult.hasResult(intent)) {
ActivityRecognitionResult result = ActivityRecognitionResult.extractResult(intent);
DetectedActivity probableActivity = result.getMostProbableActivity();
Log.i(TAG, "Activity detected:" + getActivityName(probableActivity.getType()) + ", confidence:" + probableActivity.getConfidence());
if (probableActivity.getConfidence() < 80) {
return;
}
Boolean isMoving = false;
switch (probableActivity.getType()) {
case DetectedActivity.IN_VEHICLE:
case DetectedActivity.ON_BICYCLE:
case DetectedActivity.ON_FOOT:
case DetectedActivity.RUNNING:
isMoving = true;
break;
case DetectedActivity.STILL:
break;
case DetectedActivity.UNKNOWN:
break;
case DetectedActivity.TILTING:
break;
}
boolean isPushPluginActive = BackgroundGeolocationPlugin.isActive();
if (isMoving && !isPushPluginActive) {
forceMainActivityReload();
}
EventBus.getDefault().post(probableActivity);
} else {
final Location location = intent.getParcelableExtra(FusedLocationProviderApi.KEY_LOCATION_CHANGED);
if (location != null) {
Log.i(TAG, "Location received: " + location.toString());
boolean isPushPluginActive = BackgroundGeolocationPlugin.isActive();
if (!isPushPluginActive) {
forceMainActivityReload();
}
EventBus.getDefault().post(location);
}
}
}
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.STILL:
return "still";
case DetectedActivity.UNKNOWN:
return "unknown";
case DetectedActivity.TILTING:
return "tilting";
}
return "unknown";
}
/**
* Forces the main activity to re-launch if it's unloaded.
*/
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);
}
}
\ No newline at end of file
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 = (float) 50;
/**
* @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.getDefault().register(this);
isEnabled = true;
stopOnTerminate = intent.getBooleanExtra("stopOnTerminate", true);
isDebugging = intent.getBooleanExtra("debug", false);
distanceFilter = intent.getFloatExtra("distanceFilter", 50);
desiredAccuracy = intent.getIntExtra("desiredAccuracy", 10);
locationUpdateInterval = intent.getIntExtra("locationUpdateInterval", 30000);
activityRecognitionInterval = intent.getIntExtra("activityRecognitionInterval", 60000);
stopTimeout = intent.getLongExtra("stopTimeout", 0);
forceReload = intent.getBooleanExtra("forceReload", false);
isMoving = intent.getBooleanExtra("isMoving", false);
// HTTP Configuration
url = intent.getStringExtra("url");
try {
if (intent.hasExtra("params")) {
params = new JSONObject(intent.getStringExtra("params"));
}
if (intent.hasExtra("headers")) {
headers = new JSONObject(intent.getStringExtra("headers"));
}
} catch (JSONException e) {
e.printStackTrace();
}
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_REDELIVER_INTENT;
}
@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);
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.
return 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() {
ActivityRecognition.ActivityRecognitionApi.requestActivityUpdates(googleApiClient, 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
// Configure LocationRequest
locationRequest = LocationRequest.create()
.setPriority(translateDesiredAccuracy(desiredAccuracy))
.setInterval(getLocationUpdateInterval())
.setFastestInterval(getFastestLocationUpdateInterval())
.setSmallestDisplacement(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);
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.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";
/**
* Background Geolocation Configuration params.
* If you're auto-running the service on BOOT, you need to manually configure the params here since the foreground app will not have been booted.
*/
private float distanceFilter = 50;
private Integer desiredAccuracy = 0;
private Integer locationUpdateInterval = 5000;
private Integer activityRecognitionInterval = 10000;
private long stopTimeout = 0;
private boolean debug = true;
private boolean stopOnTerminate = false;
private boolean forceReload = false;
private String url = "http://posttestserver.com/post.php?dir=cordova-background-geolocation";
private String params = "{'foo':'bar'}";
private String headers = "{'X-FOO':'BAR'}";
@Override
public void onReceive(Context context, Intent intent) {
Log.i(TAG, "- BootReceiver booting service");
Intent backgroundServiceIntent = new Intent(context, BackgroundGeolocationService.class);
// Configure background geolocation service params.
backgroundServiceIntent.putExtra("distanceFilter", distanceFilter);
backgroundServiceIntent.putExtra("desiredAccuracy", desiredAccuracy);
backgroundServiceIntent.putExtra("locationUpdateInterval", locationUpdateInterval);
backgroundServiceIntent.putExtra("activityRecognitionInterval", activityRecognitionInterval);
backgroundServiceIntent.putExtra("stopTimeout", stopTimeout);
backgroundServiceIntent.putExtra("debug", debug);
backgroundServiceIntent.putExtra("stopOnTerminate", stopOnTerminate);
backgroundServiceIntent.putExtra("forceReload", forceReload);
backgroundServiceIntent.putExtra("url", url);
backgroundServiceIntent.putExtra("params", params);
backgroundServiceIntent.putExtra("headers", headers);
// Start the service.
context.startService(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
/**
* cordova-background-geolocation
* Copyright (c) 2015, Transistor Software (9224-2932 Quebec Inc)
* All rights reserved.
* sales@transistorsoft.com
* http://transistorsoft.com
* @see LICENSE
*/
var exec = require("cordova/exec");
module.exports = {
/**
......
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