Commit f9035988 authored by Chris Scott's avatar Chris Scott

Geolocation libs for both Android & iOS are extracted to compiled libs

parent 6539611a
*.DS_Store
......@@ -9,4 +9,19 @@
</author>
<content src="index.html" />
<access origin="*" />
<allow-intent href="http://*/*" />
<allow-intent href="https://*/*" />
<allow-intent href="tel:*" />
<allow-intent href="sms:*" />
<allow-intent href="mailto:*" />
<allow-intent href="geo:*" />
<platform name="android">
<allow-intent href="market:*" />
</platform>
<platform name="ios">
<allow-intent href="itms:*" />
<allow-intent href="itms-apps:*" />
</platform>
</widget>
......@@ -27,9 +27,7 @@
<link rel="stylesheet" type="text/css" href="css/bootstrap-min.css" />
<link rel="stylesheet" type="text/css" href="css/index.css" />
<title>BG GeoLocation</title>
<script type="text/javascript"
src="http://maps.google.com/maps/api/js?sensor=true&libraries=geometry">
</script>
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=true&libraries=geometry"></script>
</head>
<body>
......
......@@ -36,7 +36,7 @@ var app = {
/**
* @property {google.maps.Marker} location The current location
*/
location: undefined,
currentLocationMarker: undefined,
/**
* @property {google.map.PolyLine} path The list of background geolocations
*/
......@@ -49,6 +49,10 @@ var app = {
* @property {Array} locations List of rendered map markers of prev locations
*/
locations: [],
/**
* @property currentLocation {Location}
*/
currentLocation: null,
/**
* @private
*/
......@@ -120,11 +124,10 @@ var app = {
onDeviceReady: function() {
app.receivedEvent('deviceready');
app.configureBackgroundGeoLocation();
app.watchPosition();
app.watchForegroundPosition();
},
configureBackgroundGeoLocation: function() {
var fgGeo = window.navigator.geolocation,
bgGeo = window.plugins.backgroundGeoLocation;
var bgGeo = window.plugins.backgroundGeoLocation;
app.onClickHome();
......@@ -132,6 +135,8 @@ var app = {
* This would be your own callback for Ajax-requests after POSTing background geolocation to your server.
*/
var yourAjaxCallback = function(response) {
// Very important to call #finish -- it signals to the native plugin that it can destroy the background thread, which your callbackFn is running in.
// IF YOU DON'T, THE OS CAN KILL YOUR APP FOR RUNNING TOO LONG IN THE BACKGROUND
bgGeo.finish();
};
......@@ -155,6 +160,12 @@ var app = {
// Only ios emits this stationary event
bgGeo.onStationary(function(location) {
console.log('[js] BackgroundGeoLocation onStationary ' + JSON.stringify(location));
app.setCurrentLocation(location);
// Center ourself on map
app.onClickHome();
if (!app.stationaryRadius) {
app.stationaryRadius = new google.maps.Circle({
fillColor: '#cc0000',
......@@ -163,7 +174,7 @@ var app = {
map: app.map
});
}
var radius = (location.accuracy < location.radius) ? location.radius : location.accuracy;
var radius = 50;
var center = new google.maps.LatLng(location.latitude, location.longitude);
app.stationaryRadius.setRadius(radius);
app.stationaryRadius.setCenter(center);
......@@ -178,6 +189,7 @@ var app = {
distanceFilter: 50,
disableElasticity: false, // <-- [iOS] Default is 'false'. Set true to disable speed-based distanceFilter elasticity
locationUpdateInterval: 5000,
fastestLocationUpdateInterval: 5000,
activityRecognitionInterval: 10000,
stopTimeout: 0,
forceReload: true, // <-- [Android] If the user closes the app **while location-tracking is started** , reboot app (WARNING: possibly distruptive to user)
......@@ -186,16 +198,15 @@ var 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',
maxDaysToPersist: 1, // <-- Maximum days to persist a location in plugin's SQLite database when HTTP fails
headers: {
"X-FOO": "bar"
},
params: {
"auth_token": "maybe_your_server_authenticates_via_token_YES?"
}
*
*/
});
// Turn ON the background-geolocation system. The user will be tracked whenever they suspend the app.
......@@ -210,22 +221,20 @@ var app = {
}
},
onClickHome: function() {
var fgGeo = window.navigator.geolocation;
// Your app must execute AT LEAST ONE call for the current position via standard Cordova geolocation,
// in order to prompt the user for Location permission.
fgGeo.getCurrentPosition(function(location) {
var location = app.currentLocation;
if (!location) {
// No location recorded yet; bail out.
return;
}
var map = app.map,
coords = location.coords,
ll = new google.maps.LatLng(coords.latitude, coords.longitude),
ll = new google.maps.LatLng(location.latitude, location.longitude),
zoom = map.getZoom();
map.setCenter(ll);
if (zoom < 15) {
map.setZoom(15);
}
app.setCurrentLocation(coords);
});
},
onClickChangePace: function(value) {
var bgGeo = window.plugins.backgroundGeoLocation,
......@@ -273,26 +282,27 @@ var app = {
bgGeo.stop();
}
},
watchPosition: function() {
/**
* We use standard cordova-plugin-geolocation to watch position in foreground.
*/
watchForegroundPosition: function() {
var fgGeo = window.navigator.geolocation;
if (app.watchId) {
if (app.foregroundWatchId) {
app.stopPositionWatch();
}
// Watch foreground location
app.watchId = fgGeo.watchPosition(function(location) {
app.foregroundWatchId = fgGeo.watchPosition(function(location) {
app.setCurrentLocation(location.coords);
}, function() {}, {
enableHighAccuracy: true,
maximumAge: 5000,
frequency: 10000,
timeout: 10000
});
},
stopPositionWatch: function() {
/**
* Stop watching position in foreground when we pause: THIS IS VERY IMPORTANT
*/
stopWatchingForegroundPosition: function() {
var fgGeo = window.navigator.geolocation;
if (app.watchId) {
fgGeo.clearWatch(app.watchId);
app.watchId = undefined;
if (app.foregroundWatchId) {
fgGeo.clearWatch(app.foregroundWatchId);
app.foregroundWatchId = undefined;
}
},
/**
......@@ -301,23 +311,26 @@ var app = {
* determine start/stop of device.
*/
onPause: function() {
console.log('- onPause');
app.stopPositionWatch();
console.log('[js] onPause');
app.stopWatchingForegroundPosition();
},
/**
* Once in foreground, re-engage foreground geolocation watch with standard Cordova GeoLocation api
*/
onResume: function() {
console.log('- onResume');
app.watchPosition();
console.log('[js] onResume');
app.watchForegroundPosition();
},
// Update DOM on a Received Event
receivedEvent: function(id) {
console.log('Received Event: ' + id);
},
setCurrentLocation: function(location) {
if (!app.location) {
app.location = new google.maps.Marker({
// Set currentLocation @property
app.currentLocation = location;
if (!app.currentLocationMarker) {
app.currentLocationMarker = new google.maps.Marker({
map: app.map,
icon: {
path: google.maps.SymbolPath.CIRCLE,
......@@ -327,7 +340,7 @@ var app = {
strokeWeight: 5
}
});
app.locationAccuracy = new google.maps.Circle({
app.locationAccuracyMarker = new google.maps.Circle({
fillColor: '#3366cc',
fillOpacity: 0.4,
strokeOpacity: 0,
......@@ -360,9 +373,9 @@ var app = {
}
// Update our current position marker and accuracy bubble.
app.location.setPosition(latlng);
app.locationAccuracy.setCenter(latlng);
app.locationAccuracy.setRadius(location.accuracy);
app.currentLocationMarker.setPosition(latlng);
app.locationAccuracyMarker.setCenter(latlng);
app.locationAccuracyMarker.setRadius(location.accuracy);
// Add breadcrumb to current Polyline path.
app.path.getPath().push(latlng);
......
......@@ -7,16 +7,13 @@
<name>BackgroundGeolocation</name>
<description>Sophisticated, battery-efficient background-geolocation plugin for Cordova</description>
<license>MIT</license>
<keywords>phonegap,background geolocation</keywords>
<keywords>cordova, phonegap, background geolocation</keywords>
<engines>
<engine name="cordova" version=">=3.0.0" />
</engines>
<dependency id="org.apache.cordova.geolocation" />
<dependency id="org.apache.cordova.dialogs" />
<dependency id="com.google.playservices" url="https://github.com/MobileChromeApps/google-play-services.git" />
<dependency id="cordova-plugin-dialogs" />
<js-module src="www/BackgroundGeoLocation.js" name="BackgroundGeoLocation">
<clobbers target="plugins.backgroundGeoLocation" />
......@@ -24,26 +21,16 @@
<!-- android -->
<platform name="android">
<framework src="com.google.android.gms:play-services-location:7.3.0" />
<source-file src="src/android/libs/eventbus-2.4.0.jar" target-dir="libs" />
<source-file src="src/android/BackgroundGeolocationPlugin.java" target-dir="src/com/transistorsoft/cordova/bggeo" />
<source-file src="src/android/BackgroundGeolocationService.java" target-dir="src/com/transistorsoft/cordova/bggeo" />
<source-file src="src/android/ActivityRecognitionService.java" target-dir="src/com/transistorsoft/cordova/bggeo" />
<source-file src="src/android/LocationService.java" target-dir="src/com/transistorsoft/cordova/bggeo" />
<source-file src="src/android/BootReceiver.java" target-dir="src/com/transistorsoft/cordova/bggeo" />
<!-- For SQLite persistence NOT YET IMPLEMENTED
<source-file src="src/android/data/LocationDAO.java" target-dir="src/com/tenforwardconsulting/cordova/bgloc/data" />
<source-file src="src/android/data/sqlite/LocationOpenHelper.java" target-dir="src/com/tenforwardconsulting/cordova/bgloc/data/sqlite" />
<source-file src="src/android/data/sqlite/SQLiteLocationDAO.java" target-dir="src/com/tenforwardconsulting/cordova/bgloc/data/sqlite" />
-->
<source-file src="src/android/libs/transistor-locationmanager.jar" target-dir="libs" />
<source-file src="src/android/CDVBackgroundGeolocation.java" target-dir="src/com/transistorsoft/cordova/bggeo" />
<config-file target="AndroidManifest.xml" parent="/manifest/application">
<service android:name="com.transistorsoft.cordova.bggeo.BackgroundGeolocationService" />
<service android:name="com.transistorsoft.cordova.bggeo.LocationService" />
<service android:name="com.transistorsoft.cordova.bggeo.ActivityRecognitionService" />
<!-- autorun on boot receiver -->
<receiver android:name="com.transistorsoft.cordova.bggeo.BootReceiver" android:enabled="true" android:exported="false">
<service android:name="com.transistorsoft.locationmanager.BackgroundGeolocationService" />
<service android:name="com.transistorsoft.locationmanager.LocationService" />
<service android:name="com.transistorsoft.locationmanager.ActivityRecognitionService" />
<receiver android:name="com.transistorsoft.locationmanager.BootReceiver" android:enabled="true" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
......@@ -63,7 +50,7 @@
</config-file>
<config-file target="res/xml/config.xml" parent="/*">
<feature name="BackgroundGeoLocation">
<param name="android-package" value="com.transistorsoft.cordova.bggeo.BackgroundGeolocationPlugin"/>
<param name="android-package" value="com.transistorsoft.cordova.bggeo.CDVBackgroundGeolocation"/>
</feature>
</config-file>
</platform>
......@@ -83,15 +70,15 @@
<config-file target="config.xml" parent="/*">
<feature name="BackgroundGeoLocation">
<param name="ios-package" value="CDVBackgroundGeoLocation"/>
<param name="ios-package" value="CDVBackgroundGeolocation"/>
</feature>
</config-file>
<framework src="AudioToolbox.framework" weak="true" />
<framework src="AVFoundation.framework" weak="true" />
<source-file src="src/ios/CDVBackgroundGeoLocation.m" />
<header-file src="src/ios/CDVBackgroundGeoLocation.h" />
<source-file src="src/ios/BackgroundGeoLocation.m" />
<header-file src="src/ios/BackgroundGeoLocation.h" />
<framework src="libsqlite3.dylib" weak="true" />
<framework src="src/ios/TSLocationManager.framework" custom="true" />
<source-file src="src/ios/CDVBackgroundGeolocation.m" />
<header-file src="src/ios/CDVBackgroundGeolocation.h" />
</platform>
<!-- wp8 -->
......
package com.transistorsoft.cordova.bggeo;
import de.greenrobot.event.EventBus;
import com.google.android.gms.location.DetectedActivity;
import com.google.android.gms.location.ActivityRecognitionResult;
import android.app.IntentService;
import android.content.Intent;
import android.util.Log;
public class ActivityRecognitionService extends IntentService {
private static final String TAG = "BackgroundGeolocation";
public ActivityRecognitionService() {
super("com.transistorsoft.cordova.bggeo.ActivityRecognitionService");
}
@Override
protected void onHandleIntent(Intent intent) {
// Determine whether the fore-ground Activity is running. If it's not, we'll reboot it.
if (ActivityRecognitionResult.hasResult(intent)) {
ActivityRecognitionResult result = ActivityRecognitionResult.extractResult(intent);
DetectedActivity probableActivity = result.getMostProbableActivity();
Log.w(TAG, "Activity detected:" + getActivityName(probableActivity.getType()) + ", confidence:" + probableActivity.getConfidence());
if (probableActivity.getConfidence() < 80) {
return;
}
switch (probableActivity.getType()) {
case DetectedActivity.IN_VEHICLE:
case DetectedActivity.ON_BICYCLE:
case DetectedActivity.ON_FOOT:
case DetectedActivity.WALKING:
case DetectedActivity.RUNNING:
case DetectedActivity.STILL:
EventBus.getDefault().post(result);
break;
case DetectedActivity.UNKNOWN:
return;
case DetectedActivity.TILTING:
return;
}
}
}
/**
* This method has no other purpose than formatting the Activity for log-messages
*/
private String getActivityName(int activityType) {
switch (activityType) {
case DetectedActivity.IN_VEHICLE:
return "in_vehicle";
case DetectedActivity.ON_BICYCLE:
return "on_bicycle";
case DetectedActivity.ON_FOOT:
return "on_foot";
case DetectedActivity.RUNNING:
return "running";
case DetectedActivity.WALKING:
return "walking";
case DetectedActivity.STILL:
return "still";
case DetectedActivity.UNKNOWN:
return "unknown";
case DetectedActivity.TILTING:
return "tilting";
}
return "unknown";
}
}
\ No newline at end of file
package com.transistorsoft.cordova.bggeo;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONException;
import org.json.JSONObject;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.location.ActivityRecognition;
import com.google.android.gms.location.ActivityRecognitionResult;
import com.google.android.gms.location.DetectedActivity;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationServices;
import de.greenrobot.event.EventBus;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.location.Location;
import android.media.AudioManager;
import android.media.ToneGenerator;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.provider.Settings.Secure;
import android.util.Log;
public class BackgroundGeolocationService extends Service implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {
private static final String TAG = "BackgroundGeolocation";
private static BackgroundGeolocationService instance = null;
public static boolean isInstanceCreated() {
return instance != null;
}
private GoogleApiClient googleApiClient;
private ToneGenerator toneGenerator;
private PendingIntent activityRecognitionPI;
private PendingIntent locationUpdatePI;
private LocationRequest locationRequest;
// Common config
/**
* @config {Integer} desiredAccuracy
*/
private Integer desiredAccuracy = 10;
/**
* @config {Float} distanceFilter
*/
private Float distanceFilter = 50f;
/**
* @config {Boolean} isDebugging
*/
private Boolean isDebugging = false;
/**
* @config {Boolean} stopOnTerminate
*/
private Boolean stopOnTerminate = false;
// Android-only config
/**
* @config {Integer} locationUpdateInterval (ms)
*/
private Integer locationUpdateInterval = 60000;
/**
* @config {Integer} fastestLocationUpdateInterval (ms)
*/
private Integer fastestLocationUpdateInterval = 30000;
/**
* @config {Integer{ activityRecognitionInterval (ms)
*/
private Integer activityRecognitionInterval = 60000;
/*
* @config {Boolean} forceReload Whether to reboot the Android Activity when detected to have closed
*/
private Boolean forceReload = false;
/**
* @config {Integer} stopTimeout The time to wait after ARS STILL to turn of GPS
*/
private long stopTimeout = 0;
// HTTP config
/**
* @config {String} url For sending location to your server
*/
private String url = null;
/**
* @config {JSONObject} params For sending location to your server
*/
private JSONObject params = new JSONObject();
/**
* @config {JSONObject} headers For sending location to your server
*/
private JSONObject headers = new JSONObject();
// Flags
private Boolean isEnabled = false;
private Boolean isMoving = false;
private Boolean isPaused = true;
private long stoppedAt = 0;
private Location stationaryLocation;
private DetectedActivity currentActivity;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
instance = this;
EventBus eventBus = EventBus.getDefault();
if (!eventBus.isRegistered(this)) {
eventBus.register(this);
}
// Load config settings
SharedPreferences settings = getSharedPreferences(TAG, 0);
isEnabled = true;
isDebugging = settings.getBoolean("debug", false);
distanceFilter = settings.getFloat("distanceFilter", 50);
desiredAccuracy = settings.getInt("desiredAccuracy", 10);
locationUpdateInterval = settings.getInt("locationUpdateInterval", 30000);
activityRecognitionInterval = settings.getInt("activityRecognitionInterval", 60000);
stopTimeout = settings.getLong("stopTimeout", 0);
stopOnTerminate = settings.getBoolean("stopOnTerminate", true);
forceReload = settings.getBoolean("forceReload", false);
isMoving = settings.getBoolean("isMoving", false);
// HTTP Configuration
url = settings.getString("url", null);
if (settings.contains("params")) {
try {
params = new JSONObject(settings.getString("params", "{}"));
} catch (JSONException e) {
Log.w(TAG, "- Faile to parse #params to JSONObject");
}
}
if (settings.contains("headers")) {
try {
headers = new JSONObject(settings.getString("headers", "{}"));
} catch (JSONException e) {
Log.w(TAG, "- Failed to parse #headers to JSONObject");
}
}
Log.i(TAG, "----------------------------------------");
Log.i(TAG, "- Start BackgroundGeolocationService");
Log.i(TAG, " debug: " + isDebugging);
Log.i(TAG, " distanceFilter: " + distanceFilter);
Log.i(TAG, " desiredAccuracy: " + desiredAccuracy);
Log.i(TAG, " locationUpdateInterval: " + locationUpdateInterval);
Log.i(TAG, " activityRecognitionInterval: " + activityRecognitionInterval);
Log.i(TAG, " stopTimeout: " + stopTimeout);
Log.i(TAG, " stopOnTerminate: " + stopOnTerminate);
Log.i(TAG, " forceReload: " + forceReload);
Log.i(TAG, " isMoving: " + isMoving);
Log.i(TAG, "----------------------------------------");
// For debug sounds, turn on ToneGenerator.
if (isDebugging) {
toneGenerator = new ToneGenerator(AudioManager.STREAM_NOTIFICATION, 100);
}
// Connect to google-play services.
if (ConnectionResult.SUCCESS == GooglePlayServicesUtil.isGooglePlayServicesAvailable(this)) {
Log.i(TAG, "- Connecting to GooglePlayServices...");
googleApiClient = new GoogleApiClient.Builder(this)
.addApi(LocationServices.API)
.addApi(ActivityRecognition.API)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
googleApiClient.connect();
} else {
Log.e(TAG, "- GooglePlayServices unavailable");
}
return Service.START_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
// TODO Auto-generated method stub
return null;
}
@Override
public void onConnectionFailed(ConnectionResult arg0) {
// TODO Auto-generated method stub
}
@Override
public void onConnected(Bundle arg0) {
Log.i(TAG, "- GooglePlayServices connected");
Intent arsIntent = new Intent(this, ActivityRecognitionService.class);
activityRecognitionPI = PendingIntent.getService(this, 0, arsIntent, PendingIntent.FLAG_UPDATE_CURRENT);
Intent locationIntent = new Intent(this, LocationService.class);
locationUpdatePI = PendingIntent.getService(this, 0, locationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
setPace(isMoving);
// Start monitoring ARS
if (googleApiClient.isConnected()) {
requestActivityUpdates();
}
}
/**
* EventBus listener
* Fired from Plugin
* @param {PausedEvent} event
*/
public void onEventMainThread(PausedEvent event) {
isPaused = event.isPaused;
if (isPaused) {
setPace(isMoving);
} else {
removeLocationUpdates();
}
}
/**
* EventBus listener
* Fired from Plugin
* @param {PaceChangeEvent} event
*/
public void onEventMainThread(PaceChangeEvent event) {
setPace(event.isMoving);
}
/**
* EventBus listener for ARS
* @param {ActivityRecognitionResult} result
*/
public void onEventMainThread(ActivityRecognitionResult result) {
currentActivity = result.getMostProbableActivity();
String probableActivityName = getActivityName(currentActivity.getType());
Log.i(TAG, "- Activity received: " + probableActivityName + ", confidence: " + currentActivity.getConfidence());
// If configured to stop when user closes app, kill this service.
if (!BackgroundGeolocationPlugin.isActive() && stopOnTerminate) {
stopSelf();
return;
}
boolean wasMoving = isMoving;
boolean nowMoving = false;
switch (currentActivity.getType()) {
case DetectedActivity.IN_VEHICLE:
case DetectedActivity.ON_BICYCLE:
case DetectedActivity.ON_FOOT:
case DetectedActivity.RUNNING:
case DetectedActivity.WALKING:
nowMoving = true;
break;
case DetectedActivity.STILL:
nowMoving = false;
break;
case DetectedActivity.UNKNOWN:
case DetectedActivity.TILTING:
// We're not interested in these modes.
return;
}
boolean startedMoving = !wasMoving && nowMoving;
boolean justStopped = wasMoving && !nowMoving;
boolean initialState = !nowMoving && (stationaryLocation == null);
// If we're using a stopTimeout, record the current activity's timestamp.
if (justStopped && stopTimeout > 0 && stoppedAt == 0) {
stoppedAt = result.getElapsedRealtimeMillis();
return;
}
// If we're using a stopTimeout, compare the current activity's timestamp with the 1st recorded STILL event.
if (!nowMoving && stoppedAt > 0) {
long elapsedMillis = result.getElapsedRealtimeMillis() - stoppedAt;
long elapsedMinutes = TimeUnit.MILLISECONDS.toMinutes(elapsedMillis);
Log.i(TAG, "- Waiting for stopTimeout (" + stopTimeout + " min): elapsed min: " + elapsedMinutes);
if (elapsedMinutes >= stopTimeout) {
justStopped = true;
} else {
return;
}
}
stoppedAt = 0;
if ( startedMoving || justStopped || initialState ) {
setPace(nowMoving);
}
}
public void onEventMainThread(Location location) {
if (location instanceof StationaryLocation) {
return;
}
Log.i(TAG, "BUS Rx:" + location.toString());
startTone("beep");
// Force main-activity reload (if not running) if we're detected to be moving.
boolean isPluginActive = BackgroundGeolocationPlugin.isActive();
if (!isPluginActive && forceReload) {
forceMainActivityReload();
}
if (url != null) {
if (isNetworkAvailable()) {
schedulePostLocation(location);
} else {
Log.i(TAG, "- No network detected");
// TODO no in-plugin persistence
}
}
}
private void setPace(Boolean moving) {
Log.i(TAG, "- setPace: " + moving);
boolean wasMoving = isMoving;
isMoving = moving;
if (moving && isEnabled) {
if (!wasMoving) {
startTone("doodly_doo");
}
stationaryLocation = null;
requestLocationUpdates();
} else {
removeLocationUpdates();
if (stationaryLocation == null) {
startTone("long_beep");
// set our stationaryLocation
stationaryLocation = LocationServices.FusedLocationApi.getLastLocation(googleApiClient);
if (stationaryLocation != null) {
EventBus.getDefault().post(new StationaryLocation(stationaryLocation));
}
}
}
}
/**
* Translates a number representing desired accuracy of GeoLocation system from set [0, 10, 100, 1000].
* 0: most aggressive, most accurate, worst battery drain
* 1000: least aggressive, least accurate, best for battery.
*/
private Integer translateDesiredAccuracy(Integer accuracy) {
switch (accuracy) {
case 1000:
accuracy = LocationRequest.PRIORITY_NO_POWER;
break;
case 100:
accuracy = LocationRequest.PRIORITY_LOW_POWER;
break;
case 10:
accuracy = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY;
break;
case 0:
accuracy = LocationRequest.PRIORITY_HIGH_ACCURACY;
break;
default:
accuracy = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY;
}
return accuracy;
}
private Integer getLocationUpdateInterval() {
// TODO Can add intelligence here based upon currentActivity.
SharedPreferences settings = getSharedPreferences(TAG, 0);
return settings.getInt("locationUpdateInterval", locationUpdateInterval);
}
private Integer getFastestLocationUpdateInterval() {
/* TODO Add intelligent calculation of fastestLocationUpdateInterval based upon currentActivity here
* switch (currentActivity.getType()) {
case DetectedActivity.IN_VEHICLE:
fastestLocationUpdateInterval = 30000;
break;
case DetectedActivity.ON_BICYCLE:
fastestLocationUpdateInterval = 30000;
break;
case DetectedActivity.ON_FOOT:
fastestLocationUpdateInterval = 30000;
break;
case DetectedActivity.RUNNING:
fastestLocationUpdateInterval = 30000;
break;
case DetectedActivity.WALKING:
fastestLocationUpdateInterval = 30000;
break;
}
*/
return fastestLocationUpdateInterval;
}
private String getActivityName(int activityType) {
switch (activityType) {
case DetectedActivity.IN_VEHICLE:
return "in_vehicle";
case DetectedActivity.ON_BICYCLE:
return "on_bicycle";
case DetectedActivity.ON_FOOT:
return "on_foot";
case DetectedActivity.RUNNING:
return "running";
case DetectedActivity.WALKING:
return "walking";
case DetectedActivity.STILL:
return "still";
case DetectedActivity.UNKNOWN:
return "unknown";
case DetectedActivity.TILTING:
return "tilting";
}
return "unknown";
}
private boolean isNetworkAvailable() {
ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();
return activeNetworkInfo != null && activeNetworkInfo.isConnected();
}
private void schedulePostLocation(Location location) {
PostLocationTask task = new BackgroundGeolocationService.PostLocationTask();
task.setLocation(location);
Log.d(TAG, "beforeexecute " + task.getStatus());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
else
task.execute();
Log.d(TAG, "afterexecute " + task.getStatus());
}
private boolean postLocation(Location location) {
try {
DefaultHttpClient httpClient = new DefaultHttpClient();
HttpPost request = new HttpPost(url);
JSONObject data = locationToJson(location);
params.put("location", data);
// Append android UUID to params so that server can map the UUID to some user in your database on server.
// If you've configured the plugin to execute on BOOT, there's no way to append your user's auth-token to the params
// since this BackgroundGeolocationService will be running in "headless" mode.
//
// It's up to you to register this UUID with your system. You can fetch this UUID using the
// Cordova Device plugin org.apache.cordova.device http://plugins.cordova.io/#/package/org.apache.cordova.device
params.put("android_id", Secure.getString(this.getContentResolver(), Secure.ANDROID_ID));
Log.i(TAG, "data: " + params.toString());
StringEntity se = new StringEntity(params.toString());
request.setEntity(se);
request.setHeader("Accept", "application/json");
request.setHeader("Content-type", "application/json");
Iterator<String> keys = headers.keys();
while( keys.hasNext() ){
String key = keys.next();
if(key != null) {
request.setHeader(key, (String)headers.getString(key));
}
}
Log.d(TAG, "Posting to " + request.getURI().toString());
HttpResponse response = httpClient.execute(request);
Log.i(TAG, "Response received: " + response.getStatusLine());
if (response.getStatusLine().getStatusCode() == 200) {
return true;
} else {
return false;
}
} catch (Throwable e) {
Log.w(TAG, "Exception posting location: " + e);
e.printStackTrace();
return false;
}
}
/**
* Forces the main activity to re-launch if it's unloaded. This is how we're able to rely upon Javascript
* running always, since we for the app to boot.
*/
private void forceMainActivityReload() {
Log.w(TAG, "- Forcing main-activity reload");
PackageManager pm = getPackageManager();
Intent launchIntent = pm.getLaunchIntentForPackage(getApplicationContext().getPackageName());
launchIntent.addFlags(Intent.FLAG_FROM_BACKGROUND);
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION);
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(launchIntent);
}
private void requestActivityUpdates() {
SharedPreferences settings = getSharedPreferences(TAG, 0);
ActivityRecognition.ActivityRecognitionApi.requestActivityUpdates(googleApiClient, settings.getInt("activityRecognitionInterval", activityRecognitionInterval), activityRecognitionPI);
}
private void removeActivityUpdates() {
ActivityRecognition.ActivityRecognitionApi.removeActivityUpdates(googleApiClient, activityRecognitionPI);
}
private void requestLocationUpdates() {
if (!isPaused || !isEnabled) { return; } // <-- Don't engage GPS when app is in foreground
SharedPreferences settings = getSharedPreferences(TAG, 0);
// Configure LocationRequest
locationRequest = LocationRequest.create()
.setPriority(translateDesiredAccuracy(settings.getInt("desiredAccuracy", desiredAccuracy)))
.setInterval(getLocationUpdateInterval())
.setFastestInterval(getFastestLocationUpdateInterval())
.setSmallestDisplacement(settings.getFloat("distanceFilter", distanceFilter));
LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, locationRequest, locationUpdatePI);
}
private void removeLocationUpdates() {
LocationServices.FusedLocationApi.removeLocationUpdates(googleApiClient, locationUpdatePI);
}
/**
* Plays debug sound
* @param name
*/
private void startTone(String name) {
int tone = 0;
int duration = 1000;
if (name.equals("beep")) {
tone = ToneGenerator.TONE_PROP_BEEP;
} else if (name.equals("beep_beep_beep")) {
tone = ToneGenerator.TONE_CDMA_CONFIRM;
} else if (name.equals("long_beep")) {
tone = ToneGenerator.TONE_CDMA_ABBR_ALERT;
} else if (name.equals("doodly_doo")) {
tone = ToneGenerator.TONE_CDMA_ALERT_NETWORK_LITE;
} else if (name.equals("chirp_chirp_chirp")) {
tone = ToneGenerator.TONE_CDMA_ALERT_CALL_GUARD;
} else if (name.equals("dialtone")) {
tone = ToneGenerator.TONE_SUP_RINGTONE;
}
if (isDebugging) {
toneGenerator.startTone(tone, duration);
}
}
@Override
public void onConnectionSuspended(int arg0) {
// TODO Auto-generated method stub
}
@Override
public void onDestroy() {
Log.w(TAG, "- Destroy service");
cleanUp();
super.onDestroy();
}
private void cleanUp() {
instance = null;
EventBus.getDefault().unregister(this);
if (googleApiClient != null && googleApiClient.isConnected()) {
removeActivityUpdates();
removeLocationUpdates();
googleApiClient.disconnect();
}
}
/**
* Convert a Location instance to JSONObject
* @param Location
* @return JSONObject
*/
public static JSONObject locationToJson(Location l) {
try {
JSONObject data = new JSONObject();
data.put("latitude", l.getLatitude());
data.put("longitude", l.getLongitude());
data.put("accuracy", l.getAccuracy());
data.put("speed", l.getSpeed());
data.put("bearing", l.getBearing());
data.put("altitude", l.getAltitude());
data.put("timestamp", l.getTime());
return data;
} catch (JSONException e) {
Log.e(TAG, "could not parse location");
return null;
}
}
public static class PausedEvent {
public boolean isPaused;
public PausedEvent(boolean paused) {
isPaused = paused;
}
}
public static class PaceChangeEvent {
public boolean isMoving;
public PaceChangeEvent(boolean moving) {
isMoving = moving;
}
}
class StationaryLocation extends Location {
public StationaryLocation(Location l) {
super(l);
}
}
private class PostLocationTask extends AsyncTask<Object, Integer, Boolean> {
private Location location;
public void setLocation(Location l) {
location = l;
}
@Override
protected Boolean doInBackground(Object...objects) {
Log.d(TAG, "Executing PostLocationTask#doInBackground");
if (postLocation(location)) {
location = null;
}
return true;
}
@Override
protected void onPostExecute(Boolean result) {
Log.d(TAG, "PostLocationTask#onPostExecture");
}
}
}
package com.transistorsoft.cordova.bggeo;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.util.Log;
/**
* This boot receiver is meant to handle the case where device is first booted after power up.
* This boot the headless BackgroundGeolocationService as configured by this class.
* @author chris scott
*
*/
public class BootReceiver extends BroadcastReceiver {
private static final String TAG = "BackgroundGeolocation";
@Override
public void onReceive(Context context, Intent intent) {
SharedPreferences settings = context.getSharedPreferences(TAG, 0);
boolean startOnBoot = settings.getBoolean("startOnBoot", false);
boolean enabled = settings.getBoolean("enabled", false);
if (!startOnBoot || !enabled) {
return;
}
Log.i(TAG, "- BootReceiver booting service");
// Start the service.
context.startService(new Intent(context, BackgroundGeolocationService.class));
}
}
......@@ -8,9 +8,10 @@ import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONException;
import com.transistorsoft.cordova.bggeo.BackgroundGeolocationService.PaceChangeEvent;
import com.transistorsoft.cordova.bggeo.BackgroundGeolocationService.PausedEvent;
import com.transistorsoft.cordova.bggeo.BackgroundGeolocationService.StationaryLocation;
import com.transistorsoft.locationmanager.BackgroundGeolocationService;
import com.transistorsoft.locationmanager.BackgroundGeolocationService.PaceChangeEvent;
import com.transistorsoft.locationmanager.BackgroundGeolocationService.PausedEvent;
//import com.transistorsoft.locationmanager.BackgroundGeolocationService.StationaryLocation;
import de.greenrobot.event.EventBus;
import android.app.Activity;
......@@ -19,7 +20,7 @@ import android.content.SharedPreferences;
import android.location.Location;
import android.util.Log;
public class BackgroundGeolocationPlugin extends CordovaPlugin {
public class CDVBackgroundGeolocation extends CordovaPlugin {
private static final String TAG = "BackgroundGeolocation";
private static CordovaWebView gWebView;
public static Boolean forceReload = false;
......@@ -109,7 +110,7 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin {
Activity activity = this.cordova.getActivity();
SharedPreferences settings = activity.getSharedPreferences(TAG, 0);
SharedPreferences settings = activity.getSharedPreferences("TSLocationManager", 0);
SharedPreferences.Editor editor = settings.edit();
editor.putBoolean("enabled", isEnabled);
editor.commit();
......@@ -130,9 +131,10 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin {
JSONObject config = data.getJSONObject(0);
Log.i(TAG, "- configure: " + config.toString());
SharedPreferences settings = activity.getSharedPreferences(TAG, 0);
SharedPreferences settings = activity.getSharedPreferences("TSLocationManager", 0);
SharedPreferences.Editor editor = settings.edit();
editor.putBoolean("activityIsActive", true);
editor.putBoolean("isMoving", isMoving);
if (config.has("distanceFilter")) {
......@@ -145,7 +147,7 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin {
editor.putInt("locationUpdateInterval", config.getInt("locationUpdateInterval"));
}
if (config.has("activityRecognitionInterval")) {
editor.putInt("activityRecognitionInterval", config.getInt("activityRecognitionInterval"));
editor.putLong("activityRecognitionInterval", config.getLong("activityRecognitionInterval"));
}
if (config.has("stopTimeout")) {
editor.putLong("stopTimeout", config.getLong("stopTimeout"));
......@@ -190,13 +192,13 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin {
public void onPause(boolean multitasking) {
Log.i(TAG, "- onPause");
if (isEnabled) {
EventBus.getDefault().post(new PausedEvent(true));
}
}
public void onResume(boolean multitasking) {
Log.i(TAG, "- onResume");
if (isEnabled) {
EventBus.getDefault().post(new PausedEvent(false));
}
}
......@@ -208,7 +210,7 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin {
PluginResult result = new PluginResult(PluginResult.Status.OK, BackgroundGeolocationService.locationToJson(location));
result.setKeepCallback(true);
if (location instanceof StationaryLocation) {
if (location instanceof com.transistorsoft.locationmanager.BackgroundGeolocationService.StationaryLocation) {
isMoving = false;
if (stationaryCallback != null) {
runInBackground(stationaryCallback, result);
......@@ -244,6 +246,13 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin {
Log.i(TAG, " stopOnTerminate: " + stopOnTerminate);
Log.i(TAG, " isEnabled: " + isEnabled);
Activity activity = this.cordova.getActivity();
SharedPreferences settings = activity.getSharedPreferences("TSLocationManager", 0);
SharedPreferences.Editor editor = settings.edit();
editor.putBoolean("activityIsActive", false);
editor.commit();
if(isEnabled && stopOnTerminate) {
this.cordova.getActivity().stopService(backgroundServiceIntent);
}
......
package com.transistorsoft.cordova.bggeo;
import de.greenrobot.event.EventBus;
import com.google.android.gms.location.FusedLocationProviderApi;
import android.app.IntentService;
import android.content.Intent;
import android.location.Location;
import android.util.Log;
public class LocationService extends IntentService {
private static final String TAG = "BackgroundGeolocation";
public LocationService() {
super("com.transistorsoft.cordova.bggeo.LocationUpdateService");
}
@Override
protected void onHandleIntent(Intent intent) {
final Location location = intent.getParcelableExtra(FusedLocationProviderApi.KEY_LOCATION_CHANGED);
if (location != null) {
Log.i(TAG, "Location received: " + location.toString());
EventBus.getDefault().post(location);
}
}
}
\ No newline at end of file
//
// BackgroundGeolocation.m
// Cordova Background GeoLocation
//
// Created by Christopher Scott on 2015-04-23.
//
//
#import "BackgroundGeolocation.h"
// Debug sounds for bg-geolocation life-cycle events.
// http://iphonedevwiki.net/index.php/AudioServices
#define exitRegionSound 1005
#define locationSyncSound 1004
#define paceChangeYesSound 1110
#define paceChangeNoSound 1112
#define acquiringLocationSound 1103
#define acquiredLocationSound 1052
#define locationErrorSound 1073
@implementation BackgroundGeolocation {
BOOL isDebugging;
BOOL enabled;
BOOL isUpdatingLocation;
BOOL stopOnTerminate;
NSString *token;
NSString *url;
UIBackgroundTaskIdentifier bgTask;
NSDate *lastBgTaskAt;
NSError *locationError;
BOOL isMoving;
NSNumber *maxBackgroundHours;
CLLocationManager *locationManager;
UILocalNotification *localNotification;
NSDictionary *locationData;
CLLocation *lastLocation;
NSMutableArray *locationQueue;
NSDate *suspendedAt;
CLLocation *stationaryLocation;
CLCircularRegion *stationaryRegion;
NSInteger locationAcquisitionAttempts;
BOOL isAcquiringStationaryLocation;
NSInteger maxStationaryLocationAttempts;
BOOL isAcquiringSpeed;
NSInteger maxSpeedAcquistionAttempts;
// @config params
NSInteger stationaryRadius;
NSInteger distanceFilter;
CLLocationAccuracy desiredAccuracy;
CLActivityType activityType;
BOOL disableElasticity;
}
- (id) init
{
locationManager = [[CLLocationManager alloc] init];
locationManager.delegate = self;
localNotification = [[UILocalNotification alloc] init];
localNotification.timeZone = [NSTimeZone defaultTimeZone];
locationQueue = [[NSMutableArray alloc] init];
// @config params
isDebugging = NO;
stopOnTerminate = NO;
stationaryRadius = 50;
distanceFilter = 50;
desiredAccuracy = kCLLocationAccuracyBest;
disableElasticity = NO;
// Flags
isMoving = NO;
isUpdatingLocation = NO;
stationaryLocation = nil;
stationaryRegion = nil;
maxStationaryLocationAttempts = 4;
maxSpeedAcquistionAttempts = 3;
// Listen to suspend/resume events
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onSuspend:) name:UIApplicationDidEnterBackgroundNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onResume:) name:UIApplicationWillEnterForegroundNotification object:nil];
bgTask = UIBackgroundTaskInvalid;
return [super init];
}
- (void) configure:(NSDictionary*)config
{
[self setConfig:config];
locationManager.activityType = activityType;
locationManager.pausesLocationUpdatesAutomatically = YES;
locationManager.distanceFilter = distanceFilter; // meters
locationManager.desiredAccuracy = desiredAccuracy;
[self log: @"CDVBackgroundGeoLocation configure %@", config];
// ios 8 requires permissions to send local-notifications
if (isDebugging) {
UIApplication *app = [UIApplication sharedApplication];
if ([app respondsToSelector:@selector(registerUserNotificationSettings:)]) {
[app registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert|UIUserNotificationTypeBadge|UIUserNotificationTypeSound categories:nil]];
}
}
}
- (void) setConfig:(NSDictionary*)config
{
if (config[@"debug"]) {
isDebugging = [[config objectForKey:@"debug"] boolValue];
}
if (config[@"stationaryRadius"]) {
stationaryRadius = [[config objectForKey:@"stationaryRadius"] intValue];
}
if (config[@"distanceFilter"]) {
distanceFilter = [[config objectForKey:@"distanceFilter"] intValue];
}
if (config[@"desiredAccuracy"]) {
desiredAccuracy = [self decodeDesiredAccuracy:[[config objectForKey:@"desiredAccuracy"] intValue]];
}
if (config[@"activityType"]) {
activityType = [self decodeActivityType:[config objectForKey:@"activityType"]];
}
if (config[@"stopOnTerminate"]) {
stopOnTerminate = [[config objectForKey:@"stopOnTerminate"] boolValue];
}
if (config[@"disableElasticity"]) {
disableElasticity = [[config objectForKey:@"disableElasticity"] boolValue];
}
}
-(NSInteger)decodeDesiredAccuracy:(NSInteger)accuracy
{
switch (accuracy) {
case 1000:
accuracy = kCLLocationAccuracyKilometer;
break;
case 100:
accuracy = kCLLocationAccuracyHundredMeters;
break;
case 10:
accuracy = kCLLocationAccuracyNearestTenMeters;
break;
case 0:
accuracy = kCLLocationAccuracyBest;
break;
case -1:
accuracy = kCLLocationAccuracyBestForNavigation;
break;
default:
accuracy = kCLLocationAccuracyBest;
}
return accuracy;
}
-(CLActivityType)decodeActivityType:(NSString*)name
{
if ([name caseInsensitiveCompare:@"AutomotiveNavigation"]) {
return CLActivityTypeAutomotiveNavigation;
} else if ([name caseInsensitiveCompare:@"OtherNavigation"]) {
return CLActivityTypeOtherNavigation;
} else if ([name caseInsensitiveCompare:@"Fitness"]) {
return CLActivityTypeFitness;
} else {
return CLActivityTypeOther;
}
}
/**
* Turn on background geolocation
*/
- (void) start
{
enabled = YES;
UIApplicationState state = [[UIApplication sharedApplication] applicationState];
[self log: @"- CDVBackgroundGeoLocation start (background? %d)", state];
[locationManager startMonitoringSignificantLocationChanges];
if (state == UIApplicationStateBackground) {
[self setPace:isMoving];
}
}
/**
* Turn it off
*/
- (void) stop
{
[self log: @"- CDVBackgroundGeoLocation stop"];
enabled = NO;
isMoving = NO;
[self stopUpdatingLocation];
[locationManager stopMonitoringSignificantLocationChanges];
if (stationaryRegion != nil) {
[locationManager stopMonitoringForRegion:stationaryRegion];
stationaryRegion = nil;
}
}
/**
* Change pace to moving/stopped
* @param {Boolean} isMoving
*/
- (void) onPaceChange:(BOOL)value
{
isMoving = value;
[self log: @"- CDVBackgroundGeoLocation onPaceChange %d", isMoving];
UIApplicationState state = [[UIApplication sharedApplication] applicationState];
if (state == UIApplicationStateBackground) {
[self setPace:isMoving];
}
}
/**
* Suspend. Turn on passive location services
*/
-(void) onSuspend:(NSNotification *) notification
{
[self log: @"- CDVBackgroundGeoLocation suspend (enabled? %d)", enabled];
suspendedAt = [NSDate date];
if (enabled) {
// Sample incoming stationary-location candidate: Is it within the current stationary-region? If not, I guess we're moving.
if (!isMoving && stationaryRegion) {
if ([self locationAge:stationaryLocation] < (5 * 60.0)) {
if (isDebugging) {
AudioServicesPlaySystemSound (acquiredLocationSound);
[self notify:[NSString stringWithFormat:@"Continue stationary\n%f,%f", [stationaryLocation coordinate].latitude, [stationaryLocation coordinate].longitude]];
}
[self queue:stationaryLocation type:@"stationary"];
return;
}
}
[self setPace: isMoving];
}
}
/**@
* Resume. Turn background off
*/
-(void) onResume:(NSNotification *) notification
{
[self log: @"- CDVBackgroundGeoLocation resume"];
if (enabled) {
[self stopUpdatingLocation];
}
}
/**
* toggle passive or aggressive location services
*/
- (void)setPace:(BOOL)value
{
[self log: @"- CDVBackgroundGeoLocation setPace %d, stationaryRegion? %d", value, stationaryRegion!=nil];
isMoving = value;
isAcquiringStationaryLocation = NO;
isAcquiringSpeed = NO;
locationAcquisitionAttempts = 0;
stationaryLocation = nil;
if (isDebugging) {
AudioServicesPlaySystemSound (isMoving ? paceChangeYesSound : paceChangeNoSound);
}
if (isMoving) {
if (stationaryRegion) {
[locationManager stopMonitoringForRegion:stationaryRegion];
stationaryRegion = nil;
}
isAcquiringSpeed = YES;
} else {
isAcquiringStationaryLocation = YES;
}
if (isAcquiringSpeed || isAcquiringStationaryLocation) {
// Crank up the GPS power temporarily to get a good fix on our current location
[self stopUpdatingLocation];
locationManager.distanceFilter = kCLDistanceFilterNone;
locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation;
[self startUpdatingLocation];
}
}
-(void) locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
[self log: @"- CDVBackgroundGeoLocation didUpdateLocations (isMoving: %d)", isMoving];
locationError = nil;
if (isMoving && !isUpdatingLocation) {
[self startUpdatingLocation];
}
CLLocation *location = [locations lastObject];
if (!isMoving && !isAcquiringStationaryLocation && !stationaryLocation) {
// Perhaps our GPS signal was interupted, re-acquire a stationaryLocation now.
[self setPace: NO];
}
// test the age of the location measurement to determine if the measurement is cached
// in most cases you will not want to rely on cached measurements
if ([self locationAge:location] > 5.0) return;
// test that the horizontal accuracy does not indicate an invalid measurement
if (location.horizontalAccuracy < 0) return;
lastLocation = location;
// test the measurement to see if it is more accurate than the previous measurement
if (isAcquiringStationaryLocation) {
[self log: @"- Acquiring stationary location, accuracy: %f", location.horizontalAccuracy];
if (isDebugging) {
AudioServicesPlaySystemSound (acquiringLocationSound);
}
if (stationaryLocation == nil || stationaryLocation.horizontalAccuracy > location.horizontalAccuracy) {
stationaryLocation = location;
}
if (++locationAcquisitionAttempts == maxStationaryLocationAttempts) {
isAcquiringStationaryLocation = NO;
[self startMonitoringStationaryRegion:stationaryLocation];
} else {
// Unacceptable stationary-location: bail-out and wait for another.
return;
}
} else if (isAcquiringSpeed) {
if (isDebugging) {
AudioServicesPlaySystemSound (acquiringLocationSound);
}
if (++locationAcquisitionAttempts == maxSpeedAcquistionAttempts) {
if (isDebugging) {
[self notify:@"Aggressive monitoring engaged"];
}
// We should have a good sample for speed now, power down our GPS as configured by user.
isAcquiringSpeed = NO;
[locationManager setDesiredAccuracy:desiredAccuracy];
[locationManager setDistanceFilter:[self calculateDistanceFilter:location.speed]];
[self startUpdatingLocation];
} else {
return;
}
} else if (isMoving) {
// Adjust distanceFilter incrementally based upon current speed
float newDistanceFilter = [self calculateDistanceFilter:location.speed];
if (newDistanceFilter != locationManager.distanceFilter) {
[self log: @"- CDVBackgroundGeoLocation updated distanceFilter, new: %f, old: %f", newDistanceFilter, locationManager.distanceFilter];
[locationManager setDistanceFilter:newDistanceFilter];
[self startUpdatingLocation];
}
} else if ([self locationIsBeyondStationaryRegion:location]) {
if (isDebugging) {
[self notify:@"Manual stationary exit-detection"];
}
[self setPace:YES];
}
[self queue:location type:@"current"];
}
/**
* Creates a new circle around user and region-monitors it for exit
*/
- (void) startMonitoringStationaryRegion:(CLLocation*)location {
stationaryLocation = location;
// fire onStationary @event for Javascript.
[self queue:location type:@"stationary"];
CLLocationCoordinate2D coord = [location coordinate];
[self log: @"- CDVBackgroundGeoLocation createStationaryRegion (%f,%f)", coord.latitude, coord.longitude];
if (isDebugging) {
AudioServicesPlaySystemSound (acquiredLocationSound);
[self notify:[NSString stringWithFormat:@"Acquired stationary location\n%f, %f", location.coordinate.latitude,location.coordinate.longitude]];
}
if (stationaryRegion != nil) {
[locationManager stopMonitoringForRegion:stationaryRegion];
}
isAcquiringStationaryLocation = NO;
stationaryRegion = [[CLCircularRegion alloc] initWithCenter: coord radius:stationaryRadius identifier:@"BackgroundGeoLocation stationary region"];
stationaryRegion.notifyOnExit = YES;
[locationManager startMonitoringForRegion:stationaryRegion];
[self stopUpdatingLocation];
locationManager.distanceFilter = distanceFilter;
locationManager.desiredAccuracy = desiredAccuracy;
}
/**
* Manual stationary location his-testing. This seems to help stationary-exit detection in some places where the automatic geo-fencing soesn't
*/
-(bool)locationIsBeyondStationaryRegion:(CLLocation*)location
{
[self log: @"- CDVBackgroundGeoLocation locationIsBeyondStationaryRegion"];
if (![stationaryRegion containsCoordinate:[location coordinate]]) {
double pointDistance = [stationaryLocation distanceFromLocation:location];
return (pointDistance - stationaryLocation.horizontalAccuracy - location.horizontalAccuracy) > stationaryRadius;
} else {
return NO;
}
}
/**
* Calculates distanceFilter by rounding speed to nearest 5 and multiplying by 10.
* - Clamped at 1km max.
* - Disabled by #disableElasticity
*/
-(float) calculateDistanceFilter:(float)speed
{
if (disableElasticity == YES) {
return distanceFilter;
}
float newDistanceFilter = distanceFilter;
if (speed < 100) {
// (rounded-speed-to-nearest-5) / 2)^2
// eg 5.2 becomes (5/2)^2
newDistanceFilter = pow((5.0 * floorf(fabsf(speed) / 5.0 + 0.5f)), 2) + distanceFilter;
}
return (newDistanceFilter < 1000) ? newDistanceFilter : 1000;
}
-(void) queue:(CLLocation*)location type:(id)type
{
[self log: @"- CDVBackgroundGeoLocation queue %@", type];
NSMutableDictionary *data = [self locationToHash:location];
[data setObject:type forKey:@"location_type"];
[locationQueue addObject:data];
[self flushQueue];
}
- (void) flushQueue
{
// Sanity-check the duration of last bgTask: If greater than 30s, kill it.
if (bgTask != UIBackgroundTaskInvalid) {
if (-[lastBgTaskAt timeIntervalSinceNow] > 30.0) {
[self log: @"- CDVBackgroundGeoLocation#flushQueue has to kill an out-standing background-task!"];
[self stopBackgroundTask];
}
return;
}
if ([locationQueue count] > 0) {
NSMutableDictionary *data = [locationQueue lastObject];
[locationQueue removeObject:data];
// Create a background-task and delegate to Javascript for syncing location
bgTask = [self createBackgroundTask];
[self runInBackground:^{
[self sync:data];
}];
}
}
- (void)runInBackground:(void (^)())block
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), block);
}
/**
* We are running in the background if this is being executed.
* We can't assume normal network access.
* bgTask is defined as an instance variable of type UIBackgroundTaskIdentifier
*/
-(void) sync:(NSMutableDictionary*)data
{
[self log: @"- CDVBackgroundGeoLocation#sync"];
[self log: @" type: %@, position: %@,%@ speed: %@", [data objectForKey:@"location_type"], [data objectForKey:@"latitude"], [data objectForKey:@"longitude"], [data objectForKey:@"speed"]];
if (isDebugging) {
[self notify:[NSString stringWithFormat:@"Location update: %s\nSPD: %0.0f | DF: %ld | ACY: %0.0f",
((isMoving) ? "MOVING" : "STATIONARY"),
[[data objectForKey:@"speed"] doubleValue],
(long) locationManager.distanceFilter,
[[data objectForKey:@"accuracy"] doubleValue]]];
AudioServicesPlaySystemSound (locationSyncSound);
}
// Build a resultset for javascript callback.
NSString *locationType = [data objectForKey:@"location_type"];
if ([locationType isEqualToString:@"stationary"]) {
// Any javascript stationaryRegion event-listeners?
[data setObject:[NSNumber numberWithDouble:stationaryRadius] forKey:@"radius"];
[[NSNotificationCenter defaultCenter] postNotificationName:@"BackgroundGeolocation.stationarylocation" object:data];
//[self fireStationaryRegionListeners:data];
} else if ([locationType isEqualToString:@"current"]) {
[[NSNotificationCenter defaultCenter] postNotificationName:@"BackgroundGeolocation.location" object:data];
} else {
[self log: @"- CDVBackgroundGeoLocation#sync could not determine location_type."];
[self stopBackgroundTask];
}
}
/**
* Called by js to signify the end of a background-geolocation event
*/
-(void) finish
{
[self log: @"- CDVBackgroundGeoLocation finish"];
[self stopBackgroundTask];
}
/**
* Fetches current stationaryLocation
*/
- (NSDictionary*) getStationaryLocation
{
NSDictionary *data = [self locationToHash:stationaryLocation];
return data;
}
- (bool) stationaryRegionContainsLocation:(CLLocation*)location {
CLCircularRegion *region = [locationManager.monitoredRegions member:stationaryRegion];
return ([region containsCoordinate:location.coordinate]) ? YES : NO;
}
-(UIBackgroundTaskIdentifier) createBackgroundTask
{
lastBgTaskAt = [NSDate date];
return [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[self stopBackgroundTask];
}];
}
- (void) stopBackgroundTask
{
UIApplication *app = [UIApplication sharedApplication];
[self log: @"- CDVBackgroundGeoLocation stopBackgroundTask (remaining t: %f)", app.backgroundTimeRemaining];
if (bgTask != UIBackgroundTaskInvalid)
{
[app endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}
[self flushQueue];
}
-(NSMutableDictionary*) locationToHash:(CLLocation*)location
{
NSMutableDictionary *returnInfo;
returnInfo = [NSMutableDictionary dictionaryWithCapacity:10];
NSNumber* timestamp = [NSNumber numberWithDouble:([location.timestamp timeIntervalSince1970] * 1000)];
[returnInfo setObject:timestamp forKey:@"timestamp"];
[returnInfo setObject:[NSNumber numberWithDouble:location.speed] forKey:@"speed"];
[returnInfo setObject:[NSNumber numberWithDouble:location.verticalAccuracy] forKey:@"altitudeAccuracy"];
[returnInfo setObject:[NSNumber numberWithDouble:location.horizontalAccuracy] forKey:@"accuracy"];
[returnInfo setObject:[NSNumber numberWithDouble:location.course] forKey:@"heading"];
[returnInfo setObject:[NSNumber numberWithDouble:location.altitude] forKey:@"altitude"];
[returnInfo setObject:[NSNumber numberWithDouble:location.coordinate.latitude] forKey:@"latitude"];
[returnInfo setObject:[NSNumber numberWithDouble:location.coordinate.longitude] forKey:@"longitude"];
return returnInfo;
}
- (NSTimeInterval) locationAge:(CLLocation*)location
{
return -[location.timestamp timeIntervalSinceNow];
}
- (void) stopUpdatingLocation
{
[locationManager stopUpdatingLocation];
isUpdatingLocation = NO;
}
- (void) startUpdatingLocation
{
SEL requestSelector = NSSelectorFromString(@"requestAlwaysAuthorization");
if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined && [locationManager respondsToSelector:requestSelector]) {
((void (*)(id, SEL))[locationManager methodForSelector:requestSelector])(locationManager, requestSelector);
[locationManager startUpdatingLocation];
isUpdatingLocation = YES;
} else {
[locationManager startUpdatingLocation];
isUpdatingLocation = YES;
}
}
- (void) locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status
{
[self log: @"- CDVBackgroundGeoLocation didChangeAuthorizationStatus %u", status];
if (isDebugging) {
[self notify:[NSString stringWithFormat:@"Authorization status changed %u", status]];
}
}
- (void) notify:(NSString*)message
{
localNotification.fireDate = [NSDate date];
localNotification.alertBody = message;
[[UIApplication sharedApplication] scheduleLocalNotification:localNotification];
}
/**
* Log a message. Only outputs when @config debug: true
*/
- (void) log:(NSString *)format, ...
{
if (isDebugging) {
va_list args;
va_start(args, format);
va_end(args);
NSLogv(format, args);
}
}
/**
* Called when user exits their stationary radius (ie: they walked ~50m away from their last recorded location.
* - turn on more aggressive location monitoring.
*/
- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region
{
[self log: @"- CDVBackgroundGeoLocation exit region"];
if (isDebugging) {
AudioServicesPlaySystemSound (exitRegionSound);
[self notify:@"Exit stationary region"];
}
[self setPace:YES];
}
/**
* 1. turn off std location services
* 2. turn on significantChanges API
* 3. create a region and start monitoring exits.
*/
- (void)locationManagerDidPauseLocationUpdates:(CLLocationManager *)manager
{
[self log: @"- CDVBackgroundGeoLocation paused location updates"];
if (isDebugging) {
[self notify:@"Stop detected"];
}
if (locationError) {
isMoving = NO;
[self startMonitoringStationaryRegion:lastLocation];
[self stopUpdatingLocation];
} else {
[self setPace:NO];
}
}
/**
* 1. Turn off significantChanges ApI
* 2. turn on std. location services
* 3. nullify stationaryRegion
*/
- (void)locationManagerDidResumeLocationUpdates:(CLLocationManager *)manager
{
[self log: @"- CDVBackgroundGeoLocation resume location updates"];
if (isDebugging) {
[self notify:@"Resume location updates"];
}
[self setPace:YES];
}
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
{
[self log: @"- CDVBackgroundGeoLocation locationManager failed: %@", error];
if (isDebugging) {
AudioServicesPlaySystemSound (locationErrorSound);
[self notify:[NSString stringWithFormat:@"Location error: %@", error.localizedDescription]];
}
locationError = error;
switch(error.code) {
case kCLErrorLocationUnknown:
case kCLErrorNetwork:
case kCLErrorRegionMonitoringDenied:
case kCLErrorRegionMonitoringSetupDelayed:
case kCLErrorRegionMonitoringResponseDelayed:
case kCLErrorGeocodeFoundNoResult:
case kCLErrorGeocodeFoundPartialResult:
case kCLErrorGeocodeCanceled:
break;
case kCLErrorDenied:
[self stopUpdatingLocation];
break;
default:
[self stopUpdatingLocation];
}
}
/**
* Termination. Checks to see if it should turn off
*/
-(void) onAppTerminate
{
[self log: @"- CDVBackgroundGeoLocation appTerminate"];
if (enabled && stopOnTerminate) {
[self log: @"- CDVBackgroundGeoLocation stoping on terminate"];
enabled = NO;
isMoving = NO;
[self stopUpdatingLocation];
[locationManager stopMonitoringSignificantLocationChanges];
if (stationaryRegion != nil) {
[locationManager stopMonitoringForRegion:stationaryRegion];
stationaryRegion = nil;
}
}
}
- (void)dealloc
{
locationManager.delegate = nil;
}
@end
......@@ -5,8 +5,9 @@
//
#import <Cordova/CDVPlugin.h>
#import <TSLocationManager/TSLocationManager.h>
@interface CDVBackgroundGeoLocation : CDVPlugin
@interface CDVBackgroundGeolocation : CDVPlugin
@property (nonatomic, strong) NSString* syncCallbackId;
@property (nonatomic, strong) NSMutableArray* stationaryRegionListeners;
......
......@@ -4,10 +4,9 @@
// Created by Chris Scott <chris@transistorsoft.com> on 2013-06-15
//
#import "CDVBackgroundGeoLocation.h"
#import "BackgroundGeolocation.h"
@implementation CDVBackgroundGeoLocation {
BackgroundGeolocation *bgGeo;
@implementation CDVBackgroundGeolocation {
TSLocationManager *bgGeo;
NSDictionary *config;
}
......@@ -15,10 +14,10 @@
- (void)pluginInitialize
{
bgGeo = [[BackgroundGeolocation alloc] init];
bgGeo = [[TSLocationManager alloc] init];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onLocationChanged:) name:@"BackgroundGeolocation.location" object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onStationaryLocation:) name:@"BackgroundGeolocation.stationarylocation" object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onLocationChanged:) name:@"TSLocationManager.location" object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onStationaryLocation:) name:@"TSLocationManager.stationary" object:nil];
}
/**
......@@ -53,11 +52,6 @@
- (void) start:(CDVInvokedUrlCommand*)command
{
[bgGeo start];
CDVPluginResult* result = nil;
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
}
/**
* Turn it off
......@@ -65,10 +59,6 @@
- (void) stop:(CDVInvokedUrlCommand*)command
{
[bgGeo stop];
CDVPluginResult* result = nil;
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
}
/**
......@@ -79,17 +69,14 @@
{
BOOL moving = [[command.arguments objectAtIndex: 0] boolValue];
[bgGeo onPaceChange:moving];
CDVPluginResult* result = nil;
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
}
/**
* location handler from BackgroundGeolocation
*/
- (void)onLocationChanged:(NSNotification*)notification {
NSDictionary *locationData = notification.object;
CLLocation *location = notification.object;
NSDictionary *locationData = [bgGeo locationToDictionary:location];
CDVPluginResult* result = nil;
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:locationData];
......@@ -99,7 +86,8 @@
- (void) onStationaryLocation:(NSNotification*)notification
{
NSMutableDictionary *locationData = notification.object;
CLLocation *location = notification.object;
NSDictionary *locationData = [bgGeo locationToDictionary:location];
if (![self.stationaryRegionListeners count]) {
[bgGeo stopBackgroundTask];
......
Versions/Current/Headers
\ No newline at end of file
Versions/Current/Resources
\ No newline at end of file
Versions/Current/TSLocationManager
\ No newline at end of file
#import <CoreLocation/CoreLocation.h>
#import <AudioToolbox/AudioToolbox.h>
#import <sqlite3.h>
@interface BackgroundGeolocation : NSObject <CLLocationManagerDelegate>
@interface TSLocationManager : NSObject <CLLocationManagerDelegate>
@property (nonatomic, weak) id <CDVCommandDelegate> commandDelegate;
@property (strong, nonatomic) NSString *databasePath;
@property (nonatomic) sqlite3 *db;
- (void) configure:(NSDictionary*)config;
- (void) start;
......@@ -16,6 +18,8 @@
- (void) onSuspend:(NSNotification *)notification;
- (void) onResume:(NSNotification *)notification;
- (void) onAppTerminate;
- (BOOL) isEnabled;
- (NSMutableDictionary*) locationToDictionary:(CLLocation*)location;
@end
framework module TSLocationManager {
umbrella header "TSLocationManager.h"
export *
module * { export * }
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>files</key>
<dict>
<key>Headers/TSLocationManager.h</key>
<data>
IDPizw9H7yMm0B/vqQy2MaU2iCs=
</data>
<key>Info.plist</key>
<data>
xolLvzV3rJusVEx/xOJXBAgSQRw=
</data>
<key>Modules/module.modulemap</key>
<data>
ZlH8rkma2jB7yWJU5dFqHfgbHZc=
</data>
</dict>
<key>files2</key>
<dict>
<key>Headers/TSLocationManager.h</key>
<data>
IDPizw9H7yMm0B/vqQy2MaU2iCs=
</data>
<key>Modules/module.modulemap</key>
<data>
ZlH8rkma2jB7yWJU5dFqHfgbHZc=
</data>
</dict>
<key>rules</key>
<dict>
<key>^</key>
<true/>
<key>^.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^version.plist$</key>
<true/>
</dict>
<key>rules2</key>
<dict>
<key>.*\.dSYM($|/)</key>
<dict>
<key>weight</key>
<real>11</real>
</dict>
<key>^</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^(.*/)?\.DS_Store$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>2000</real>
</dict>
<key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
<dict>
<key>nested</key>
<true/>
<key>weight</key>
<real>10</real>
</dict>
<key>^.*</key>
<true/>
<key>^.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Info\.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^PkgInfo$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^[^/]+$</key>
<dict>
<key>nested</key>
<true/>
<key>weight</key>
<real>10</real>
</dict>
<key>^embedded\.provisionprofile$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^version\.plist$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
</dict>
</dict>
</plist>
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