Commit 9504eac7 authored by Chris Scott's avatar Chris Scott

Re-implement android background-service. Migrate control out of the...

Re-implement android background-service.  Migrate control out of the CordovaPlugin and into BackgroundGeolocationService.  This is required for those who may still wish to have the plugin transmit locations to server via HTTP.  Re-implement HTTP layer with same options as free plugin (namely url, params, headers).  There is no persistence layer currently, so when in-plugin HTTP is enabled, the plugin won't re-try when no network is detected.
parent 8dca2e04
...@@ -30,6 +30,8 @@ ...@@ -30,6 +30,8 @@
<config-file target="AndroidManifest.xml" parent="/manifest/application"> <config-file target="AndroidManifest.xml" parent="/manifest/application">
<service android:name="com.transistorsoft.cordova.bggeo.BackgroundGeolocationService" /> <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 --> <!-- autorun on boot receiver -->
<receiver android:name="com.transistorsoft.cordova.bggeo.BootReceiver" android:enabled="true" android:exported="false"> <receiver android:name="com.transistorsoft.cordova.bggeo.BootReceiver" android:enabled="true" android:exported="false">
......
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.i(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; package com.transistorsoft.cordova.bggeo;
import java.util.concurrent.TimeUnit;
import org.apache.cordova.CallbackContext; import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin; import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView; import org.apache.cordova.CordovaWebView;
...@@ -9,31 +8,18 @@ import org.json.JSONArray; ...@@ -9,31 +8,18 @@ import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import org.json.JSONException; import org.json.JSONException;
import de.greenrobot.event.EventBus; import com.transistorsoft.cordova.bggeo.BackgroundGeolocationService.StationaryLocation;
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 com.google.android.gms.location.LocationListener;
import de.greenrobot.event.EventBus;
import android.app.Activity; import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent; import android.content.Intent;
import android.location.Location; import android.location.Location;
import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.media.AudioManager; public class BackgroundGeolocationPlugin extends CordovaPlugin {
import android.media.ToneGenerator;
public class BackgroundGeolocationPlugin extends CordovaPlugin implements LocationListener, GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {
private static final String TAG = "BackgroundGeolocation"; 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_START = "start";
public static final String ACTION_STOP = "stop"; public static final String ACTION_STOP = "stop";
...@@ -42,66 +28,24 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Locati ...@@ -42,66 +28,24 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Locati
public static final String ACTION_SET_CONFIG = "setConfig"; public static final String ACTION_SET_CONFIG = "setConfig";
public static final String ACTION_ON_STATIONARY = "addStationaryRegionListener"; public static final String ACTION_ON_STATIONARY = "addStationaryRegionListener";
private PendingIntent locationUpdateService;
private Boolean isEnabled = false; private Boolean isEnabled = false;
private Boolean isMoving = false;
// Common config
private Integer desiredAccuracy = 10;
private Float distanceFilter = (float) 50;
private Boolean isDebugging = false;
private Boolean stopOnTerminate = false; private Boolean stopOnTerminate = false;
// Android-only config private Intent backgroundServiceIntent;
private Integer locationUpdateInterval = 60000;
private Integer activityRecognitionInterval = 60000;
/**
* @config {Integer} stopTimeout The time to wait after ARS STILL to turn of GPS
*/
private long stopTimeout = 0;
// The elapsed millis when the ARS detected STILL
private long stoppedAt = 0;
// Geolocation callback // Geolocation callback
private CallbackContext locationCallback; private CallbackContext locationCallback;
// Called when DetectedActivity is STILL // Called when DetectedActivity is STILL
private CallbackContext stationaryCallback; private CallbackContext stationaryCallback;
private Location stationaryLocation;
private GoogleApiClient googleApiClient; private Location stationaryLocation;
private DetectedActivity currentActivity;
private static CordovaWebView gWebView;
private ToneGenerator toneGenerator;
@Override @Override
protected void pluginInitialize() { protected void pluginInitialize() {
gWebView = this.webView; 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();
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" // Register for events fired by our IntentService "LocationService"
EventBus.getDefault().register(this); EventBus.getDefault().register(this);
} }
...@@ -114,26 +58,18 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Locati ...@@ -114,26 +58,18 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Locati
if (ACTION_START.equalsIgnoreCase(action) && !isEnabled) { if (ACTION_START.equalsIgnoreCase(action) && !isEnabled) {
result = true; result = true;
isEnabled = true; isEnabled = true;
if (!BackgroundGeolocationService.isInstanceCreated()) {
if (googleApiClient.isConnected()) { this.cordova.getActivity().startService(backgroundServiceIntent);
requestActivityUpdates();
} }
} else if (ACTION_STOP.equalsIgnoreCase(action)) { } else if (ACTION_STOP.equalsIgnoreCase(action)) {
result = true; result = true;
isEnabled = false; isEnabled = false;
isMoving = false; this.cordova.getActivity().stopService(backgroundServiceIntent);
removeLocationUpdates();
removeActivityUpdates();
callbackContext.success(); callbackContext.success();
} else if (ACTION_CONFIGURE.equalsIgnoreCase(action)) { } else if (ACTION_CONFIGURE.equalsIgnoreCase(action)) {
result = applyConfig(data); result = applyConfig(data);
if (result) { if (result) {
this.locationCallback = callbackContext; this.locationCallback = callbackContext;
if (isDebugging) {
toneGenerator = new ToneGenerator(AudioManager.STREAM_NOTIFICATION, 100);
}
} else { } else {
callbackContext.error("- Configuration error!"); callbackContext.error("- Configuration error!");
} }
...@@ -144,13 +80,14 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Locati ...@@ -144,13 +80,14 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Locati
callbackContext.error("Cannot #changePace while in #stop mode"); callbackContext.error("Cannot #changePace while in #stop mode");
} else { } else {
result = true; result = true;
isMoving = data.getBoolean(0);
callbackContext.success(); callbackContext.success();
} }
} else if (ACTION_SET_CONFIG.equalsIgnoreCase(action)) { } else if (ACTION_SET_CONFIG.equalsIgnoreCase(action)) {
this.cordova.getActivity().stopService(backgroundServiceIntent);
result = applyConfig(data); result = applyConfig(data);
// TODO reconfigure Service // TODO reconfigure Service
if (result) { if (result) {
this.cordova.getActivity().stopService(backgroundServiceIntent);
callbackContext.success(); callbackContext.success();
} else { } else {
callbackContext.error("- Configuration error!"); callbackContext.error("- Configuration error!");
...@@ -163,60 +100,54 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Locati ...@@ -163,60 +100,54 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Locati
return result; return result;
} }
private boolean applyConfig(JSONArray data) { private boolean applyConfig(JSONArray data) {
// This is the IntentService we'll provide to google-play API.
Activity activity = this.cordova.getActivity();
backgroundServiceIntent = new Intent(activity, BackgroundGeolocationService.class);
try { try {
JSONObject config = data.getJSONObject(0); JSONObject config = data.getJSONObject(0);
Log.i(TAG, "- configure: " + config.toString()); Log.i(TAG, "- configure: " + config.toString());
if (config.has("distanceFilter")) { if (config.has("distanceFilter")) {
distanceFilter = (float) config.getInt("distanceFilter"); backgroundServiceIntent.putExtra("distanceFilter", (float) config.getInt("distanceFilter"));
} }
if (config.has("desiredAccuracy")) { if (config.has("desiredAccuracy")) {
desiredAccuracy = config.getInt("desiredAccuracy"); backgroundServiceIntent.putExtra("desiredAccuracy", config.getInt("desiredAccuracy"));
} }
if (config.has("locationUpdateInterval")) { if (config.has("locationUpdateInterval")) {
locationUpdateInterval = config.getInt("locationUpdateInterval"); backgroundServiceIntent.putExtra("locationUpdateInterval", config.getInt("locationUpdateInterval"));
} }
if (config.has("activityRecognitionInterval")) { if (config.has("activityRecognitionInterval")) {
activityRecognitionInterval = config.getInt("activityRecognitionInterval"); backgroundServiceIntent.putExtra("activityRecognitionInterval", config.getInt("activityRecognitionInterval"));
} }
if (config.has("stopTimeout")) { if (config.has("stopTimeout")) {
stopTimeout = config.getLong("stopTimeout"); backgroundServiceIntent.putExtra("stopTimeout", config.getLong("stopTimeout"));
} }
if (config.has("debug")) { if (config.has("debug")) {
isDebugging = config.getBoolean("debug"); backgroundServiceIntent.putExtra("debug", config.getBoolean("debug"));
} }
if (config.has("stopOnTerminate")) { if (config.has("stopOnTerminate")) {
stopOnTerminate = config.getBoolean("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; return true;
} catch (JSONException e) { } catch (JSONException e) {
return false; return false;
} }
} }
/**
* 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;
}
public static boolean isActive() { public static boolean isActive() {
return gWebView != null; return gWebView != null;
...@@ -225,108 +156,26 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Locati ...@@ -225,108 +156,26 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Locati
public void onPause(boolean multitasking) { public void onPause(boolean multitasking) {
Log.i(TAG, "- onPause"); Log.i(TAG, "- onPause");
if (isEnabled) { if (isEnabled) {
setPace(isMoving); //setPace(isMoving);
} }
} }
public void onResume(boolean multitasking) { public void onResume(boolean multitasking) {
Log.i(TAG, "- onResume"); Log.i(TAG, "- onResume");
if (isEnabled) { if (isEnabled) {
removeLocationUpdates(); //removeLocationUpdates();
} }
} }
private void setPace(Boolean moving) { public void onEventMainThread(Location location) {
Log.i(TAG, "- setPace: " + moving); if (location instanceof StationaryLocation) {
boolean wasMoving = isMoving; stationaryLocation = location;
isMoving = moving;
if (moving && isEnabled) {
if (!wasMoving) {
startTone("doodly_doo");
}
stationaryLocation = null;
// Here's where the FusedLocationProvider is controlled.
LocationRequest request = LocationRequest.create()
.setPriority(translateDesiredAccuracy(desiredAccuracy))
.setInterval(this.locationUpdateInterval)
.setFastestInterval(30000)
.setSmallestDisplacement(distanceFilter);
requestLocationUpdates(request);
} else {
removeLocationUpdates();
if (stationaryLocation == null) {
startTone("long_beep");
// Re-set our stationaryLocation
stationaryLocation = LocationServices.FusedLocationApi.getLastLocation(googleApiClient);
// Inform Javascript of our stationaryLocation
fireStationaryListener(); fireStationaryListener();
}
}
}
public void onEventMainThread(ActivityRecognitionResult result) {
currentActivity = result.getMostProbableActivity();
String probableActivityName = getActivityName(currentActivity.getType());
Log.w(TAG, "- DetectedActivity: " + probableActivityName + ", confidence: " + currentActivity.getConfidence());
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:
nowMoving = isMoving;
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 { } else {
return; PluginResult result = new PluginResult(PluginResult.Status.OK, BackgroundGeolocationService.locationToJson(location));
}
}
stoppedAt = 0;
if ( startedMoving || justStopped || initialState ) {
setPace(nowMoving);
}
}
public void onEventMainThread(Location location) {
Log.i(TAG, "BUS Rx:" + location.toString());
startTone("beep");
PluginResult result = new PluginResult(PluginResult.Status.OK, locationToJson(location));
result.setKeepCallback(true); result.setKeepCallback(true);
runInBackground(locationCallback, result); runInBackground(locationCallback, result);
} }
}
/** /**
* Execute onStationary javascript callback when device is determined to have just stopped * Execute onStationary javascript callback when device is determined to have just stopped
...@@ -334,34 +183,12 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Locati ...@@ -334,34 +183,12 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Locati
private void fireStationaryListener() { private void fireStationaryListener() {
Log.i(TAG, "- fire stationary listener"); Log.i(TAG, "- fire stationary listener");
if ( (stationaryCallback != null) && (stationaryLocation != null) ) { if ( (stationaryCallback != null) && (stationaryLocation != null) ) {
final PluginResult result = new PluginResult(PluginResult.Status.OK, locationToJson(stationaryLocation)); final PluginResult result = new PluginResult(PluginResult.Status.OK, BackgroundGeolocationService.locationToJson(stationaryLocation));
result.setKeepCallback(true); result.setKeepCallback(true);
runInBackground(stationaryCallback, result); runInBackground(stationaryCallback, result);
} }
} }
/**
* Convert a Location instance to JSONObject
* @param Location
* @return JSONObject
*/
private 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;
}
}
/** /**
* Run a javascript callback in Background * Run a javascript callback in Background
* @param cb * @param cb
...@@ -377,96 +204,6 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Locati ...@@ -377,96 +204,6 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Locati
} }
} }
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 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
*/
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 onLocationChanged(Location arg0) {
// TODO Auto-generated method stub
}
public void onConnectionFailed(ConnectionResult arg0) {
// TODO Auto-generated method stub
Log.i(TAG, "- onConnectionFailed");
}
public void onConnected(Bundle arg0) {
// TODO Auto-generated method stub
Log.i(TAG, "- onConnected");
if (isEnabled) {
requestActivityUpdates();
}
}
public void onConnectionSuspended(int arg0) {
// TODO Auto-generated method stub
}
/** /**
* Override method in CordovaPlugin. * Override method in CordovaPlugin.
* Checks to see if it should turn off * Checks to see if it should turn off
...@@ -476,9 +213,8 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Locati ...@@ -476,9 +213,8 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Locati
Log.i(TAG, " stopOnTerminate: " + stopOnTerminate); Log.i(TAG, " stopOnTerminate: " + stopOnTerminate);
Log.i(TAG, " isEnabled: " + isEnabled); Log.i(TAG, " isEnabled: " + isEnabled);
if(isEnabled && stopOnTerminate || !isEnabled) { if(isEnabled && stopOnTerminate) {
removeActivityUpdates(); this.cordova.getActivity().stopService(backgroundServiceIntent);
removeLocationUpdates();
} }
} }
} }
package com.transistorsoft.cordova.bggeo; 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 org.apache.http.HttpResponse;
import com.google.android.gms.location.FusedLocationProviderApi; 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.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.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.location.Location; 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.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.util.Log; import android.util.Log;
public class BackgroundGeolocationService extends IntentService { public class BackgroundGeolocationService extends Service implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {
private static BackgroundGeolocationService instance = null;
public static boolean isInstanceCreated() {
return instance != null;
}
private static final String TAG = "BackgroundGeolocation";
private WakeLock wakeLock;
private PendingIntent activityRecognitionPI;
private PendingIntent locationUpdatePI;
private Boolean isEnabled = false;
private Boolean isMoving = false;
// Common config
private Integer desiredAccuracy = 10;
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 forceReload = false;
private String url = null;
private JSONObject params = new JSONObject();
private JSONObject headers = null;
/**
* @config {Integer} stopTimeout The time to wait after ARS STILL to turn of GPS
*/
private long stopTimeout = 0;
// The elapsed millis when the ARS detected STILL
private long stoppedAt = 0;
private Location stationaryLocation;
private static final String TAG = "BackgroundGeolocationService"; private GoogleApiClient googleApiClient;
private DetectedActivity currentActivity;
public BackgroundGeolocationService() { private ToneGenerator toneGenerator;
super("com.transistorsoft.cordova.bggeo.BackgroundGeolocationService");
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
instance = this;
EventBus.getDefault().register(this);
isEnabled = 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", 10000);
stopTimeout = intent.getLongExtra("stopTimeout", 0);
forceReload = intent.getBooleanExtra("forceReload", false);
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, "----------------------------------------");
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");
}
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
wakeLock.acquire();
return Service.START_REDELIVER_INTENT;
} }
@Override @Override
protected void onHandleIntent(Intent intent) { public IBinder onBind(Intent intent) {
// Determine whether the fore-ground Activity is running. If it's not, we'll reboot it. // TODO Auto-generated method stub
boolean isPluginActive = BackgroundGeolocationPlugin.isActive(); return null;
}
if (ActivityRecognitionResult.hasResult(intent)) { @Override
ActivityRecognitionResult result = ActivityRecognitionResult.extractResult(intent); public void onConnectionFailed(ConnectionResult arg0) {
DetectedActivity probableActivity = result.getMostProbableActivity(); // TODO Auto-generated method stub
Log.i(TAG, "Activity detected:" + getActivityName(probableActivity.getType()) + ", confidence:" + probableActivity.getConfidence());
if (probableActivity.getConfidence() < 80) {
return;
} }
Boolean isMoving = false; @Override
switch (probableActivity.getType()) { public void onConnected(Bundle arg0) {
// TODO Auto-generated method stub
Intent arsIntent = new Intent(this, ActivityRecognitionService.class);
activityRecognitionPI = PendingIntent.getService(this, 0, arsIntent, PendingIntent.FLAG_UPDATE_CURRENT);
Log.i(TAG, "- GooglePlayServices connected");
Intent locationIntent = new Intent(this, LocationService.class);
locationUpdatePI = PendingIntent.getService(this, 0, locationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
if (googleApiClient.isConnected()) {
requestActivityUpdates();
}
}
public void onEventMainThread(ActivityRecognitionResult result) {
currentActivity = result.getMostProbableActivity();
String probableActivityName = getActivityName(currentActivity.getType());
Log.w(TAG, "- DetectedActivity: " + probableActivityName + ", confidence: " + currentActivity.getConfidence());
boolean wasMoving = isMoving;
boolean nowMoving = false;
switch (currentActivity.getType()) {
case DetectedActivity.IN_VEHICLE: case DetectedActivity.IN_VEHICLE:
case DetectedActivity.ON_BICYCLE: case DetectedActivity.ON_BICYCLE:
case DetectedActivity.ON_FOOT: case DetectedActivity.ON_FOOT:
case DetectedActivity.WALKING:
case DetectedActivity.RUNNING: case DetectedActivity.RUNNING:
isMoving = true; case DetectedActivity.WALKING:
nowMoving = true;
break; break;
case DetectedActivity.STILL: case DetectedActivity.STILL:
nowMoving = false;
break; break;
case DetectedActivity.UNKNOWN: case DetectedActivity.UNKNOWN:
break;
case DetectedActivity.TILTING: case DetectedActivity.TILTING:
break; nowMoving = isMoving;
return;
} }
// Force main-activity reload (if not running) if we're detected to be moving. boolean startedMoving = !wasMoving && nowMoving;
if (isMoving && !isPluginActive) { boolean justStopped = wasMoving && !nowMoving;
forceMainActivityReload(); 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;
} }
// Post activity to the bus. // If we're using a stopTimeout, compare the current activity's timestamp with the 1st recorded STILL event.
EventBus.getDefault().post(result); 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 { } else {
final Location location = intent.getParcelableExtra(FusedLocationProviderApi.KEY_LOCATION_CHANGED); return;
if (location != null) { }
Log.i(TAG, "Location received: " + location.toString()); }
stoppedAt = 0;
if ( startedMoving || justStopped || initialState ) {
setPace(nowMoving);
}
}
// Force main-activity reload when a location comes in. public void onEventMainThread(Location location) {
if (!isPluginActive) { if (location instanceof StationaryLocation) {
return;
}
Log.i(TAG, "BUS Rx:" + location.toString());
startTone("beep");
Log.i(TAG, "- forceReload: " + BackgroundGeolocationPlugin.forceReload);
// Force main-activity reload (if not running) if we're detected to be moving.
boolean isPluginActive = BackgroundGeolocationPlugin.isActive();
if (forceReload && !isPluginActive) {
forceMainActivityReload(); forceMainActivityReload();
} }
EventBus.getDefault().post(location); 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;
// Here's where the FusedLocationProvider is controlled.
LocationRequest request = LocationRequest.create()
.setPriority(translateDesiredAccuracy(desiredAccuracy))
.setInterval(this.locationUpdateInterval)
.setFastestInterval(30000)
.setSmallestDisplacement(distanceFilter);
requestLocationUpdates(request);
} else {
removeLocationUpdates();
if (stationaryLocation == null) {
startTone("long_beep");
// Re-set our stationaryLocation
stationaryLocation = LocationServices.FusedLocationApi.getLastLocation(googleApiClient);
stationaryLocation.removeSpeed();
// Inform Javascript of our stationaryLocation
StationaryLocation l = new BackgroundGeolocationService.StationaryLocation(stationaryLocation);
EventBus.getDefault().post(l);
//fireStationaryListener();
}
}
}
/** /**
* This method has no other purpose than formatting the Activity for log-messages * 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 String getActivityName(int activityType) { private String getActivityName(int activityType) {
switch (activityType) { switch (activityType) {
case DetectedActivity.IN_VEHICLE: case DetectedActivity.IN_VEHICLE:
...@@ -96,6 +336,64 @@ public class BackgroundGeolocationService extends IntentService { ...@@ -96,6 +336,64 @@ public class BackgroundGeolocationService extends IntentService {
return "unknown"; 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);
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 * 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. * running always, since we for the app to boot.
...@@ -109,4 +407,122 @@ public class BackgroundGeolocationService extends IntentService { ...@@ -109,4 +407,122 @@ public class BackgroundGeolocationService extends IntentService {
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); launchIntent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(launchIntent); startActivity(launchIntent);
} }
private void requestActivityUpdates() {
ActivityRecognition.ActivityRecognitionApi.requestActivityUpdates(googleApiClient, activityRecognitionInterval, activityRecognitionPI);
}
private void removeActivityUpdates() {
ActivityRecognition.ActivityRecognitionApi.removeActivityUpdates(googleApiClient, activityRecognitionPI);
}
private void requestLocationUpdates(LocationRequest request) {
LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, request, 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, "--------------------------------------");
Log.w(TAG, "- Destroy service");
Log.w(TAG, "--------------------------------------");
cleanUp();
super.onDestroy();
}
private void cleanUp() {
instance = null;
EventBus.getDefault().unregister(this);
removeActivityUpdates();
removeLocationUpdates();
stopSelf();
wakeLock.release();
}
/**
* 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;
}
}
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");
}
}
} }
...@@ -51,7 +51,7 @@ public class BootReceiver extends BroadcastReceiver implements GoogleApiClient.C ...@@ -51,7 +51,7 @@ public class BootReceiver extends BroadcastReceiver implements GoogleApiClient.C
} }
// This is the IntentService we'll provide to google-play API. // This is the IntentService we'll provide to google-play API.
locationUpdateService = PendingIntent.getService(context, 0, new Intent(context, BackgroundGeolocationService.class), PendingIntent.FLAG_UPDATE_CURRENT); locationUpdateService = PendingIntent.getService(context, 0, new Intent(context, LocationService.class), PendingIntent.FLAG_UPDATE_CURRENT);
} }
private void requestActivityUpdates() { private void requestActivityUpdates() {
......
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
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