Commit a4c45e4e authored by Chris Scott's avatar Chris Scott

Implementing exception handling when running callbacks executed within...

Implementing exception handling when running callbacks executed within background threads.  Since these callbacks are provided a taskId which must be passed back to the native code when a Javascript callback is complete, a Javascript error in application-code can cause cause the bgGeo.finish(taskId) method to fail being called, resulting in the OS killing the app for poor background behaviour (espcially iOS).  These errors can be difficult to debug, so I've wrapped callbacks provided with a taskId in try/catch blocks.  If your application code throws an excpetion, the plugin catches it and finishes the task for you.  In addition, the exception handler signals to the native-code that a javascript exception occurred, providing it with some error details.  This allows the native code to show an alert box (in debug-mode) and play an annoying alert-sound so that developers know that something went wrong
parent a46e882b
...@@ -15,7 +15,8 @@ import com.google.android.gms.location.DetectedActivity; ...@@ -15,7 +15,8 @@ import com.google.android.gms.location.DetectedActivity;
import com.transistorsoft.locationmanager.BackgroundGeolocationService; import com.transistorsoft.locationmanager.BackgroundGeolocationService;
import com.google.android.gms.location.Geofence; import com.google.android.gms.location.Geofence;
import com.google.android.gms.location.GeofencingEvent; import com.google.android.gms.location.GeofencingEvent;
//import com.transistorsoft.locationmanager.BackgroundGeolocationService.StationaryLocation; import android.app.AlertDialog;
import android.content.DialogInterface;
import de.greenrobot.event.EventBus; import de.greenrobot.event.EventBus;
import android.app.Activity; import android.app.Activity;
...@@ -23,6 +24,8 @@ import android.content.Intent; ...@@ -23,6 +24,8 @@ import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.location.Location; import android.location.Location;
import android.util.Log; import android.util.Log;
import android.media.AudioManager;
import android.media.ToneGenerator;
public class CDVBackgroundGeolocation extends CordovaPlugin { public class CDVBackgroundGeolocation extends CordovaPlugin {
private static final String TAG = "BackgroundGeolocation"; private static final String TAG = "BackgroundGeolocation";
...@@ -32,6 +35,7 @@ public class CDVBackgroundGeolocation extends CordovaPlugin { ...@@ -32,6 +35,7 @@ public class CDVBackgroundGeolocation extends CordovaPlugin {
public static final String ACTION_START = "start"; public static final String ACTION_START = "start";
public static final String ACTION_STOP = "stop"; public static final String ACTION_STOP = "stop";
public static final String ACTION_FINISH = "finish"; public static final String ACTION_FINISH = "finish";
public static final String ACTION_ERROR = "error";
public static final String ACTION_ON_PACE_CHANGE = "onPaceChange"; public static final String ACTION_ON_PACE_CHANGE = "onPaceChange";
public static final String ACTION_CONFIGURE = "configure"; public static final String ACTION_CONFIGURE = "configure";
public static final String ACTION_SET_CONFIG = "setConfig"; public static final String ACTION_SET_CONFIG = "setConfig";
...@@ -64,6 +68,8 @@ public class CDVBackgroundGeolocation extends CordovaPlugin { ...@@ -64,6 +68,8 @@ public class CDVBackgroundGeolocation extends CordovaPlugin {
private CallbackContext paceChangeCallback; private CallbackContext paceChangeCallback;
private CallbackContext getGeofencesCallback; private CallbackContext getGeofencesCallback;
private ToneGenerator toneGenerator;
private List<CallbackContext> geofenceCallbacks = new ArrayList<CallbackContext>(); private List<CallbackContext> geofenceCallbacks = new ArrayList<CallbackContext>();
public static boolean isActive() { public static boolean isActive() {
...@@ -76,6 +82,7 @@ public class CDVBackgroundGeolocation extends CordovaPlugin { ...@@ -76,6 +82,7 @@ public class CDVBackgroundGeolocation extends CordovaPlugin {
backgroundServiceIntent = new Intent(this.cordova.getActivity(), BackgroundGeolocationService.class); backgroundServiceIntent = new Intent(this.cordova.getActivity(), BackgroundGeolocationService.class);
toneGenerator = new ToneGenerator(AudioManager.STREAM_NOTIFICATION, 100);
// Register for events fired by our IntentService "LocationService" // Register for events fired by our IntentService "LocationService"
} }
...@@ -96,6 +103,10 @@ public class CDVBackgroundGeolocation extends CordovaPlugin { ...@@ -96,6 +103,10 @@ public class CDVBackgroundGeolocation extends CordovaPlugin {
} else if (ACTION_FINISH.equalsIgnoreCase(action)) { } else if (ACTION_FINISH.equalsIgnoreCase(action)) {
result = true; result = true;
callbackContext.success(); callbackContext.success();
} else if (ACTION_ERROR.equalsIgnoreCase(action)) {
result = true;
this.onError(data.getString(1));
callbackContext.success();
} else if (ACTION_CONFIGURE.equalsIgnoreCase(action)) { } else if (ACTION_CONFIGURE.equalsIgnoreCase(action)) {
result = applyConfig(data); result = applyConfig(data);
if (result) { if (result) {
...@@ -213,11 +224,7 @@ public class CDVBackgroundGeolocation extends CordovaPlugin { ...@@ -213,11 +224,7 @@ public class CDVBackgroundGeolocation extends CordovaPlugin {
EventBus.getDefault().post(event); EventBus.getDefault().post(event);
} else if (ACTION_PLAY_SOUND.equalsIgnoreCase(action)) { } else if (ACTION_PLAY_SOUND.equalsIgnoreCase(action)) {
result = true; result = true;
Bundle event = new Bundle(); playSound(data.getInt(0));
event.putString("name", action);
event.putBoolean("request", true);
event.putInt("soundId", data.getInt(0));
EventBus.getDefault().post(event);
callbackContext.success(); callbackContext.success();
} }
return result; return result;
...@@ -350,8 +357,10 @@ public class CDVBackgroundGeolocation extends CordovaPlugin { ...@@ -350,8 +357,10 @@ public class CDVBackgroundGeolocation extends CordovaPlugin {
if (ACTION_GET_LOCATIONS.equalsIgnoreCase(name)) { if (ACTION_GET_LOCATIONS.equalsIgnoreCase(name)) {
try { try {
JSONArray json = new JSONArray(event.getString("data")); JSONObject params = new JSONObject();
PluginResult result = new PluginResult(PluginResult.Status.OK, json); params.put("locations", new JSONArray(event.getString("data")));
params.put("taskId", "android-bg-task-id");
PluginResult result = new PluginResult(PluginResult.Status.OK, params);
runInBackground(getLocationsCallback, result); runInBackground(getLocationsCallback, result);
} catch (JSONException e) { } catch (JSONException e) {
// TODO Auto-generated catch block // TODO Auto-generated catch block
...@@ -363,8 +372,10 @@ public class CDVBackgroundGeolocation extends CordovaPlugin { ...@@ -363,8 +372,10 @@ public class CDVBackgroundGeolocation extends CordovaPlugin {
Boolean success = event.getBoolean("success"); Boolean success = event.getBoolean("success");
if (success) { if (success) {
try { try {
JSONArray json = new JSONArray(event.getString("data")); JSONObject params = new JSONObject();
PluginResult result = new PluginResult(PluginResult.Status.OK, json); params.put("locations", new JSONArray(event.getString("data")));
params.put("taskId", "android-bg-task-id");
PluginResult result = new PluginResult(PluginResult.Status.OK, params);
runInBackground(syncCallback, result); runInBackground(syncCallback, result);
} catch (JSONException e) { } catch (JSONException e) {
// TODO Auto-generated catch block // TODO Auto-generated catch block
...@@ -459,6 +470,12 @@ public class CDVBackgroundGeolocation extends CordovaPlugin { ...@@ -459,6 +470,12 @@ public class CDVBackgroundGeolocation extends CordovaPlugin {
} }
} }
private void playSound(int soundId) {
int duration = 1000;
toneGenerator = new ToneGenerator(AudioManager.STREAM_NOTIFICATION, 100);
toneGenerator.startTone(soundId, duration);
}
/** /**
* Run a javascript callback in Background * Run a javascript callback in Background
* @param cb * @param cb
...@@ -474,6 +491,27 @@ public class CDVBackgroundGeolocation extends CordovaPlugin { ...@@ -474,6 +491,27 @@ public class CDVBackgroundGeolocation extends CordovaPlugin {
} }
} }
private void onError(String error) {
String message = "BG Geolocation caught a Javascript exception while running in background-thread:\n".concat(error);
Log.e(TAG, message);
SharedPreferences settings = this.cordova.getActivity().getSharedPreferences("TSLocationManager", 0);
// Show alert popup with js error
if (settings.contains("debug") && settings.getBoolean("debug", false)) {
playSound(68);
AlertDialog.Builder builder = new AlertDialog.Builder(this.cordova.getActivity());
builder.setMessage(message)
.setCancelable(false)
.setNegativeButton("OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
//do things
}
});
AlertDialog alert = builder.create();
alert.show();
}
}
/** /**
* Override method in CordovaPlugin. * Override method in CordovaPlugin.
* Checks to see if it should turn off * Checks to see if it should turn off
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
@interface CDVBackgroundGeolocation : CDVPlugin @interface CDVBackgroundGeolocation : CDVPlugin
@property (nonatomic, strong) NSString* syncCallbackId; @property (nonatomic, strong) NSString* syncCallbackId;
@property (nonatomic) UIBackgroundTaskIdentifier syncTaskId;
@property (nonatomic, strong) NSString* locationCallbackId; @property (nonatomic, strong) NSString* locationCallbackId;
@property (nonatomic, strong) NSMutableArray* geofenceListeners; @property (nonatomic, strong) NSMutableArray* geofenceListeners;
@property (nonatomic, strong) NSMutableArray* stationaryRegionListeners; @property (nonatomic, strong) NSMutableArray* stationaryRegionListeners;
...@@ -18,6 +19,7 @@ ...@@ -18,6 +19,7 @@
- (void) start:(CDVInvokedUrlCommand*)command; - (void) start:(CDVInvokedUrlCommand*)command;
- (void) stop:(CDVInvokedUrlCommand*)command; - (void) stop:(CDVInvokedUrlCommand*)command;
- (void) finish:(CDVInvokedUrlCommand*)command; - (void) finish:(CDVInvokedUrlCommand*)command;
- (void) error:(CDVInvokedUrlCommand*)command;
- (void) onPaceChange:(CDVInvokedUrlCommand*)command; - (void) onPaceChange:(CDVInvokedUrlCommand*)command;
- (void) setConfig:(CDVInvokedUrlCommand*)command; - (void) setConfig:(CDVInvokedUrlCommand*)command;
- (void) addStationaryRegionListener:(CDVInvokedUrlCommand*)command; - (void) addStationaryRegionListener:(CDVInvokedUrlCommand*)command;
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
NSDictionary *config; NSDictionary *config;
} }
@synthesize syncCallbackId, locationCallbackId, geofenceListeners, stationaryRegionListeners; @synthesize syncCallbackId, syncTaskId, locationCallbackId, geofenceListeners, stationaryRegionListeners;
- (void)pluginInitialize - (void)pluginInitialize
{ {
...@@ -149,10 +149,16 @@ ...@@ -149,10 +149,16 @@
- (void) onSyncComplete:(NSNotification*)notification - (void) onSyncComplete:(NSNotification*)notification
{ {
NSArray *locations = [notification.userInfo objectForKey:@"locations"]; NSDictionary *params = @{
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:locations]; @"locations": [notification.userInfo objectForKey:@"locations"],
@"taskId": @(syncTaskId)
};
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:params];
[self.commandDelegate sendPluginResult:result callbackId:syncCallbackId]; [self.commandDelegate sendPluginResult:result callbackId:syncCallbackId];
syncCallbackId = nil;
// Ready for another sync task.
syncCallbackId = nil;
syncTaskId = UIBackgroundTaskInvalid;
} }
/** /**
...@@ -170,10 +176,12 @@ ...@@ -170,10 +176,12 @@
* Fetches current stationaryLocation * Fetches current stationaryLocation
*/ */
- (void) getLocations:(CDVInvokedUrlCommand *)command - (void) getLocations:(CDVInvokedUrlCommand *)command
{ {
NSArray* locations = [bgGeo getLocations]; NSDictionary *params = @{
@"locations": [bgGeo getLocations],
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:locations]; @"taskId": @([bgGeo createBackgroundTask])
};
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:params];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
} }
...@@ -187,10 +195,15 @@ ...@@ -187,10 +195,15 @@
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
return; return;
} }
syncCallbackId = command.callbackId;
// Important to set these before we execute #sync since this fires a *very fast* async NSNotification event!
syncCallbackId = command.callbackId;
syncTaskId = [bgGeo createBackgroundTask];
NSArray* locations = [bgGeo sync]; NSArray* locations = [bgGeo sync];
if (locations == nil) { if (locations == nil) {
syncCallbackId = nil; syncCallbackId = nil;
syncTaskId = UIBackgroundTaskInvalid;
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Sync failed. Is there a network connection?"]; CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Sync failed. Is there a network connection?"];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
} }
...@@ -273,6 +286,17 @@ ...@@ -273,6 +286,17 @@
UIBackgroundTaskIdentifier taskId = [[command.arguments objectAtIndex: 0] integerValue]; UIBackgroundTaskIdentifier taskId = [[command.arguments objectAtIndex: 0] integerValue];
[bgGeo stopBackgroundTask:taskId]; [bgGeo stopBackgroundTask:taskId];
} }
/**
* Called by js to signal a caught exception from application code.
*/
-(void) error:(CDVInvokedUrlCommand*)command
{
UIBackgroundTaskIdentifier taskId = [[command.arguments objectAtIndex: 0] integerValue];
NSString *error = [command.arguments objectAtIndex:1];
[bgGeo error:taskId message:error];
}
/** /**
* If you don't stopMonitoring when application terminates, the app will be awoken still when a * If you don't stopMonitoring when application terminates, the app will be awoken still when a
* new location arrives, essentially monitoring the user's location even when they've killed the app. * new location arrives, essentially monitoring the user's location even when they've killed the app.
......
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
- (NSArray*) getLocations; - (NSArray*) getLocations;
- (UIBackgroundTaskIdentifier) createBackgroundTask; - (UIBackgroundTaskIdentifier) createBackgroundTask;
- (void) stopBackgroundTask:(UIBackgroundTaskIdentifier)taskId; - (void) stopBackgroundTask:(UIBackgroundTaskIdentifier)taskId;
- (void) error:(UIBackgroundTaskIdentifier)taskId message:(NSString*)message;
- (void) onPaceChange:(BOOL)value; - (void) onPaceChange:(BOOL)value;
- (void) setConfig:(NSDictionary*)command; - (void) setConfig:(NSDictionary*)command;
- (NSDictionary*) getStationaryLocation; - (NSDictionary*) getStationaryLocation;
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
<dict> <dict>
<key>Headers/TSLocationManager.h</key> <key>Headers/TSLocationManager.h</key>
<data> <data>
D1StsS2xjuq9sRvFuNw3O74hUYA= ++tA66F/FJQ/qMup0fGBER20r9U=
</data> </data>
<key>Info.plist</key> <key>Info.plist</key>
<data> <data>
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
<dict> <dict>
<key>Headers/TSLocationManager.h</key> <key>Headers/TSLocationManager.h</key>
<data> <data>
D1StsS2xjuq9sRvFuNw3O74hUYA= ++tA66F/FJQ/qMup0fGBER20r9U=
</data> </data>
<key>Modules/module.modulemap</key> <key>Modules/module.modulemap</key>
<data> <data>
......
...@@ -16,7 +16,9 @@ module.exports = { ...@@ -16,7 +16,9 @@ module.exports = {
* @property {Object} config * @property {Object} config
*/ */
config: {}, config: {},
/**
* @private {Error} error
*/
configure: function(success, failure, config) { configure: function(success, failure, config) {
var me = this; var me = this;
config = config || {}; config = config || {};
...@@ -32,7 +34,9 @@ module.exports = { ...@@ -32,7 +34,9 @@ module.exports = {
if (location.timestamp) { if (location.timestamp) {
location.timestamp = new Date(location.timestamp); location.timestamp = new Date(location.timestamp);
} }
success.call(this, location, taskId); me._runBackgroundTask(taskId, function() {
success.call(this, location, taskId);
});
} }
exec(mySuccess, exec(mySuccess,
failure || function() {}, failure || function() {},
...@@ -65,6 +69,16 @@ module.exports = { ...@@ -65,6 +69,16 @@ module.exports = {
'finish', 'finish',
[taskId]); [taskId]);
}, },
error: function(taskId, message) {
if (!taskId) {
throw "BackgroundGeolocation#error must now be provided with a taskId as 1st param, eg: bgGeo.finish(taskId). taskId is provided by 2nd param in callback";
}
exec(function() {},
function() {},
'BackgroundGeoLocation',
'error',
[taskId, message]);
},
changePace: function(isMoving, success, failure) { changePace: function(isMoving, success, failure) {
exec(success || function() {}, exec(success || function() {},
failure || function() {}, failure || function() {},
...@@ -79,7 +93,7 @@ module.exports = { ...@@ -79,7 +93,7 @@ module.exports = {
* @param {Integer} timeout * @param {Integer} timeout
*/ */
setConfig: function(success, failure, config) { setConfig: function(success, failure, config) {
this.apply(this.config, config); this._apply(this.config, config);
exec(success || function() {}, exec(success || function() {},
failure || function() {}, failure || function() {},
'BackgroundGeoLocation', 'BackgroundGeoLocation',
...@@ -111,7 +125,10 @@ module.exports = { ...@@ -111,7 +125,10 @@ module.exports = {
taskId = params.taskId || 'task-id-undefined'; taskId = params.taskId || 'task-id-undefined';
me.stationaryLocation = location; me.stationaryLocation = location;
success.call(me, location, taskId);
me._runBackgroundTask(taskId, function() {
success.call(me, location, taskId);
}, failure);
}; };
exec(callback, exec(callback,
failure || function() {}, failure || function() {},
...@@ -124,8 +141,12 @@ module.exports = { ...@@ -124,8 +141,12 @@ module.exports = {
throw "BackgroundGeolocation#getLocations requires a success callback"; throw "BackgroundGeolocation#getLocations requires a success callback";
} }
var me = this; var me = this;
var mySuccess = function(locations) { var mySuccess = function(params) {
success.call(this, me._setTimestamp(locations)); var taskId = params.taskId;
var locations = me._setTimestamp(params.locations);
me._runBackgroundTask(taskId, function() {
success.call(me, locations, taskId);
});
} }
exec(mySuccess, exec(mySuccess,
failure || function() {}, failure || function() {},
...@@ -141,8 +162,13 @@ module.exports = { ...@@ -141,8 +162,13 @@ module.exports = {
throw "BackgroundGeolocation#sync requires a success callback"; throw "BackgroundGeolocation#sync requires a success callback";
} }
var me = this; var me = this;
var mySuccess = function(locations) { var mySuccess = function(params) {
success.call(this, me._setTimestamp(locations)); var locations = me._setTimestamp(params.locations);
var taskId = params.taskId;
me._runBackgroundTask(taskId, function() {
success.call(me, locations, taskId);
});
} }
exec(mySuccess, exec(mySuccess,
failure || function() {}, failure || function() {},
...@@ -215,7 +241,10 @@ module.exports = { ...@@ -215,7 +241,10 @@ module.exports = {
var mySuccess = function(params) { var mySuccess = function(params) {
var taskId = params.taskId || 'task-id-undefined'; var taskId = params.taskId || 'task-id-undefined';
delete(params.taskId); delete(params.taskId);
success.call(me, params, taskId);
me._runBackgroundTask(taskId, function() {
success.call(me, params, taskId);
}, failure);
}; };
exec(mySuccess, exec(mySuccess,
failure || function() {}, failure || function() {},
...@@ -257,7 +286,24 @@ module.exports = { ...@@ -257,7 +286,24 @@ module.exports = {
} }
return rs; return rs;
}, },
apply: function(destination, source) { _runBackgroundTask: function(taskId, callback) {
var me = this;
try {
callback.call(this);
} catch(e) {
console.log("*************************************************************************************");
console.error("BackgroundGeolocation caught a Javascript Exception in your application code");
console.log(" while running in a background thread. Auto-finishing background-task:", taskId);
console.log(" to prevent application crash");
console.log("*************************************************************************************");
console.log("STACK:\n", e.stack);
console.error(e);
// And finally, here's our raison d'etre: catching the error in order to ensure background-task is completed.
this.error(taskId, e.message);
}
},
_apply: function(destination, source) {
source = source || {}; source = source || {};
for (var property in source) { for (var property in source) {
if (source.hasOwnProperty(property)) { if (source.hasOwnProperty(property)) {
......
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