diff --git a/plugin.xml b/plugin.xml index 5f79115b..931177e1 100644 --- a/plugin.xml +++ b/plugin.xml @@ -1,7 +1,7 @@ + version="1.3.0"> DataCollection Background data collection FTW! This is the part that I really @@ -73,7 +73,6 @@ - - + + + diff --git a/src/android/BootReceiver.java b/src/android/BootReceiver.java index 11cdb497..93544d92 100644 --- a/src/android/BootReceiver.java +++ b/src/android/BootReceiver.java @@ -36,7 +36,7 @@ public void onReceive(Context ctx, Intent intent) { // Re-initialize the state machine if we haven't stopped tracking if (!TripDiaryStateMachineService.getState(ctx).equals( ctx.getString(R.string.state_tracking_stopped))) { - ctx.sendBroadcast(new Intent(ctx.getString(R.string.transition_initialize))); + ctx.sendBroadcast(new ExplicitIntent(ctx, R.string.transition_initialize)); } } } diff --git a/src/android/ConfigManager.java b/src/android/ConfigManager.java index 379a0fb8..987d9e7e 100644 --- a/src/android/ConfigManager.java +++ b/src/android/ConfigManager.java @@ -5,9 +5,12 @@ import com.google.gson.JsonParseException; import com.google.gson.JsonSyntaxException; +import org.apache.cordova.ConfigXmlParser; + import java.util.HashMap; import edu.berkeley.eecs.emission.R; + import edu.berkeley.eecs.emission.cordova.tracker.wrapper.ConsentConfig; import edu.berkeley.eecs.emission.cordova.tracker.wrapper.LocationTrackingConfig; import edu.berkeley.eecs.emission.cordova.unifiedlogger.Log; @@ -53,6 +56,13 @@ protected static void updateConfig(Context context, LocationTrackingConfig newCo cachedConfig = newConfig; } + public static String getReqConsent(Context ctxt) { + ConfigXmlParser parser = new ConfigXmlParser(); + parser.parse(ctxt); + String reqConsent = parser.getPreferences().getString("emSensorDataCollectionProtocolApprovalDate", null); + return reqConsent; + } + public static boolean isConsented(Context context, String reqConsent) { try { ConsentConfig currConfig = UserCacheFactory.getUserCache(context) diff --git a/src/android/DataCollectionPlugin.java b/src/android/DataCollectionPlugin.java index c4ad5345..162104f8 100644 --- a/src/android/DataCollectionPlugin.java +++ b/src/android/DataCollectionPlugin.java @@ -7,6 +7,7 @@ import org.json.JSONException; import org.json.JSONObject; +import android.Manifest; import android.app.Activity; import android.app.NotificationManager; import android.app.PendingIntent; @@ -14,6 +15,7 @@ import android.content.Intent; import android.content.IntentSender; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.preference.PreferenceManager; import com.google.android.gms.common.ConnectionResult; @@ -37,27 +39,20 @@ public class DataCollectionPlugin extends CordovaPlugin { public static final String TAG = "DataCollectionPlugin"; - public static final int ENABLE_LOCATION_CODE = 362253; + public static String LOCATION_PERMISSION = Manifest.permission.ACCESS_FINE_LOCATION; + + public static final int ENABLE_LOCATION_SETTINGS = 362253738; + public static final int ENABLE_LOCATION_PERMISSION = 362253737; + + public static final String ENABLE_LOCATION_PERMISSION_ACTION = "ENABLE_LOCATION_PERMISSION"; @Override public void pluginInitialize() { final Activity myActivity = cordova.getActivity(); - int connectionResult = GooglePlayServicesUtil.isGooglePlayServicesAvailable(myActivity); - if (connectionResult == ConnectionResult.SUCCESS) { - Log.d(myActivity, TAG, "google play services available, initializing state machine"); - cordova.getThreadPool().execute(new Runnable() { - @Override - public void run() { - TripDiaryStateMachineReceiver.initOnUpgrade(myActivity); - } - }); - } else { - Log.e(myActivity, TAG, "unable to connect to google play services"); - NotificationHelper.createNotification(myActivity, Constants.TRACKING_ERROR_ID, - "Unable to connect to google play services, tracking turned off"); - } BuiltinUserCache.getDatabase(myActivity).putMessage(R.string.key_usercache_client_nav_event, new StatsEvent(myActivity, R.string.app_launched)); + + TripDiaryStateMachineReceiver.initOnUpgrade(myActivity); TripDiaryStateMachineReceiver.startForegroundIfNeeded(myActivity); } @@ -74,7 +69,10 @@ public boolean execute(String action, JSONArray data, final CallbackContext call ConsentConfig cfg = new Gson().fromJson(newConsent.toString(), ConsentConfig.class); ConfigManager.setConsented(ctxt, cfg); // Now, really initialize the state machine - TripDiaryStateMachineReceiver.initOnUpgrade(ctxt); + // Note that we don't call initOnUpgrade so that we can handle the case where the + // user deleted the consent and re-consented, but didn't upgrade the app + checkAndPromptPermissions(); + // ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_initialize)); // TripDiaryStateMachineReceiver.restartCollection(ctxt); callbackContext.success(); return true; @@ -109,7 +107,7 @@ public void run() { Map transitionMap = getTransitionMap(ctxt); if (transitionMap.containsKey(generalTransition)) { String androidTransition = transitionMap.get(generalTransition); - ctxt.sendBroadcast(new Intent(androidTransition)); + ctxt.sendBroadcast(new ExplicitIntent(ctxt, androidTransition)); callbackContext.success(androidTransition); } else { callbackContext.error(generalTransition + " not supported, ignoring"); @@ -142,32 +140,86 @@ private static Map getTransitionMap(Context ctxt) { return retVal; } - @Override - public void onNewIntent(Intent intent) { - Log.d(cordova.getActivity(), TAG, "onNewIntent(" + intent.getDataString() + ")"); - Log.d(cordova.getActivity(), TAG, "Found extras " + intent.getExtras()); - PendingIntent piFromIntent = intent.getParcelableExtra(NotificationHelper.RESOLUTION_PENDING_INTENT_KEY); - if (piFromIntent != null) { + private void checkAndPromptPermissions() { + if(cordova.hasPermission(LOCATION_PERMISSION)) { + TripDiaryStateMachineService.restartFSMIfStartState(cordova.getActivity()); + } else { + cordova.requestPermission(this, ENABLE_LOCATION_PERMISSION, LOCATION_PERMISSION); + } + } + + private void displayResolution(PendingIntent resolution) { + if (resolution != null) { try { - // cordova.setActivityResultCallback(this); - cordova.getActivity().startIntentSenderForResult(piFromIntent.getIntentSender(), ENABLE_LOCATION_CODE, null, 0, 0, 0, null); + cordova.setActivityResultCallback(this); + cordova.getActivity().startIntentSenderForResult(resolution.getIntentSender(), ENABLE_LOCATION_SETTINGS, null, 0, 0, 0, null); } catch (IntentSender.SendIntentException e) { NotificationHelper.createNotification(cordova.getActivity(), Constants.TRACKING_ERROR_ID, "Unable to resolve issue"); } } } - /* + @Override + public void onNewIntent(Intent intent) { + Context mAct = cordova.getActivity(); + Log.d(mAct, TAG, "onNewIntent(" + intent.getAction() + ")"); + Log.d(mAct, TAG, "Found extras " + intent.getExtras()); + + if(ENABLE_LOCATION_PERMISSION_ACTION.equals(intent.getAction())) { + checkAndPromptPermissions(); + return; + } + if (NotificationHelper.DISPLAY_RESOLUTION_ACTION.equals(intent.getAction())) { + PendingIntent piFromIntent = intent.getParcelableExtra( + NotificationHelper.RESOLUTION_PENDING_INTENT_KEY); + displayResolution(piFromIntent); + return; + } + Log.i(mAct, TAG, "Action "+intent.getAction()+" unknown, ignoring "); + } + + @Override + public void onRequestPermissionResult(int requestCode, String[] permissions, + int[] grantResults) throws JSONException + { + /* + Let us figure out if we want to sent a javascript callback with the error. + This is currently only called from markConsented, and I don't think we listen to failures there + for(int r:grantResults) + { + if(r == PackageManager.PERMISSION_DENIED) + { + this.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, PERMISSION_DENIED_ERROR)); + return; + } + } + */ + switch(requestCode) + { + case ENABLE_LOCATION_PERMISSION: + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + NotificationHelper.cancelNotification(cordova.getActivity(), ENABLE_LOCATION_PERMISSION); + TripDiaryStateMachineService.restartFSMIfStartState(cordova.getActivity()); + } else if (grantResults[0] == PackageManager.PERMISSION_DENIED) { + TripDiaryStateMachineService.generateLocationEnableNotification(cordova.getActivity()); + } + break; + default: + Log.e(cordova.getActivity(), TAG, "Unknown permission code "+requestCode+" ignoring"); + } + } + @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { Log.d(cordova.getActivity(), TAG, "received onActivityResult("+requestCode+","+ resultCode+","+data.getDataString()+")"); switch (requestCode) { - case ENABLE_LOCATION_CODE: + case ENABLE_LOCATION_SETTINGS: Activity mAct = cordova.getActivity(); Log.d(mAct, TAG, requestCode + " is our code, handling callback"); cordova.setActivityResultCallback(null); final LocationSettingsStates states = LocationSettingsStates.fromIntent(data); + Log.d(cordova.getActivity(), TAG, "at this point, isLocationUsable = "+states.isLocationUsable()); switch (resultCode) { case Activity.RESULT_OK: // All required changes were successfully made @@ -180,6 +232,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { Log.e(cordova.getActivity(), TAG, "User chose not to change settings, dunno what to do"); break; default: + Log.e(cordova.getActivity(), TAG, "Unknown result code while enabling location "+resultCode); break; } break; @@ -187,6 +240,5 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { Log.d(cordova.getActivity(), TAG, "Got unsupported request code "+requestCode+ " , ignoring..."); } } - */ } diff --git a/src/android/ExplicitIntent.java b/src/android/ExplicitIntent.java new file mode 100644 index 00000000..f522289a --- /dev/null +++ b/src/android/ExplicitIntent.java @@ -0,0 +1,28 @@ +package edu.berkeley.eecs.emission.cordova.tracker; + +import android.content.Context; +import android.content.Intent; + +/* + * After API 26, android does not support delivery of implicit intents. + * We use broadcast intents extensively in the FSM, so instead of changing every location, + * we create an explicit intent that sets the package name correctly and converts it to an implicit intent. + * And while we are here, we can also simplify the arguments passed in to the interface + */ + +public class ExplicitIntent extends Intent { + public ExplicitIntent(Context context, int actionId) { + super(context.getString(actionId)); + setPackage(context.getPackageName()); + } + + public ExplicitIntent(Context context, String actionString) { + super(actionString); + setPackage(context.getPackageName()); + } + + public ExplicitIntent(Context context, Intent intent) { + super(intent); + setPackage(context.getPackageName()); + } +} diff --git a/src/android/location/GeofenceExitIntentService.java b/src/android/location/GeofenceExitIntentService.java index 603e46c5..4a1ba6b9 100644 --- a/src/android/location/GeofenceExitIntentService.java +++ b/src/android/location/GeofenceExitIntentService.java @@ -5,6 +5,7 @@ import android.app.IntentService; import android.content.Intent; +import edu.berkeley.eecs.emission.cordova.tracker.ExplicitIntent; import edu.berkeley.eecs.emission.cordova.unifiedlogger.Log; import edu.berkeley.eecs.emission.cordova.usercache.UserCacheFactory; import edu.berkeley.eecs.emission.cordova.tracker.wrapper.SimpleLocation; @@ -60,7 +61,7 @@ protected void onHandleIntent(Intent intent) { // in case we need it on the other side. // intent.setAction(getString(R.string.transition_exited_geofence)); // sendBroadcast(intent); - sendBroadcast(new Intent(getString(R.string.transition_exited_geofence))); + sendBroadcast(new ExplicitIntent(this, R.string.transition_exited_geofence)); } else if (parsedEvent.getGeofenceTransition() == -1) { // This must be a location services on/off transition // https://github.com/e-mission/e-mission-data-collection/issues/128#issuecomment-250304943 diff --git a/src/android/location/LocationChangeIntentService.java b/src/android/location/LocationChangeIntentService.java index c3089aa2..d3fb834a 100644 --- a/src/android/location/LocationChangeIntentService.java +++ b/src/android/location/LocationChangeIntentService.java @@ -8,6 +8,8 @@ import edu.berkeley.eecs.emission.cordova.tracker.ConfigManager; import edu.berkeley.eecs.emission.cordova.tracker.Constants; import edu.berkeley.eecs.emission.R; + +import edu.berkeley.eecs.emission.cordova.tracker.ExplicitIntent; import edu.berkeley.eecs.emission.cordova.tracker.sensors.PollSensorManager; import android.app.IntentService; import android.content.Intent; @@ -161,7 +163,7 @@ protected void onHandleIntent(Intent intent) { Intent stopMonitoringIntent = new Intent(); stopMonitoringIntent.setAction(getString(R.string.transition_stopped_moving)); stopMonitoringIntent.putExtra(FusedLocationProviderApi.KEY_LOCATION_CHANGED, loc); - sendBroadcast(stopMonitoringIntent); + sendBroadcast(new ExplicitIntent(this, stopMonitoringIntent)); Log.d(this, TAG, "Finished broadcasting state change to receiver, ending trip now"); // DataUtils.endTrip(this); } diff --git a/src/android/location/TripDiaryStateMachineForegroundService.java b/src/android/location/TripDiaryStateMachineForegroundService.java index 435d75f2..69012e91 100644 --- a/src/android/location/TripDiaryStateMachineForegroundService.java +++ b/src/android/location/TripDiaryStateMachineForegroundService.java @@ -1,8 +1,10 @@ package edu.berkeley.eecs.emission.cordova.tracker.location; import android.app.Notification; +import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; +import android.content.Context; import android.content.Intent; import android.os.Build; import android.os.IBinder; @@ -10,6 +12,7 @@ import de.appplant.cordova.plugin.notification.ClickActivity; import edu.berkeley.eecs.emission.MainActivity; + import edu.berkeley.eecs.emission.cordova.unifiedlogger.Log; import edu.berkeley.eecs.emission.cordova.unifiedlogger.NotificationHelper; @@ -35,8 +38,9 @@ public int onStartCommand(Intent intent, int flags, int startId) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { Log.d(this, TAG, "onStartCommand called on oreo+, starting foreground service"); // Go to the foreground with a dummy notification + NotificationManager nMgr = (NotificationManager)this.getSystemService(Context.NOTIFICATION_SERVICE); Notification.Builder builder = NotificationHelper.getNotificationBuilderForApp(this, - "background trip tracking started"); + nMgr, "background trip tracking started"); builder.setOngoing(true); Intent activityIntent = new Intent(this, MainActivity.class); diff --git a/src/android/location/TripDiaryStateMachineReceiver.java b/src/android/location/TripDiaryStateMachineReceiver.java index 4d7d8894..9c25cac6 100644 --- a/src/android/location/TripDiaryStateMachineReceiver.java +++ b/src/android/location/TripDiaryStateMachineReceiver.java @@ -25,6 +25,7 @@ import edu.berkeley.eecs.emission.R; import edu.berkeley.eecs.emission.cordova.tracker.ConfigManager; +import edu.berkeley.eecs.emission.cordova.tracker.ExplicitIntent; import edu.berkeley.eecs.emission.cordova.tracker.sensors.BatteryUtils; import edu.berkeley.eecs.emission.cordova.tracker.wrapper.Battery; import edu.berkeley.eecs.emission.cordova.tracker.wrapper.LocationTrackingConfig; @@ -79,8 +80,7 @@ public void onReceive(Context context, Intent intent) { context.getString(R.string.transition_stopped_moving), context.getString(R.string.transition_stop_tracking), context.getString(R.string.transition_start_tracking), - context.getString(R.string.transition_tracking_error), - LocationManager.MODE_CHANGED_ACTION + context.getString(R.string.transition_tracking_error) })); if (!validTransitions.contains(intent.getAction())) { @@ -88,65 +88,65 @@ public void onReceive(Context context, Intent intent) { return; } - /* - * Before we initialize the state machine, let's check to see whether the user has - * consented to the current data collection. - */ if (intent.getAction().equals(context.getString(R.string.transition_initialize))) { + String reqConsent = ConfigManager.getReqConsent(context); + if (ConfigManager.isConsented(context, reqConsent)) { + Log.i(context, TAG, reqConsent + " is the current consented version, sending msg to service..."); + } else { JSONObject introDoneResult = null; try { introDoneResult = BuiltinUserCache.getDatabase(context).getLocalStorage("intro_done", false); } catch(JSONException e) { Log.i(context, TAG, "unable to read intro done state, skipping prompt"); + return; } if (introDoneResult != null) { - String reqConsent = getReqConsent(context); - if (!ConfigManager.isConsented(context, reqConsent)) { Log.i(context, TAG, reqConsent + " is not the current consented version, skipping init..."); NotificationHelper.createNotification(context, STARTUP_IN_NUMBERS, "New data collection terms - collection paused until consent"); return; } else { - Log.i(context, TAG, reqConsent + " is the current consented version, sending msg to service..."); - } - } else { Log.i(context, TAG, "onboarding is not complete, skipping prompt"); + return; + } } } + // we should only get here if the user has consented Intent serviceStartIntent = getStateMachineServiceIntent(context); serviceStartIntent.setAction(intent.getAction()); context.startService(serviceStartIntent); } - /* - * TODO: Need to find a place to put this. - */ - private static String getReqConsent(Context ctxt) { - ConfigXmlParser parser = new ConfigXmlParser(); - parser.parse(ctxt); - String reqConsent = parser.getPreferences().getString("emSensorDataCollectionProtocolApprovalDate", null); - return reqConsent; - } - public static void performPeriodicActivity(Context ctxt) { /* * Now, do some validation of the current state and clean it up if necessary. This should * help with issues we have seen in the field where location updates pause mysteriously, or * geofences are never exited. */ + checkLocationStillAvailable(ctxt); validateAndCleanupState(ctxt); initOnUpgrade(ctxt); saveBatteryAndSimulateUser(ctxt); } + public static void checkLocationStillAvailable(Context ctxt) { + GoogleApiClient mApiClient = new GoogleApiClient.Builder(ctxt) + .addApi(LocationServices.API) + .build(); + // This runs as part of the service thread and not the UI thread, so can block + // can switch to Tasks later anyway + mApiClient.blockingConnect(); + TripDiaryStateMachineService.checkLocationSettingsAndPermissions(ctxt, mApiClient); + } + public static void validateAndCleanupState(Context ctxt) { /* * Check for being in geofence if in waiting_for_trip_state. */ if (TripDiaryStateMachineService.getState(ctxt).equals(ctxt.getString(R.string.state_start))) { Log.d(ctxt, TAG, "Still in start state, sending initialize..."); - ctxt.sendBroadcast(new Intent(ctxt.getString(R.string.transition_initialize))); + ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_initialize)); } else if (TripDiaryStateMachineService.getState(ctxt).equals( ctxt.getString(R.string.state_waiting_for_trip_start))) { // We cannot check to see whether there is an existing geofence and whether we are in it. @@ -169,6 +169,8 @@ public static void initOnUpgrade(Context ctxt) { System.out.println("All preferences are "+sp.getAll()); int currentCompleteVersion = sp.getInt(SETUP_COMPLETE_KEY, 0); + Log.d(ctxt, TAG, "Comparing installed version "+currentCompleteVersion + + " with new version " + BuildConfig.VERSION_CODE); if(currentCompleteVersion != BuildConfig.VERSION_CODE) { Log.d(ctxt, TAG, "Setup not complete, sending initialize"); // this is the code that checks whether the native collection has been upgraded and @@ -177,7 +179,7 @@ public static void initOnUpgrade(Context ctxt) { // https://github.com/e-mission/e-mission-data-collection/commit/5544afd64b0c731e1633d1dd9f51a713fdea85fa // Since every consent change is (presumably) tied to a native code change, we can // just check for the consent here before reinitializing. - ctxt.sendBroadcast(new Intent(ctxt.getString(R.string.transition_initialize))); + ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_initialize)); SharedPreferences.Editor prefsEditor = sp.edit(); // TODO: This is supposed to be set from the javascript as part of the onboarding process. // However, it looks like it doesn't actually work - it looks like the app preferences plugin @@ -213,7 +215,7 @@ public static void restartCollection(Context ctxt) { + " early return"); return; } - ctxt.sendBroadcast(new Intent(ctxt.getString(R.string.transition_stop_tracking))); + ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_stop_tracking)); final Context fCtxt = ctxt; new Thread(new Runnable() { @Override @@ -230,7 +232,7 @@ public void run() { stateChanged = true; } } - fCtxt.sendBroadcast(new Intent(fCtxt.getString(R.string.transition_start_tracking))); + fCtxt.sendBroadcast(new ExplicitIntent(fCtxt, R.string.transition_start_tracking)); } }).start(); } diff --git a/src/android/location/TripDiaryStateMachineService.java b/src/android/location/TripDiaryStateMachineService.java index 181aebea..c02a1ccb 100644 --- a/src/android/location/TripDiaryStateMachineService.java +++ b/src/android/location/TripDiaryStateMachineService.java @@ -1,13 +1,17 @@ package edu.berkeley.eecs.emission.cordova.tracker.location; +import android.Manifest; +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.LocationManager; import android.os.Bundle; import android.os.IBinder; import android.preference.PreferenceManager; +import android.support.v4.content.ContextCompat; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.Batch; @@ -29,9 +33,14 @@ import edu.berkeley.eecs.emission.cordova.tracker.ConfigManager; import edu.berkeley.eecs.emission.cordova.tracker.Constants; +import edu.berkeley.eecs.emission.cordova.tracker.DataCollectionPlugin; +import edu.berkeley.eecs.emission.cordova.tracker.ExplicitIntent; import edu.berkeley.eecs.emission.cordova.tracker.sensors.BatteryUtils; import edu.berkeley.eecs.emission.cordova.unifiedlogger.NotificationHelper; import edu.berkeley.eecs.emission.R; +import edu.berkeley.eecs.emission.MainActivity; + + import edu.berkeley.eecs.emission.cordova.tracker.location.actions.ActivityRecognitionActions; import edu.berkeley.eecs.emission.cordova.tracker.location.actions.GeofenceActions; @@ -67,6 +76,7 @@ public class TripDiaryStateMachineService extends Service implements private SharedPreferences mPrefs = null; // List of actions being currently processed private List currActions = null; + private int ongoingOperations; public TripDiaryStateMachineService() { super(); @@ -112,10 +122,10 @@ public int onStartCommand(Intent intent, int flags, int startId) { Log.d(this, TAG, "service restarted! need to check idempotency!"); } mTransition = intent.getAction(); + ongoingOperations = ongoingOperations + 1; if (currActions.contains(mTransition)) { Log.i(this, TAG, "Service started again for "+mTransition+ " while processing "+currActions+" early exit from id " + startId); - // stopSelf(startId); return START_REDELIVER_INTENT; } Log.i(this, TAG, "Handling new action "+mTransition+ @@ -157,6 +167,10 @@ public void onConnected(Bundle connectionHint) { Log.d(this, TAG, "onConnected("+connectionHint+") called"); ConnectionResult locResult = mApiClient.getConnectionResult(LocationServices.API); ConnectionResult activityResult = mApiClient.getConnectionResult(ActivityRecognition.API); + if (locResult.isSuccess() && activityResult.isSuccess()) { + // we go ahead and handle the original issue + handleAction(this, mApiClient, mCurrState, mTransition); + } if (!locResult.isSuccess()) { if (locResult.hasResolution()) { NotificationHelper.createNotification(this, Constants.TRACKING_ERROR_ID, locResult.getErrorMessage(), @@ -164,6 +178,8 @@ public void onConnected(Bundle connectionHint) { } else { NotificationHelper.createNotification(this, Constants.TRACKING_ERROR_ID, locResult.getErrorMessage()); } + // we have generated a notification, and we are not going to handle the action, so we can stop the service now + setNewState(mCurrState); } if (!activityResult.isSuccess()) { if (activityResult.hasResolution()) { @@ -172,10 +188,10 @@ public void onConnected(Bundle connectionHint) { } else { NotificationHelper.createNotification(this, Constants.TRACKING_ERROR_ID, activityResult.getErrorMessage()); } + // we have generated a notification, and we are not going to handle the action, so we can stop the service now + setNewState(mCurrState); } - handleAction(this, mApiClient, mCurrState, mTransition); - // Note that it does NOT work to disconnect from here because the actions in the state // might happen asynchronously, and we disconnect too early, then the async callbacks // are never invoked. In particular, anything called from the "main/UI" thread, such as @@ -199,7 +215,13 @@ public static void restartFSMIfStartState(Context ctxt) { Log.i(ctxt, TAG, "in restartFSMIfStartState, currState = "+currState); if (START_STATE.equals(currState)) { Log.i(ctxt, TAG, "in start state, sending initialize"); - ctxt.sendBroadcast(new Intent(ctxt.getString(R.string.transition_initialize))); + ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_initialize)); + } + } + + private void markOngoingOperationFinished() { + synchronized (this) { + ongoingOperations = ongoingOperations - 1; } } @@ -212,10 +234,15 @@ public void setNewState(String newState) { Log.d(this, TAG, "newState saved in prefManager is "+ PreferenceManager.getDefaultSharedPreferences(this).getString( this.getString(R.string.curr_state_key), "not found")); - currActions.remove(mTransition); - Log.i(this, TAG, "After removing transition "+mTransition+ - ", currActions = "+currActions); + synchronized (this) { + ongoingOperations = ongoingOperations - 1; + if (ongoingOperations == 0) { + Log.i(this, TAG, "About to stop service after handling "+currActions); stopSelf(); + } else { + Log.i(this, TAG, ongoingOperations + " ongoingOperations pending, waiting to stop"); + } + } } @Override @@ -228,13 +255,14 @@ public void onConnectionSuspended(int cause) { } NotificationHelper.createNotification(this, STATE_IN_NUMBERS, "google API client connection suspended"+causeStr); + // let's leave this here in case the connection is restored } @Override public void onConnectionFailed(ConnectionResult cr) { NotificationHelper.createNotification(this, STATE_IN_NUMBERS, "google API client connection failed"+cr.toString()); - + setNewState(this.getString(R.string.state_start)); } private Intent getForegroundServiceIntent() { @@ -250,7 +278,7 @@ private Intent getForegroundServiceIntent() { * as parameters, makes the call, and issues the broadcast in the callback */ private void handleAction(Context ctxt, GoogleApiClient apiClient, String currState, String actionString) { - Log.d(this, TAG, "handleAction("+currState+", "+actionString+") calle"); + Log.d(this, TAG, "handleAction("+currState+", "+actionString+") called"); assert(currState != null); // The current state is stored in the shared preferences, so on reboot, for example, we would // store that we are in ongoing_trip, but no listeners would be registered. We can have @@ -266,11 +294,6 @@ private void handleAction(Context ctxt, GoogleApiClient apiClient, String currSt BatteryUtils.getBatteryInfo(ctxt)); if (actionString.equals(ctxt.getString(R.string.transition_initialize))) { handleStart(ctxt, apiClient, actionString); - } else if (LocationManager.MODE_CHANGED_ACTION.equals(actionString)) { - // should we do a handleXXX() wrapper for this too? - checkLocationSettings(ctxt, apiClient); - // stay in the current state, but do all the service cleanup stuff - setNewState(currState); } else if (currState.equals(ctxt.getString(R.string.state_start))) { handleStart(ctxt, apiClient, actionString); } else if (currState.equals(ctxt.getString(R.string.state_waiting_for_trip_start))) { @@ -280,6 +303,7 @@ private void handleAction(Context ctxt, GoogleApiClient apiClient, String currSt } else if (currState.equals(ctxt.getString(R.string.state_tracking_stopped))) { handleTrackingStopped(ctxt, apiClient, actionString); } + Log.d(this, TAG, "handleAction("+currState+", "+actionString+") completed, waiting for async operations to complete"); } private void handleStart(final Context ctxt, final GoogleApiClient apiClient, String actionString) { @@ -288,6 +312,7 @@ private void handleStart(final Context ctxt, final GoogleApiClient apiClient, St if (actionString.equals(ctxt.getString(R.string.transition_initialize)) && !mCurrState.equals(ctxt.getString(R.string.state_tracking_stopped))) { createGeofenceInThread(ctxt, apiClient, actionString); + // we will wait for async geofence creation to complete } // One would think that we don't need to deal with anything other than starting from the start @@ -315,11 +340,23 @@ public void handleTripStart(Context ctxt, final GoogleApiClient apiClient, final if (actionString.equals(ctxt.getString(R.string.transition_exited_geofence))) { // Delete geofence + // we cannot add null elements to the token list. + // the LocationTracking start action can now return null + // so we need to handle it similar to the createGeofence in handleTripEnd final List> tokenList = new LinkedList>(); Batch.Builder resultBarrier = new Batch.Builder(apiClient); tokenList.add(resultBarrier.add(new GeofenceActions(ctxt, apiClient).remove())); - tokenList.add(resultBarrier.add(new LocationTrackingActions(ctxt, apiClient).start())); tokenList.add(resultBarrier.add(new ActivityRecognitionActions(ctxt, apiClient).start())); + PendingResult locationTrackingResult = new LocationTrackingActions(ctxt, apiClient).start(); + if (locationTrackingResult != null) { + tokenList.add(resultBarrier.add(locationTrackingResult)); + } else { + // if we can't turn on the location tracking, we may as well not start the activity + // recognition + tokenList.remove(1); + } + final boolean locationTrackingPossible = locationTrackingResult != null; + // TODO: How to pass in the token list? // Also, the callback is currently the same for all of them, but could potentially be // different in the future once we add in failure handling because we may want to do @@ -330,39 +367,62 @@ public void handleTripStart(Context ctxt, final GoogleApiClient apiClient, final resultBarrier.build().setResultCallback(new ResultCallback() { @Override public void onResult(BatchResult batchResult) { - String newState = fCtxt.getString(R.string.state_ongoing_trip); + String newState; + if (locationTrackingPossible) { + newState = fCtxt.getString(R.string.state_ongoing_trip); + } else { + // If we are not going to be able to start location tracking, then we don't + // want to go to ongoing_trip, because then we will never exit + // from it. Instead, we go to state_start so that we will try to get + // out of it at every sync. + newState = fCtxt.getString(R.string.state_start); + } + if (batchResult.getStatus().isSuccess()) { - setNewState(newState); + if (locationTrackingPossible) { startService(getForegroundServiceIntent()); + } if (ConfigManager.getConfig(fCtxt).isSimulateUserInteraction()) { NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, "Success moving to "+newState); } + setNewState(newState); } else { if (batchResult.getStatus().hasResolution()) { - NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, + NotificationHelper.createResolveNotification(fCtxt, STATE_IN_NUMBERS, "Error " + batchResult.getStatus().getStatusCode()+" while creating geofence", batchResult.getStatus().getResolution()); + // we should set something here to stop the service since our async call + // is complete and since we already have a resolution, we are not going to do anything more + // but the set value depends on the result if the geofence deletion failed but the tracking started, we want + // to go to the ongoing_trip state anyway... + if (locationTrackingPossible && batchResult.take(tokenList.get(2)).isSuccess()) { + // the location tracking started successfully + setNewState(fCtxt.getString(R.string.state_ongoing_trip)); + } else { + setNewState(fCtxt.getString(R.string.state_start)); + } } else { - NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, - "Error " + batchResult.getStatus().getStatusCode()+" while creating geofence"); - checkLocationSettings(TripDiaryStateMachineService.this, mApiClient); + // NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, + // "Error " + batchResult.getStatus().getStatusCode()+" while creating geofence"); + // this will perform some additional checks which we should wait for + // let's mark this operation as done since the other one is static + markOngoingOperationFinished(); + checkLocationSettingsAndPermissions(TripDiaryStateMachineService.this, mApiClient); } if (ConfigManager.getConfig(fCtxt).isSimulateUserInteraction()) { NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, "Failed moving to "+newState); } - setNewState(mCurrState); - } - Log.d(fCtxt, TAG, "About to disconnect the api client"); - mApiClient.disconnect(); - } - }); + } // all three branches have called setState or are waiting for sth else + } // onResult function end + }); // result callback inner class end } if(actionString.equals(ctxt.getString(R.string.transition_stop_tracking))) { // Delete geofence deleteGeofence(ctxt, apiClient, ctxt.getString(R.string.state_tracking_stopped)); + // when this completes, it should generate transitions and move to the final state } if (actionString.equals(ctxt.getString(R.string.transition_tracking_error))) { @@ -372,7 +432,7 @@ public void onResult(BatchResult batchResult) { */ Log.i(this, TAG, "Got tracking_error moving to start state"); deleteGeofence(ctxt, mApiClient, ctxt.getString(R.string.state_start)); - setNewState(getString(R.string.state_start)); + // when this completes, it should generate transitions and move to the final state } } @@ -389,8 +449,7 @@ public void run() { tokenList.add(resultBarrier.add(new LocationTrackingActions(ctxt, apiClient).stop())); tokenList.add(resultBarrier.add(new ActivityRecognitionActions(ctxt, apiClient).stop())); // TODO: change once we move to chained promises - PendingResult createGeofenceResult = - new GeofenceActions(ctxt, apiClient).create(); + PendingResult createGeofenceResult = new GeofenceActions(ctxt, apiClient).create(); if (createGeofenceResult != null) { tokenList.add(resultBarrier.add(createGeofenceResult)); } @@ -410,30 +469,46 @@ public void onResult(BatchResult batchResult) { newState = fCtxt.getString(R.string.state_start); } if (batchResult.getStatus().isSuccess()) { - setNewState(newState); stopService(getForegroundServiceIntent()); if (ConfigManager.getConfig(ctxt).isSimulateUserInteraction()) { NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, "Success moving to "+newState); } + setNewState(newState); } else { if (batchResult.getStatus().hasResolution()) { - NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, + NotificationHelper.createResolveNotification(fCtxt, STATE_IN_NUMBERS, "Error " + batchResult.getStatus().getStatusCode()+" while creating geofence", batchResult.getStatus().getResolution()); + // we should set something here to stop the service since our async call + // is complete and since we already have a resolution, we are not going to do anything more + // but the set value depends on the result if the geofence creation failed, we + // want to go to the start state, but if the location tracking stop failed, + // we want to stay in the ongoing trip state + if (!batchResult.take(tokenList.get(0)).isSuccess()) { + // the location tracking stop failed + setNewState(fCtxt.getString(R.string.state_ongoing_trip)); + } else if (geofenceCreationPossible && + batchResult.take(tokenList.get(2)).isSuccess()) { + setNewState(fCtxt.getString(R.string.state_waiting_for_trip_start)); + } else { + // geofence creation is not possible or it failed but location tracking + // did successfully stop. Let's go to the start state + setNewState(fCtxt.getString(R.string.state_start)); + } } else { - NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, - "Error " + batchResult.getStatus().getStatusCode()+" while creating geofence"); - checkLocationSettings(TripDiaryStateMachineService.this, mApiClient); + // NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, + // "Error " + batchResult.getStatus().getStatusCode()+" while creating geofence"); + // let's mark this operation as done since the other one is static + markOngoingOperationFinished(); + checkLocationSettingsAndPermissions(TripDiaryStateMachineService.this, mApiClient); + // will wait for async call to complete } if (ConfigManager.getConfig(ctxt).isSimulateUserInteraction()) { NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, "Failed moving to "+newState); } - setNewState(mCurrState); } - Log.d(fCtxt, TAG, "About to disconnect the api client"); - mApiClient.disconnect(); } }); } @@ -442,6 +517,7 @@ public void onResult(BatchResult batchResult) { if (actionString.equals(ctxt.getString(R.string.transition_stop_tracking))) { stopAll(ctxt, apiClient, ctxt.getString(R.string.state_tracking_stopped)); + // will wait for stopAll to set the state } if (actionString.equals(ctxt.getString(R.string.transition_tracking_error))) { @@ -452,14 +528,16 @@ public void onResult(BatchResult batchResult) { Log.i(this, TAG, "Got tracking_error moving to start state"); // should I stop everything? maybe to be consistent with the start state stopAll(this, mApiClient, ctxt.getString(R.string.state_start)); - setNewState(getString(R.string.state_start)); + // ditto } } private void handleTrackingStopped(final Context ctxt, final GoogleApiClient apiClient, String actionString) { Log.d(this, TAG, "TripDiaryStateMachineReceiver handleTrackingStopped(" + actionString + ") called"); if (actionString.equals(ctxt.getString(R.string.transition_start_tracking))) { - createGeofenceInThread(ctxt, apiClient, actionString); + ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_initialize)); + setNewState(ctxt.getString(R.string.state_start)); + // createGeofenceInThread(ctxt, apiClient, actionString); } else { stopAll(ctxt, apiClient, ctxt.getString(R.string.state_tracking_stopped)); } @@ -507,28 +585,31 @@ public void onResult(Status status) { "Success moving to " + newState); } } else { - setNewState(mCurrState); if (status.hasResolution()) { - NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, + NotificationHelper.createResolveNotification(fCtxt, STATE_IN_NUMBERS, "Error " + status.getStatusCode()+" while creating geofence", status.getResolution()); + // we have a resolution so we will exit the service now + setNewState(mCurrState); } else { - NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, - "Error " + status.getStatusCode()+" while creating geofence"); - checkLocationSettings(TripDiaryStateMachineService.this, mApiClient); + // NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, + // "Error " + status.getStatusCode()+" while creating geofence"); + // let's mark this operation as done since the other one is static + markOngoingOperationFinished(); + checkLocationSettingsAndPermissions(TripDiaryStateMachineService.this, mApiClient); } if (ConfigManager.getConfig(fCtxt).isSimulateUserInteraction()) { NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, "Failed moving to " + newState); } } - Log.d(fCtxt, TAG, "About to disconnect the api client"); - fApiClient.disconnect(); } }); } else { - // Geofence was not created properly. let's generate a tracking error so that the user is notified - checkLocationSettings(fCtxt, fApiClient); - setNewState(mCurrState); + // Geofence was not created properly. let's make an async call that will generate its + // own state change + // let's mark this operation as done since the other one is static + markOngoingOperationFinished(); + checkLocationSettingsAndPermissions(fCtxt, fApiClient); } } }).start(); @@ -551,25 +632,24 @@ public void onResult(BatchResult batchResult) { } } else { if (batchResult.getStatus().hasResolution()) { - NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, + NotificationHelper.createResolveNotification(fCtxt, STATE_IN_NUMBERS, "Error " + batchResult.getStatus().getStatusCode() + " while creating geofence", batchResult.getStatus().getResolution()); + setNewState(mCurrState); } else { - NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, - "Error " + batchResult.getStatus().getStatusCode() + " while creating geofence"); - checkLocationSettings(TripDiaryStateMachineService.this, mApiClient); + // NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, + // "Error " + batchResult.getStatus().getStatusCode() + " while creating geofence"); + // let's mark this operation as done since the other one is static + markOngoingOperationFinished(); + checkLocationSettingsAndPermissions(TripDiaryStateMachineService.this, mApiClient); } if (ConfigManager.getConfig(fCtxt).isSimulateUserInteraction()) { NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, "Failed moving to " + newState); } - setNewState(mCurrState); } - Log.d(fCtxt, TAG, "About to disconnect the api client"); - mApiClient.disconnect(); } }); - } private void stopAll(Context ctxt, GoogleApiClient apiClient, final String targetState) { @@ -586,29 +666,71 @@ private void stopAll(Context ctxt, GoogleApiClient apiClient, final String targe public void onResult(BatchResult batchResult) { String newState = targetState; if (batchResult.getStatus().isSuccess()) { - setNewState(newState); stopService(getForegroundServiceIntent()); if (ConfigManager.getConfig(fCtxt).isSimulateUserInteraction()) { NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, "Success moving to "+newState); } + setNewState(newState); } else { if (ConfigManager.getConfig(fCtxt).isSimulateUserInteraction()) { NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, "Failed moving to "+newState); } - setNewState(mCurrState); + + if (!batchResult.take(tokenList.get(1)).isSuccess()) { + // the location tracking stop failed + setNewState(fCtxt.getString(R.string.state_ongoing_trip)); + } else { + setNewState(newState); + } } - Log.d(fCtxt, TAG, "About to disconnect the api client"); - mApiClient.disconnect(); } }); } - - public static void checkLocationSettings(final Context ctxt, GoogleApiClient apiClient) { + public static void checkLocationSettingsAndPermissions(final Context ctxt, final GoogleApiClient apiClient) { LocationRequest request = new LocationTrackingActions(ctxt, apiClient).getLocationRequest(); - Log.d(ctxt, TAG, "Checking location settings for request "+request); + Log.d(ctxt, TAG, "Checking location settings and permissions for request "+request); + // let's do the permission check first since it is synchronous + if (checkLocationPermissions(ctxt, apiClient, request)) { + Log.d(ctxt, TAG, "checkPermissions returned true, checking settings"); + checkLocationSettings(ctxt, apiClient, request); + // final state will be set in this async call + } else { + Log.d(ctxt, TAG, "checkPermissions returned false, no point checking settings"); + ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_tracking_error)); + } + } + + public static boolean checkLocationPermissions(final Context ctxt, + final GoogleApiClient apiClient, + final LocationRequest request) { + // Ideally, we would use the request accuracy to figure out the permissions requested + // but I can't find an authoritative mapping, and I'm running out of time for + // fancy stuff + int result = ContextCompat.checkSelfPermission(ctxt, DataCollectionPlugin.LOCATION_PERMISSION); + Log.d(ctxt, TAG, "checkSelfPermission returned "+result); + if (PackageManager.PERMISSION_GRANTED == result) { + return true; + } else { + generateLocationEnableNotification(ctxt); + return false; + } + } + + public static void generateLocationEnableNotification(Context ctxt) { + Intent activityIntent = new Intent(ctxt, MainActivity.class); + activityIntent.setAction(DataCollectionPlugin.ENABLE_LOCATION_PERMISSION_ACTION); + PendingIntent pi = PendingIntent.getActivity(ctxt, DataCollectionPlugin.ENABLE_LOCATION_PERMISSION, + activityIntent, PendingIntent.FLAG_UPDATE_CURRENT); + NotificationHelper.createNotification(ctxt, DataCollectionPlugin.ENABLE_LOCATION_PERMISSION, + "Location permission off, click to enable", pi); + } + + public static void checkLocationSettings(final Context ctxt, + final GoogleApiClient apiClient, + final LocationRequest request) { LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder() .addLocationRequest(request); @@ -631,27 +753,27 @@ public void onResult(LocationSettingsResult result) { case LocationSettingsStatusCodes.RESOLUTION_REQUIRED: // Location settings are not satisfied. But could be fixed by showing the user // a dialog. - ctxt.sendBroadcast(new Intent(ctxt.getString(R.string.transition_tracking_error))); if (status.hasResolution()) { - NotificationHelper.createNotification(ctxt, Constants.TRACKING_ERROR_ID, + NotificationHelper.createResolveNotification(ctxt, Constants.TRACKING_ERROR_ID, "Error " + status.getStatusCode() + " in location settings", status.getResolution()); } else { NotificationHelper.createNotification(ctxt, Constants.TRACKING_ERROR_ID, "Error " + status.getStatusCode() + " in location settings"); } + ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_tracking_error)); break; case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE: // Location settings are not satisfied. However, we have no way to fix the // settings so we won't show the dialog. - ctxt.sendBroadcast(new Intent(ctxt.getString(R.string.transition_tracking_error))); NotificationHelper.createNotification(ctxt, Constants.TRACKING_ERROR_ID, "Error " + status.getStatusCode() + " in location settings"); + ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_tracking_error)); break; default: - ctxt.sendBroadcast(new Intent(ctxt.getString(R.string.transition_tracking_error))); NotificationHelper.createNotification(ctxt, Constants.TRACKING_ERROR_ID, "Unknown error while reading location, please check your settings"); + ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_tracking_error)); } } }); diff --git a/src/android/location/TripDiaryStateMachineServiceOngoing.java b/src/android/location/TripDiaryStateMachineServiceOngoing.java index 97f6c2f7..83d43593 100644 --- a/src/android/location/TripDiaryStateMachineServiceOngoing.java +++ b/src/android/location/TripDiaryStateMachineServiceOngoing.java @@ -27,6 +27,7 @@ import edu.berkeley.eecs.emission.cordova.unifiedlogger.NotificationHelper; import edu.berkeley.eecs.emission.R; + import edu.berkeley.eecs.emission.cordova.tracker.location.actions.ActivityRecognitionActions; import edu.berkeley.eecs.emission.cordova.tracker.location.actions.LocationTrackingActions; import edu.berkeley.eecs.emission.cordova.unifiedlogger.Log; @@ -236,9 +237,6 @@ private void handleAction(Context ctxt, GoogleApiClient apiClient, String currSt // - have initialize function as a reset, which stops any current stuff and starts the new one if (actionString.equals(ctxt.getString(R.string.transition_initialize))) { handleStart(ctxt, apiClient, actionString); - } else if (LocationManager.MODE_CHANGED_ACTION.equals(actionString)) { - // should we do a handleXXX() wrapper for this too? - TripDiaryStateMachineService.checkLocationSettings(ctxt, apiClient); } else if (currState.equals(ctxt.getString(R.string.state_start))) { handleStart(ctxt, apiClient, actionString); } else if (currState.equals(ctxt.getString(R.string.state_waiting_for_trip_start))) { diff --git a/src/android/location/actions/GeofenceActions.java b/src/android/location/actions/GeofenceActions.java index 0e3e70e1..f27a9c9e 100644 --- a/src/android/location/actions/GeofenceActions.java +++ b/src/android/location/actions/GeofenceActions.java @@ -16,7 +16,6 @@ import com.google.android.gms.common.api.CommonStatusCodes; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.PendingResult; -import com.google.android.gms.common.api.ResultCallback; import com.google.android.gms.common.api.Status; import com.google.android.gms.location.Geofence; import com.google.android.gms.location.GeofencingRequest; @@ -64,6 +63,7 @@ public GeofenceActions(Context ctxt, GoogleApiClient googleApiClient) { * see @GeofenceActions.createGeofenceRequest */ public PendingResult create() { + try { Location mLastLocation = LocationServices.FusedLocationApi.getLastLocation( mGoogleApiClient); Log.d(mCtxt, TAG, "Last location would have been " + mLastLocation +" if we hadn't reset it"); @@ -82,9 +82,13 @@ public PendingResult create() { return null; } } + } catch (SecurityException e) { + Log.e(mCtxt, TAG, "Found security error "+e.getMessage()+" while creating geofence"); + return null; + } } - private PendingResult createGeofenceAtLocation(Location currLoc) { + private PendingResult createGeofenceAtLocation(Location currLoc) throws SecurityException { Log.d(mCtxt, TAG, "creating geofence at location " + currLoc); // This is also an asynchronous call. We can either wait for the result, // or we can provide a callback. Let's provide a callback to keep the async @@ -94,7 +98,7 @@ private PendingResult createGeofenceAtLocation(Location currLoc) { getGeofenceExitPendingIntent(mCtxt)); } - private Location readAndReturnCurrentLocation() { + private Location readAndReturnCurrentLocation() throws SecurityException { Intent geofenceLocIntent = new Intent(mCtxt, GeofenceLocationIntentService.class); final PendingIntent geofenceLocationIntent = PendingIntent.getService(mCtxt, 0, geofenceLocIntent, PendingIntent.FLAG_UPDATE_CURRENT); diff --git a/src/android/location/actions/LocationTrackingActions.java b/src/android/location/actions/LocationTrackingActions.java index 4abcb6fd..c9b027fc 100644 --- a/src/android/location/actions/LocationTrackingActions.java +++ b/src/android/location/actions/LocationTrackingActions.java @@ -33,10 +33,15 @@ public LocationTrackingActions(Context ctxt, GoogleApiClient googleApiClient) { } public PendingResult start() { + try { return LocationServices.FusedLocationApi.requestLocationUpdates(mGoogleApiClient, getLocationRequest(), getLocationTrackingPendingIntent(mCtxt)); // .setResultCallback(startCallback); + } catch (SecurityException e) { + Log.e(mCtxt, TAG, "Found security error "+e.getMessage()+" while creating geofence"); + return null; + } } public LocationRequest getLocationRequest() {