Commit ca77173d authored by Chris Scott's avatar Chris Scott

Merge pull request #8 from christocracy/http_service

Http service
parents 8dca2e04 1707abbd
......@@ -132,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();
};
......@@ -174,15 +172,28 @@ var app = {
// BackgroundGeoLocation is highly configurable.
bgGeo.configure(callbackFn, failureFn, {
debug: true, // <-- enable this hear sounds for background-geolocation life-cycle.
desiredAccuracy: 0,
stationaryRadius: 50,
distanceFilter: 30,
locationUpdateInterval: 30000,
distanceFilter: 50,
locationUpdateInterval: 5000,
activityRecognitionInterval: 10000,
stopTimeout: 0, // <-- Minutes to wait before turning off GPS after stop-detection.
stopTimeout: 1,
forceReload: true, // <-- If the user closes the app **while location-tracking is started** , reboot app (WARNING: possibly distruptive to user)
activityType: 'AutomotiveNavigation',
debug: true, // <-- enable this hear sounds for background-geolocation life-cycle.
stopOnTerminate: false // <-- enable this to clear background location settings when the app terminates
stopOnTerminate: false // <-- Allow the background-service to run headless when user closes the app.
/**
* 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.
......
......@@ -27,9 +27,19 @@
<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" />
<!-- 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">
......
package com.transistorsoft.cordova.bggeo;
import de.greenrobot.event.EventBus;
import com.google.android.gms.location.DetectedActivity;
import com.google.android.gms.location.ActivityRecognitionResult;
import android.app.IntentService;
import android.content.Intent;
import android.util.Log;
public class ActivityRecognitionService extends IntentService {
private static final String TAG = "BackgroundGeolocation";
public ActivityRecognitionService() {
super("com.transistorsoft.cordova.bggeo.ActivityRecognitionService");
}
@Override
protected void onHandleIntent(Intent intent) {
// Determine whether the fore-ground Activity is running. If it's not, we'll reboot it.
if (ActivityRecognitionResult.hasResult(intent)) {
ActivityRecognitionResult result = ActivityRecognitionResult.extractResult(intent);
DetectedActivity probableActivity = result.getMostProbableActivity();
Log.w(TAG, "Activity detected:" + getActivityName(probableActivity.getType()) + ", confidence:" + probableActivity.getConfidence());
if (probableActivity.getConfidence() < 80) {
return;
}
switch (probableActivity.getType()) {
case DetectedActivity.IN_VEHICLE:
case DetectedActivity.ON_BICYCLE:
case DetectedActivity.ON_FOOT:
case DetectedActivity.WALKING:
case DetectedActivity.RUNNING:
case DetectedActivity.STILL:
EventBus.getDefault().post(result);
break;
case DetectedActivity.UNKNOWN:
return;
case DetectedActivity.TILTING:
return;
}
}
}
/**
* This method has no other purpose than formatting the Activity for log-messages
*/
private String getActivityName(int activityType) {
switch (activityType) {
case DetectedActivity.IN_VEHICLE:
return "in_vehicle";
case DetectedActivity.ON_BICYCLE:
return "on_bicycle";
case DetectedActivity.ON_FOOT:
return "on_foot";
case DetectedActivity.RUNNING:
return "running";
case DetectedActivity.WALKING:
return "walking";
case DetectedActivity.STILL:
return "still";
case DetectedActivity.UNKNOWN:
return "unknown";
case DetectedActivity.TILTING:
return "tilting";
}
return "unknown";
}
}
\ No newline at end of file
package com.transistorsoft.cordova.bggeo;
import java.util.concurrent.TimeUnit;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView;
......@@ -9,31 +8,20 @@ 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.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 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";
......@@ -42,66 +30,25 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Locati
public static final String ACTION_SET_CONFIG = "setConfig";
public static final String ACTION_ON_STATIONARY = "addStationaryRegionListener";
private PendingIntent locationUpdateService;
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;
/**
* @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 Intent backgroundServiceIntent;
// Geolocation callback
private CallbackContext locationCallback;
// Called when DetectedActivity is STILL
private CallbackContext stationaryCallback;
private Location stationaryLocation;
private GoogleApiClient googleApiClient;
private DetectedActivity currentActivity;
private static CordovaWebView gWebView;
private ToneGenerator toneGenerator;
public static boolean isActive() {
return gWebView != null;
}
@Override
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();
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);
}
......@@ -109,48 +56,45 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Locati
public boolean execute(String action, JSONArray data, CallbackContext callbackContext) throws JSONException {
Log.d(TAG, "execute / action : " + action);
Activity activity = this.cordova.getActivity();
Boolean result = false;
if (ACTION_START.equalsIgnoreCase(action) && !isEnabled) {
result = true;
isEnabled = true;
if (googleApiClient.isConnected()) {
requestActivityUpdates();
if (!BackgroundGeolocationService.isInstanceCreated()) {
activity.startService(backgroundServiceIntent);
}
} else if (ACTION_STOP.equalsIgnoreCase(action)) {
result = true;
isEnabled = false;
isMoving = false;
removeLocationUpdates();
removeActivityUpdates();
activity.stopService(backgroundServiceIntent);
callbackContext.success();
} else if (ACTION_CONFIGURE.equalsIgnoreCase(action)) {
result = applyConfig(data);
if (result) {
this.locationCallback = callbackContext;
if (isDebugging) {
toneGenerator = new ToneGenerator(AudioManager.STREAM_NOTIFICATION, 100);
}
} 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");
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)) {
activity.stopService(backgroundServiceIntent);
result = applyConfig(data);
// TODO reconfigure Service
if (result) {
activity.stopService(backgroundServiceIntent);
callbackContext.success();
} else {
callbackContext.error("- Configuration error!");
......@@ -159,206 +103,88 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Locati
result = true;
this.stationaryCallback = callbackContext;
}
return result;
}
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 {
JSONObject config = data.getJSONObject(0);
Log.i(TAG, "- configure: " + config.toString());
if (config.has("distanceFilter")) {
distanceFilter = (float) config.getInt("distanceFilter");
backgroundServiceIntent.putExtra("distanceFilter", (float) config.getInt("distanceFilter"));
}
if (config.has("desiredAccuracy")) {
desiredAccuracy = config.getInt("desiredAccuracy");
backgroundServiceIntent.putExtra("desiredAccuracy", config.getInt("desiredAccuracy"));
}
if (config.has("locationUpdateInterval")) {
locationUpdateInterval = config.getInt("locationUpdateInterval");
backgroundServiceIntent.putExtra("locationUpdateInterval", config.getInt("locationUpdateInterval"));
}
if (config.has("activityRecognitionInterval")) {
activityRecognitionInterval = config.getInt("activityRecognitionInterval");
backgroundServiceIntent.putExtra("activityRecognitionInterval", config.getInt("activityRecognitionInterval"));
}
if (config.has("stopTimeout")) {
stopTimeout = config.getLong("stopTimeout");
backgroundServiceIntent.putExtra("stopTimeout", config.getLong("stopTimeout"));
}
if (config.has("debug")) {
isDebugging = config.getBoolean("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;
}
}
/**
* 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() {
return gWebView != null;
}
public void onPause(boolean multitasking) {
Log.i(TAG, "- onPause");
if (isEnabled) {
setPace(isMoving);
//setPace(isMoving);
EventBus.getDefault().post(new PausedEvent(true));
}
}
public void onResume(boolean multitasking) {
Log.i(TAG, "- onResume");
if (isEnabled) {
removeLocationUpdates();
}
}
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);
// Inform Javascript of our stationaryLocation
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;
EventBus.getDefault().post(new PausedEvent(false));
}
// 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) {
Log.i(TAG, "BUS Rx:" + location.toString());
startTone("beep");
PluginResult result = new PluginResult(PluginResult.Status.OK, locationToJson(location));
result.setKeepCallback(true);
runInBackground(locationCallback, result);
}
/**
* Execute onStationary javascript callback when device is determined to have just stopped
* EventBus listener
* @param {Location} location
*/
private void fireStationaryListener() {
Log.i(TAG, "- fire stationary listener");
if ( (stationaryCallback != null) && (stationaryLocation != null) ) {
final PluginResult result = new PluginResult(PluginResult.Status.OK, locationToJson(stationaryLocation));
public void onEventMainThread(Location location) {
PluginResult result = new PluginResult(PluginResult.Status.OK, BackgroundGeolocationService.locationToJson(location));
result.setKeepCallback(true);
if (location instanceof StationaryLocation) {
if (stationaryCallback != null) {
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;
} else {
result.setKeepCallback(true);
runInBackground(locationCallback, result);
}
}
......@@ -377,96 +203,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.
* Checks to see if it should turn off
......@@ -476,9 +212,8 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Locati
Log.i(TAG, " stopOnTerminate: " + stopOnTerminate);
Log.i(TAG, " isEnabled: " + isEnabled);
if(isEnabled && stopOnTerminate || !isEnabled) {
removeActivityUpdates();
removeLocationUpdates();
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.util.Log;
public class BackgroundGeolocationService extends IntentService {
public class BackgroundGeolocationService extends Service implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {
private static final String TAG = "BackgroundGeolocation";
private static final String TAG = "BackgroundGeolocationService";
private static BackgroundGeolocationService instance = null;
public BackgroundGeolocationService() {
super("com.transistorsoft.cordova.bggeo.BackgroundGeolocationService");
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{ 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 = null;
// Flags
private Boolean isEnabled = false;
private Boolean isMoving = false;
private Boolean isPaused = true;
private long stoppedAt = 0;
private Location stationaryLocation;
private DetectedActivity currentActivity;
@Override
protected void onHandleIntent(Intent intent) {
// Determine whether the fore-ground Activity is running. If it's not, we'll reboot it.
boolean isPluginActive = BackgroundGeolocationPlugin.isActive();
public int onStartCommand(Intent intent, int flags, int startId) {
instance = this;
if (ActivityRecognitionResult.hasResult(intent)) {
ActivityRecognitionResult result = ActivityRecognitionResult.extractResult(intent);
DetectedActivity probableActivity = result.getMostProbableActivity();
EventBus.getDefault().register(this);
Log.i(TAG, "Activity detected:" + getActivityName(probableActivity.getType()) + ", confidence:" + probableActivity.getConfidence());
if (probableActivity.getConfidence() < 80) {
return;
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);
// 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();
}
Boolean isMoving = false;
switch (probableActivity.getType()) {
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, " forceReload: " + forceReload);
Log.i(TAG, "----------------------------------------");
// For debug sounds, turn on ToneGenerator.
if (isDebugging) {
toneGenerator = new ToneGenerator(AudioManager.STREAM_NOTIFICATION, 100);
}
// Configure FusedLocationProvider
locationRequest = LocationRequest.create()
.setPriority(translateDesiredAccuracy(desiredAccuracy))
.setInterval(this.locationUpdateInterval)
.setFastestInterval(30000)
.setSmallestDisplacement(distanceFilter);
// 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);
// 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());
boolean wasMoving = isMoving;
boolean nowMoving = false;
switch (currentActivity.getType()) {
case DetectedActivity.IN_VEHICLE:
case DetectedActivity.ON_BICYCLE:
case DetectedActivity.ON_FOOT:
case DetectedActivity.WALKING:
case DetectedActivity.RUNNING:
isMoving = true;
case DetectedActivity.WALKING:
nowMoving = true;
break;
case DetectedActivity.STILL:
nowMoving = false;
break;
case DetectedActivity.UNKNOWN:
break;
case DetectedActivity.TILTING:
break;
// We're not interested in these modes.
return;
}
// Force main-activity reload (if not running) if we're detected to be moving.
if (isMoving && !isPluginActive) {
forceMainActivityReload();
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;
}
// Post activity to the bus.
EventBus.getDefault().post(result);
// 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 {
final Location location = intent.getParcelableExtra(FusedLocationProviderApi.KEY_LOCATION_CHANGED);
if (location != null) {
Log.i(TAG, "Location received: " + location.toString());
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();
// Force main-activity reload when a location comes in.
if (!isPluginActive) {
if (!isPluginActive && forceReload) {
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;
requestLocationUpdates();
} else {
removeLocationUpdates();
if (stationaryLocation == null) {
startTone("long_beep");
// set our stationaryLocation
stationaryLocation = LocationServices.FusedLocationApi.getLastLocation(googleApiClient);
EventBus.getDefault().post(new StationaryLocation(stationaryLocation));
}
}
}
/**
* 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) {
switch (activityType) {
case DetectedActivity.IN_VEHICLE:
......@@ -96,6 +381,64 @@ public class BackgroundGeolocationService extends IntentService {
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
* running always, since we for the app to boot.
......@@ -109,4 +452,133 @@ public class BackgroundGeolocationService extends IntentService {
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
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");
}
}
}
......@@ -51,7 +51,7 @@ public class BootReceiver extends BroadcastReceiver implements GoogleApiClient.C
}
// 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() {
......
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
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