From 484528adde852e9fcca4ba74344a1c00cc8aa977 Mon Sep 17 00:00:00 2001 From: Shankari Date: Sun, 24 Oct 2021 11:01:16 -0700 Subject: [PATCH 01/35] Upgrade all the libraries to the latest versions Everything still seems to be working. At least, it all compiles and runs. --- plugin.xml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/plugin.xml b/plugin.xml index 6fd2af7..d51c328 100644 --- a/plugin.xml +++ b/plugin.xml @@ -117,9 +117,9 @@ - + - + @@ -196,10 +196,16 @@ - + + + + + + + + - From c179fe26c1f23ea3ff2dd2fb2d2170bca7c2f1df Mon Sep 17 00:00:00 2001 From: Shankari Date: Mon, 25 Oct 2021 18:26:00 -0700 Subject: [PATCH 02/35] Ensure that all the imports are for the main e-mission packages And not CEO ebike project --- src/android/verification/SensorControlBackgroundChecker.java | 5 ++--- .../verification/SensorControlForegroundDelegate.java | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/android/verification/SensorControlBackgroundChecker.java b/src/android/verification/SensorControlBackgroundChecker.java index 77134da..fcb9e43 100644 --- a/src/android/verification/SensorControlBackgroundChecker.java +++ b/src/android/verification/SensorControlBackgroundChecker.java @@ -23,9 +23,8 @@ import edu.berkeley.eecs.emission.cordova.tracker.location.actions.LocationTrackingActions; import edu.berkeley.eecs.emission.cordova.unifiedlogger.Log; import edu.berkeley.eecs.emission.cordova.unifiedlogger.NotificationHelper; -import gov.colorado.energyoffice.emission.MainActivity; -// Auto fixed by post-plugin hook -import gov.colorado.energyoffice.emission.R; +import edu.berkeley.eecs.emission.cordova.MainActivity; +import edu.berkeley.eecs.emission.cordova.R; /* * Deals with settings and resolutions from the background as a service. diff --git a/src/android/verification/SensorControlForegroundDelegate.java b/src/android/verification/SensorControlForegroundDelegate.java index ff31723..a1365d1 100644 --- a/src/android/verification/SensorControlForegroundDelegate.java +++ b/src/android/verification/SensorControlForegroundDelegate.java @@ -3,7 +3,7 @@ import edu.berkeley.eecs.emission.cordova.tracker.Constants; import edu.berkeley.eecs.emission.cordova.tracker.location.TripDiaryStateMachineService; import edu.berkeley.eecs.emission.cordova.unifiedlogger.Log; -import gov.colorado.energyoffice.emission.R; +import edu.berkeley.eecs.emission.cordova.R; /* * Deals with settings and resolutions when the app activity is visible. From 3f558721a6a6e720db84f479e26c3cc70fd1c4b5 Mon Sep 17 00:00:00 2001 From: Shankari Date: Tue, 26 Oct 2021 12:54:03 -0700 Subject: [PATCH 03/35] Remove the status code while formatting the exception At these lines ``` 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. Log.i(ctxt, TAG, "location settings are not valid, but cannot be fixed by showing a dialog"); NotificationHelper.createNotification(ctxt, Constants.TRACKING_ERROR_ID, ctxt.getString(R.string.error_location_settings, exception.getStatusCode())); ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_tracking_error)); break; ``` This is due to the change in the resource string https://github.com/e-mission/e-mission-data-collection/commit/bbf5b429e1a816d3bf0749e083aaff71be869d9a We made a similar change in that commit, but only in one of the affected locations. I am not quite sure why this did not break the build before. Testing done: - Did not compile without this change - Compiles with this change --- .../verification/SensorControlBackgroundChecker.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/android/verification/SensorControlBackgroundChecker.java b/src/android/verification/SensorControlBackgroundChecker.java index fcb9e43..c3851fa 100644 --- a/src/android/verification/SensorControlBackgroundChecker.java +++ b/src/android/verification/SensorControlBackgroundChecker.java @@ -171,9 +171,9 @@ private static void checkLocationSettings(final Context ctxt, ResolvableApiException resolvable = (ResolvableApiException) exception; // Show the dialog by calling startResolutionForResult(), // and check the result in onActivityResult(). - NotificationHelper.createResolveNotification(ctxt, Constants.TRACKING_ERROR_ID, - ctxt.getString(R.string.error_location_settings), - resolvable.getResolution()); + NotificationHelper.createResolveNotification(ctxt, Constants.TRACKING_ERROR_ID, + ctxt.getString(R.string.error_location_settings), + resolvable.getResolution()); ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_tracking_error)); } catch (ClassCastException e) { // Ignore, should be an impossible error. @@ -184,7 +184,7 @@ private static void checkLocationSettings(final Context ctxt, // settings so we won't show the dialog. Log.i(ctxt, TAG, "location settings are not valid, but cannot be fixed by showing a dialog"); NotificationHelper.createNotification(ctxt, Constants.TRACKING_ERROR_ID, - ctxt.getString(R.string.error_location_settings, exception.getStatusCode())); + ctxt.getString(R.string.error_location_settings)); ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_tracking_error)); break; default: From 1e00711295e2393e25846b4c3ad931d7b8547a0d Mon Sep 17 00:00:00 2001 From: Shankari Date: Wed, 27 Oct 2021 12:53:35 -0700 Subject: [PATCH 04/35] Initial implementation of the check and fix for the location settings - add a new high level file to pull out sensor control checks between the foreground and background checks - change the foreground and background code to call the common code https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-952326608 - change the foreground implementation to callback to cordova properly and asynchronously so we can update the state - have two foreground implementations for check and fix - store the callback and change the request handling to callback to cordova as well - return the currently usable states after JSON-ifying them - open the location settings page in the case of an unrecoverable error (currently untested) --- plugin.xml | 1 + res/android/values/dc_strings.xml | 1 + src/android/DataCollectionPlugin.java | 10 ++ .../SensorControlBackgroundChecker.java | 15 +-- .../verification/SensorControlChecks.java | 47 +++++++ .../verification/SensorControlConstants.java | 1 + .../SensorControlForegroundDelegate.java | 125 ++++++++++++++++-- www/datacollection.js | 30 +++++ 8 files changed, 211 insertions(+), 19 deletions(-) create mode 100644 src/android/verification/SensorControlChecks.java diff --git a/plugin.xml b/plugin.xml index d51c328..221af0f 100644 --- a/plugin.xml +++ b/plugin.xml @@ -144,6 +144,7 @@ + diff --git a/res/android/values/dc_strings.xml b/res/android/values/dc_strings.xml index 53f6f77..8285ebd 100644 --- a/res/android/values/dc_strings.xml +++ b/res/android/values/dc_strings.xml @@ -28,6 +28,7 @@ Error reading stored tracking config, reset to defaults + It looks like you canceled the requested change. Please accept it instead. Unrecoverable error in tracking, report at Profile -> Upload Log Ready for your next trip diff --git a/src/android/DataCollectionPlugin.java b/src/android/DataCollectionPlugin.java index d399f91..74a4d9a 100644 --- a/src/android/DataCollectionPlugin.java +++ b/src/android/DataCollectionPlugin.java @@ -1,4 +1,6 @@ package edu.berkeley.eecs.emission.cordova.tracker; +// Auto fixed by post-plugin hook +import edu.berkeley.eecs.emission.R; import org.apache.cordova.CordovaPlugin; import org.apache.cordova.CordovaInterface; @@ -71,6 +73,14 @@ public boolean execute(String action, JSONArray data, final CallbackContext call // TripDiaryStateMachineReceiver.restartCollection(ctxt); callbackContext.success(); return true; + } else if (action.equals("fixLocationSettings")) { + Log.d(cordova.getActivity(), TAG, "fixing location settings"); + mControlDelegate.checkAndPromptLocationSettings(callbackContext); + return true; + } else if (action.equals("isValidLocationSettings")) { + Log.d(cordova.getActivity(), TAG, "checking location settings"); + mControlDelegate.checkLocationSettings(callbackContext); + return true; } else if (action.equals("storeBatteryLevel")) { Context ctxt = cordova.getActivity(); TripDiaryStateMachineReceiver.saveBatteryAndSimulateUser(ctxt); diff --git a/src/android/verification/SensorControlBackgroundChecker.java b/src/android/verification/SensorControlBackgroundChecker.java index c3851fa..585511b 100644 --- a/src/android/verification/SensorControlBackgroundChecker.java +++ b/src/android/verification/SensorControlBackgroundChecker.java @@ -57,7 +57,7 @@ public static void checkLocationSettingsAndPermissions(final Context ctxt) { if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) || checkBackgroundLocPermissions(ctxt, request)) { Log.d(ctxt, TAG, "checkBackgroundLocPermissions returned true, checking location settings"); - checkLocationSettings(ctxt, request); + checkLocationSettings(ctxt); } else { Log.d(ctxt, TAG, "check background permissions returned false, no point checking settings"); ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_tracking_error)); @@ -143,17 +143,10 @@ public static void generateMotionActivityEnableNotification(Context ctxt) { pi); } - private static void checkLocationSettings(final Context ctxt, - final LocationRequest request) { - LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder() - .addLocationRequest(request); - - Task task = - LocationServices.getSettingsClient(ctxt).checkLocationSettings(builder.build()); - Log.d(ctxt, TAG, "Got back result "+task); - task.addOnCompleteListener(resultTask -> { + private static void checkLocationSettings(final Context ctxt) { + SensorControlChecks.checkLocationSettings(ctxt, resultTask -> { try { - LocationSettingsResponse response = task.getResult(ApiException.class); + LocationSettingsResponse response = resultTask.getResult(ApiException.class); // All location settings are satisfied. The client can initialize location // requests here. Log.i(ctxt, TAG, "All settings are valid, checking current state"); diff --git a/src/android/verification/SensorControlChecks.java b/src/android/verification/SensorControlChecks.java new file mode 100644 index 0000000..e62fb68 --- /dev/null +++ b/src/android/verification/SensorControlChecks.java @@ -0,0 +1,47 @@ +package edu.berkeley.eecs.emission.cordova.tracker.verification; +// Auto fixed by post-plugin hook +import edu.berkeley.eecs.emission.R; + +import android.app.PendingIntent; +import android.content.Context; +import android.location.Location; + +import com.google.android.gms.common.api.ApiException; +import com.google.android.gms.common.api.ResolvableApiException; +import com.google.android.gms.location.LocationRequest; +import com.google.android.gms.location.LocationServices; +import com.google.android.gms.location.LocationSettingsRequest; +import com.google.android.gms.location.LocationSettingsResponse; +import com.google.android.gms.location.LocationSettingsStates; +import com.google.android.gms.location.LocationSettingsStatusCodes; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; + + +import edu.berkeley.eecs.emission.cordova.tracker.Constants; +import edu.berkeley.eecs.emission.cordova.tracker.ExplicitIntent; +import edu.berkeley.eecs.emission.cordova.tracker.location.actions.LocationTrackingActions; +import edu.berkeley.eecs.emission.cordova.unifiedlogger.Log; +import edu.berkeley.eecs.emission.cordova.unifiedlogger.NotificationHelper; + +public class SensorControlChecks { + public static final String TAG = "SensorControlChecks"; + + public static void checkLocationSettings(final Context ctxt, + OnCompleteListener callback) { + Log.i(ctxt, TAG, "About to check location settings"); + LocationRequest request = new LocationTrackingActions(ctxt).getLocationRequest(); + Log.d(ctxt, TAG, "Checking location settings for request "+request); + LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder() + .addLocationRequest(request); + + Task task = + LocationServices.getSettingsClient(ctxt).checkLocationSettings(builder.build()); + Log.d(ctxt, TAG, "Got back result "+task); + task.addOnCompleteListener(callback); + } + + public static void fixLocationSettings(final Context ctxt) { + Log.i(ctxt, TAG, "About to fix location settings, checking first..."); + } +} diff --git a/src/android/verification/SensorControlConstants.java b/src/android/verification/SensorControlConstants.java index e0e4c9a..5f15ce4 100644 --- a/src/android/verification/SensorControlConstants.java +++ b/src/android/verification/SensorControlConstants.java @@ -9,6 +9,7 @@ public class SensorControlConstants { public static String MOTION_ACTIVITY_PERMISSION = Manifest.permission.ACTIVITY_RECOGNITION; public static final int ENABLE_LOCATION_SETTINGS = 362253738; + public static final int ENABLE_LOCATION_SETTINGS_MANUAL = 362253736; public static final int ENABLE_LOCATION_PERMISSION = 362253737; public static final int ENABLE_BACKGROUND_LOC_PERMISSION = 362253739; public static final int ENABLE_BOTH_PERMISSION = 362253740; diff --git a/src/android/verification/SensorControlForegroundDelegate.java b/src/android/verification/SensorControlForegroundDelegate.java index a1365d1..859e15f 100644 --- a/src/android/verification/SensorControlForegroundDelegate.java +++ b/src/android/verification/SensorControlForegroundDelegate.java @@ -1,9 +1,11 @@ package edu.berkeley.eecs.emission.cordova.tracker.verification; // Auto fixed by post-plugin hook +import edu.berkeley.eecs.emission.R; + + import edu.berkeley.eecs.emission.cordova.tracker.Constants; -import edu.berkeley.eecs.emission.cordova.tracker.location.TripDiaryStateMachineService; import edu.berkeley.eecs.emission.cordova.unifiedlogger.Log; -import edu.berkeley.eecs.emission.cordova.R; + /* * Deals with settings and resolutions when the app activity is visible. @@ -13,9 +15,11 @@ * - Dealing with user responses */ +import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaInterface; import org.apache.cordova.CordovaPlugin; import org.json.JSONException; +import org.json.JSONObject; import android.app.Activity; import android.content.Context; @@ -28,9 +32,14 @@ import android.provider.Settings; +import com.google.android.gms.common.api.ApiException; +import com.google.android.gms.common.api.ResolvableApiException; +import com.google.android.gms.location.LocationSettingsResponse; import com.google.android.gms.location.LocationSettingsStates; +import com.google.android.gms.location.LocationSettingsStatusCodes; import java.util.Arrays; +import java.util.Objects; import edu.berkeley.eecs.emission.cordova.unifiedlogger.NotificationHelper; @@ -39,10 +48,99 @@ public class SensorControlForegroundDelegate { private CordovaPlugin plugin = null; private CordovaInterface cordova = null; + private CallbackContext cordovaCallback = null; + + public SensorControlForegroundDelegate(CordovaPlugin inPlugin, + CordovaInterface inCordova) { + plugin = inPlugin; + cordova = inCordova; + } - public SensorControlForegroundDelegate(CordovaPlugin iplugin, CordovaInterface icordova) { - plugin = iplugin; - cordova = icordova; + // Invokes the callback with a boolean indicating whether + // the location settings are correct or not + public void checkLocationSettings(CallbackContext cordovaCallback) { + Activity currActivity = cordova.getActivity(); + SensorControlChecks.checkLocationSettings(currActivity, + resultTask -> { + try { + LocationSettingsResponse response = resultTask.getResult(ApiException.class); + // All location settings are satisfied. The client can initialize location + // requests here. + Log.i(currActivity, TAG, "All settings are valid, checking current state"); + Log.i(currActivity, TAG, "Current location settings are "+response.getLocationSettingsStates()); + cordovaCallback.success(Objects.requireNonNull(response.getLocationSettingsStates()).toString()); + } catch (ApiException exception) { + Log.i(currActivity, TAG, "Settings are not valid, returning "+exception.getMessage()); + cordovaCallback.error(exception.getLocalizedMessage()); + } + }); + } + + public void checkAndPromptLocationSettings(CallbackContext callbackContext) { + Activity currActivity = cordova.getActivity(); + SensorControlChecks.checkLocationSettings(currActivity, resultTask -> { + try { + LocationSettingsResponse response = resultTask.getResult(ApiException.class); + // All location settings are satisfied. The client can initialize location + // requests here. + Log.i(currActivity, TAG, "All settings are valid, checking current state"); + JSONObject lssJSON = statesToJSON(response.getLocationSettingsStates()); + Log.i(currActivity, TAG, "Current location settings are "+lssJSON); + callbackContext.success(lssJSON); + } catch (ApiException exception) { + switch (exception.getStatusCode()) { + case LocationSettingsStatusCodes.RESOLUTION_REQUIRED: + Log.i(currActivity, TAG, "location settings are not valid, but could be fixed by showing the user a dialog"); + // Location settings are not satisfied. But could be fixed by showing the + // user a dialog. + try { + // Cast to a resolvable exception. + ResolvableApiException resolvable = (ResolvableApiException) exception; + // Show the dialog by calling startResolutionForResult(), + // and check the result in onActivityResult(). + // Experiment with "send" instead of this resolution code + this.cordovaCallback = callbackContext; + cordova.setActivityResultCallback(plugin); + resolvable.startResolutionForResult(currActivity, SensorControlConstants.ENABLE_LOCATION_SETTINGS); + } catch (IntentSender.SendIntentException | ClassCastException sie) { + callbackContext.error(sie.getLocalizedMessage()); + } + 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. + Log.i(currActivity, TAG, "location settings are not valid, but cannot be fixed by showing a dialog"); + openLocationSettingsPage(callbackContext); + break; + default: + Log.i(currActivity, TAG, "unknown error reading location"); + openLocationSettingsPage(callbackContext); + } + } catch (JSONException e) { + callbackContext.error(e.getLocalizedMessage()); + } + }); + } + + private static JSONObject statesToJSON(LocationSettingsStates lss) throws JSONException { + // TODO: Does this need to be internationalized? + if (lss == null) { + throw new JSONException("null input"); + } + JSONObject jo = new JSONObject(); + jo.put("Bluetooth", lss.isBleUsable()); + jo.put("GPS", lss.isGpsUsable()); + jo.put("Network", lss.isNetworkLocationUsable()); + jo.put("location", lss.isLocationUsable()); + return jo; + } + + private void openLocationSettingsPage(CallbackContext callbackContext) { + this.cordovaCallback = callbackContext; + cordova.setActivityResultCallback(plugin); + Intent locSettingsIntent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS); + cordova.getActivity().startActivityForResult(locSettingsIntent, + SensorControlConstants.ENABLE_LOCATION_SETTINGS_MANUAL); } public void checkAndPromptPermissions() { @@ -207,22 +305,33 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { 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()); + Log.d(cordova.getActivity(), TAG, "at this point, isLocationUsable = " + (states != null && states.isLocationUsable())); switch (resultCode) { case Activity.RESULT_OK: // All required changes were successfully made Log.i(cordova.getActivity(), TAG, "All changes successfully made, reinitializing"); - NotificationHelper.cancelNotification(mAct, Constants.TRACKING_ERROR_ID); - SensorControlBackgroundChecker.restartFSMIfStartState(mAct); + try { + cordovaCallback.success(statesToJSON(states)); + } catch (JSONException e) { + cordovaCallback.error(mAct.getString(R.string.unknown_error_location_settings)); + } break; case Activity.RESULT_CANCELED: // The user was asked to change settings, but chose not to Log.e(cordova.getActivity(), TAG, "User chose not to change settings, dunno what to do"); + cordovaCallback.error(mAct.getString(R.string.user_rejected_setting)); break; default: + cordovaCallback.error(mAct.getString(R.string.unable_resolve_issue)); Log.e(cordova.getActivity(), TAG, "Unknown result code while enabling location " + resultCode); break; } + case SensorControlConstants.ENABLE_LOCATION_SETTINGS_MANUAL: + Log.d(mAct, TAG, requestCode + " is our code, handling callback"); + cordova.setActivityResultCallback(null); + // this will call the callback with success or error + checkLocationSettings(cordovaCallback); + break; case SensorControlConstants.ENABLE_BOTH_PERMISSION: Log.d(mAct, TAG, requestCode + " is our code, handling callback"); cordova.setActivityResultCallback(null); diff --git a/www/datacollection.js b/www/datacollection.js index 4d7d5bc..2e7add2 100644 --- a/www/datacollection.js +++ b/www/datacollection.js @@ -21,6 +21,36 @@ var DataCollection = { exec(resolve, reject, "DataCollection", "markConsented", [newConsent]); }); }, + fixLocationSettings: function () { + return new Promise(function(resolve, reject) { + exec(resolve, reject, "DataCollection", "fixLocationSettings", []); + }); + }, + isValidLocationSettings: function () { + return new Promise(function(resolve, reject) { + exec(resolve, reject, "DataCollection", "isValidLocationSettings", []); + }); + }, + fixLocationPermissions: function () { + return new Promise(function(resolve, reject) { + exec(resolve, reject, "DataCollection", "fixLocationPermissions", []); + }); + }, + isValidLocationPermissions: function () { + return new Promise(function(resolve, reject) { + exec(resolve, reject, "DataCollection", "isValidLocationPermissions", []); + }); + }, + fixFitnessPermissions: function () { + return new Promise(function(resolve, reject) { + exec(resolve, reject, "DataCollection", "fixFitnessPermissions", []); + }); + }, + isValidFitnessPermissions: function () { + return new Promise(function(resolve, reject) { + exec(resolve, reject, "DataCollection", "isValidFitnessPermissions", []); + }); + }, storeBatteryLevel: function () { return new Promise(function(resolve, reject) { exec(resolve, reject, "DataCollection", "storeBatteryLevel", []); From f98bd38f769d5156c1b18667849ce339fedc52c8 Mon Sep 17 00:00:00 2001 From: Shankari Date: Wed, 27 Oct 2021 21:44:49 -0700 Subject: [PATCH 05/35] Add checks for location permission Similar to https://github.com/e-mission/e-mission-phone/pull/804/commits/29d2d855acfb7bf91b6bffde1ab8bb94c6fb1a4c for location settings --- src/android/DataCollectionPlugin.java | 8 +++ .../verification/SensorControlChecks.java | 11 +++- .../SensorControlForegroundDelegate.java | 50 +++++++++++-------- 3 files changed, 45 insertions(+), 24 deletions(-) diff --git a/src/android/DataCollectionPlugin.java b/src/android/DataCollectionPlugin.java index 74a4d9a..1f09d48 100644 --- a/src/android/DataCollectionPlugin.java +++ b/src/android/DataCollectionPlugin.java @@ -81,6 +81,14 @@ public boolean execute(String action, JSONArray data, final CallbackContext call Log.d(cordova.getActivity(), TAG, "checking location settings"); mControlDelegate.checkLocationSettings(callbackContext); return true; + } else if (action.equals("fixLocationPermissions")) { + Log.d(cordova.getActivity(), TAG, "fixing location settings"); + mControlDelegate.checkAndPromptLocationPermissions(callbackContext); + return true; + } else if (action.equals("isValidLocationPermissions")) { + Log.d(cordova.getActivity(), TAG, "checking location settings"); + mControlDelegate.checkLocationPermissions(callbackContext); + return true; } else if (action.equals("storeBatteryLevel")) { Context ctxt = cordova.getActivity(); TripDiaryStateMachineReceiver.saveBatteryAndSimulateUser(ctxt); diff --git a/src/android/verification/SensorControlChecks.java b/src/android/verification/SensorControlChecks.java index e62fb68..b73147b 100644 --- a/src/android/verification/SensorControlChecks.java +++ b/src/android/verification/SensorControlChecks.java @@ -6,6 +6,9 @@ import android.content.Context; import android.location.Location; +import androidx.core.content.ContextCompat; +import androidx.core.content.PermissionChecker; + import com.google.android.gms.common.api.ApiException; import com.google.android.gms.common.api.ResolvableApiException; import com.google.android.gms.location.LocationRequest; @@ -41,7 +44,11 @@ public static void checkLocationSettings(final Context ctxt, task.addOnCompleteListener(callback); } - public static void fixLocationSettings(final Context ctxt) { - Log.i(ctxt, TAG, "About to fix location settings, checking first..."); + // TODO: Figure out how to integrate this with the background code + // https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-953403832 + public static boolean checkLocationPermissions(final Context ctxt) { + boolean foregroundPerm = ContextCompat.checkSelfPermission(ctxt, SensorControlConstants.LOCATION_PERMISSION) == PermissionChecker.PERMISSION_GRANTED; + boolean backgroundPerm = ContextCompat.checkSelfPermission(ctxt, SensorControlConstants.BACKGROUND_LOC_PERMISSION) == PermissionChecker.PERMISSION_GRANTED; + return foregroundPerm && backgroundPerm; } } diff --git a/src/android/verification/SensorControlForegroundDelegate.java b/src/android/verification/SensorControlForegroundDelegate.java index 859e15f..ff4f48f 100644 --- a/src/android/verification/SensorControlForegroundDelegate.java +++ b/src/android/verification/SensorControlForegroundDelegate.java @@ -143,16 +143,25 @@ private void openLocationSettingsPage(CallbackContext callbackContext) { SensorControlConstants.ENABLE_LOCATION_SETTINGS_MANUAL); } + public void checkLocationPermissions(CallbackContext cordovaCallback) { + boolean validPerms = SensorControlChecks.checkLocationPermissions(cordova.getActivity()); + if(validPerms) { + cordovaCallback.success(); + } else { + cordovaCallback.error(cordova.getActivity().getString(R.string.location_permission_off)); + } + } + public void checkAndPromptPermissions() { - checkAndPromptLocationPermissions(); + checkAndPromptLocationPermissions(null); checkAndPromptMotionActivityPermissions(); } - private void checkAndPromptLocationPermissions() { + public void checkAndPromptLocationPermissions(CallbackContext cordovaCallback) { if(cordova.hasPermission(SensorControlConstants.LOCATION_PERMISSION) && cordova.hasPermission(SensorControlConstants.BACKGROUND_LOC_PERMISSION)) { SensorControlBackgroundChecker.restartFSMIfStartState(cordova.getActivity()); - return; + cordovaCallback.success(); } // If this is android 11 (API 30), we want to launch the app settings instead of prompting for permission // because the default permission prompting does not offer "always" as an option @@ -173,6 +182,7 @@ private void checkAndPromptLocationPermissions() { // These are to hopefully help us get a callback once the settings are changed Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); intent.setData(Uri.fromParts("package", mAct.getPackageName(), null)); + this.cordovaCallback = cordovaCallback; cordova.setActivityResultCallback(plugin); mAct.startActivityForResult(intent, SensorControlConstants.ENABLE_BOTH_PERMISSION); return; @@ -181,17 +191,20 @@ private void checkAndPromptLocationPermissions() { (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) && !cordova.hasPermission(SensorControlConstants.BACKGROUND_LOC_PERMISSION)) { Log.i(cordova.getActivity(), TAG, "Both permissions missing, requesting both"); + this.cordovaCallback = cordovaCallback; cordova.requestPermissions(plugin, SensorControlConstants.ENABLE_BOTH_PERMISSION, new String[]{SensorControlConstants.LOCATION_PERMISSION, SensorControlConstants.BACKGROUND_LOC_PERMISSION}); return; } if(!cordova.hasPermission(SensorControlConstants.LOCATION_PERMISSION)) { Log.i(cordova.getActivity(), TAG, "Only location permission missing, requesting it"); + this.cordovaCallback = cordovaCallback; cordova.requestPermission(plugin, SensorControlConstants.ENABLE_LOCATION_PERMISSION, SensorControlConstants.LOCATION_PERMISSION); return; } if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !cordova.hasPermission(SensorControlConstants.BACKGROUND_LOC_PERMISSION)) { Log.i(cordova.getActivity(), TAG, "Only background permission missing, requesting it"); + this.cordovaCallback = cordovaCallback; cordova.requestPermission(plugin, SensorControlConstants.ENABLE_BACKGROUND_LOC_PERMISSION, SensorControlConstants.BACKGROUND_LOC_PERMISSION); return; } @@ -220,7 +233,7 @@ private void displayResolution(PendingIntent resolution) { public void onNewIntent(Intent intent) { if(SensorControlConstants.ENABLE_LOCATION_PERMISSION_ACTION.equals(intent.getAction()) || SensorControlConstants.ENABLE_BACKGROUND_LOC_PERMISSION_ACTION.equals(intent.getAction())) { - checkAndPromptLocationPermissions(); + checkAndPromptLocationPermissions(null); return; } @@ -260,28 +273,25 @@ public void onRequestPermissionResult(int requestCode, String[] permissions, case SensorControlConstants.ENABLE_BOTH_PERMISSION: if ((grantResults[0] == PackageManager.PERMISSION_GRANTED) && (grantResults[1] == PackageManager.PERMISSION_GRANTED)) { - NotificationHelper.cancelNotification(cordova.getActivity(), SensorControlConstants.ENABLE_BOTH_PERMISSION); - SensorControlBackgroundChecker.restartFSMIfStartState(cordova.getActivity()); + cordovaCallback.success(); } else if (grantResults[0] == PackageManager.PERMISSION_DENIED) { - SensorControlBackgroundChecker.generateLocationEnableNotification(cordova.getActivity()); + cordovaCallback.error(cordova.getActivity().getString(R.string.location_permission_off)); } else if (grantResults[1] == PackageManager.PERMISSION_DENIED) { - SensorControlBackgroundChecker.generateBackgroundLocEnableNotification(cordova.getActivity()); + cordovaCallback.error(cordova.getActivity().getString(R.string.location_permission_off)); } break; case SensorControlConstants.ENABLE_LOCATION_PERMISSION: if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - NotificationHelper.cancelNotification(cordova.getActivity(), SensorControlConstants.ENABLE_LOCATION_PERMISSION); - SensorControlBackgroundChecker.restartFSMIfStartState(cordova.getActivity()); + cordovaCallback.success(); } else if (grantResults[0] == PackageManager.PERMISSION_DENIED) { - SensorControlBackgroundChecker.generateLocationEnableNotification(cordova.getActivity()); + cordovaCallback.error(cordova.getActivity().getString(R.string.location_permission_off)); } break; case SensorControlConstants.ENABLE_BACKGROUND_LOC_PERMISSION: if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - NotificationHelper.cancelNotification(cordova.getActivity(), SensorControlConstants.ENABLE_BACKGROUND_LOC_PERMISSION); - SensorControlBackgroundChecker.restartFSMIfStartState(cordova.getActivity()); + cordovaCallback.success(); } else if (grantResults[0] == PackageManager.PERMISSION_DENIED) { - SensorControlBackgroundChecker.generateBackgroundLocEnableNotification(cordova.getActivity()); + cordovaCallback.error(cordova.getActivity().getString(R.string.location_permission_off)); } break; case SensorControlConstants.ENABLE_MOTION_ACTIVITY_PERMISSION: @@ -336,15 +346,11 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { Log.d(mAct, TAG, requestCode + " is our code, handling callback"); cordova.setActivityResultCallback(null); Log.d(mAct, TAG, "Got permission callback from launching app settings"); - if (cordova.hasPermission(SensorControlConstants.LOCATION_PERMISSION)) { - // location permission enabled, cancelling notification - NotificationHelper.cancelNotification(cordova.getActivity(), SensorControlConstants.ENABLE_LOCATION_PERMISSION); - } - if (cordova.hasPermission(SensorControlConstants.BACKGROUND_LOC_PERMISSION)) { - // background location permission enabled, cancelling notification - NotificationHelper.cancelNotification(cordova.getActivity(), SensorControlConstants.ENABLE_BACKGROUND_LOC_PERMISSION); + if (SensorControlChecks.checkLocationPermissions(cordova.getActivity())) { + cordovaCallback.success(); + } else { + cordovaCallback.error(cordova.getActivity().getString(R.string.location_permission_off)); } - SensorControlBackgroundChecker.restartFSMIfStartState(cordova.getActivity()); break; default: Log.d(cordova.getActivity(), TAG, "Got unsupported request code " + requestCode + " , ignoring..."); From f0b6855a7d7e915e464d87d60e8bf63e69ac7201 Mon Sep 17 00:00:00 2001 From: Shankari Date: Wed, 27 Oct 2021 22:06:15 -0700 Subject: [PATCH 06/35] Add missing string value as well --- res/android/values/dc_strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/res/android/values/dc_strings.xml b/res/android/values/dc_strings.xml index 8285ebd..f748661 100644 --- a/res/android/values/dc_strings.xml +++ b/res/android/values/dc_strings.xml @@ -17,6 +17,7 @@ Success moving to %1$s Failed moving to %1$s + Insufficient location permissions, please fix Location permission off, click to enable Background location permission off, click to enable Activity permission off, click to enable From cf2712af5496463ecc724cf4e541e5fcf596a2f5 Mon Sep 17 00:00:00 2001 From: Shankari Date: Thu, 28 Oct 2021 09:35:14 -0700 Subject: [PATCH 07/35] Support enabling/disabling the fitness permission as well --- res/android/values/dc_strings.xml | 1 + src/android/DataCollectionPlugin.java | 14 +++++++-- .../verification/SensorControlChecks.java | 11 +++++++ .../SensorControlForegroundDelegate.java | 31 ++++++++++++------- 4 files changed, 42 insertions(+), 15 deletions(-) diff --git a/res/android/values/dc_strings.xml b/res/android/values/dc_strings.xml index f748661..932e162 100644 --- a/res/android/values/dc_strings.xml +++ b/res/android/values/dc_strings.xml @@ -20,6 +20,7 @@ Insufficient location permissions, please fix Location permission off, click to enable Background location permission off, click to enable + Activity permission off, please fix Activity permission off, click to enable Error in location settings, click to resolve Unknown error while reading location, please check your settings diff --git a/src/android/DataCollectionPlugin.java b/src/android/DataCollectionPlugin.java index 1f09d48..c7abfa3 100644 --- a/src/android/DataCollectionPlugin.java +++ b/src/android/DataCollectionPlugin.java @@ -68,7 +68,7 @@ public boolean execute(String action, JSONArray data, final CallbackContext call // Now, really initialize the state machine // 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 - mControlDelegate.checkAndPromptPermissions(); + // mControlDelegate.checkAndPromptPermissions(); // ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_initialize)); // TripDiaryStateMachineReceiver.restartCollection(ctxt); callbackContext.success(); @@ -82,13 +82,21 @@ public boolean execute(String action, JSONArray data, final CallbackContext call mControlDelegate.checkLocationSettings(callbackContext); return true; } else if (action.equals("fixLocationPermissions")) { - Log.d(cordova.getActivity(), TAG, "fixing location settings"); + Log.d(cordova.getActivity(), TAG, "fixing location permissions"); mControlDelegate.checkAndPromptLocationPermissions(callbackContext); return true; } else if (action.equals("isValidLocationPermissions")) { - Log.d(cordova.getActivity(), TAG, "checking location settings"); + Log.d(cordova.getActivity(), TAG, "checking location permissions"); mControlDelegate.checkLocationPermissions(callbackContext); return true; + } else if (action.equals("fixFitnessPermissions")) { + Log.d(cordova.getActivity(), TAG, "fixing fitness permissions"); + mControlDelegate.checkAndPromptMotionActivityPermissions(callbackContext); + return true; + } else if (action.equals("isValidFitnessPermissions")) { + Log.d(cordova.getActivity(), TAG, "checking fitness permissions"); + mControlDelegate.checkMotionActivityPermissions(callbackContext); + return true; } else if (action.equals("storeBatteryLevel")) { Context ctxt = cordova.getActivity(); TripDiaryStateMachineReceiver.saveBatteryAndSimulateUser(ctxt); diff --git a/src/android/verification/SensorControlChecks.java b/src/android/verification/SensorControlChecks.java index b73147b..3f0df8a 100644 --- a/src/android/verification/SensorControlChecks.java +++ b/src/android/verification/SensorControlChecks.java @@ -5,6 +5,7 @@ import android.app.PendingIntent; import android.content.Context; import android.location.Location; +import android.os.Build; import androidx.core.content.ContextCompat; import androidx.core.content.PermissionChecker; @@ -51,4 +52,14 @@ public static boolean checkLocationPermissions(final Context ctxt) { boolean backgroundPerm = ContextCompat.checkSelfPermission(ctxt, SensorControlConstants.BACKGROUND_LOC_PERMISSION) == PermissionChecker.PERMISSION_GRANTED; return foregroundPerm && backgroundPerm; } + + // TODO: Figure out how to integrate this with the background code + // https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-953403832 + public static boolean checkMotionActivityPermissions(final Context ctxt) { + // apps before version 29 did not need to prompt for dynamic permissions related + // to motion activity + boolean version29Check = Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q; + boolean permCheck = ContextCompat.checkSelfPermission(ctxt, SensorControlConstants.MOTION_ACTIVITY_PERMISSION) == PermissionChecker.PERMISSION_GRANTED; + return version29Check || permCheck; + } } diff --git a/src/android/verification/SensorControlForegroundDelegate.java b/src/android/verification/SensorControlForegroundDelegate.java index ff4f48f..97183e8 100644 --- a/src/android/verification/SensorControlForegroundDelegate.java +++ b/src/android/verification/SensorControlForegroundDelegate.java @@ -152,11 +152,6 @@ public void checkLocationPermissions(CallbackContext cordovaCallback) { } } - public void checkAndPromptPermissions() { - checkAndPromptLocationPermissions(null); - checkAndPromptMotionActivityPermissions(); - } - public void checkAndPromptLocationPermissions(CallbackContext cordovaCallback) { if(cordova.hasPermission(SensorControlConstants.LOCATION_PERMISSION) && cordova.hasPermission(SensorControlConstants.BACKGROUND_LOC_PERMISSION)) { @@ -210,11 +205,23 @@ public void checkAndPromptLocationPermissions(CallbackContext cordovaCallback) { } } - private void checkAndPromptMotionActivityPermissions() { - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !cordova.hasPermission(SensorControlConstants.MOTION_ACTIVITY_PERMISSION)) { - Log.i(cordova.getActivity(), TAG, "Only motion activity permission missing, requesting it"); + public void checkMotionActivityPermissions(CallbackContext cordovaCallback) { + boolean validPerms = SensorControlChecks.checkMotionActivityPermissions(cordova.getActivity()); + if(validPerms) { + cordovaCallback.success(); + } else { + cordovaCallback.error(cordova.getActivity().getString(R.string.activity_permission_off)); + } + } + + public void checkAndPromptMotionActivityPermissions(CallbackContext cordovaCallback) { + boolean validPerms = SensorControlChecks.checkMotionActivityPermissions(cordova.getActivity()); + if(validPerms) { + cordovaCallback.success(); + } else { + Log.i(cordova.getActivity(), TAG, "Motion activity permission missing, requesting it"); + this.cordovaCallback = cordovaCallback; cordova.requestPermission(plugin, SensorControlConstants.ENABLE_MOTION_ACTIVITY_PERMISSION, SensorControlConstants.MOTION_ACTIVITY_PERMISSION); - return; } } @@ -238,7 +245,7 @@ public void onNewIntent(Intent intent) { } if(SensorControlConstants.ENABLE_MOTION_ACTIVITY_PERMISSION_ACTION.equals(intent.getAction())) { - checkAndPromptMotionActivityPermissions(); + checkAndPromptMotionActivityPermissions(null); return; } if (NotificationHelper.DISPLAY_RESOLUTION_ACTION.equals(intent.getAction())) { @@ -296,10 +303,10 @@ public void onRequestPermissionResult(int requestCode, String[] permissions, break; case SensorControlConstants.ENABLE_MOTION_ACTIVITY_PERMISSION: if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - NotificationHelper.cancelNotification(cordova.getActivity(), SensorControlConstants.ENABLE_MOTION_ACTIVITY_PERMISSION); + cordovaCallback.success(); // motion activity does not affect the FSM } else if (grantResults[0] == PackageManager.PERMISSION_DENIED) { - SensorControlBackgroundChecker.generateMotionActivityEnableNotification(cordova.getActivity()); + cordovaCallback.error(cordova.getActivity().getString(R.string.activity_permission_off)); } break; default: From 216dd7e378da5b2a915e2ed5c172642dce091cb3 Mon Sep 17 00:00:00 2001 From: Shankari Date: Fri, 29 Oct 2021 10:08:08 -0700 Subject: [PATCH 08/35] Add notification enabled status checks Modeled on the location and motion activity permissions. So that people can "fix" the notifications and then get notifications of other things that are broken. --- res/android/values/dc_strings.xml | 1 + src/android/DataCollectionPlugin.java | 10 ++++- .../verification/SensorControlChecks.java | 30 ++++++++++++- .../verification/SensorControlConstants.java | 1 + .../SensorControlForegroundDelegate.java | 44 ++++++++++++++----- www/datacollection.js | 10 +++++ 6 files changed, 82 insertions(+), 14 deletions(-) diff --git a/res/android/values/dc_strings.xml b/res/android/values/dc_strings.xml index 932e162..1198d73 100644 --- a/res/android/values/dc_strings.xml +++ b/res/android/values/dc_strings.xml @@ -22,6 +22,7 @@ Background location permission off, click to enable Activity permission off, please fix Activity permission off, click to enable + Notifications blocked, please enable Error in location settings, click to resolve Unknown error while reading location, please check your settings In state %1$s diff --git a/src/android/DataCollectionPlugin.java b/src/android/DataCollectionPlugin.java index c7abfa3..bf9c841 100644 --- a/src/android/DataCollectionPlugin.java +++ b/src/android/DataCollectionPlugin.java @@ -1,5 +1,5 @@ package edu.berkeley.eecs.emission.cordova.tracker; -// Auto fixed by post-plugin hook + import edu.berkeley.eecs.emission.R; import org.apache.cordova.CordovaPlugin; @@ -97,6 +97,14 @@ public boolean execute(String action, JSONArray data, final CallbackContext call Log.d(cordova.getActivity(), TAG, "checking fitness permissions"); mControlDelegate.checkMotionActivityPermissions(callbackContext); return true; + } else if (action.equals("fixShowNotifications")) { + Log.d(cordova.getActivity(), TAG, "checking notification enable"); + mControlDelegate.checkAndPromptShowNotificationsEnabled(callbackContext); + return true; + } else if (action.equals("isValidShowNotifications")) { + Log.d(cordova.getActivity(), TAG, "checking notification enable"); + mControlDelegate.checkShowNotificationsEnabled(callbackContext); + return true; } else if (action.equals("storeBatteryLevel")) { Context ctxt = cordova.getActivity(); TripDiaryStateMachineReceiver.saveBatteryAndSimulateUser(ctxt); diff --git a/src/android/verification/SensorControlChecks.java b/src/android/verification/SensorControlChecks.java index 3f0df8a..26aa47e 100644 --- a/src/android/verification/SensorControlChecks.java +++ b/src/android/verification/SensorControlChecks.java @@ -1,12 +1,16 @@ package edu.berkeley.eecs.emission.cordova.tracker.verification; -// Auto fixed by post-plugin hook + import edu.berkeley.eecs.emission.R; +import android.app.NotificationChannel; +import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.location.Location; import android.os.Build; +import androidx.annotation.RequiresApi; +import androidx.core.app.NotificationManagerCompat; import androidx.core.content.ContextCompat; import androidx.core.content.PermissionChecker; @@ -22,6 +26,8 @@ import com.google.android.gms.tasks.Task; +import java.util.List; + import edu.berkeley.eecs.emission.cordova.tracker.Constants; import edu.berkeley.eecs.emission.cordova.tracker.ExplicitIntent; import edu.berkeley.eecs.emission.cordova.tracker.location.actions.LocationTrackingActions; @@ -49,7 +55,11 @@ public static void checkLocationSettings(final Context ctxt, // https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-953403832 public static boolean checkLocationPermissions(final Context ctxt) { boolean foregroundPerm = ContextCompat.checkSelfPermission(ctxt, SensorControlConstants.LOCATION_PERMISSION) == PermissionChecker.PERMISSION_GRANTED; - boolean backgroundPerm = ContextCompat.checkSelfPermission(ctxt, SensorControlConstants.BACKGROUND_LOC_PERMISSION) == PermissionChecker.PERMISSION_GRANTED; + // the background permission is only valid for Q+ + boolean backgroundPerm = true; + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + backgroundPerm = ContextCompat.checkSelfPermission(ctxt, SensorControlConstants.BACKGROUND_LOC_PERMISSION) == PermissionChecker.PERMISSION_GRANTED; + } return foregroundPerm && backgroundPerm; } @@ -62,4 +72,20 @@ public static boolean checkMotionActivityPermissions(final Context ctxt) { boolean permCheck = ContextCompat.checkSelfPermission(ctxt, SensorControlConstants.MOTION_ACTIVITY_PERMISSION) == PermissionChecker.PERMISSION_GRANTED; return version29Check || permCheck; } + + public static boolean checkNotificationsEnabled(final Context ctxt) { + NotificationManagerCompat nMgr = NotificationManagerCompat.from(ctxt); + boolean appDisabled = nMgr.areNotificationsEnabled(); + boolean channelsDisabled = false; + // notification channels did not exist before oreo, so they + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + List channels = nMgr.getNotificationChannels(); + for (NotificationChannel c : channels) { + boolean currChannelDisabled = false; + currChannelDisabled = (c.getImportance() == NotificationManager.IMPORTANCE_NONE); + channelsDisabled = channelsDisabled || currChannelDisabled; + } + } + return appDisabled || channelsDisabled; + } } diff --git a/src/android/verification/SensorControlConstants.java b/src/android/verification/SensorControlConstants.java index 5f15ce4..2885df6 100644 --- a/src/android/verification/SensorControlConstants.java +++ b/src/android/verification/SensorControlConstants.java @@ -14,6 +14,7 @@ public class SensorControlConstants { public static final int ENABLE_BACKGROUND_LOC_PERMISSION = 362253739; public static final int ENABLE_BOTH_PERMISSION = 362253740; public static final int ENABLE_MOTION_ACTIVITY_PERMISSION = 362253741; + public static final int ENABLE_NOTIFICATIONS = 362253742; public static final String ENABLE_LOCATION_PERMISSION_ACTION = "ENABLE_LOCATION_PERMISSION"; public static final String ENABLE_BACKGROUND_LOC_PERMISSION_ACTION = "ENABLE_BACKGROUND_LOC_PERMISSION"; diff --git a/src/android/verification/SensorControlForegroundDelegate.java b/src/android/verification/SensorControlForegroundDelegate.java index 97183e8..3cc3569 100644 --- a/src/android/verification/SensorControlForegroundDelegate.java +++ b/src/android/verification/SensorControlForegroundDelegate.java @@ -163,23 +163,14 @@ public void checkAndPromptLocationPermissions(CallbackContext cordovaCallback) { // https://github.com/e-mission/e-mission-docs/issues/608 // we don't really care about which level of permission is missing since the prompt doesn't // do anything anyway. If either permission is missing, we just open the app settings - // Note also that we should actually check for VERSION_CODES.R - // but since we are not targeting API 30 yet, we can't do that - // so we use Q (29) + 1 instead. I think that is more readable than 30 - if ((Build.VERSION.SDK_INT >= (Build.VERSION_CODES.Q + 1)) && + if ((Build.VERSION.SDK_INT >= (Build.VERSION_CODES.R)) && (!cordova.hasPermission(SensorControlConstants.LOCATION_PERMISSION) || !cordova.hasPermission(SensorControlConstants.BACKGROUND_LOC_PERMISSION))) { - Activity mAct = cordova.getActivity(); String msgString = " LOC = "+cordova.hasPermission(SensorControlConstants.LOCATION_PERMISSION)+ " BACKGROUND LOC "+ cordova.hasPermission(SensorControlConstants.BACKGROUND_LOC_PERMISSION)+ " Android R+, so opening app settings anyway"; Log.i(cordova.getActivity(), TAG, msgString); - // These are to hopefully help us get a callback once the settings are changed - Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - intent.setData(Uri.fromParts("package", mAct.getPackageName(), null)); - this.cordovaCallback = cordovaCallback; - cordova.setActivityResultCallback(plugin); - mAct.startActivityForResult(intent, SensorControlConstants.ENABLE_BOTH_PERMISSION); + openAppSettingsPage(cordovaCallback, SensorControlConstants.ENABLE_BOTH_PERMISSION); return; } if(!cordova.hasPermission(SensorControlConstants.LOCATION_PERMISSION) && @@ -225,6 +216,27 @@ public void checkAndPromptMotionActivityPermissions(CallbackContext cordovaCallb } } + public void checkShowNotificationsEnabled(CallbackContext cordovaCallback) { + boolean validPerms = SensorControlChecks.checkNotificationsEnabled(cordova.getActivity()); + if(validPerms) { + cordovaCallback.success(); + } else { + cordovaCallback.error(cordova.getActivity().getString(R.string.activity_permission_off)); + } + } + + public void checkAndPromptShowNotificationsEnabled(CallbackContext cordovaCallback) { + boolean validPerms = SensorControlChecks.checkNotificationsEnabled(cordova.getActivity()); + if(validPerms) { + cordovaCallback.success(); + } else { + Log.i(cordova.getActivity(), TAG, "Notifications not enabled, opening app page"); + // TODO: switch to Settings.ACTION_APP_NOTIFICATION_SETTINGS instead of the app page + // once our min SDK goes up to oreo + openAppSettingsPage(cordovaCallback, SensorControlConstants.ENABLE_NOTIFICATIONS); + } + } + private void displayResolution(PendingIntent resolution) { if (resolution != null) { try { @@ -359,6 +371,16 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { cordovaCallback.error(cordova.getActivity().getString(R.string.location_permission_off)); } break; + case SensorControlConstants.ENABLE_NOTIFICATIONS: + Log.d(mAct, TAG, requestCode + " is our code, handling callback"); + cordova.setActivityResultCallback(null); + Log.d(mAct, TAG, "Got notification callback from launching app settings"); + if (SensorControlChecks.checkNotificationsEnabled(cordova.getActivity())) { + cordovaCallback.success(); + } else { + cordovaCallback.error(cordova.getActivity().getString(R.string.notifications_blocked)); + } + break; default: Log.d(cordova.getActivity(), TAG, "Got unsupported request code " + requestCode + " , ignoring..."); } diff --git a/www/datacollection.js b/www/datacollection.js index 2e7add2..dad47a7 100644 --- a/www/datacollection.js +++ b/www/datacollection.js @@ -51,6 +51,16 @@ var DataCollection = { exec(resolve, reject, "DataCollection", "isValidFitnessPermissions", []); }); }, + fixShowNotifications: function () { + return new Promise(function(resolve, reject) { + exec(resolve, reject, "DataCollection", "fixShowNotifications", []); + }); + }, + isValidShowNotifications: function () { + return new Promise(function(resolve, reject) { + exec(resolve, reject, "DataCollection", "isValidShowNotifications", []); + }); + }, storeBatteryLevel: function () { return new Promise(function(resolve, reject) { exec(resolve, reject, "DataCollection", "storeBatteryLevel", []); From 8b33aaa0d67b91ddb296bb4c03f4371e6de2455d Mon Sep 17 00:00:00 2001 From: Shankari Date: Fri, 29 Oct 2021 10:25:57 -0700 Subject: [PATCH 09/35] Add missing function Not sure why this was not captured by the diffing code --- .../verification/SensorControlForegroundDelegate.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/android/verification/SensorControlForegroundDelegate.java b/src/android/verification/SensorControlForegroundDelegate.java index 3cc3569..609203d 100644 --- a/src/android/verification/SensorControlForegroundDelegate.java +++ b/src/android/verification/SensorControlForegroundDelegate.java @@ -143,6 +143,14 @@ private void openLocationSettingsPage(CallbackContext callbackContext) { SensorControlConstants.ENABLE_LOCATION_SETTINGS_MANUAL); } + private void openAppSettingsPage(CallbackContext callbackContext, int requestCode) { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.fromParts("package", cordova.getActivity().getPackageName(), null)); + this.cordovaCallback = cordovaCallback; + cordova.setActivityResultCallback(plugin); + cordova.getActivity().startActivityForResult(intent, requestCode); + } + public void checkLocationPermissions(CallbackContext cordovaCallback) { boolean validPerms = SensorControlChecks.checkLocationPermissions(cordova.getActivity()); if(validPerms) { From 55f366af26d274f2eb5ee44ec6f361878a1d80d3 Mon Sep 17 00:00:00 2001 From: Shankari Date: Fri, 29 Oct 2021 11:45:52 -0700 Subject: [PATCH 10/35] Add support for checking paused notifications as well However, we will not include this in the setting screen UI initially https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-954958935 --- res/android/values/dc_strings.xml | 1 + src/android/DataCollectionPlugin.java | 6 +++++- src/android/verification/SensorControlChecks.java | 11 +++++++++++ .../verification/SensorControlForegroundDelegate.java | 10 ++++++++++ www/datacollection.js | 5 +++++ 5 files changed, 32 insertions(+), 1 deletion(-) diff --git a/res/android/values/dc_strings.xml b/res/android/values/dc_strings.xml index 1198d73..2592e5b 100644 --- a/res/android/values/dc_strings.xml +++ b/res/android/values/dc_strings.xml @@ -23,6 +23,7 @@ Activity permission off, please fix Activity permission off, click to enable Notifications blocked, please enable + Notifications paused. This can only be fixed by the app developer. Please report to your admin. Error in location settings, click to resolve Unknown error while reading location, please check your settings In state %1$s diff --git a/src/android/DataCollectionPlugin.java b/src/android/DataCollectionPlugin.java index bf9c841..09aff9a 100644 --- a/src/android/DataCollectionPlugin.java +++ b/src/android/DataCollectionPlugin.java @@ -98,13 +98,17 @@ public boolean execute(String action, JSONArray data, final CallbackContext call mControlDelegate.checkMotionActivityPermissions(callbackContext); return true; } else if (action.equals("fixShowNotifications")) { - Log.d(cordova.getActivity(), TAG, "checking notification enable"); + Log.d(cordova.getActivity(), TAG, "fixing notification enable"); mControlDelegate.checkAndPromptShowNotificationsEnabled(callbackContext); return true; } else if (action.equals("isValidShowNotifications")) { Log.d(cordova.getActivity(), TAG, "checking notification enable"); mControlDelegate.checkShowNotificationsEnabled(callbackContext); return true; + } else if (action.equals("isNotificationsUnpaused")) { + Log.d(cordova.getActivity(), TAG, "checking notification unpause"); + mControlDelegate.checkPausedNotifications(callbackContext); + return true; } else if (action.equals("storeBatteryLevel")) { Context ctxt = cordova.getActivity(); TripDiaryStateMachineReceiver.saveBatteryAndSimulateUser(ctxt); diff --git a/src/android/verification/SensorControlChecks.java b/src/android/verification/SensorControlChecks.java index 26aa47e..93b64b5 100644 --- a/src/android/verification/SensorControlChecks.java +++ b/src/android/verification/SensorControlChecks.java @@ -88,4 +88,15 @@ public static boolean checkNotificationsEnabled(final Context ctxt) { } return appDisabled || channelsDisabled; } + + public static boolean checkNotificationsUnpaused(final Context ctxt) { + NotificationManager nMgr = (NotificationManager) ctxt.getSystemService(Context.NOTIFICATION_SERVICE); + boolean appUnpaused = true; + // app notification pausing apparently did not exist before API 29, so we return unpaused = true + // by default + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + appUnpaused = !(nMgr.areNotificationsPaused()); + } + return appUnpaused; + } } diff --git a/src/android/verification/SensorControlForegroundDelegate.java b/src/android/verification/SensorControlForegroundDelegate.java index 609203d..3024831 100644 --- a/src/android/verification/SensorControlForegroundDelegate.java +++ b/src/android/verification/SensorControlForegroundDelegate.java @@ -245,6 +245,16 @@ public void checkAndPromptShowNotificationsEnabled(CallbackContext cordovaCallba } } + public void checkPausedNotifications(CallbackContext cordovaCallback) { + boolean unpaused = SensorControlChecks.checkNotificationsUnpaused(cordova.getActivity()); + if(unpaused) { + cordovaCallback.success(); + } else { + Log.i(cordova.getActivity(), TAG, "Notifications paused, asking user to report"); + cordovaCallback.error(cordova.getActivity().getString(R.string.notifications_paused)); + } + } + private void displayResolution(PendingIntent resolution) { if (resolution != null) { try { diff --git a/www/datacollection.js b/www/datacollection.js index dad47a7..48ce396 100644 --- a/www/datacollection.js +++ b/www/datacollection.js @@ -61,6 +61,11 @@ var DataCollection = { exec(resolve, reject, "DataCollection", "isValidShowNotifications", []); }); }, + isNotificationsUnpaused: function () { + return new Promise(function(resolve, reject) { + exec(resolve, reject, "DataCollection", "isNotificationsUnpaused", []); + }); + }, storeBatteryLevel: function () { return new Promise(function(resolve, reject) { exec(resolve, reject, "DataCollection", "storeBatteryLevel", []); From 300336c3258b3a6ed6aecd4e7ea1eb49a5bfc7e4 Mon Sep 17 00:00:00 2001 From: Shankari Date: Sat, 30 Oct 2021 06:00:52 -0700 Subject: [PATCH 11/35] Bump up the compileSdk version to 31 and add the android corex plugin This will enable us to call methods in the androidx compat libraries that give users the option to turn off "auto-reset" of permissions for apps that are not used very often. Auto-reset permission background: https://developer.android.com/topic/performance/app-hibernation The most recent version of the androidx libraries requires API 31 https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-955122628 This is a workaround to use API 31 in cordova now https://github.com/apache/cordova-android/issues/1373 --- plugin.xml | 4 ++++ src/android/enable_api_31.gradle | 7 +++++++ 2 files changed, 11 insertions(+) create mode 100644 src/android/enable_api_31.gradle diff --git a/plugin.xml b/plugin.xml index 221af0f..e213652 100644 --- a/plugin.xml +++ b/plugin.xml @@ -120,6 +120,10 @@ + + + + diff --git a/src/android/enable_api_31.gradle b/src/android/enable_api_31.gradle new file mode 100644 index 0000000..0f7b8e4 --- /dev/null +++ b/src/android/enable_api_31.gradle @@ -0,0 +1,7 @@ +ext.postBuildExtras = { + android { + defaultConfig { + compileSdkVersion 31 + } + } +} From eb24a4746d9bcdb2cdc11f094cda0f5a12702811 Mon Sep 17 00:00:00 2001 From: Shankari Date: Mon, 1 Nov 2021 18:10:39 -0700 Subject: [PATCH 12/35] Add support for disabling reset of unused app permissions Fairly simple and straightforward and similar to prior changes -e.g. https://github.com/e-mission/e-mission-data-collection/pull/195/commits/cf2712af5496463ecc724cf4e541e5fcf596a2f5 https://github.com/e-mission/e-mission-data-collection/pull/195/commits/216dd7e378da5b2a915e2ed5c172642dce091cb3 Main difference is that this requires androidx core libraries v1.7.0, which requires us to use java 11 for as the JDK. --- res/android/values/dc_strings.xml | 1 + src/android/DataCollectionPlugin.java | 8 ++++ .../verification/SensorControlChecks.java | 25 +++++++++++- .../verification/SensorControlConstants.java | 1 + .../SensorControlForegroundDelegate.java | 38 ++++++++++++++++++- www/datacollection.js | 15 ++++++++ 6 files changed, 86 insertions(+), 2 deletions(-) diff --git a/res/android/values/dc_strings.xml b/res/android/values/dc_strings.xml index 2592e5b..552005c 100644 --- a/res/android/values/dc_strings.xml +++ b/res/android/values/dc_strings.xml @@ -24,6 +24,7 @@ Activity permission off, click to enable Notifications blocked, please enable Notifications paused. This can only be fixed by the app developer. Please report to your admin. + Please allow this app to read sensor data even if you launch it infrequently. Error in location settings, click to resolve Unknown error while reading location, please check your settings In state %1$s diff --git a/src/android/DataCollectionPlugin.java b/src/android/DataCollectionPlugin.java index 09aff9a..771e4f9 100644 --- a/src/android/DataCollectionPlugin.java +++ b/src/android/DataCollectionPlugin.java @@ -109,6 +109,14 @@ public boolean execute(String action, JSONArray data, final CallbackContext call Log.d(cordova.getActivity(), TAG, "checking notification unpause"); mControlDelegate.checkPausedNotifications(callbackContext); return true; + } else if (action.equals("fixUnusedAppRestrictions")) { + Log.d(cordova.getActivity(), TAG, "fixing unused app restrictions"); + mControlDelegate.checkAndPromptUnusedAppsUnrestricted(callbackContext); + return true; + } else if (action.equals("isUnusedAppUnrestricted")) { + Log.d(cordova.getActivity(), TAG, "checking unused app restrictions"); + mControlDelegate.checkUnusedAppsUnrestricted(callbackContext); + return true; } else if (action.equals("storeBatteryLevel")) { Context ctxt = cordova.getActivity(); TripDiaryStateMachineReceiver.saveBatteryAndSimulateUser(ctxt); diff --git a/src/android/verification/SensorControlChecks.java b/src/android/verification/SensorControlChecks.java index 93b64b5..094c03c 100644 --- a/src/android/verification/SensorControlChecks.java +++ b/src/android/verification/SensorControlChecks.java @@ -13,6 +13,8 @@ import androidx.core.app.NotificationManagerCompat; import androidx.core.content.ContextCompat; import androidx.core.content.PermissionChecker; +import androidx.core.content.PackageManagerCompat; +import androidx.core.content.UnusedAppRestrictionsConstants; import com.google.android.gms.common.api.ApiException; import com.google.android.gms.common.api.ResolvableApiException; @@ -24,9 +26,11 @@ import com.google.android.gms.location.LocationSettingsStatusCodes; import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.Task; +import com.google.common.util.concurrent.ListenableFuture; import java.util.List; +import java.util.concurrent.ExecutionException; import edu.berkeley.eecs.emission.cordova.tracker.Constants; import edu.berkeley.eecs.emission.cordova.tracker.ExplicitIntent; @@ -68,7 +72,7 @@ public static boolean checkLocationPermissions(final Context ctxt) { public static boolean checkMotionActivityPermissions(final Context ctxt) { // apps before version 29 did not need to prompt for dynamic permissions related // to motion activity - boolean version29Check = Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q; + boolean version29Check = Build.VERSION.SDK_INT < Build.VERSION_CODES.Q; boolean permCheck = ContextCompat.checkSelfPermission(ctxt, SensorControlConstants.MOTION_ACTIVITY_PERMISSION) == PermissionChecker.PERMISSION_GRANTED; return version29Check || permCheck; } @@ -99,4 +103,23 @@ public static boolean checkNotificationsUnpaused(final Context ctxt) { } return appUnpaused; } + + public static boolean checkUnusedAppsUnrestricted(final Context ctxt) { + ListenableFuture future = PackageManagerCompat.getUnusedAppRestrictionsStatus(ctxt); + try { + Integer appRestrictionStatus = future.get(); + switch(appRestrictionStatus) { + case UnusedAppRestrictionsConstants.ERROR: return false; + case UnusedAppRestrictionsConstants.FEATURE_NOT_AVAILABLE: return true; + case UnusedAppRestrictionsConstants.DISABLED: return true; + case UnusedAppRestrictionsConstants.API_30_BACKPORT: + case UnusedAppRestrictionsConstants.API_30: + case UnusedAppRestrictionsConstants.API_31: + return false; + } + } catch (ExecutionException | InterruptedException e) { + return false; + } + return false; + } } diff --git a/src/android/verification/SensorControlConstants.java b/src/android/verification/SensorControlConstants.java index 2885df6..0c6c6bb 100644 --- a/src/android/verification/SensorControlConstants.java +++ b/src/android/verification/SensorControlConstants.java @@ -15,6 +15,7 @@ public class SensorControlConstants { public static final int ENABLE_BOTH_PERMISSION = 362253740; public static final int ENABLE_MOTION_ACTIVITY_PERMISSION = 362253741; public static final int ENABLE_NOTIFICATIONS = 362253742; + public static final int REMOVE_UNUSED_APP_RESTRICTIONS = 362253743; public static final String ENABLE_LOCATION_PERMISSION_ACTION = "ENABLE_LOCATION_PERMISSION"; public static final String ENABLE_BACKGROUND_LOC_PERMISSION_ACTION = "ENABLE_BACKGROUND_LOC_PERMISSION"; diff --git a/src/android/verification/SensorControlForegroundDelegate.java b/src/android/verification/SensorControlForegroundDelegate.java index 3024831..87ca3b5 100644 --- a/src/android/verification/SensorControlForegroundDelegate.java +++ b/src/android/verification/SensorControlForegroundDelegate.java @@ -32,6 +32,8 @@ import android.provider.Settings; +import androidx.core.content.IntentCompat; + import com.google.android.gms.common.api.ApiException; import com.google.android.gms.common.api.ResolvableApiException; import com.google.android.gms.location.LocationSettingsResponse; @@ -146,7 +148,7 @@ private void openLocationSettingsPage(CallbackContext callbackContext) { private void openAppSettingsPage(CallbackContext callbackContext, int requestCode) { Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); intent.setData(Uri.fromParts("package", cordova.getActivity().getPackageName(), null)); - this.cordovaCallback = cordovaCallback; + this.cordovaCallback = callbackContext; cordova.setActivityResultCallback(plugin); cordova.getActivity().startActivityForResult(intent, requestCode); } @@ -255,6 +257,30 @@ public void checkPausedNotifications(CallbackContext cordovaCallback) { } } + public void checkUnusedAppsUnrestricted(CallbackContext cordovaCallback) { + boolean unrestricted = SensorControlChecks.checkUnusedAppsUnrestricted(cordova.getActivity()); + if (unrestricted) { + cordovaCallback.success(); + } else { + Log.i(cordova.getActivity(), TAG, "Unused apps restricted, asking user to unrestrict"); + cordovaCallback.error(cordova.getActivity().getString(R.string.unused_apps_restricted)); + } + } + + + public void checkAndPromptUnusedAppsUnrestricted(CallbackContext cordovaCallback) { + boolean unrestricted = SensorControlChecks.checkUnusedAppsUnrestricted(cordova.getActivity()); + if (unrestricted) { + cordovaCallback.success(); + } else { + Log.i(cordova.getActivity(), TAG, "Unused apps restricted, asking user to unrestrict"); + this.cordovaCallback = cordovaCallback; + cordova.setActivityResultCallback(plugin); + Intent intent = IntentCompat.createManageUnusedAppRestrictionsIntent(cordova.getActivity(), cordova.getActivity().getPackageName()); + cordova.getActivity().startActivityForResult(intent, SensorControlConstants.REMOVE_UNUSED_APP_RESTRICTIONS); + } + } + private void displayResolution(PendingIntent resolution) { if (resolution != null) { try { @@ -399,6 +425,16 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { cordovaCallback.error(cordova.getActivity().getString(R.string.notifications_blocked)); } break; + case SensorControlConstants.REMOVE_UNUSED_APP_RESTRICTIONS: + Log.d(mAct, TAG, requestCode + " is our code, handling callback"); + cordova.setActivityResultCallback(null); + Log.d(mAct, TAG, "Got unused app restrictions callback from launching app settings"); + if (SensorControlChecks.checkUnusedAppsUnrestricted(cordova.getActivity())) { + cordovaCallback.success(); + } else { + cordovaCallback.error(cordova.getActivity().getString(R.string.notifications_blocked)); + } + break; default: Log.d(cordova.getActivity(), TAG, "Got unsupported request code " + requestCode + " , ignoring..."); } diff --git a/www/datacollection.js b/www/datacollection.js index 48ce396..912ae6b 100644 --- a/www/datacollection.js +++ b/www/datacollection.js @@ -66,6 +66,21 @@ var DataCollection = { exec(resolve, reject, "DataCollection", "isNotificationsUnpaused", []); }); }, + fixUnusedAppRestrictions: function () { + return new Promise(function(resolve, reject) { + exec(resolve, reject, "DataCollection", "fixUnusedAppRestrictions", []); + }); + }, + isUnusedAppUnrestricted: function () { + return new Promise(function(resolve, reject) { + exec(resolve, reject, "DataCollection", "isUnusedAppUnrestricted", []); + }); + }, + fixOEMBackgroundRestrictions: function () { + return new Promise(function(resolve, reject) { + exec(resolve, reject, "DataCollection", "fixOEMBackgroundRestrictions", []); + }); + }, storeBatteryLevel: function () { return new Promise(function(resolve, reject) { exec(resolve, reject, "DataCollection", "storeBatteryLevel", []); From 61a7b8b99ffecd22acc41e9644429b75f5f8312f Mon Sep 17 00:00:00 2001 From: Shankari Date: Fri, 12 Nov 2021 17:54:26 -0800 Subject: [PATCH 13/35] Actually handle the "deny" code correctly - Determine whether the user has used "deny" or "deny and don't ask again" - or the system has done it automatically for them (as in Android 11) - Change the error return message appropriately - Change the instructions in the UI appropriately Algorithm to determine current status is: https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-958438153 --- res/android/values/dc_strings.xml | 2 + .../SensorControlForegroundDelegate.java | 193 ++++++++++++++---- 2 files changed, 160 insertions(+), 35 deletions(-) diff --git a/res/android/values/dc_strings.xml b/res/android/values/dc_strings.xml index 552005c..0f887d5 100644 --- a/res/android/values/dc_strings.xml +++ b/res/android/values/dc_strings.xml @@ -18,9 +18,11 @@ Success moving to %1$s Failed moving to %1$s Insufficient location permissions, please fix + Insufficient location permissions, please fix in app settings Location permission off, click to enable Background location permission off, click to enable Activity permission off, please fix + Activity permission off, please fix in app settings Activity permission off, click to enable Notifications blocked, please enable Notifications paused. This can only be fixed by the app developer. Please report to your admin. diff --git a/src/android/verification/SensorControlForegroundDelegate.java b/src/android/verification/SensorControlForegroundDelegate.java index 87ca3b5..73c9b85 100644 --- a/src/android/verification/SensorControlForegroundDelegate.java +++ b/src/android/verification/SensorControlForegroundDelegate.java @@ -32,6 +32,7 @@ import android.provider.Settings; +import androidx.core.app.ActivityCompat; import androidx.core.content.IntentCompat; import com.google.android.gms.common.api.ApiException; @@ -41,7 +42,11 @@ import com.google.android.gms.location.LocationSettingsStatusCodes; import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; +import java.util.Set; import edu.berkeley.eecs.emission.cordova.unifiedlogger.NotificationHelper; @@ -51,6 +56,81 @@ public class SensorControlForegroundDelegate { private CordovaPlugin plugin = null; private CordovaInterface cordova = null; private CallbackContext cordovaCallback = null; + private PermissionPopupChecker permissionChecker = null; + private Map permissionCheckerMap = new HashMap<>(); + + class PermissionPopupChecker { + int permissionStatusConstant = -1; + boolean shouldShowRequestRationaleBefore = false; + String deniedString; + String retryString; + boolean openAppSettings; + + String[] currPermissions; + + public PermissionPopupChecker(int permissionStatusConstant, + String[] permissions, + String retryString, + String deniedString) { + this.permissionStatusConstant = permissionStatusConstant; + currPermissions = permissions; + this.deniedString = deniedString; + this.retryString = retryString; + } + + private boolean shouldShowRequestForCurrPermissions() { + boolean ssrrb = false; + for (String cp: currPermissions){ + boolean css = ActivityCompat.shouldShowRequestPermissionRationale(cordova.getActivity(), cp); + Log.d(cordova.getActivity(), TAG, "For permission "+cp+" shouldShowRequest = " + css); + ssrrb |= css; + } + return ssrrb; + } + + void requestPermission() { + shouldShowRequestRationaleBefore = shouldShowRequestForCurrPermissions(); + Log.d(cordova.getActivity(), TAG, + String.format("After iterating over all entries in %s shouldShowRequest = %s", currPermissions, shouldShowRequestRationaleBefore)); + if (openAppSettings) { + SensorControlForegroundDelegate.this.openAppSettingsPage(cordovaCallback, permissionStatusConstant); + } else { + if (currPermissions.length > 1) { + cordova.requestPermissions(plugin, permissionStatusConstant, currPermissions); + } else { + Log.e(cordova.getActivity(), TAG, "currPermissions.length = " + currPermissions.length); + cordova.requestPermission(plugin, permissionStatusConstant, currPermissions[0]); + } + } + } + + void generateErrorCallback() { + boolean shouldShowRequestRationaleAfter = shouldShowRequestForCurrPermissions(); + Log.d(cordova.getActivity(), TAG, "In permission prompt, error callback,"+ + " before = "+shouldShowRequestRationaleBefore+" after = "+shouldShowRequestRationaleAfter); + // see the issue for more details + // https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-958438153 + + if (!shouldShowRequestRationaleBefore && !shouldShowRequestRationaleAfter) { + // before = FALSE, after = FALSE => user had denied it earlier + openAppSettings = true; + cordovaCallback.error(deniedString); + } + if (!shouldShowRequestRationaleBefore && shouldShowRequestRationaleAfter) { + // before = FALSE, after = TRUE => first time ask + cordovaCallback.error(retryString); + } + if (shouldShowRequestRationaleBefore && !shouldShowRequestRationaleAfter) { + // before = TRUE, after = FALSE => popup was shown, user hit don't ask + openAppSettings = true; + cordovaCallback.error(deniedString); + } + if (shouldShowRequestRationaleBefore && shouldShowRequestRationaleAfter) { + // before = TRUE, after = TRUE => popup was shown, user hit deny ONLY + cordovaCallback.error(retryString); + } + } + } public SensorControlForegroundDelegate(CordovaPlugin inPlugin, CordovaInterface inCordova) { @@ -58,6 +138,25 @@ public SensorControlForegroundDelegate(CordovaPlugin inPlugin, cordova = inCordova; } + private PermissionPopupChecker getPermissionChecker(int permissionStatusConstant, + String permission, + String retryString, + String deniedString) { + return getPermissionChecker(permissionStatusConstant, new String[]{permission}, retryString, deniedString); + } + private PermissionPopupChecker getPermissionChecker(int permissionStatusConstant, + String[] permissions, + String retryString, + String deniedString) { + PermissionPopupChecker pc = permissionCheckerMap.get(Integer.valueOf(permissionStatusConstant)); + if (pc == null) { + pc = new PermissionPopupChecker(permissionStatusConstant, + permissions, retryString, deniedString); + permissionCheckerMap.put(Integer.valueOf(permissionStatusConstant), pc); + } + return pc; + } + // Invokes the callback with a boolean indicating whether // the location settings are correct or not public void checkLocationSettings(CallbackContext cordovaCallback) { @@ -184,24 +283,39 @@ public void checkAndPromptLocationPermissions(CallbackContext cordovaCallback) { return; } if(!cordova.hasPermission(SensorControlConstants.LOCATION_PERMISSION) && - (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) && + (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) && !cordova.hasPermission(SensorControlConstants.BACKGROUND_LOC_PERMISSION)) { Log.i(cordova.getActivity(), TAG, "Both permissions missing, requesting both"); this.cordovaCallback = cordovaCallback; - cordova.requestPermissions(plugin, SensorControlConstants.ENABLE_BOTH_PERMISSION, - new String[]{SensorControlConstants.LOCATION_PERMISSION, SensorControlConstants.BACKGROUND_LOC_PERMISSION}); + this.permissionChecker = getPermissionChecker( + SensorControlConstants.ENABLE_BOTH_PERMISSION, + new String[]{SensorControlConstants.BACKGROUND_LOC_PERMISSION, SensorControlConstants.LOCATION_PERMISSION}, + cordova.getActivity().getString(R.string.location_permission_off), + cordova.getActivity().getString(R.string.location_permission_off_app_open)); + this.permissionChecker.requestPermission(); return; } if(!cordova.hasPermission(SensorControlConstants.LOCATION_PERMISSION)) { + Log.i(cordova.getActivity(), TAG, "before call shouldShowRequestPermissionRationale = "+ ActivityCompat.shouldShowRequestPermissionRationale(cordova.getActivity(), SensorControlConstants.LOCATION_PERMISSION)); Log.i(cordova.getActivity(), TAG, "Only location permission missing, requesting it"); this.cordovaCallback = cordovaCallback; - cordova.requestPermission(plugin, SensorControlConstants.ENABLE_LOCATION_PERMISSION, SensorControlConstants.LOCATION_PERMISSION); + this.permissionChecker = getPermissionChecker( + SensorControlConstants.ENABLE_LOCATION_PERMISSION, + SensorControlConstants.LOCATION_PERMISSION, + cordova.getActivity().getString(R.string.location_permission_off), + cordova.getActivity().getString(R.string.location_permission_off_app_open)); + this.permissionChecker.requestPermission(); return; } if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !cordova.hasPermission(SensorControlConstants.BACKGROUND_LOC_PERMISSION)) { Log.i(cordova.getActivity(), TAG, "Only background permission missing, requesting it"); this.cordovaCallback = cordovaCallback; - cordova.requestPermission(plugin, SensorControlConstants.ENABLE_BACKGROUND_LOC_PERMISSION, SensorControlConstants.BACKGROUND_LOC_PERMISSION); + this.permissionChecker = getPermissionChecker( + SensorControlConstants.ENABLE_BACKGROUND_LOC_PERMISSION, + SensorControlConstants.BACKGROUND_LOC_PERMISSION, + cordova.getActivity().getString(R.string.location_permission_off), + cordova.getActivity().getString(R.string.location_permission_off_app_open)); + this.permissionChecker.requestPermission(); return; } } @@ -220,9 +334,15 @@ public void checkAndPromptMotionActivityPermissions(CallbackContext cordovaCallb if(validPerms) { cordovaCallback.success(); } else { + Log.i(cordova.getActivity(), TAG, "before call shouldShowRequestPermissionRationale = "+ ActivityCompat.shouldShowRequestPermissionRationale(cordova.getActivity(), SensorControlConstants.MOTION_ACTIVITY_PERMISSION)); Log.i(cordova.getActivity(), TAG, "Motion activity permission missing, requesting it"); this.cordovaCallback = cordovaCallback; - cordova.requestPermission(plugin, SensorControlConstants.ENABLE_MOTION_ACTIVITY_PERMISSION, SensorControlConstants.MOTION_ACTIVITY_PERMISSION); + this.permissionChecker = getPermissionChecker( + SensorControlConstants.ENABLE_MOTION_ACTIVITY_PERMISSION, + SensorControlConstants.MOTION_ACTIVITY_PERMISSION, + cordova.getActivity().getString(R.string.activity_permission_off), + cordova.getActivity().getString(R.string.activity_permission_off_app_open)); + this.permissionChecker.requestPermission(); } } @@ -334,36 +454,27 @@ public void onRequestPermissionResult(int requestCode, String[] permissions, switch(requestCode) { case SensorControlConstants.ENABLE_BOTH_PERMISSION: + Log.i(cordova.getActivity(), TAG, "in callback shouldShowRequestPermissionRationale = "+ ActivityCompat.shouldShowRequestPermissionRationale(cordova.getActivity(), SensorControlConstants.LOCATION_PERMISSION)); if ((grantResults[0] == PackageManager.PERMISSION_GRANTED) && (grantResults[1] == PackageManager.PERMISSION_GRANTED)) { cordovaCallback.success(); - } else if (grantResults[0] == PackageManager.PERMISSION_DENIED) { - cordovaCallback.error(cordova.getActivity().getString(R.string.location_permission_off)); - } else if (grantResults[1] == PackageManager.PERMISSION_DENIED) { - cordovaCallback.error(cordova.getActivity().getString(R.string.location_permission_off)); + } else if (grantResults[0] == PackageManager.PERMISSION_DENIED || grantResults[1] == PackageManager.PERMISSION_DENIED) { + this.permissionChecker.generateErrorCallback(); } + this.permissionChecker = null; break; case SensorControlConstants.ENABLE_LOCATION_PERMISSION: - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - cordovaCallback.success(); - } else if (grantResults[0] == PackageManager.PERMISSION_DENIED) { - cordovaCallback.error(cordova.getActivity().getString(R.string.location_permission_off)); - } - break; case SensorControlConstants.ENABLE_BACKGROUND_LOC_PERMISSION: - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - cordovaCallback.success(); - } else if (grantResults[0] == PackageManager.PERMISSION_DENIED) { - cordovaCallback.error(cordova.getActivity().getString(R.string.location_permission_off)); - } - break; case SensorControlConstants.ENABLE_MOTION_ACTIVITY_PERMISSION: + // Code for all these is the same. We ask for a single permission + // and if it is denied, we generate the error callback + // the exact message is stored in the permission checker object if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { cordovaCallback.success(); - // motion activity does not affect the FSM } else if (grantResults[0] == PackageManager.PERMISSION_DENIED) { - cordovaCallback.error(cordova.getActivity().getString(R.string.activity_permission_off)); + this.permissionChecker.generateErrorCallback(); } + this.permissionChecker = null; break; default: Log.e(cordova.getActivity(), TAG, "Unknown permission code "+requestCode+" ignoring"); @@ -373,10 +484,10 @@ public void onRequestPermissionResult(int requestCode, String[] permissions, public void onActivityResult(int requestCode, int resultCode, Intent data) { Activity mAct = cordova.getActivity(); + cordova.setActivityResultCallback(null); switch (requestCode) { case SensorControlConstants.ENABLE_LOCATION_SETTINGS: 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 != null && states.isLocationUsable())); switch (resultCode) { @@ -390,34 +501,47 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { } break; case Activity.RESULT_CANCELED: - // The user was asked to change settings, but chose not to - Log.e(cordova.getActivity(), TAG, "User chose not to change settings, dunno what to do"); - cordovaCallback.error(mAct.getString(R.string.user_rejected_setting)); + Log.i(cordova.getActivity(), TAG, "request " + requestCode + " cancelled, failing"); + cordova.setActivityResultCallback(null); + cordovaCallback.error(cordova.getActivity().getString(R.string.user_rejected_setting)); break; default: cordovaCallback.error(mAct.getString(R.string.unable_resolve_issue)); Log.e(cordova.getActivity(), TAG, "Unknown result code while enabling location " + resultCode); break; } + break; case SensorControlConstants.ENABLE_LOCATION_SETTINGS_MANUAL: Log.d(mAct, TAG, requestCode + " is our code, handling callback"); - cordova.setActivityResultCallback(null); // this will call the callback with success or error checkLocationSettings(cordovaCallback); break; case SensorControlConstants.ENABLE_BOTH_PERMISSION: + case SensorControlConstants.ENABLE_LOCATION_PERMISSION: + case SensorControlConstants.ENABLE_BACKGROUND_LOC_PERMISSION: Log.d(mAct, TAG, requestCode + " is our code, handling callback"); - cordova.setActivityResultCallback(null); - Log.d(mAct, TAG, "Got permission callback from launching app settings"); + Log.d(mAct, TAG, "Got permission callback from launching app settings when prompt failed"); if (SensorControlChecks.checkLocationPermissions(cordova.getActivity())) { cordovaCallback.success(); } else { - cordovaCallback.error(cordova.getActivity().getString(R.string.location_permission_off)); + // this is the activity result callback, so only launched when the app settings are used + // so we don't need to use the permission checker, we know that the only option + // is to launch the settings + cordovaCallback.error(cordova.getActivity().getString(R.string.location_permission_off_app_open)); } break; + case SensorControlConstants.ENABLE_MOTION_ACTIVITY_PERMISSION: + Log.d(mAct, TAG, requestCode + " is our code, handling callback"); + Log.d(mAct, TAG, "Got permission callback from launching app settings"); + if (SensorControlChecks.checkMotionActivityPermissions(cordova.getActivity())) { + cordovaCallback.success(); + } else { + permissionChecker.generateErrorCallback(); + } + permissionChecker = null; + break; case SensorControlConstants.ENABLE_NOTIFICATIONS: Log.d(mAct, TAG, requestCode + " is our code, handling callback"); - cordova.setActivityResultCallback(null); Log.d(mAct, TAG, "Got notification callback from launching app settings"); if (SensorControlChecks.checkNotificationsEnabled(cordova.getActivity())) { cordovaCallback.success(); @@ -427,12 +551,11 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { break; case SensorControlConstants.REMOVE_UNUSED_APP_RESTRICTIONS: Log.d(mAct, TAG, requestCode + " is our code, handling callback"); - cordova.setActivityResultCallback(null); Log.d(mAct, TAG, "Got unused app restrictions callback from launching app settings"); if (SensorControlChecks.checkUnusedAppsUnrestricted(cordova.getActivity())) { cordovaCallback.success(); } else { - cordovaCallback.error(cordova.getActivity().getString(R.string.notifications_blocked)); + cordovaCallback.error(cordova.getActivity().getString(R.string.unused_apps_restricted)); } break; default: From d8080ea36b16c21ea1418c41e43baa821c41428b Mon Sep 17 00:00:00 2001 From: Shankari Date: Tue, 16 Nov 2021 21:53:14 -0800 Subject: [PATCH 14/35] Refactor the background notification code as well Instead of duplicating the error checks, potentially generating multiple notifications and then handling the notifications in the foreground delegate, we simply do all the checks and redirect to the status screen if anything is incorrect. https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-969662303 + add null title to all existing notifications (consistent with the method signature change) + add title and text to new "open app status screen" notification --- src/android/ConfigManager.java | 2 +- .../location/ForegroundServiceComm.java | 2 +- ...ripDiaryStateMachineForegroundService.java | 6 +- .../TripDiaryStateMachineReceiver.java | 6 +- .../TripDiaryStateMachineService.java | 28 ++-- .../TripDiaryStateMachineServiceOngoing.java | 8 +- .../location/actions/GeofenceActions.java | 2 +- .../SensorControlBackgroundChecker.java | 144 ++++-------------- .../verification/SensorControlConstants.java | 2 + .../SensorControlForegroundDelegate.java | 48 +++--- 10 files changed, 85 insertions(+), 163 deletions(-) diff --git a/src/android/ConfigManager.java b/src/android/ConfigManager.java index c3065de..7c36fc1 100644 --- a/src/android/ConfigManager.java +++ b/src/android/ConfigManager.java @@ -37,7 +37,7 @@ public static LocationTrackingConfig getConfig(Context context) { } catch(JsonParseException e) { Log.e(context, TAG, "Found error " + e + "parsing sync config json, resetting to defaults"); NotificationHelper.createNotification(context, Constants.TRACKING_ERROR_ID, - context.getString(R.string.error_reading_stored_config)); + null, context.getString(R.string.error_reading_stored_config)); cachedConfig = new LocationTrackingConfig(); updateConfig(context, cachedConfig); } diff --git a/src/android/location/ForegroundServiceComm.java b/src/android/location/ForegroundServiceComm.java index bce42e3..97aa1e1 100644 --- a/src/android/location/ForegroundServiceComm.java +++ b/src/android/location/ForegroundServiceComm.java @@ -45,7 +45,7 @@ public void setNewState(String newState) { mCtxt.bindService(fsi, connection, 0); } else { NotificationHelper.createNotification(mCtxt, Constants.TRACKING_ERROR_ID, - mCtxt.getString(R.string.unable_resolve_issue)); + null, mCtxt.getString(R.string.unable_resolve_issue)); Log.e(mCtxt, TAG, "Too many recursions, generating notification"); } } diff --git a/src/android/location/TripDiaryStateMachineForegroundService.java b/src/android/location/TripDiaryStateMachineForegroundService.java index cd62d1d..c9b54b4 100644 --- a/src/android/location/TripDiaryStateMachineForegroundService.java +++ b/src/android/location/TripDiaryStateMachineForegroundService.java @@ -73,7 +73,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { " flags = " + flags + " and startId = " + startId); String message = humanizeState(this, TripDiaryStateMachineService.getState(this)); if (intent == null) { - SensorControlBackgroundChecker.checkLocationSettingsAndPermissions(this); + SensorControlBackgroundChecker.checkAppState(this); message = humanizeState(this, TripDiaryStateMachineService.getState(this)); } handleStart(message, intent, flags, startId); @@ -133,8 +133,8 @@ public void setStateMessage(String newState) { private Notification getNotification(String msg) { NotificationManager nMgr = (NotificationManager)this.getSystemService(Context.NOTIFICATION_SERVICE); - Notification.Builder builder = NotificationHelper.getNotificationBuilderForApp(this, - nMgr, msg); + Notification.Builder builder = NotificationHelper.getNotificationBuilderForApp(this, null, + msg); 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 7c5076d..8ad0cb1 100644 --- a/src/android/location/TripDiaryStateMachineReceiver.java +++ b/src/android/location/TripDiaryStateMachineReceiver.java @@ -97,7 +97,7 @@ public void onReceive(Context context, Intent intent) { if (introDoneResult != null) { Log.i(context, TAG, reqConsent + " is not the current consented version, skipping init..."); NotificationHelper.createNotification(context, STARTUP_IN_NUMBERS, - context.getString(R.string.new_data_collection_terms)); + null, context.getString(R.string.new_data_collection_terms)); return; } else { Log.i(context, TAG, "onboarding is not complete, skipping prompt"); @@ -129,7 +129,7 @@ public static void performPeriodicActivity(Context ctxt) { } public static void checkLocationStillAvailable(Context ctxt) { - SensorControlBackgroundChecker.checkLocationSettingsAndPermissions(ctxt); + SensorControlBackgroundChecker.checkAppState(ctxt); } public static void validateAndCleanupState(Context ctxt) { @@ -173,7 +173,7 @@ public static void saveBatteryAndSimulateUser(Context ctxt) { Battery currInfo = BatteryUtils.getBatteryInfo(ctxt); UserCacheFactory.getUserCache(ctxt).putSensorData(R.string.key_usercache_battery, currInfo); if (ConfigManager.getConfig(ctxt).isSimulateUserInteraction()) { - NotificationHelper.createNotification(ctxt, 1234, ctxt.getString(R.string.battery_level, + NotificationHelper.createNotification(ctxt, 1234, null, ctxt.getString(R.string.battery_level, currInfo.getBatteryLevelPct())); } } diff --git a/src/android/location/TripDiaryStateMachineService.java b/src/android/location/TripDiaryStateMachineService.java index b65ee62..ad1a600 100644 --- a/src/android/location/TripDiaryStateMachineService.java +++ b/src/android/location/TripDiaryStateMachineService.java @@ -115,7 +115,7 @@ public void setNewState(String newState, boolean doChecks) { // This makes the rest of the code much simpler, allows us to catch issues as quickly as possible, // and if (doChecks) { - SensorControlBackgroundChecker.checkLocationSettingsAndPermissions(TripDiaryStateMachineService.this); + SensorControlBackgroundChecker.checkAppState(TripDiaryStateMachineService.this); } stopSelf(); } @@ -239,7 +239,7 @@ public void handleTripStart(Context ctxt, final String actionString) { if (TripDiaryStateMachineService.isAllSuccessful(resultList)) { if (ConfigManager.getConfig(fCtxt).isSimulateUserInteraction()) { NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, - fCtxt.getString(R.string.success_moving_new_state, newState)); + null, fCtxt.getString(R.string.success_moving_new_state, newState)); } setNewState(newState, true); } else { @@ -255,7 +255,7 @@ public void handleTripStart(Context ctxt, final String actionString) { } if (ConfigManager.getConfig(fCtxt).isSimulateUserInteraction()) { NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, - fCtxt.getString(R.string.failed_moving_new_state,newState)); + null, fCtxt.getString(R.string.failed_moving_new_state,newState)); } // both branches have called setState or are waiting for sth else }); // listener end return; // handled the transition, returning @@ -318,7 +318,7 @@ public void run() { if (TripDiaryStateMachineService.isAllSuccessful(resultList)) { if (ConfigManager.getConfig(ctxt).isSimulateUserInteraction()) { NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, - fCtxt.getString(R.string.success_moving_new_state, newState)); + null, fCtxt.getString(R.string.success_moving_new_state, newState)); } setNewState(newState, true); } else { @@ -337,12 +337,12 @@ public void run() { // "Error " + batchResult.getStatus().getStatusCode()+" while creating geofence"); // let's mark this operation as done since the other one is static // markOngoingOperationFinished(); - SensorControlBackgroundChecker.checkLocationSettingsAndPermissions(TripDiaryStateMachineService.this); + SensorControlBackgroundChecker.checkAppState(TripDiaryStateMachineService.this); // will wait for async call to complete } if (ConfigManager.getConfig(ctxt).isSimulateUserInteraction()) { NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, - fCtxt.getString(R.string.failed_moving_new_state, newState)); + null, fCtxt.getString(R.string.failed_moving_new_state, newState)); } }); } @@ -425,7 +425,7 @@ public void run() { setNewState(newState, true); if (ConfigManager.getConfig(ctxt).isSimulateUserInteraction()) { NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, - fCtxt.getString(R.string.success_moving_new_state, newState)); + null, fCtxt.getString(R.string.success_moving_new_state, newState)); } } else { Log.e(fCtxt, TAG, "error while creating geofence, staying in the current state"); @@ -439,7 +439,7 @@ public void run() { } if (ConfigManager.getConfig(fCtxt).isSimulateUserInteraction()) { NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, - fCtxt.getString(R.string.failed_moving_new_state, newState)); + null, fCtxt.getString(R.string.failed_moving_new_state, newState)); } }); } else { @@ -447,7 +447,7 @@ public void run() { // own state change // let's mark this operation as done since the other one is static // markOngoingOperationFinished(); - SensorControlBackgroundChecker.checkLocationSettingsAndPermissions(fCtxt); + SensorControlBackgroundChecker.checkAppState(fCtxt); } } }).start(); @@ -464,16 +464,16 @@ private void deleteGeofence(Context ctxt, final String targetState) { setNewState(newState, true); if (ConfigManager.getConfig(fCtxt).isSimulateUserInteraction()) { NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, - fCtxt.getString(R.string.success_moving_new_state, newState)); + null, fCtxt.getString(R.string.success_moving_new_state, newState)); } } else { setNewState(mCurrState, true); // markOngoingOperationFinished(); - SensorControlBackgroundChecker.checkLocationSettingsAndPermissions(TripDiaryStateMachineService.this); + SensorControlBackgroundChecker.checkAppState(TripDiaryStateMachineService.this); } if (ConfigManager.getConfig(fCtxt).isSimulateUserInteraction()) { NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, - fCtxt.getString(R.string.failed_moving_new_state, newState)); + null, fCtxt.getString(R.string.failed_moving_new_state, newState)); } }); } @@ -492,13 +492,13 @@ private void stopAll(Context ctxt, final String targetState) { if (TripDiaryStateMachineService.isAllSuccessful(resultList)) { if (ConfigManager.getConfig(fCtxt).isSimulateUserInteraction()) { NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, - fCtxt.getString(R.string.success_moving_new_state, newState)); + null, fCtxt.getString(R.string.success_moving_new_state, newState)); } setNewState(newState, false); } else { if (ConfigManager.getConfig(fCtxt).isSimulateUserInteraction()) { NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, - fCtxt.getString(R.string.failed_moving_new_state, newState)); + null, fCtxt.getString(R.string.failed_moving_new_state, newState)); } if (!resultList.get(1).isSuccessful()) { diff --git a/src/android/location/TripDiaryStateMachineServiceOngoing.java b/src/android/location/TripDiaryStateMachineServiceOngoing.java index af2019a..8457a7d 100644 --- a/src/android/location/TripDiaryStateMachineServiceOngoing.java +++ b/src/android/location/TripDiaryStateMachineServiceOngoing.java @@ -244,12 +244,12 @@ private void startEverything(final Context ctxt, String actionString) { setNewState(newState); if (ConfigManager.getConfig(ctxt).isSimulateUserInteraction()) { NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, - fCtxt.getString(R.string.success_moving_new_state, newState)); + null, fCtxt.getString(R.string.success_moving_new_state, newState)); } } else { if (ConfigManager.getConfig(ctxt).isSimulateUserInteraction()) { NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, - fCtxt.getString(R.string.failed_moving_new_state,newState)); + null, fCtxt.getString(R.string.failed_moving_new_state,newState)); } } }); @@ -266,12 +266,12 @@ private void stopEverything(final Context ctxt, final String targetState) { setNewState(newState); if (ConfigManager.getConfig(fCtxt).isSimulateUserInteraction()) { NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, - fCtxt.getString(R.string.success_moving_new_state, newState)); + null, fCtxt.getString(R.string.success_moving_new_state, newState)); } } else { if (ConfigManager.getConfig(fCtxt).isSimulateUserInteraction()) { NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, - fCtxt.getString(R.string.failed_moving_new_state,newState)); + null, fCtxt.getString(R.string.failed_moving_new_state,newState)); } } }); diff --git a/src/android/location/actions/GeofenceActions.java b/src/android/location/actions/GeofenceActions.java index a8d3878..a630cfa 100644 --- a/src/android/location/actions/GeofenceActions.java +++ b/src/android/location/actions/GeofenceActions.java @@ -202,7 +202,7 @@ public void notifyFailure() { Log.w(mCtxt, TAG, "Unable to detect current location even after forcing, will retry at next sync"); NotificationHelper.createNotification(mCtxt, GEOFENCE_IN_NUMBERS, - mCtxt.getString(R.string.unable_detect_current_location)); + null, mCtxt.getString(R.string.unable_detect_current_location)); } /* diff --git a/src/android/verification/SensorControlBackgroundChecker.java b/src/android/verification/SensorControlBackgroundChecker.java index 585511b..7ce3a76 100644 --- a/src/android/verification/SensorControlBackgroundChecker.java +++ b/src/android/verification/SensorControlBackgroundChecker.java @@ -48,98 +48,42 @@ public static void restartFSMIfStartState(Context ctxt) { } } - public static void checkLocationSettingsAndPermissions(final Context ctxt) { - LocationRequest request = new LocationTrackingActions(ctxt).getLocationRequest(); - 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, request)) { - Log.d(ctxt, TAG, "checkLocationPermissions returned true, checking background permission"); - if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) || - checkBackgroundLocPermissions(ctxt, request)) { - Log.d(ctxt, TAG, "checkBackgroundLocPermissions returned true, checking location settings"); + public static void checkAppState(final Context ctxt) { + NotificationHelper.cancelNotification(ctxt, + SensorControlConstants.OPEN_APP_STATUS_PAGE); + // check location settings. This is a separate function because it + // currently has a callback. The others can be inlined here for greater + // readability. checkLocationSettings(ctxt); - } else { - Log.d(ctxt, TAG, "check background permissions returned false, no point checking settings"); - ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_tracking_error)); + boolean[] allOtherChecks = new boolean[]{ + SensorControlChecks.checkLocationPermissions(ctxt), + SensorControlChecks.checkMotionActivityPermissions(ctxt), + SensorControlChecks.checkNotificationsEnabled(ctxt), + SensorControlChecks.checkUnusedAppsUnrestricted(ctxt) + }; + boolean allOtherChecksPass = true; + for (boolean check: allOtherChecks) { + allOtherChecksPass = allOtherChecksPass && check; } - // final state will be set in this async call + if (allOtherChecksPass) { + Log.d(ctxt, TAG, "All permissions (except location settings) valid, nothing to prompt"); } else { - Log.d(ctxt, TAG, "check location permissions returned false, no point checking settings"); + Log.i(ctxt, TAG, "Curr status check results = "+ + " loc permission, motion permission, notification, unused apps "+ Arrays.toString(allOtherChecks)); ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_tracking_error)); + generateOpenAppSettingsNotification(ctxt); } - - if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) && checkMotionActivityPermissions(ctxt)) { - Log.d(ctxt, TAG, "checkMotionActivityPermissions returned true, nothing more yet"); - } else { - Log.d(ctxt, TAG, "checkMotionActivityPermissions returned false, but that's not a tracking error"); - } - } - - private static boolean checkLocationPermissions(final Context ctxt, - 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, SensorControlConstants.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(SensorControlConstants.ENABLE_LOCATION_PERMISSION_ACTION); - PendingIntent pi = PendingIntent.getActivity(ctxt, SensorControlConstants.ENABLE_LOCATION_PERMISSION, - activityIntent, PendingIntent.FLAG_UPDATE_CURRENT); - NotificationHelper.createNotification(ctxt, SensorControlConstants.ENABLE_LOCATION_PERMISSION, - ctxt.getString(R.string.location_permission_off_enable), - pi); - } - - private static boolean checkBackgroundLocPermissions(final Context ctxt, - final LocationRequest request) { - int result = ContextCompat.checkSelfPermission(ctxt, SensorControlConstants.BACKGROUND_LOC_PERMISSION); - Log.d(ctxt, TAG, "checkSelfPermission returned "+result); - if (PackageManager.PERMISSION_GRANTED == result) { - return true; - } else { - generateBackgroundLocEnableNotification(ctxt); - return false; - } + restartFSMIfStartState(ctxt); } - public static void generateBackgroundLocEnableNotification(Context ctxt) { + public static void generateOpenAppSettingsNotification(Context ctxt) { Intent activityIntent = new Intent(ctxt, MainActivity.class); - activityIntent.setAction(SensorControlConstants.ENABLE_BACKGROUND_LOC_PERMISSION_ACTION); - PendingIntent pi = PendingIntent.getActivity(ctxt, SensorControlConstants.ENABLE_BACKGROUND_LOC_PERMISSION, + activityIntent.setAction(SensorControlConstants.OPEN_APP_STATUS_PAGE_ACTION); + PendingIntent pi = PendingIntent.getActivity(ctxt, SensorControlConstants.OPEN_APP_STATUS_PAGE, activityIntent, PendingIntent.FLAG_UPDATE_CURRENT); NotificationHelper.createNotification(ctxt, SensorControlConstants.ENABLE_BACKGROUND_LOC_PERMISSION, - ctxt.getString(R.string.background_loc_permission_off_enable), - pi); - } - - private static boolean checkMotionActivityPermissions(final Context ctxt) { - int result = ContextCompat.checkSelfPermission(ctxt, SensorControlConstants.MOTION_ACTIVITY_PERMISSION); - Log.d(ctxt, TAG, "checkSelfPermission returned "+result); - if (PackageManager.PERMISSION_GRANTED == result) { - return true; - } else { - generateMotionActivityEnableNotification(ctxt); - return false; - } - } - - public static void generateMotionActivityEnableNotification(Context ctxt) { - Intent activityIntent = new Intent(ctxt, MainActivity.class); - activityIntent.setAction(SensorControlConstants.ENABLE_MOTION_ACTIVITY_PERMISSION_ACTION); - PendingIntent pi = PendingIntent.getActivity(ctxt, SensorControlConstants.ENABLE_MOTION_ACTIVITY_PERMISSION, - activityIntent, PendingIntent.FLAG_UPDATE_CURRENT); - NotificationHelper.createNotification(ctxt, SensorControlConstants.ENABLE_MOTION_ACTIVITY_PERMISSION, - ctxt.getString(R.string.activity_permission_off_enable), + ctxt.getString(R.string.fix_app_status_title), + ctxt.getString(R.string.fix_app_status_text), pi); } @@ -151,42 +95,8 @@ private static void checkLocationSettings(final Context ctxt) { // requests here. Log.i(ctxt, TAG, "All settings are valid, checking current state"); Log.i(ctxt, TAG, "Current location settings are "+response); - NotificationHelper.cancelNotification(ctxt, Constants.TRACKING_ERROR_ID); - restartFSMIfStartState(ctxt); } catch (ApiException exception) { - switch (exception.getStatusCode()) { - case LocationSettingsStatusCodes.RESOLUTION_REQUIRED: - Log.i(ctxt, TAG, "location settings are not valid, but could be fixed by showing the user a dialog"); - // Location settings are not satisfied. But could be fixed by showing the - // user a dialog. - try { - // Cast to a resolvable exception. - ResolvableApiException resolvable = (ResolvableApiException) exception; - // Show the dialog by calling startResolutionForResult(), - // and check the result in onActivityResult(). - NotificationHelper.createResolveNotification(ctxt, Constants.TRACKING_ERROR_ID, - ctxt.getString(R.string.error_location_settings), - resolvable.getResolution()); - ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_tracking_error)); - } catch (ClassCastException e) { - // Ignore, should be an impossible 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. - Log.i(ctxt, TAG, "location settings are not valid, but cannot be fixed by showing a dialog"); - NotificationHelper.createNotification(ctxt, Constants.TRACKING_ERROR_ID, - ctxt.getString(R.string.error_location_settings)); - ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_tracking_error)); - break; - default: - Log.i(ctxt, TAG, "unkown error reading location"); - NotificationHelper.createNotification(ctxt, Constants.TRACKING_ERROR_ID, - ctxt.getString(R.string.unknown_error_location_settings)); - ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_tracking_error)); - - } + generateOpenAppSettingsNotification(ctxt); } }); } diff --git a/src/android/verification/SensorControlConstants.java b/src/android/verification/SensorControlConstants.java index 0c6c6bb..72b66f3 100644 --- a/src/android/verification/SensorControlConstants.java +++ b/src/android/verification/SensorControlConstants.java @@ -16,9 +16,11 @@ public class SensorControlConstants { public static final int ENABLE_MOTION_ACTIVITY_PERMISSION = 362253741; public static final int ENABLE_NOTIFICATIONS = 362253742; public static final int REMOVE_UNUSED_APP_RESTRICTIONS = 362253743; + public static final int OPEN_APP_STATUS_PAGE = 362253744; public static final String ENABLE_LOCATION_PERMISSION_ACTION = "ENABLE_LOCATION_PERMISSION"; public static final String ENABLE_BACKGROUND_LOC_PERMISSION_ACTION = "ENABLE_BACKGROUND_LOC_PERMISSION"; public static final String ENABLE_MOTION_ACTIVITY_PERMISSION_ACTION = "ENABLE_MOTION_ACTIVITY_PERMISSION"; + public static final String OPEN_APP_STATUS_PAGE_ACTION ="OPEN_APP_STATUS_PAGE"; } diff --git a/src/android/verification/SensorControlForegroundDelegate.java b/src/android/verification/SensorControlForegroundDelegate.java index 73c9b85..06da8ea 100644 --- a/src/android/verification/SensorControlForegroundDelegate.java +++ b/src/android/verification/SensorControlForegroundDelegate.java @@ -42,11 +42,9 @@ import com.google.android.gms.location.LocationSettingsStatusCodes; import java.util.Arrays; -import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Objects; -import java.util.Set; import edu.berkeley.eecs.emission.cordova.unifiedlogger.NotificationHelper; @@ -59,6 +57,22 @@ public class SensorControlForegroundDelegate { private PermissionPopupChecker permissionChecker = null; private Map permissionCheckerMap = new HashMap<>(); + private static JSONObject OPEN_APP_STATUS_PAGE(Context ctxt) { + try { + JSONObject config = new JSONObject(); + config.put("id", SensorControlConstants.OPEN_APP_STATUS_PAGE); + config.put("title", ctxt.getString(R.string.fix_app_status_title)); + config.put("text", ctxt.getString(R.string.fix_app_status_text)); + JSONObject redirectData = new JSONObject(); + redirectData.put("redirectTo", "root.main.control"); + config.put("data", redirectData); + return config; + } catch (JSONException e) { + e.printStackTrace(); + } + return null; + } + class PermissionPopupChecker { int permissionStatusConstant = -1; boolean shouldShowRequestRationaleBefore = false; @@ -105,6 +119,10 @@ void requestPermission() { } void generateErrorCallback() { + if (cordovaCallback == null) { + NotificationHelper.createNotification(mAct, Constants.TRACKING_ERROR_ID, null, "Please upload log and report issue in generateErrorCallback"); + return; + } boolean shouldShowRequestRationaleAfter = shouldShowRequestForCurrPermissions(); Log.d(cordova.getActivity(), TAG, "In permission prompt, error callback,"+ " before = "+shouldShowRequestRationaleBefore+" after = "+shouldShowRequestRationaleAfter); @@ -408,29 +426,16 @@ private void displayResolution(PendingIntent resolution) { cordova.getActivity().startIntentSenderForResult(resolution.getIntentSender(), SensorControlConstants.ENABLE_LOCATION_SETTINGS, null, 0, 0, 0, null); } catch (IntentSender.SendIntentException e) { Context mAct = cordova.getActivity(); - NotificationHelper.createNotification(mAct, Constants.TRACKING_ERROR_ID, mAct.getString(R.string.unable_resolve_issue)); + NotificationHelper.createNotification(mAct, Constants.TRACKING_ERROR_ID, null, mAct.getString(R.string.unable_resolve_issue)); } } } public void onNewIntent(Intent intent) { - if(SensorControlConstants.ENABLE_LOCATION_PERMISSION_ACTION.equals(intent.getAction()) || - SensorControlConstants.ENABLE_BACKGROUND_LOC_PERMISSION_ACTION.equals(intent.getAction())) { - checkAndPromptLocationPermissions(null); - return; + if (SensorControlConstants.OPEN_APP_STATUS_PAGE_ACTION.equals(intent.getAction())) { + Context ctxt = cordova.getActivity(); + NotificationHelper.schedulePluginCompatibleNotification(ctxt, OPEN_APP_STATUS_PAGE(ctxt), null); } - - if(SensorControlConstants.ENABLE_MOTION_ACTIVITY_PERMISSION_ACTION.equals(intent.getAction())) { - checkAndPromptMotionActivityPermissions(null); - return; - } - if (NotificationHelper.DISPLAY_RESOLUTION_ACTION.equals(intent.getAction())) { - PendingIntent piFromIntent = intent.getParcelableExtra( - NotificationHelper.RESOLUTION_PENDING_INTENT_KEY); - displayResolution(piFromIntent); - return; - } - Log.i(cordova.getActivity(), TAG, "Action "+intent.getAction()+" unknown, ignoring "); } public void onRequestPermissionResult(int requestCode, String[] permissions, @@ -439,6 +444,7 @@ public void onRequestPermissionResult(int requestCode, String[] permissions, Log.i(cordova.getActivity(), TAG, "onRequestPermissionResult called with "+requestCode); Log.i(cordova.getActivity(), TAG, "permissions are "+ Arrays.toString(permissions)); Log.i(cordova.getActivity(), TAG, "grantResults are "+Arrays.toString(grantResults)); + /* 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 @@ -451,6 +457,10 @@ public void onRequestPermissionResult(int requestCode, String[] permissions, } } */ + if (this.permissionChecker == null) { + NotificationHelper.createNotification(mAct, Constants.TRACKING_ERROR_ID, null, "Please upload log and report issue in requestPermissionResult"); + return; + } switch(requestCode) { case SensorControlConstants.ENABLE_BOTH_PERMISSION: From 497c9e0170391c89cb2508cf78000b69dd9b8627 Mon Sep 17 00:00:00 2001 From: Shankari Date: Tue, 16 Nov 2021 22:27:24 -0800 Subject: [PATCH 15/35] Additional fixes to the notification code + log properly in the foreground delegate + add import + add strings --- res/android/values/dc_strings.xml | 2 ++ .../location/ActivityRecognitionChangeIntentService.java | 2 +- src/android/verification/SensorControlBackgroundChecker.java | 2 ++ src/android/verification/SensorControlForegroundDelegate.java | 4 ++-- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/res/android/values/dc_strings.xml b/res/android/values/dc_strings.xml index 0f887d5..a351563 100644 --- a/res/android/values/dc_strings.xml +++ b/res/android/values/dc_strings.xml @@ -27,6 +27,8 @@ Notifications blocked, please enable Notifications paused. This can only be fixed by the app developer. Please report to your admin. Please allow this app to read sensor data even if you launch it infrequently. + Incorrect app settings + Click to view and fix app status Error in location settings, click to resolve Unknown error while reading location, please check your settings In state %1$s diff --git a/src/android/location/ActivityRecognitionChangeIntentService.java b/src/android/location/ActivityRecognitionChangeIntentService.java index 3ac469d..1103e9d 100644 --- a/src/android/location/ActivityRecognitionChangeIntentService.java +++ b/src/android/location/ActivityRecognitionChangeIntentService.java @@ -49,7 +49,7 @@ protected void onHandleIntent(Intent intent) { DetectedActivity mostProbableActivity = result.getMostProbableActivity(); Log.i(this, TAG, "Detected new activity "+mostProbableActivity); if (ConfigManager.getConfig(this).isSimulateUserInteraction()) { - NotificationHelper.createNotification(this, ACTIVITY_IN_NUMBERS,this.getString(R.string.detected_new_activity, activityType2Name(mostProbableActivity.getType(), this))); + NotificationHelper.createNotification(this, ACTIVITY_IN_NUMBERS, null, this.getString(R.string.detected_new_activity, activityType2Name(mostProbableActivity.getType(), this))); } // TODO: Do we want to compare activity and only store when different? // Can easily do that by getting the last activity diff --git a/src/android/verification/SensorControlBackgroundChecker.java b/src/android/verification/SensorControlBackgroundChecker.java index 7ce3a76..72c8b3b 100644 --- a/src/android/verification/SensorControlBackgroundChecker.java +++ b/src/android/verification/SensorControlBackgroundChecker.java @@ -17,6 +17,8 @@ import com.google.android.gms.location.LocationSettingsStatusCodes; import com.google.android.gms.tasks.Task; +import java.util.Arrays; + import edu.berkeley.eecs.emission.cordova.tracker.Constants; import edu.berkeley.eecs.emission.cordova.tracker.ExplicitIntent; import edu.berkeley.eecs.emission.cordova.tracker.location.TripDiaryStateMachineService; diff --git a/src/android/verification/SensorControlForegroundDelegate.java b/src/android/verification/SensorControlForegroundDelegate.java index 06da8ea..b4c0b16 100644 --- a/src/android/verification/SensorControlForegroundDelegate.java +++ b/src/android/verification/SensorControlForegroundDelegate.java @@ -120,7 +120,7 @@ void requestPermission() { void generateErrorCallback() { if (cordovaCallback == null) { - NotificationHelper.createNotification(mAct, Constants.TRACKING_ERROR_ID, null, "Please upload log and report issue in generateErrorCallback"); + NotificationHelper.createNotification(cordova.getActivity(), Constants.TRACKING_ERROR_ID, null, "Please upload log and report issue in generateErrorCallback"); return; } boolean shouldShowRequestRationaleAfter = shouldShowRequestForCurrPermissions(); @@ -458,7 +458,7 @@ public void onRequestPermissionResult(int requestCode, String[] permissions, } */ if (this.permissionChecker == null) { - NotificationHelper.createNotification(mAct, Constants.TRACKING_ERROR_ID, null, "Please upload log and report issue in requestPermissionResult"); + NotificationHelper.createNotification(cordova.getActivity(), Constants.TRACKING_ERROR_ID, null, "Please upload log and report issue in requestPermissionResult"); return; } switch(requestCode) From 6a7d6cd06a743d3373d54010a8b36e7e88e0ae90 Mon Sep 17 00:00:00 2001 From: Shankari Date: Wed, 17 Nov 2021 09:55:18 -0800 Subject: [PATCH 16/35] Add the redirect params to the local notification To be compatible with https://github.com/e-mission/e-mission-phone/pull/804/commits/6314932b75863921dc26d7c0f5f036b025891955 --- src/android/verification/SensorControlForegroundDelegate.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/android/verification/SensorControlForegroundDelegate.java b/src/android/verification/SensorControlForegroundDelegate.java index b4c0b16..4124d3a 100644 --- a/src/android/verification/SensorControlForegroundDelegate.java +++ b/src/android/verification/SensorControlForegroundDelegate.java @@ -65,6 +65,9 @@ private static JSONObject OPEN_APP_STATUS_PAGE(Context ctxt) { config.put("text", ctxt.getString(R.string.fix_app_status_text)); JSONObject redirectData = new JSONObject(); redirectData.put("redirectTo", "root.main.control"); + JSONObject redirectParams = new JSONObject(); + redirectParams.put("launchAppStatusModal", true); + redirectData.put("redirectParams", redirectParams); config.put("data", redirectData); return config; } catch (JSONException e) { From 3f48d1ff257de898710993400e6a130672f60c04 Mon Sep 17 00:00:00 2001 From: Shankari Date: Mon, 20 Dec 2021 20:15:30 -0800 Subject: [PATCH 17/35] Ensure that we use >= for all version O checks We were using `>` in this one location, which meant that if the version was exactly 8.0, then the foreground service would not be restarted when killed. Tested by @sichen1234 as part of https://github.com/e-mission/e-mission-docs/issues/693 ``` ../plugins/e-mission-data-collection/src/android/location/TripDiaryStateMachineForegroundService.java|90| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { ../plugins/e-mission-data-collection/src/android/location/TripDiaryStateMachineForegroundService.java|106| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { ../plugins/e-mission-data-collection/src/android/location/TripDiaryStateMachineForegroundService.java|163| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { ../plugins/e-mission-data-collection/src/android/location/TripDiaryStateMachineForegroundService.java|175| if(Build.VERSION.SDK_INT >Build.VERSION_CODES.O) { ../plugins/e-mission-data-collection/src//android/verification/SensorControlChecks.java|85| if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { ``` This fixes https://github.com/e-mission/e-mission-docs/issues/693 --- .../location/TripDiaryStateMachineForegroundService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/android/location/TripDiaryStateMachineForegroundService.java b/src/android/location/TripDiaryStateMachineForegroundService.java index c9b54b4..8f082f5 100644 --- a/src/android/location/TripDiaryStateMachineForegroundService.java +++ b/src/android/location/TripDiaryStateMachineForegroundService.java @@ -172,7 +172,7 @@ private static Intent getForegroundServiceIntent(Context ctxt) { } public static void checkForegroundNotification(Context ctxt) { - if(Build.VERSION.SDK_INT >Build.VERSION_CODES.O) { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationManager mgr = (NotificationManager) ctxt.getSystemService(Context.NOTIFICATION_SERVICE); StatusBarNotification[] activeNotifications = mgr.getActiveNotifications(); Log.d(ctxt, TAG, "In checkForegroundNotification, found " + activeNotifications.length + " active notifications"); From 7ad4b403d8b1efcecfe0ac0e426ef80ec36b0e1c Mon Sep 17 00:00:00 2001 From: Shankari Date: Thu, 27 Jan 2022 23:07:44 -0800 Subject: [PATCH 18/35] Remove unused import Which is not rewritten by the hook since it is unused. https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1023941859 --- src/android/verification/SensorControlChecks.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/android/verification/SensorControlChecks.java b/src/android/verification/SensorControlChecks.java index 094c03c..40e8fc7 100644 --- a/src/android/verification/SensorControlChecks.java +++ b/src/android/verification/SensorControlChecks.java @@ -1,7 +1,5 @@ package edu.berkeley.eecs.emission.cordova.tracker.verification; -import edu.berkeley.eecs.emission.R; - import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; From ea4434c9d32a6e16e741fe94c291c0a8f980ed33 Mon Sep 17 00:00:00 2001 From: Shankari Date: Tue, 1 Feb 2022 22:37:30 -0800 Subject: [PATCH 19/35] Fix incorrect initialization during onboarding when `markConsented` is called This fixes https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-971953421 **Behavior before the change:** Receive initialize as part of plugin creation ``` public void pluginInitialize() { ... TripDiaryStateMachineReceiver.initOnUpgrade(myActivity); } public static void initOnUpgrade(Context ctxt) { SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(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"); ... ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_initialize)); ``` ``` 2022-02-01 18:04:08.152 25551-25551/edu.berkeley.eecs.emission D/TripDiaryStateMachineRcvr: Setup not complete, sending initialize ``` But because the intro is not done, we exit early ``` if (introDoneResult != null) { Log.i(context, TAG, reqConsent + " is not the current consented version, skipping init..."); NotificationHelper.createNotification(context, STARTUP_IN_NUMBERS, null, context.getString(R.string.new_data_collection_terms)); return; } else { Log.i(context, TAG, "onboarding is not complete, skipping prompt"); return; } ``` So far so good. We could add a check in initOnUpgrade that only generates the initialize if the intro is not done, but that is an optimization. Next: UI calls `markConsented` - calls `TripDiaryStateMachineForegroundService.startProperly` - calls `startForegroundService` with `getForegroundServiceIntent` - calls `onStartCommand` which calls `humanizeState` ``` String message = humanizeState(this, TripDiaryStateMachineService.getState(this)); ``` The default state (since we have not initialized) is the start state, so the humanized message is `Cannot start app, see next pop up` We don't call `initialize` after that during the onboarding process. Let's dig a bit deeper into this. The current data collection plugin code currently has the line `ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_initialize));` but it was commented out as part of https://github.com/e-mission/e-mission-data-collection/commit/3e94966d4fd597bbf33273e99a421a91585d6e96 in which it was replaced by `checkAndPromptPermissions();`. The new function calls `restartFSMIfStartState` if we have the location permission. ``` private void checkAndPromptPermissions() { if(cordova.hasPermission(LOCATION_PERMISSION)) { TripDiaryStateMachineService.restartFSMIfStartState(cordova.getActivity()); } else { cordova.requestPermission(this, ENABLE_LOCATION_PERMISSION, LOCATION_PERMISSION); } } ``` and `restartFSMIfStartState` in turn generates the initialize transition. In this refactoring, we have commented out both `mControlDelegate.checkAndPromptPermissions` and `ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_initialize));` so the FSM is never initialized and the app stays in the start state. I can confirm that if I `initialize`, the FSM starts up correctly and the message is changed. The obvious fix is to go back to generating `initialize`. Note that there are some checks around `intro_done` and we haven't actually finished the intro at this point. However those checks are only triggered if the user did not consent. Since we are in `markConsented`, those checks are not even invoked. **New fixed flow:** - initial `initialize` from `initOnUpgrade`, ignored since no consent and no intro_done - `markConsented` called. This calls `setConsented` and then sends an `initialize` event - `initalize` is received, consent is true, intro_done checks are skipped and foreground service `startProperly` is called - foreground `startService` is called and the function returns - foreground service `onStartCommand` is called, message is "Cannot start app, see next pop up", also shown in UI - handle the `initialize` in the FSM - calls `handleStart`, which sets the state to `waiting_for_trip_start` - State Machine `setNewState` -> ForegroundServiceComm `setNewState` -> foreground service `setStateMessage` - message is now "Ready for your next trip" both in the debugger and in the UI - `checkAppState` is called from State Machine `setNewState`, but the app state is fine - after onboarding, state is `waiting_for_trip_start` --- src/android/DataCollectionPlugin.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/android/DataCollectionPlugin.java b/src/android/DataCollectionPlugin.java index 771e4f9..8b8bea8 100644 --- a/src/android/DataCollectionPlugin.java +++ b/src/android/DataCollectionPlugin.java @@ -64,12 +64,12 @@ public boolean execute(String action, JSONArray data, final CallbackContext call JSONObject newConsent = data.getJSONObject(0); ConsentConfig cfg = new Gson().fromJson(newConsent.toString(), ConsentConfig.class); ConfigManager.setConsented(ctxt, cfg); - TripDiaryStateMachineForegroundService.startProperly(cordova.getActivity().getApplication()); + // TripDiaryStateMachineForegroundService.startProperly(cordova.getActivity().getApplication()); // Now, really initialize the state machine // 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 // mControlDelegate.checkAndPromptPermissions(); - // ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_initialize)); + ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_initialize)); // TripDiaryStateMachineReceiver.restartCollection(ctxt); callbackContext.success(); return true; From 18ac9d5e9a31ca596c36234b84e418fbfe167aa8 Mon Sep 17 00:00:00 2001 From: Shankari Date: Fri, 4 Feb 2022 14:00:40 -0800 Subject: [PATCH 20/35] Do not generate a tracking error unless the location cannot be read We've had a mismatch about the way we deal with sensors other than location. In the UI, we require them. In the background checks, we generate a tracking error if they don't exist. But in the state machine, we move to `waiting_for_trip_start` once the geofence was created successfully. This led to an infinite loop between the `waiting_for_trip_start` and `start` states. https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1029693955 For now, we are generating a `tracking_error` only if there is no access to the location sensor. Note that this is still inconsistent with the UI. We should unify the UI and background options or (shudder) make it configurable. Filed https://github.com/e-mission/e-mission-docs/issues/700 to discuss with stakeholders and make it consistent. --- .../SensorControlBackgroundChecker.java | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/android/verification/SensorControlBackgroundChecker.java b/src/android/verification/SensorControlBackgroundChecker.java index 72c8b3b..1f4ad8a 100644 --- a/src/android/verification/SensorControlBackgroundChecker.java +++ b/src/android/verification/SensorControlBackgroundChecker.java @@ -67,9 +67,26 @@ public static void checkAppState(final Context ctxt) { for (boolean check: allOtherChecks) { allOtherChecksPass = allOtherChecksPass && check; } + + /* + Using index-based iteration since we need to start from index 1 instead of 0 and array slices + are hard in Java + */ + boolean nonLocChecksPass = true; + for (int i = 1; i < allOtherChecks.length; i++) { + nonLocChecksPass = nonLocChecksPass && allOtherChecks[i]; + } + if (allOtherChecksPass) { Log.d(ctxt, TAG, "All permissions (except location settings) valid, nothing to prompt"); - } else { + } + else if (allOtherChecks[0]) { + Log.i(ctxt, TAG, "all checks = "+allOtherChecksPass+" but location status "+allOtherChecks[0]+" should be true "+ + " so one of the non-location checks must be false: loc permission, motion permission, notification, unused apps" + Arrays.toString(allOtherChecks)); + Log.i(ctxt, TAG, "a non-local check failed, generating only user visible notification"); + generateOpenAppSettingsNotification(ctxt); + } + else { Log.i(ctxt, TAG, "Curr status check results = "+ " loc permission, motion permission, notification, unused apps "+ Arrays.toString(allOtherChecks)); ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_tracking_error)); From fe24a2daa5805753e6539ab4289f5f55fedd1ba4 Mon Sep 17 00:00:00 2001 From: Shankari Date: Mon, 7 Feb 2022 16:38:37 -0800 Subject: [PATCH 21/35] Generate a plugin-compatible notification directly Instead of creating a notification that launches the main activity, and in turn, launches the the plugin-compatible notification, launch the plugin-compatible notification directly. This ensures that: - we only have one notification, not two - clicking on the notification goes directly to the status screen - the notification is removed when it is clicked This required some careful investigation: - https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1031083191 - https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1031097154 And a related UI-only change https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1031912649 --- .../SensorControlBackgroundChecker.java | 35 +++++++++++++------ .../SensorControlForegroundDelegate.java | 24 +------------ 2 files changed, 26 insertions(+), 33 deletions(-) diff --git a/src/android/verification/SensorControlBackgroundChecker.java b/src/android/verification/SensorControlBackgroundChecker.java index 1f4ad8a..a42e24d 100644 --- a/src/android/verification/SensorControlBackgroundChecker.java +++ b/src/android/verification/SensorControlBackgroundChecker.java @@ -17,6 +17,9 @@ import com.google.android.gms.location.LocationSettingsStatusCodes; import com.google.android.gms.tasks.Task; +import org.json.JSONException; +import org.json.JSONObject; + import java.util.Arrays; import edu.berkeley.eecs.emission.cordova.tracker.Constants; @@ -25,8 +28,8 @@ import edu.berkeley.eecs.emission.cordova.tracker.location.actions.LocationTrackingActions; import edu.berkeley.eecs.emission.cordova.unifiedlogger.Log; import edu.berkeley.eecs.emission.cordova.unifiedlogger.NotificationHelper; -import edu.berkeley.eecs.emission.cordova.MainActivity; -import edu.berkeley.eecs.emission.cordova.R; + + /* * Deals with settings and resolutions from the background as a service. @@ -40,6 +43,25 @@ public class SensorControlBackgroundChecker { private static int STATE_IN_NUMBERS = 78283; + private static JSONObject OPEN_APP_STATUS_PAGE(Context ctxt) { + try { + JSONObject config = new JSONObject(); + config.put("id", SensorControlConstants.OPEN_APP_STATUS_PAGE); + config.put("title", ctxt.getString(R.string.fix_app_status_title)); + config.put("text", ctxt.getString(R.string.fix_app_status_text)); + JSONObject redirectData = new JSONObject(); + redirectData.put("redirectTo", "root.main.control"); + JSONObject redirectParams = new JSONObject(); + redirectParams.put("launchAppStatusModal", true); + redirectData.put("redirectParams", redirectParams); + config.put("data", redirectData); + return config; + } catch (JSONException e) { + e.printStackTrace(); + } + return null; + } + public static void restartFSMIfStartState(Context ctxt) { String START_STATE = ctxt.getString(R.string.state_start); String currState = TripDiaryStateMachineService.getState(ctxt); @@ -96,14 +118,7 @@ else if (allOtherChecks[0]) { } public static void generateOpenAppSettingsNotification(Context ctxt) { - Intent activityIntent = new Intent(ctxt, MainActivity.class); - activityIntent.setAction(SensorControlConstants.OPEN_APP_STATUS_PAGE_ACTION); - PendingIntent pi = PendingIntent.getActivity(ctxt, SensorControlConstants.OPEN_APP_STATUS_PAGE, - activityIntent, PendingIntent.FLAG_UPDATE_CURRENT); - NotificationHelper.createNotification(ctxt, SensorControlConstants.ENABLE_BACKGROUND_LOC_PERMISSION, - ctxt.getString(R.string.fix_app_status_title), - ctxt.getString(R.string.fix_app_status_text), - pi); + NotificationHelper.schedulePluginCompatibleNotification(ctxt, OPEN_APP_STATUS_PAGE(ctxt), null); } private static void checkLocationSettings(final Context ctxt) { diff --git a/src/android/verification/SensorControlForegroundDelegate.java b/src/android/verification/SensorControlForegroundDelegate.java index 4124d3a..be0a405 100644 --- a/src/android/verification/SensorControlForegroundDelegate.java +++ b/src/android/verification/SensorControlForegroundDelegate.java @@ -57,25 +57,6 @@ public class SensorControlForegroundDelegate { private PermissionPopupChecker permissionChecker = null; private Map permissionCheckerMap = new HashMap<>(); - private static JSONObject OPEN_APP_STATUS_PAGE(Context ctxt) { - try { - JSONObject config = new JSONObject(); - config.put("id", SensorControlConstants.OPEN_APP_STATUS_PAGE); - config.put("title", ctxt.getString(R.string.fix_app_status_title)); - config.put("text", ctxt.getString(R.string.fix_app_status_text)); - JSONObject redirectData = new JSONObject(); - redirectData.put("redirectTo", "root.main.control"); - JSONObject redirectParams = new JSONObject(); - redirectParams.put("launchAppStatusModal", true); - redirectData.put("redirectParams", redirectParams); - config.put("data", redirectData); - return config; - } catch (JSONException e) { - e.printStackTrace(); - } - return null; - } - class PermissionPopupChecker { int permissionStatusConstant = -1; boolean shouldShowRequestRationaleBefore = false; @@ -435,10 +416,7 @@ private void displayResolution(PendingIntent resolution) { } public void onNewIntent(Intent intent) { - if (SensorControlConstants.OPEN_APP_STATUS_PAGE_ACTION.equals(intent.getAction())) { - Context ctxt = cordova.getActivity(); - NotificationHelper.schedulePluginCompatibleNotification(ctxt, OPEN_APP_STATUS_PAGE(ctxt), null); - } + Log.i(cordova.getActivity(), TAG, "onNewIntent("+intent+") received, ignoring"); } public void onRequestPermissionResult(int requestCode, String[] permissions, From a476db75fcce0a9a09a901e91838382bde0a9134 Mon Sep 17 00:00:00 2001 From: Shankari Date: Tue, 8 Feb 2022 14:24:03 -0800 Subject: [PATCH 22/35] Initialize the FSM after fixing permission issues At least when the issue is fixed from the UI. This avoids the gap between the issue being fixed, and the tracking starting again. If the issue is fixed directly from the settings screen, we don't get a callback, so we can't reinitialize immediately and will have to wait until the next periodic sync. https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1032956805 https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1033118560 --- .../verification/SensorControlBackgroundChecker.java | 3 +++ .../SensorControlForegroundDelegate.java | 12 +++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/android/verification/SensorControlBackgroundChecker.java b/src/android/verification/SensorControlBackgroundChecker.java index a42e24d..5837ccd 100644 --- a/src/android/verification/SensorControlBackgroundChecker.java +++ b/src/android/verification/SensorControlBackgroundChecker.java @@ -1,5 +1,7 @@ package edu.berkeley.eecs.emission.cordova.tracker.verification; // Auto fixed by post-plugin hook +import edu.berkeley.eecs.emission.R; +// Auto fixed by post-plugin hook import android.app.PendingIntent; import android.content.Context; import android.content.Intent; @@ -101,6 +103,7 @@ public static void checkAppState(final Context ctxt) { if (allOtherChecksPass) { Log.d(ctxt, TAG, "All permissions (except location settings) valid, nothing to prompt"); + restartFSMIfStartState(ctxt); } else if (allOtherChecks[0]) { Log.i(ctxt, TAG, "all checks = "+allOtherChecksPass+" but location status "+allOtherChecks[0]+" should be true "+ diff --git a/src/android/verification/SensorControlForegroundDelegate.java b/src/android/verification/SensorControlForegroundDelegate.java index be0a405..b7cf3db 100644 --- a/src/android/verification/SensorControlForegroundDelegate.java +++ b/src/android/verification/SensorControlForegroundDelegate.java @@ -189,6 +189,7 @@ public void checkAndPromptLocationSettings(CallbackContext callbackContext) { Log.i(currActivity, TAG, "All settings are valid, checking current state"); JSONObject lssJSON = statesToJSON(response.getLocationSettingsStates()); Log.i(currActivity, TAG, "Current location settings are "+lssJSON); + SensorControlBackgroundChecker.restartFSMIfStartState(cordova.getActivity()); callbackContext.success(lssJSON); } catch (ApiException exception) { switch (exception.getStatusCode()) { @@ -334,6 +335,7 @@ public void checkMotionActivityPermissions(CallbackContext cordovaCallback) { public void checkAndPromptMotionActivityPermissions(CallbackContext cordovaCallback) { boolean validPerms = SensorControlChecks.checkMotionActivityPermissions(cordova.getActivity()); if(validPerms) { + SensorControlBackgroundChecker.restartFSMIfStartState(cordova.getActivity()); cordovaCallback.success(); } else { Log.i(cordova.getActivity(), TAG, "before call shouldShowRequestPermissionRationale = "+ ActivityCompat.shouldShowRequestPermissionRationale(cordova.getActivity(), SensorControlConstants.MOTION_ACTIVITY_PERMISSION)); @@ -360,6 +362,7 @@ public void checkShowNotificationsEnabled(CallbackContext cordovaCallback) { public void checkAndPromptShowNotificationsEnabled(CallbackContext cordovaCallback) { boolean validPerms = SensorControlChecks.checkNotificationsEnabled(cordova.getActivity()); if(validPerms) { + SensorControlBackgroundChecker.restartFSMIfStartState(cordova.getActivity()); cordovaCallback.success(); } else { Log.i(cordova.getActivity(), TAG, "Notifications not enabled, opening app page"); @@ -393,6 +396,7 @@ public void checkUnusedAppsUnrestricted(CallbackContext cordovaCallback) { public void checkAndPromptUnusedAppsUnrestricted(CallbackContext cordovaCallback) { boolean unrestricted = SensorControlChecks.checkUnusedAppsUnrestricted(cordova.getActivity()); if (unrestricted) { + SensorControlBackgroundChecker.restartFSMIfStartState(cordova.getActivity()); cordovaCallback.success(); } else { Log.i(cordova.getActivity(), TAG, "Unused apps restricted, asking user to unrestrict"); @@ -448,6 +452,7 @@ public void onRequestPermissionResult(int requestCode, String[] permissions, Log.i(cordova.getActivity(), TAG, "in callback shouldShowRequestPermissionRationale = "+ ActivityCompat.shouldShowRequestPermissionRationale(cordova.getActivity(), SensorControlConstants.LOCATION_PERMISSION)); if ((grantResults[0] == PackageManager.PERMISSION_GRANTED) && (grantResults[1] == PackageManager.PERMISSION_GRANTED)) { + SensorControlBackgroundChecker.restartFSMIfStartState(cordova.getActivity()); cordovaCallback.success(); } else if (grantResults[0] == PackageManager.PERMISSION_DENIED || grantResults[1] == PackageManager.PERMISSION_DENIED) { this.permissionChecker.generateErrorCallback(); @@ -461,6 +466,7 @@ public void onRequestPermissionResult(int requestCode, String[] permissions, // and if it is denied, we generate the error callback // the exact message is stored in the permission checker object if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + SensorControlBackgroundChecker.restartFSMIfStartState(cordova.getActivity()); cordovaCallback.success(); } else if (grantResults[0] == PackageManager.PERMISSION_DENIED) { this.permissionChecker.generateErrorCallback(); @@ -472,7 +478,6 @@ public void onRequestPermissionResult(int requestCode, String[] permissions, } } - public void onActivityResult(int requestCode, int resultCode, Intent data) { Activity mAct = cordova.getActivity(); cordova.setActivityResultCallback(null); @@ -486,6 +491,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { // All required changes were successfully made Log.i(cordova.getActivity(), TAG, "All changes successfully made, reinitializing"); try { + SensorControlBackgroundChecker.restartFSMIfStartState(cordova.getActivity()); cordovaCallback.success(statesToJSON(states)); } catch (JSONException e) { cordovaCallback.error(mAct.getString(R.string.unknown_error_location_settings)); @@ -513,6 +519,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { Log.d(mAct, TAG, requestCode + " is our code, handling callback"); Log.d(mAct, TAG, "Got permission callback from launching app settings when prompt failed"); if (SensorControlChecks.checkLocationPermissions(cordova.getActivity())) { + SensorControlBackgroundChecker.restartFSMIfStartState(cordova.getActivity()); cordovaCallback.success(); } else { // this is the activity result callback, so only launched when the app settings are used @@ -525,6 +532,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { Log.d(mAct, TAG, requestCode + " is our code, handling callback"); Log.d(mAct, TAG, "Got permission callback from launching app settings"); if (SensorControlChecks.checkMotionActivityPermissions(cordova.getActivity())) { + SensorControlBackgroundChecker.restartFSMIfStartState(cordova.getActivity()); cordovaCallback.success(); } else { permissionChecker.generateErrorCallback(); @@ -535,6 +543,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { Log.d(mAct, TAG, requestCode + " is our code, handling callback"); Log.d(mAct, TAG, "Got notification callback from launching app settings"); if (SensorControlChecks.checkNotificationsEnabled(cordova.getActivity())) { + SensorControlBackgroundChecker.restartFSMIfStartState(cordova.getActivity()); cordovaCallback.success(); } else { cordovaCallback.error(cordova.getActivity().getString(R.string.notifications_blocked)); @@ -544,6 +553,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { Log.d(mAct, TAG, requestCode + " is our code, handling callback"); Log.d(mAct, TAG, "Got unused app restrictions callback from launching app settings"); if (SensorControlChecks.checkUnusedAppsUnrestricted(cordova.getActivity())) { + SensorControlBackgroundChecker.restartFSMIfStartState(cordova.getActivity()); cordovaCallback.success(); } else { cordovaCallback.error(cordova.getActivity().getString(R.string.unused_apps_restricted)); From 499c11537a89f47928ad33bcd9747e6026d2480d Mon Sep 17 00:00:00 2001 From: Shankari Date: Tue, 8 Feb 2022 17:41:35 -0800 Subject: [PATCH 23/35] Move the permission checks into the success callback of the settings check Without this change, the location settings were separate from the permissions check, which meant that: 1) we were not generating a tracking error if the location settings were incorrect, so the foreground notification still said "Ready for a trip!" 2) if we did generate a tracking error, we also still generated an initialize because all the permissions were correct, which led to the infinite loop of doom. Fixed this by: - deleting the existing checkLocationSettings - moving the check into the base `checkAppStatus` - calling the permission checks from the success callback of the check - calling tracking error from the error callback of the check Everything works now! https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1033179053 --- .../SensorControlBackgroundChecker.java | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/src/android/verification/SensorControlBackgroundChecker.java b/src/android/verification/SensorControlBackgroundChecker.java index 5837ccd..7fcc429 100644 --- a/src/android/verification/SensorControlBackgroundChecker.java +++ b/src/android/verification/SensorControlBackgroundChecker.java @@ -17,6 +17,7 @@ import com.google.android.gms.location.LocationSettingsRequest; import com.google.android.gms.location.LocationSettingsResponse; import com.google.android.gms.location.LocationSettingsStatusCodes; +import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.Task; import org.json.JSONException; @@ -77,10 +78,15 @@ public static void restartFSMIfStartState(Context ctxt) { public static void checkAppState(final Context ctxt) { NotificationHelper.cancelNotification(ctxt, SensorControlConstants.OPEN_APP_STATUS_PAGE); - // check location settings. This is a separate function because it - // currently has a callback. The others can be inlined here for greater - // readability. - checkLocationSettings(ctxt); + SensorControlChecks.checkLocationSettings(ctxt, resultTask -> { + try { + LocationSettingsResponse response = resultTask.getResult(ApiException.class); + // All location settings are satisfied. The client can initialize location + // requests here. + Log.i(ctxt, TAG, "All settings are valid, checking current state"); + Log.i(ctxt, TAG, "Current location settings are "+response); + + // Now that we know that the location settings are correct, we start the permission checks boolean[] allOtherChecks = new boolean[]{ SensorControlChecks.checkLocationPermissions(ctxt), SensorControlChecks.checkMotionActivityPermissions(ctxt), @@ -102,39 +108,31 @@ public static void checkAppState(final Context ctxt) { } if (allOtherChecksPass) { - Log.d(ctxt, TAG, "All permissions (except location settings) valid, nothing to prompt"); + Log.d(ctxt, TAG, "All settings valid, nothing to prompt"); restartFSMIfStartState(ctxt); } else if (allOtherChecks[0]) { - Log.i(ctxt, TAG, "all checks = "+allOtherChecksPass+" but location status "+allOtherChecks[0]+" should be true "+ + Log.i(ctxt, TAG, "all checks = "+allOtherChecksPass+" but location permission status "+allOtherChecks[0]+" should be true "+ " so one of the non-location checks must be false: loc permission, motion permission, notification, unused apps" + Arrays.toString(allOtherChecks)); Log.i(ctxt, TAG, "a non-local check failed, generating only user visible notification"); generateOpenAppSettingsNotification(ctxt); } else { - Log.i(ctxt, TAG, "Curr status check results = "+ + Log.i(ctxt, TAG, "location settings are valid, but location permission is not, generating tracking error and visible notification"); + Log.i(ctxt, TAG, "curr status check results = " + " loc permission, motion permission, notification, unused apps "+ Arrays.toString(allOtherChecks)); ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_tracking_error)); generateOpenAppSettingsNotification(ctxt); } - restartFSMIfStartState(ctxt); - } - - public static void generateOpenAppSettingsNotification(Context ctxt) { - NotificationHelper.schedulePluginCompatibleNotification(ctxt, OPEN_APP_STATUS_PAGE(ctxt), null); - } - - private static void checkLocationSettings(final Context ctxt) { - SensorControlChecks.checkLocationSettings(ctxt, resultTask -> { - try { - LocationSettingsResponse response = resultTask.getResult(ApiException.class); - // All location settings are satisfied. The client can initialize location - // requests here. - Log.i(ctxt, TAG, "All settings are valid, checking current state"); - Log.i(ctxt, TAG, "Current location settings are "+response); } catch (ApiException exception) { + Log.i(ctxt, TAG, "location settings are invalid, generating tracking error and visible notification"); + ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_tracking_error)); generateOpenAppSettingsNotification(ctxt); } }); } + + public static void generateOpenAppSettingsNotification(Context ctxt) { + NotificationHelper.schedulePluginCompatibleNotification(ctxt, OPEN_APP_STATUS_PAGE(ctxt), null); + } } From 9d31e1008566f1563995484300e76b5f32636b16 Mon Sep 17 00:00:00 2001 From: Shankari Date: Wed, 9 Feb 2022 13:38:19 -0800 Subject: [PATCH 24/35] Initial scaffolding for the iOS status screen Changes: - Add the new methods to both the .m and .h files - Create a new `NOP_RETURN_TRUE` to return TRUE from unimplemented/irrelevant methods - Use it for `launchInit`, `fixOEMBackgroundRestrictions`, or `isNotificationsUnpaused` - Implement `isValidShowNotifications` and `fixShowNotifications` to ensure that the end-to-end call works. This works as long as the user always selects "Allow". Still need to fix the case where the user selects "Don't Allow". Note that the `didRegisterUserNotificationSettings` callback apparently works from only the AppDelegate, which is problematic wrt modularity. Will refactor the existing location and motion activity permissions first, and then get back to notifications. May take this opportunity to move to the newer, non-deprecated version and see if it has a smoother flow. --- res/ios/en.lproj/DCLocalizable.strings | 1 + src/ios/BEMAppDelegate.h | 1 + src/ios/BEMAppDelegate.m | 9 ++ src/ios/BEMDataCollection.h | 11 +++ src/ios/BEMDataCollection.m | 117 +++++++++++++++++++++++-- 5 files changed, 133 insertions(+), 6 deletions(-) diff --git a/res/ios/en.lproj/DCLocalizable.strings b/res/ios/en.lproj/DCLocalizable.strings index 2847310..15df887 100644 --- a/res/ios/en.lproj/DCLocalizable.strings +++ b/res/ios/en.lproj/DCLocalizable.strings @@ -13,3 +13,4 @@ "location-permission-problem" = "The app does not have the 'always' permission - background trip tracking will not work."; "fix-permission-action-button" = "Fix permission"; "precise-location-problem" = "The app does not have permission to read 'precise' location - background trip tracking will not work."; +"notifications_blocked" = "Notifications blocked, please enable"; diff --git a/src/ios/BEMAppDelegate.h b/src/ios/BEMAppDelegate.h index 9af4c70..3f0553e 100644 --- a/src/ios/BEMAppDelegate.h +++ b/src/ios/BEMAppDelegate.h @@ -10,6 +10,7 @@ #import "TripDiaryStateMachine.h" #import "AppDelegate.h" #import "BEMServerSyncCommunicationHelper.h" +#define NotificationCallback @"NOTIFICATION_CALLBACK" @interface AppDelegate (datacollection) diff --git a/src/ios/BEMAppDelegate.m b/src/ios/BEMAppDelegate.m index 1960fbf..6025d05 100644 --- a/src/ios/BEMAppDelegate.m +++ b/src/ios/BEMAppDelegate.m @@ -42,6 +42,15 @@ + (BOOL)didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { return YES; } +-(void) application:(UIApplication*)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings +{ + [LocalNotificationManager addNotification:[NSString stringWithFormat: + @"Received callback from user notification settings "] + showUI:FALSE]; + [[NSNotificationCenter defaultCenter] postNotificationName:NotificationCallback + object:notificationSettings]; + +} - (void)applicationWillResignActive:(UIApplication *)application { // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. diff --git a/src/ios/BEMDataCollection.h b/src/ios/BEMDataCollection.h index c3c74de..eb1a4e4 100644 --- a/src/ios/BEMDataCollection.h +++ b/src/ios/BEMDataCollection.h @@ -4,6 +4,17 @@ @interface BEMDataCollection: CDVPlugin - (void) pluginInitialize; +- (void) markConsented:(CDVInvokedUrlCommand*)command; +- (void) fixLocationSettings:(CDVInvokedUrlCommand*)command; +- (void) isValidLocationSettings:(CDVInvokedUrlCommand*)command; +- (void) fixLocationPermissions:(CDVInvokedUrlCommand*)command; +- (void) isValidLocationPermissions:(CDVInvokedUrlCommand*)command; +- (void) fixFitnessPermissions:(CDVInvokedUrlCommand*)command; +- (void) isValidFitnessPermissions:(CDVInvokedUrlCommand*)command; +- (void) fixShowNotifications:(CDVInvokedUrlCommand*)command; +- (void) isValidShowNotifications:(CDVInvokedUrlCommand*)command; +- (void) isNotificationsUnpaused:(CDVInvokedUrlCommand*)command; +- (void) fixOEMBackgroundRestrictions: (CDVInvokedUrlCommand*) command; - (void) launchInit:(CDVInvokedUrlCommand*)command; - (void) getConfig:(CDVInvokedUrlCommand*)command; - (void) setConfig:(CDVInvokedUrlCommand*)command; diff --git a/src/ios/BEMDataCollection.m b/src/ios/BEMDataCollection.m index 6bb5940..3d32549 100644 --- a/src/ios/BEMDataCollection.m +++ b/src/ios/BEMDataCollection.m @@ -79,17 +79,89 @@ - (void)markConsented:(CDVInvokedUrlCommand*)command } } -- (void)storeBatteryLevel:(CDVInvokedUrlCommand*)command +- (void)fixLocationSettings:(CDVInvokedUrlCommand*)command +{ +} + +- (void)isValidLocationSettings:(CDVInvokedUrlCommand*)command +{ +} + +- (void)fixLocationPermissions:(CDVInvokedUrlCommand*)command +{ +} + +- (void)isValidLocationPermissions:(CDVInvokedUrlCommand*)command +{ +} + +- (void)fixFitnessPermissions:(CDVInvokedUrlCommand*)command +{ +} + +- (void)isValidFitnessPermissions:(CDVInvokedUrlCommand*)command +{ +} + +- (UIUserNotificationSettings*) REQUESTED_NOTIFICATION_TYPES { + return [UIUserNotificationSettings + settingsForTypes:UIUserNotificationTypeAlert|UIUserNotificationTypeBadge + categories:nil]; +} + +- (void)fixShowNotifications:(CDVInvokedUrlCommand*)command { NSString* callbackId = [command callbackId]; @try { - [DataUtils saveBatteryAndSimulateUser]; + if ([UIApplication instancesRespondToSelector:@selector(registerUserNotificationSettings:)]) { + UIUserNotificationSettings* requestedSettings = [self REQUESTED_NOTIFICATION_TYPES]; + [[UIApplication sharedApplication] registerUserNotificationSettings:requestedSettings]; + [[NSNotificationCenter defaultCenter] addObserverForName:NotificationCallback object:nil queue:nil + usingBlock:^(NSNotification* note) { + if (requestedSettings.types == ((UIUserNotificationSettings*)note.object).types) { + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_OK]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } else { + NSString* msg = NSLocalizedStringFromTable(@"notifications_blocked", @"DCLocalizable", nil); + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_ERROR + messageAsString:msg]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } + }]; + } + } + @catch (NSException *exception) { + NSString* msg = [NSString stringWithFormat: @"While getting settings, error %@", exception]; CDVPluginResult* result = [CDVPluginResult - resultWithStatus:CDVCommandStatus_OK]; + resultWithStatus:CDVCommandStatus_ERROR + messageAsString:msg]; [self.commandDelegate sendPluginResult:result callbackId:callbackId]; +} +} + + +- (void)isValidShowNotifications:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = [command callbackId]; + @try { + UIUserNotificationSettings* requestedSettings = [self REQUESTED_NOTIFICATION_TYPES]; + UIUserNotificationSettings* currSettings = [[UIApplication sharedApplication] currentUserNotificationSettings]; + if (requestedSettings.types == currSettings.types) { + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_OK]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } else { + NSString* msg = NSLocalizedStringFromTable(@"notifications_blocked", @"DCLocalizable", nil); + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_ERROR + messageAsString:msg]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } } @catch (NSException *exception) { - NSString* msg = [NSString stringWithFormat: @"While storing battery, error %@", exception]; + NSString* msg = [NSString stringWithFormat: @"While getting settings, error %@", exception]; CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:msg]; @@ -97,14 +169,23 @@ - (void)storeBatteryLevel:(CDVInvokedUrlCommand*)command } } +- (void)isNotificationsUnpaused:(CDVInvokedUrlCommand*)command +{ + [self NOP_RETURN_TRUE:command forMethod:@"isNotificationsUnpaused"]; +} -- (void)launchInit:(CDVInvokedUrlCommand*)command +- (void)fixOEMBackgroundRestrictions: (CDVInvokedUrlCommand*) command +{ + [self NOP_RETURN_TRUE:command forMethod:@"fixOEMBackgroundRestrictions"]; +} + +- (void)NOP_RETURN_TRUE:(CDVInvokedUrlCommand*) command forMethod:(NSString*) methodName { NSString* callbackId = [command callbackId]; @try { [LocalNotificationManager addNotification:[NSString stringWithFormat: - @"launchInit called, is NOP on iOS"] showUI:FALSE]; + @"%@ called, is NOP on iOS", methodName] showUI:FALSE]; CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; [self.commandDelegate sendPluginResult:result callbackId:callbackId]; @@ -118,6 +199,30 @@ - (void)launchInit:(CDVInvokedUrlCommand*)command } } +- (void)storeBatteryLevel:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = [command callbackId]; + @try { + [DataUtils saveBatteryAndSimulateUser]; + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_OK]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } + @catch (NSException *exception) { + NSString* msg = [NSString stringWithFormat: @"While storing battery, error %@", exception]; + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_ERROR + messageAsString:msg]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } +} + + +- (void)launchInit:(CDVInvokedUrlCommand*)command +{ + [self NOP_RETURN_TRUE:command forMethod:@"launchInit"]; +} + - (void)getConfig:(CDVInvokedUrlCommand *)command { NSString* callbackId = [command callbackId]; From bed39c902a10bcd69fa756a057e46df215a07e41 Mon Sep 17 00:00:00 2001 From: Shankari Date: Wed, 9 Feb 2022 14:34:51 -0800 Subject: [PATCH 25/35] Move the files into a "verification" directory before refactoring --- src/ios/{Location => Verification}/TripDiarySettingsCheck.h | 0 src/ios/{Location => Verification}/TripDiarySettingsCheck.m | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/ios/{Location => Verification}/TripDiarySettingsCheck.h (100%) rename src/ios/{Location => Verification}/TripDiarySettingsCheck.m (100%) diff --git a/src/ios/Location/TripDiarySettingsCheck.h b/src/ios/Verification/TripDiarySettingsCheck.h similarity index 100% rename from src/ios/Location/TripDiarySettingsCheck.h rename to src/ios/Verification/TripDiarySettingsCheck.h diff --git a/src/ios/Location/TripDiarySettingsCheck.m b/src/ios/Verification/TripDiarySettingsCheck.m similarity index 100% rename from src/ios/Location/TripDiarySettingsCheck.m rename to src/ios/Verification/TripDiarySettingsCheck.m From d6088e0d06615d481636e292e9ec032a07a6cee6 Mon Sep 17 00:00:00 2001 From: Shankari Date: Wed, 9 Feb 2022 14:38:44 -0800 Subject: [PATCH 26/35] Rename the file from `SettingsCheck` to `SensorControlChecks` To be consistent with android --- .../{TripDiarySettingsCheck.h => TripDiarySensorControlChecks.h} | 0 .../{TripDiarySettingsCheck.m => TripDiarySensorControlChecks.m} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/ios/Verification/{TripDiarySettingsCheck.h => TripDiarySensorControlChecks.h} (100%) rename src/ios/Verification/{TripDiarySettingsCheck.m => TripDiarySensorControlChecks.m} (100%) diff --git a/src/ios/Verification/TripDiarySettingsCheck.h b/src/ios/Verification/TripDiarySensorControlChecks.h similarity index 100% rename from src/ios/Verification/TripDiarySettingsCheck.h rename to src/ios/Verification/TripDiarySensorControlChecks.h diff --git a/src/ios/Verification/TripDiarySettingsCheck.m b/src/ios/Verification/TripDiarySensorControlChecks.m similarity index 100% rename from src/ios/Verification/TripDiarySettingsCheck.m rename to src/ios/Verification/TripDiarySensorControlChecks.m From 2fc078b93c443e03d15712bef81835fb8665bed2 Mon Sep 17 00:00:00 2001 From: Shankari Date: Thu, 10 Feb 2022 15:34:19 -0800 Subject: [PATCH 27/35] Initial implmentation of the status screen - End-to-end implementation of the "check" parts of the interface - Code structure similar to the existing android code for simplicity - checks defined in `TripDiarySensorControlChecks` - interface with the plugin in `SensorControlForegroundDelegate` - interface with the background code to generate error notifications in `SensorControlBackgroundChecker` Testing done: With the UI changes in: https://github.com/e-mission/e-mission-phone/pull/812/commits/d69f3a50bfacf52a63f5a0c134650831c6ad1962 the plugin -> foreground delegate -> checks pipeline works https://github.com/e-mission/e-mission-phone/pull/812#issuecomment-1035657284 TO DO: - Background checks need to be tested - Implement the "fix" methods in addition to the "check" methods --- plugin.xml | 8 +- res/ios/en.lproj/DCLocalizable.strings | 9 ++ src/ios/BEMDataCollection.m | 43 +++------ src/ios/BEMRemotePushNotificationHandler.h | 1 - src/ios/BEMRemotePushNotificationHandler.m | 20 +---- src/ios/Location/TripDiaryDelegate.m | 8 +- src/ios/Location/TripDiaryStateMachine.m | 26 ++++-- .../SensorControlBackgroundChecker.h | 11 +++ .../SensorControlBackgroundChecker.m | 90 +++++++++++++++++++ .../SensorControlForegroundDelegate.h | 15 ++++ .../SensorControlForegroundDelegate.m | 82 +++++++++++++++++ .../TripDiarySensorControlChecks.h | 8 +- .../TripDiarySensorControlChecks.m | 47 +++++++++- 13 files changed, 307 insertions(+), 61 deletions(-) create mode 100644 src/ios/Verification/SensorControlBackgroundChecker.h create mode 100644 src/ios/Verification/SensorControlBackgroundChecker.m create mode 100644 src/ios/Verification/SensorControlForegroundDelegate.h create mode 100644 src/ios/Verification/SensorControlForegroundDelegate.m diff --git a/plugin.xml b/plugin.xml index a4e862c..63848d3 100644 --- a/plugin.xml +++ b/plugin.xml @@ -221,7 +221,9 @@ - + + + @@ -241,7 +243,9 @@ - + + + diff --git a/res/ios/en.lproj/DCLocalizable.strings b/res/ios/en.lproj/DCLocalizable.strings index 15df887..825d3eb 100644 --- a/res/ios/en.lproj/DCLocalizable.strings +++ b/res/ios/en.lproj/DCLocalizable.strings @@ -14,3 +14,12 @@ "fix-permission-action-button" = "Fix permission"; "precise-location-problem" = "The app does not have permission to read 'precise' location - background trip tracking will not work."; "notifications_blocked" = "Notifications blocked, please enable"; +"notifications_blocked_app_open" = "Notifications blocked, please fix in app settings"; +"location_not_enabled" = "Location turned off, please turn on"; +"location_permission_off" = "Insufficient location permissions, please fix"; +"location_permission_off_app_open" = "Insufficient location permissions, please fix in app settings"; +"location_permission_off_enable" = "Insufficient location permissions, please enable"; +"activity_permission_off" = "Motion and Fitness permission off, please enable"; +"activity_permission_off_app_open" = "Motion and Fitness permission off, please fix in app settings"; +"fix_app_status_title" = "Incorrect app settings"; +"fix_app_status_text" = "Click to view and fix app status"; diff --git a/src/ios/BEMDataCollection.m b/src/ios/BEMDataCollection.m index 3d32549..08f0828 100644 --- a/src/ios/BEMDataCollection.m +++ b/src/ios/BEMDataCollection.m @@ -6,7 +6,7 @@ #import "DataUtils.h" #import "StatsEvent.h" #import "BEMBuiltinUserCache.h" -#import "TripDiarySettingsCheck.h" +#import "SensorControlForegroundDelegate.h" #import @implementation BEMDataCollection @@ -65,7 +65,9 @@ - (void)markConsented:(CDVInvokedUrlCommand*)command // which is actually easier ("always allow") // so in that case, we continue calling the init code in TripDiaryStateMachine [self initWithConsent]; + /* [TripDiarySettingsCheck checkMotionSettingsAndPermission:FALSE]; + */ CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; [self.commandDelegate sendPluginResult:result callbackId:callbackId]; @@ -85,6 +87,8 @@ - (void)fixLocationSettings:(CDVInvokedUrlCommand*)command - (void)isValidLocationSettings:(CDVInvokedUrlCommand*)command { + [SensorControlForegroundDelegate checkLocationSettings:self.commandDelegate + forCommand:command]; } - (void)fixLocationPermissions:(CDVInvokedUrlCommand*)command @@ -93,6 +97,9 @@ - (void)fixLocationPermissions:(CDVInvokedUrlCommand*)command - (void)isValidLocationPermissions:(CDVInvokedUrlCommand*)command { + [SensorControlForegroundDelegate checkLocationPermissions:self.commandDelegate + forCommand:command]; + } - (void)fixFitnessPermissions:(CDVInvokedUrlCommand*)command @@ -101,16 +108,14 @@ - (void)fixFitnessPermissions:(CDVInvokedUrlCommand*)command - (void)isValidFitnessPermissions:(CDVInvokedUrlCommand*)command { -} + [SensorControlForegroundDelegate checkMotionActivityPermissions:self.commandDelegate + forCommand:command]; -- (UIUserNotificationSettings*) REQUESTED_NOTIFICATION_TYPES { - return [UIUserNotificationSettings - settingsForTypes:UIUserNotificationTypeAlert|UIUserNotificationTypeBadge - categories:nil]; } - (void)fixShowNotifications:(CDVInvokedUrlCommand*)command { + /* NSString* callbackId = [command callbackId]; @try { if ([UIApplication instancesRespondToSelector:@selector(registerUserNotificationSettings:)]) { @@ -139,34 +144,14 @@ - (void)fixShowNotifications:(CDVInvokedUrlCommand*)command messageAsString:msg]; [self.commandDelegate sendPluginResult:result callbackId:callbackId]; } + */ } - (void)isValidShowNotifications:(CDVInvokedUrlCommand*)command { - NSString* callbackId = [command callbackId]; - @try { - UIUserNotificationSettings* requestedSettings = [self REQUESTED_NOTIFICATION_TYPES]; - UIUserNotificationSettings* currSettings = [[UIApplication sharedApplication] currentUserNotificationSettings]; - if (requestedSettings.types == currSettings.types) { - CDVPluginResult* result = [CDVPluginResult - resultWithStatus:CDVCommandStatus_OK]; - [self.commandDelegate sendPluginResult:result callbackId:callbackId]; - } else { - NSString* msg = NSLocalizedStringFromTable(@"notifications_blocked", @"DCLocalizable", nil); - CDVPluginResult* result = [CDVPluginResult - resultWithStatus:CDVCommandStatus_ERROR - messageAsString:msg]; - [self.commandDelegate sendPluginResult:result callbackId:callbackId]; - } - } - @catch (NSException *exception) { - NSString* msg = [NSString stringWithFormat: @"While getting settings, error %@", exception]; - CDVPluginResult* result = [CDVPluginResult - resultWithStatus:CDVCommandStatus_ERROR - messageAsString:msg]; - [self.commandDelegate sendPluginResult:result callbackId:callbackId]; - } + [SensorControlForegroundDelegate checkNotificationsEnabled:self.commandDelegate + forCommand:command]; } - (void)isNotificationsUnpaused:(CDVInvokedUrlCommand*)command diff --git a/src/ios/BEMRemotePushNotificationHandler.h b/src/ios/BEMRemotePushNotificationHandler.h index 8cf45b4..4163f3c 100644 --- a/src/ios/BEMRemotePushNotificationHandler.h +++ b/src/ios/BEMRemotePushNotificationHandler.h @@ -3,7 +3,6 @@ @interface BEMRemotePushNotificationHandler : NSObject + (BEMRemotePushNotificationHandler*) instance; + (void) performPeriodicActivity; -+ (void) validateAndCleanupState; - (void) handleNotifications:(NSNotification*)note; @property NSMutableArray* silentPushHandlerList; diff --git a/src/ios/BEMRemotePushNotificationHandler.m b/src/ios/BEMRemotePushNotificationHandler.m index bc81b77..ae2ad9f 100644 --- a/src/ios/BEMRemotePushNotificationHandler.m +++ b/src/ios/BEMRemotePushNotificationHandler.m @@ -1,6 +1,6 @@ #import "BEMRemotePushNotificationHandler.h" #import "TripDiaryStateMachine.h" -#import "TripDiarySettingsCheck.h" +#import "SensorControlBackgroundChecker.h" #import "LocalNotificationManager.h" @implementation BEMRemotePushNotificationHandler @@ -100,22 +100,8 @@ - (void)handleNotifications:(NSNotification*)note { + (void) performPeriodicActivity { - [TripDiarySettingsCheck checkSettingsAndPermission]; - [BEMRemotePushNotificationHandler validateAndCleanupState]; -} - -+ (void) validateAndCleanupState -{ - NSUInteger currState = [TripDiaryStateMachine instance].currState; - if (currState == kStartState) { - [LocalNotificationManager addNotification:[NSString stringWithFormat: - @"Still in start state, sending initialize..."] showUI:TRUE]; - [[NSNotificationCenter defaultCenter] postNotificationName:CFCTransitionNotificationName - object:CFCTransitionInitialize]; - } else { - [LocalNotificationManager addNotification:[NSString stringWithFormat: - @"In valid state %@, nothing to do...", [TripDiaryStateMachine getStateName:currState]] showUI:FALSE]; - } + [SensorControlBackgroundChecker checkAppState]; + [SensorControlBackgroundChecker restartFSMIfStartState]; } @end diff --git a/src/ios/Location/TripDiaryDelegate.m b/src/ios/Location/TripDiaryDelegate.m index ca4c58f..a9ffb83 100644 --- a/src/ios/Location/TripDiaryDelegate.m +++ b/src/ios/Location/TripDiaryDelegate.m @@ -15,7 +15,7 @@ #import "LocationTrackingConfig.h" #import "ConfigManager.h" #import "BEMAppDelegate.h" -#import "TripDiarySettingsCheck.h" +#import "SensorControlBackgroundChecker.h" #define ACCURACY_THRESHOLD 200 @@ -228,9 +228,11 @@ - (void)locationManager:(CLLocationManager *)manager // and the background call to checkLocationSettingsAndPermission from the remote push code // doesn't have that reference. Can simplify this after we stop supporting iOS13. if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined) { - [TripDiarySettingsCheck promptForPermission:manager]; + // STATUS SCREEN: handle with checkAndFix + // [TripDiarySettingsCheck promptForPermission:manager]; } else { - [TripDiarySettingsCheck checkLocationSettingsAndPermission:FALSE]; + // STATUS SCREEN: handle with checkAndFix + // [TripDiarySettingsCheck checkLocationSettingsAndPermission:FALSE]; } if (_tdsm.currState == kStartState) { diff --git a/src/ios/Location/TripDiaryStateMachine.m b/src/ios/Location/TripDiaryStateMachine.m index 5d84b75..83ceb56 100644 --- a/src/ios/Location/TripDiaryStateMachine.m +++ b/src/ios/Location/TripDiaryStateMachine.m @@ -9,7 +9,7 @@ #import "TripDiaryStateMachine.h" #import "TripDiaryActions.h" #import "TripDiaryDelegate.h" -#import "TripDiarySettingsCheck.h" +#import "SensorControlBackgroundChecker.h" #import "LocalNotificationManager.h" @@ -99,25 +99,38 @@ - (id) init { // would be good to test, though. } - + // STATUS SCREEN: Figure out how to fix recursion when [TripDiaryStateMachine instance] is called + // [SensorControlBackgroundChecker checkAppState]; + /* The only times we should get here are: + * - if we re-install a previously installed app, and so it is already authorized for background location collection BUT is in the start state, or + * - another option might be a re-launch of the app when the user has manually stopped tracking. + * It would be bad to automatically restart the tracking if the user has manully stopped tracking. + * One way to deal with this would be to have separate states for "start" and for "tracking suspended". + * Another way would be to just remove this transition from here... + * TODO: Figure out how to deal with it. + */ + // STATUS SCREEN: Figure out how to fix recursion when [TripDiaryStateMachine instance] is called + // [SensorControlBackgroundChecker restartFSMIfStartState]; + /* if ([CLLocationManager authorizationStatus] != kCLAuthorizationStatusAuthorizedAlways) { [TripDiarySettingsCheck promptForPermission:self.locMgr]; } else { NSLog(@"Current location authorization = %d, always = %d", [CLLocationManager authorizationStatus], kCLAuthorizationStatusAuthorizedAlways); - /* The only times we should get here are: + The only times we should get here are: * - if we re-install a previously installed app, and so it is already authorized for background location collection BUT is in the start state, or * - another option might be a re-launch of the app when the user has manually stopped tracking. * It would be bad to automatically restart the tracking if the user has manully stopped tracking. * One way to deal with this would be to have separate states for "start" and for "tracking suspended". * Another way would be to just remove this transition from here... * TODO: Figure out how to deal with it. - */ + if (self.currState == kStartState) { [[NSNotificationCenter defaultCenter] postNotificationName:CFCTransitionNotificationName object:CFCTransitionInitialize]; } } + */ if (![ConfigManager instance].is_duty_cycling && self.currState != kTrackingStoppedState) { /* If we are not using geofencing, and the tracking is not manually turned off, then we don't need to listen @@ -203,6 +216,7 @@ -(void)setState:(TripDiaryStates) newState { [TripDiaryStateMachine getStateName:newState]]]; self.currState = newState; + [SensorControlBackgroundChecker checkAppState]; } /* @@ -248,7 +262,7 @@ -(void) handleStart:(NSString*) transition withUserInfo:(NSDictionary*) userInfo transition, [TripDiaryStateMachine getStateName:self.currState], [TripDiaryStateMachine getStateName:self.currState]); - [TripDiarySettingsCheck checkSettingsAndPermission]; + [SensorControlBackgroundChecker checkAppState]; } else if ([transition isEqualToString:CFCTransitionExitedGeofence]) { [TripDiaryActions startTracking:transition withLocationMgr:self.locMgr]; [TripDiaryActions deleteGeofence:self.locMgr]; @@ -397,8 +411,8 @@ - (void) handleOngoingTrip:(NSString*) transition withUserInfo:(NSDictionary*) u return nil; }]; } else if ([transition isEqualToString:CFCTransitionGeofenceCreationError]) { + // setState will call SensorControlBackgroundChecker checkAppState by default [self setState:kStartState]; - [TripDiarySettingsCheck checkSettingsAndPermission]; } else if ([transition isEqualToString:CFCTransitionForceStopTracking]) { [TripDiaryActions resetFSM:transition withLocationMgr:self.locMgr]; } else if ([transition isEqualToString:CFCTransitionTrackingStopped]) { diff --git a/src/ios/Verification/SensorControlBackgroundChecker.h b/src/ios/Verification/SensorControlBackgroundChecker.h new file mode 100644 index 0000000..7c56502 --- /dev/null +++ b/src/ios/Verification/SensorControlBackgroundChecker.h @@ -0,0 +1,11 @@ +#import +#import +#import +#import + +@interface SensorControlBackgroundChecker: NSObject + ++(void)restartFSMIfStartState; ++(void)checkAppState; + +@end diff --git a/src/ios/Verification/SensorControlBackgroundChecker.m b/src/ios/Verification/SensorControlBackgroundChecker.m new file mode 100644 index 0000000..2940bd9 --- /dev/null +++ b/src/ios/Verification/SensorControlBackgroundChecker.m @@ -0,0 +1,90 @@ +#import "SensorControlBackgroundChecker.h" +#import "TripDiarySensorControlChecks.h" +#import "TripDiaryStateMachine.h" +#import "LocalNotificationManager.h" +#import "BEMAppDelegate.h" +#import "BEMActivitySync.h" + +#import +#define OPEN_APP_STATUS_PAGE_ID @362253744 + +@implementation SensorControlBackgroundChecker + ++(NSDictionary*)OPEN_APP_STATUS_PAGE +{ + NSDictionary* config = @{ + @"id": OPEN_APP_STATUS_PAGE_ID, + @"title": NSLocalizedStringFromTable(@"fix_app_status_title", @"DCLocalizable", nil), + @"text": NSLocalizedStringFromTable(@"fix_app_status_text", @"DCLocalizable", nil), + @"data": @{ + @"redirectTo": @"root.main.control", + @"redirectParams": @{ + @"launchAppStatusModal": @true + } + } + }; + return config; +} + ++(void)restartFSMIfStartState +{ + NSUInteger currState = [TripDiaryStateMachine instance].currState; + if (currState == kStartState) { + [LocalNotificationManager addNotification:[NSString stringWithFormat:@"Still in start state, sending initialize..."] showUI:TRUE]; + [[NSNotificationCenter defaultCenter] postNotificationName:CFCTransitionNotificationName + object:CFCTransitionInitialize]; + } else { + [LocalNotificationManager addNotification:[NSString stringWithFormat:@"In valid state %@, nothing to do...", [TripDiaryStateMachine getStateName:currState]] showUI:FALSE]; + } +} + ++(void)checkAppState +{ + [LocalNotificationManager cancelNotification:OPEN_APP_STATUS_PAGE_ID]; + + NSArray* allChecks = @[ + @([TripDiarySensorControlChecks checkLocationSettings]), + @([TripDiarySensorControlChecks checkLocationPermissions]), + @([TripDiarySensorControlChecks checkMotionActivitySettings]), + @([TripDiarySensorControlChecks checkMotionActivityPermissions]), + @([TripDiarySensorControlChecks checkNotificationsEnabled]) + ]; + BOOL allChecksPass = true; + for (id check in allChecks) { + allChecksPass = allChecksPass && check; + } + + BOOL locChecksPass = allChecks[0] && allChecks[1]; + + if (allChecksPass) { + [LocalNotificationManager addNotification:[NSString stringWithFormat:@"All settings valid, nothing to prompt"]]; + [self restartFSMIfStartState]; + } + else if (locChecksPass) { + /* + Log.i(ctxt, TAG, "all checks = "+allOtherChecksPass+" but location permission status "+allOtherChecks[0]+" should be true "+ + " so one of the non-location checks must be false: loc permission, motion permission, notification, unused apps" + Arrays.toString(allOtherChecks)); + Log.i(ctxt, TAG, "a non-local check failed, generating only user visible notification"); + */ + [self generateOpenAppSettingsNotification]; + } + else { + /* + Log.i(ctxt, TAG, "location settings are valid, but location permission is not, generating tracking error and visible notification"); + Log.i(ctxt, TAG, "curr status check results = " + + " loc permission, motion permission, notification, unused apps "+ Arrays.toString(allOtherChecks)); + */ + // Should replace with TRACKING_ERROR but looks like we + // don't have any + [[NSNotificationCenter defaultCenter] + postNotificationName:CFCTransitionNotificationName + object:CFCTransitionGeofenceCreationError]; + [self generateOpenAppSettingsNotification]; + } +} + ++(void)generateOpenAppSettingsNotification +{ + [LocalNotificationManager schedulePluginCompatibleNotification:[self OPEN_APP_STATUS_PAGE] withNewData:NULL]; +} +@end diff --git a/src/ios/Verification/SensorControlForegroundDelegate.h b/src/ios/Verification/SensorControlForegroundDelegate.h new file mode 100644 index 0000000..6c59573 --- /dev/null +++ b/src/ios/Verification/SensorControlForegroundDelegate.h @@ -0,0 +1,15 @@ +#import +#import +#import +#import +#import + +@interface SensorControlForegroundDelegate: NSObject + ++(void)checkLocationSettings:(id)delegate forCommand:(CDVInvokedUrlCommand*)command; ++(void)checkLocationPermissions:(id)delegate forCommand:(CDVInvokedUrlCommand*)command; ++(void)checkMotionActivitySettings:(id)delegate +forCommand:(CDVInvokedUrlCommand*)command; ++(void)checkMotionActivityPermissions:(id)delegate forCommand:(CDVInvokedUrlCommand*)command; ++(void)checkNotificationsEnabled:(id)delegate forCommand:(CDVInvokedUrlCommand*)command; +@end diff --git a/src/ios/Verification/SensorControlForegroundDelegate.m b/src/ios/Verification/SensorControlForegroundDelegate.m new file mode 100644 index 0000000..47c5c01 --- /dev/null +++ b/src/ios/Verification/SensorControlForegroundDelegate.m @@ -0,0 +1,82 @@ +#import "SensorControlForegroundDelegate.h" +#import "TripDiarySensorControlChecks.h" +#import "LocalNotificationManager.h" +#import "BEMAppDelegate.h" +#import "BEMActivitySync.h" + +#import + +@implementation SensorControlForegroundDelegate +// typedef BOOL (*CheckFnType)(void); + ++(void) sendCheckResult:(BOOL)result + forDelegate:(id) commandDelegate + forCommand:(CDVInvokedUrlCommand*)command + errorKey:(NSString*)localizableErrorKey +{ + NSString* callbackId = [command callbackId]; + @try { + if (result) { + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_OK]; + [commandDelegate sendPluginResult:result callbackId:callbackId]; + } else { + NSString* msg = NSLocalizedStringFromTable(localizableErrorKey, @"DCLocalizable", nil); + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_ERROR + messageAsString:msg]; + [commandDelegate sendPluginResult:result callbackId:callbackId]; + } + } + @catch (NSException *exception) { + NSString* msg = [NSString stringWithFormat: @"While getting settings, error %@", exception]; + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_ERROR + messageAsString:msg]; + [commandDelegate sendPluginResult:result callbackId:callbackId]; + } +} + ++(void)checkLocationSettings:(id)delegate forCommand:(CDVInvokedUrlCommand*)command +{ + BOOL result = [TripDiarySensorControlChecks checkLocationSettings]; + [self sendCheckResult:result + forDelegate:delegate forCommand:command + errorKey:@"location_not_enabled"]; +} + ++(void)checkLocationPermissions:(id)delegate forCommand:(CDVInvokedUrlCommand*)command +{ + BOOL result = [TripDiarySensorControlChecks checkLocationPermissions]; + [self sendCheckResult:result + forDelegate:delegate forCommand:command + errorKey:@"location_permission_off"]; +} + ++(void)checkMotionActivitySettings:(id)delegate forCommand:(CDVInvokedUrlCommand*)command +{ + BOOL result = [TripDiarySensorControlChecks checkMotionActivitySettings]; + [self sendCheckResult:result + forDelegate:delegate forCommand:command + errorKey:@"activity_settings_off"]; +} + + ++(void)checkMotionActivityPermissions:(id)delegate forCommand:(CDVInvokedUrlCommand*)command +{ + BOOL result = [TripDiarySensorControlChecks checkMotionActivityPermissions]; + [self sendCheckResult:result + forDelegate:delegate forCommand:command + errorKey:@"activity_permission_off"]; +} + ++(void)checkNotificationsEnabled:(id)delegate forCommand:(CDVInvokedUrlCommand*)command +{ + BOOL result = [TripDiarySensorControlChecks checkNotificationsEnabled]; + [self sendCheckResult:result + forDelegate:delegate forCommand:command + errorKey:@"notifications_blocked"]; +} + + +@end diff --git a/src/ios/Verification/TripDiarySensorControlChecks.h b/src/ios/Verification/TripDiarySensorControlChecks.h index df0f710..e951dea 100644 --- a/src/ios/Verification/TripDiarySensorControlChecks.h +++ b/src/ios/Verification/TripDiarySensorControlChecks.h @@ -3,7 +3,13 @@ #import #import -@interface TripDiarySettingsCheck: NSObject +@interface TripDiarySensorControlChecks: NSObject + ++(BOOL)checkLocationSettings; ++(BOOL)checkLocationPermissions; ++(BOOL)checkMotionActivitySettings; ++(BOOL)checkMotionActivityPermissions; ++(BOOL)checkNotificationsEnabled; +(void)checkSettingsAndPermission; +(void)checkLocationSettingsAndPermission:(BOOL)inBackground; diff --git a/src/ios/Verification/TripDiarySensorControlChecks.m b/src/ios/Verification/TripDiarySensorControlChecks.m index b367dc7..522de69 100644 --- a/src/ios/Verification/TripDiarySensorControlChecks.m +++ b/src/ios/Verification/TripDiarySensorControlChecks.m @@ -1,12 +1,54 @@ -#import "TripDiarySettingsCheck.h" +#import "TripDiarySensorControlChecks.h" #import "LocalNotificationManager.h" #import "BEMAppDelegate.h" #import "BEMActivitySync.h" #import -@implementation TripDiarySettingsCheck +@implementation TripDiarySensorControlChecks ++(BOOL)checkLocationSettings { + return [CLLocationManager locationServicesEnabled]; +} + +// TODO: Decide whether we want to have a separate check for precise + ++(BOOL)checkLocationPermissions { + BOOL alwaysPerm = [CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorizedAlways; + BOOL precisePerm = TRUE; + CLLocationManager* currLocMgr = [TripDiaryStateMachine instance].locMgr; + if (@available(iOS 14.0, *)) { + CLAccuracyAuthorization preciseOrNot = [currLocMgr accuracyAuthorization]; + precisePerm = preciseOrNot == CLAccuracyAuthorizationFullAccuracy; + } else { + [LocalNotificationManager addNotification:[NSString stringWithFormat:@"No precise location check needed for iOS < 14"]]; + } + [LocalNotificationManager addNotification:[NSString stringWithFormat:@"Returning combination of always = %@ and precise %@", @(alwaysPerm), @(precisePerm)]]; + return alwaysPerm && precisePerm; +} + ++(BOOL)checkMotionActivitySettings { + return [CMMotionActivityManager isActivityAvailable] == YES; +} + ++(BOOL)checkMotionActivityPermissions { + CMAuthorizationStatus currAuthStatus = [CMMotionActivityManager authorizationStatus]; + return currAuthStatus == CMAuthorizationStatusAuthorized; +} + ++(BOOL)checkNotificationsEnabled { + UIUserNotificationSettings* requestedSettings = [self REQUESTED_NOTIFICATION_TYPES]; + UIUserNotificationSettings* currSettings = [[UIApplication sharedApplication] currentUserNotificationSettings]; + return requestedSettings.types == currSettings.types; +} + ++(UIUserNotificationSettings*) REQUESTED_NOTIFICATION_TYPES { + return [UIUserNotificationSettings + settingsForTypes:UIUserNotificationTypeAlert|UIUserNotificationTypeBadge + categories:nil]; +} + +/* +(void)checkSettingsAndPermission { [TripDiarySettingsCheck checkLocationSettingsAndPermission:TRUE]; [TripDiarySettingsCheck checkMotionSettingsAndPermission:TRUE]; @@ -139,5 +181,6 @@ +(void) showSettingsAlert:(UIAlertController*)alert { CDVViewController *vc = ad.viewController; [vc presentViewController:alert animated:YES completion:nil]; } +*/ @end From b7ff5d6a3ee9df7b1cb7572f25a6625792a9d68d Mon Sep 17 00:00:00 2001 From: Shankari Date: Thu, 10 Feb 2022 22:46:05 -0800 Subject: [PATCH 28/35] Change the foreground delegate from a static class to an instance So that we can keep track of state for various callbacks. This is also consistent with the android implementation. Testing done: - Launched app, which exercised the plugin -> delegate -> checks code path. - Clicked on "fix", confirmed that the corresponding method was invoked --- res/ios/en.lproj/DCLocalizable.strings | 2 +- src/ios/BEMDataCollection.m | 20 +++--- .../SensorControlForegroundDelegate.h | 15 +++-- .../SensorControlForegroundDelegate.m | 62 +++++++++++++++---- 4 files changed, 70 insertions(+), 29 deletions(-) diff --git a/res/ios/en.lproj/DCLocalizable.strings b/res/ios/en.lproj/DCLocalizable.strings index 825d3eb..790ae99 100644 --- a/res/ios/en.lproj/DCLocalizable.strings +++ b/res/ios/en.lproj/DCLocalizable.strings @@ -7,7 +7,7 @@ "activity-turned-off-problem" = "Motion & Fitness Service disabled - automatic mode detection will not work. Turn it on (Settings -> Privacy)"; "travel-mode-unknown" = "Travel mode detection unavailable - all trips will be UNKNOWN."; "bad-loc-tracking-problem" = "Background location accuracy is consistently poor - trip tracking may not work. Report problem."; -"location-turned-off-problem" = "Location Services are turned off - trip tracking will not work. Turn it on (Settings -> Privacy)"; +"location-turned-off-problem" = "Location Services are turned off. Turn it on (Settings -> Privacy)"; "fix-service-action-button" = "Launch Settings"; "permission-problem-title" = "Incorrect permission"; "location-permission-problem" = "The app does not have the 'always' permission - background trip tracking will not work."; diff --git a/src/ios/BEMDataCollection.m b/src/ios/BEMDataCollection.m index 08f0828..d80f790 100644 --- a/src/ios/BEMDataCollection.m +++ b/src/ios/BEMDataCollection.m @@ -83,12 +83,15 @@ - (void)markConsented:(CDVInvokedUrlCommand*)command - (void)fixLocationSettings:(CDVInvokedUrlCommand*)command { + [[[SensorControlForegroundDelegate alloc] initWithDelegate:self.commandDelegate + forCommand:command] checkAndPromptLocationSettings]; } - (void)isValidLocationSettings:(CDVInvokedUrlCommand*)command { - [SensorControlForegroundDelegate checkLocationSettings:self.commandDelegate - forCommand:command]; + [[[SensorControlForegroundDelegate alloc] initWithDelegate:self.commandDelegate + forCommand:command] checkLocationSettings]; + } - (void)fixLocationPermissions:(CDVInvokedUrlCommand*)command @@ -97,9 +100,8 @@ - (void)fixLocationPermissions:(CDVInvokedUrlCommand*)command - (void)isValidLocationPermissions:(CDVInvokedUrlCommand*)command { - [SensorControlForegroundDelegate checkLocationPermissions:self.commandDelegate - forCommand:command]; - + [[[SensorControlForegroundDelegate alloc] initWithDelegate:self.commandDelegate + forCommand:command] checkLocationPermissions]; } - (void)fixFitnessPermissions:(CDVInvokedUrlCommand*)command @@ -108,8 +110,8 @@ - (void)fixFitnessPermissions:(CDVInvokedUrlCommand*)command - (void)isValidFitnessPermissions:(CDVInvokedUrlCommand*)command { - [SensorControlForegroundDelegate checkMotionActivityPermissions:self.commandDelegate - forCommand:command]; + [[[SensorControlForegroundDelegate alloc] initWithDelegate:self.commandDelegate + forCommand:command] checkMotionActivityPermissions]; } @@ -150,8 +152,8 @@ - (void)fixShowNotifications:(CDVInvokedUrlCommand*)command - (void)isValidShowNotifications:(CDVInvokedUrlCommand*)command { - [SensorControlForegroundDelegate checkNotificationsEnabled:self.commandDelegate - forCommand:command]; + [[[SensorControlForegroundDelegate alloc] initWithDelegate:self.commandDelegate + forCommand:command] checkNotificationsEnabled]; } - (void)isNotificationsUnpaused:(CDVInvokedUrlCommand*)command diff --git a/src/ios/Verification/SensorControlForegroundDelegate.h b/src/ios/Verification/SensorControlForegroundDelegate.h index 6c59573..70c2d50 100644 --- a/src/ios/Verification/SensorControlForegroundDelegate.h +++ b/src/ios/Verification/SensorControlForegroundDelegate.h @@ -6,10 +6,13 @@ @interface SensorControlForegroundDelegate: NSObject -+(void)checkLocationSettings:(id)delegate forCommand:(CDVInvokedUrlCommand*)command; -+(void)checkLocationPermissions:(id)delegate forCommand:(CDVInvokedUrlCommand*)command; -+(void)checkMotionActivitySettings:(id)delegate -forCommand:(CDVInvokedUrlCommand*)command; -+(void)checkMotionActivityPermissions:(id)delegate forCommand:(CDVInvokedUrlCommand*)command; -+(void)checkNotificationsEnabled:(id)delegate forCommand:(CDVInvokedUrlCommand*)command; +- (id)initWithDelegate:(id)delegate forCommand:(CDVInvokedUrlCommand*)command;; +- (void)checkLocationSettings; +- (void)checkLocationPermissions; +- (void)checkMotionActivitySettings; +- (void)checkMotionActivityPermissions; +- (void)checkNotificationsEnabled; + +- (void) checkAndPromptLocationSettings; +- (void) checkAndPromptLocationPermissions; @end diff --git a/src/ios/Verification/SensorControlForegroundDelegate.m b/src/ios/Verification/SensorControlForegroundDelegate.m index 47c5c01..540532c 100644 --- a/src/ios/Verification/SensorControlForegroundDelegate.m +++ b/src/ios/Verification/SensorControlForegroundDelegate.m @@ -1,17 +1,27 @@ #import "SensorControlForegroundDelegate.h" #import "TripDiarySensorControlChecks.h" +#import "SensorControlBackgroundChecker.h" #import "LocalNotificationManager.h" #import "BEMAppDelegate.h" #import "BEMActivitySync.h" #import +@interface SensorControlForegroundDelegate() { + id commandDelegate; + CDVInvokedUrlCommand* command; +} +@end + @implementation SensorControlForegroundDelegate + +/* + * BEGIN: "check" implementations + */ + // typedef BOOL (*CheckFnType)(void); -+(void) sendCheckResult:(BOOL)result - forDelegate:(id) commandDelegate - forCommand:(CDVInvokedUrlCommand*)command +-(void) sendCheckResult:(BOOL)result errorKey:(NSString*)localizableErrorKey { NSString* callbackId = [command callbackId]; @@ -37,46 +47,72 @@ +(void) sendCheckResult:(BOOL)result } } -+(void)checkLocationSettings:(id)delegate forCommand:(CDVInvokedUrlCommand*)command +-(id)initWithDelegate:(id)delegate + forCommand:(CDVInvokedUrlCommand *)command +{ + self->command = command; + self->commandDelegate = delegate; + return [super init]; +} + + +-(void)checkLocationSettings { BOOL result = [TripDiarySensorControlChecks checkLocationSettings]; [self sendCheckResult:result - forDelegate:delegate forCommand:command errorKey:@"location_not_enabled"]; } -+(void)checkLocationPermissions:(id)delegate forCommand:(CDVInvokedUrlCommand*)command +-(void)checkLocationPermissions { BOOL result = [TripDiarySensorControlChecks checkLocationPermissions]; [self sendCheckResult:result - forDelegate:delegate forCommand:command errorKey:@"location_permission_off"]; } -+(void)checkMotionActivitySettings:(id)delegate forCommand:(CDVInvokedUrlCommand*)command +-(void)checkMotionActivitySettings { BOOL result = [TripDiarySensorControlChecks checkMotionActivitySettings]; [self sendCheckResult:result - forDelegate:delegate forCommand:command errorKey:@"activity_settings_off"]; } -+(void)checkMotionActivityPermissions:(id)delegate forCommand:(CDVInvokedUrlCommand*)command +-(void)checkMotionActivityPermissions { BOOL result = [TripDiarySensorControlChecks checkMotionActivityPermissions]; [self sendCheckResult:result - forDelegate:delegate forCommand:command errorKey:@"activity_permission_off"]; } -+(void)checkNotificationsEnabled:(id)delegate forCommand:(CDVInvokedUrlCommand*)command +-(void)checkNotificationsEnabled { BOOL result = [TripDiarySensorControlChecks checkNotificationsEnabled]; [self sendCheckResult:result - forDelegate:delegate forCommand:command errorKey:@"notifications_blocked"]; } +/* + * END: "check" implementations + */ + +/* + * BEGIN: "fix" implementations + */ +/* + * In iOS, we cannot open overall settings, only the app settings. + * Should we open it anyway? Or just tell the user what to do? + * If we open it, we don't appear to get any callbacks when the location services are enabled. + * there are callbacks only for the auth/permission changes. + * https://developer.apple.com/documentation/corelocation/cllocationmanagerdelegate?language=objc + * So let's just prompt the user. Which means that we can reuse the check with a different error message + */ + +-(void) checkAndPromptLocationSettings +{ + BOOL result = [TripDiarySensorControlChecks checkLocationSettings]; + [self sendCheckResult:result + errorKey:@"location-turned-off-problem"]; +} @end From ca979bd85f585161cdb387687bbd2acba4f3cb85 Mon Sep 17 00:00:00 2001 From: Shankari Date: Fri, 11 Feb 2022 23:06:15 -0800 Subject: [PATCH 29/35] Finish the full cycle for the location permissions - create a new category for the TripDiaryDelegate in the ForegroundDelegate - since the trip diary state manager is singleton, its delegate is also a singleton - expose the singleton delegate - register the foreground delegate as a callback to the TripDiaryDelegate from foreground calls - if the callback is for a foreground call, return the appropriate response from the plugin - if not, check the settings and generate a notification Includes fixes to the boolean checks (using @(NO) in a boolean check always returns TRUE) Includes copying over the `promptForPermissions` and `openAppStatus` methods. Related design decisions: - https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1035972636 - https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1035976420 - https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1035984060 - https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1036744874 --- src/ios/BEMDataCollection.m | 2 + src/ios/Location/TripDiaryStateMachine.h | 1 + src/ios/Location/TripDiaryStateMachine.m | 5 + .../SensorControlBackgroundChecker.m | 10 +- .../SensorControlForegroundDelegate.h | 9 ++ .../SensorControlForegroundDelegate.m | 124 ++++++++++++++++++ .../TripDiarySensorControlChecks.m | 54 +------- 7 files changed, 147 insertions(+), 58 deletions(-) diff --git a/src/ios/BEMDataCollection.m b/src/ios/BEMDataCollection.m index d80f790..becf412 100644 --- a/src/ios/BEMDataCollection.m +++ b/src/ios/BEMDataCollection.m @@ -96,6 +96,8 @@ - (void)isValidLocationSettings:(CDVInvokedUrlCommand*)command - (void)fixLocationPermissions:(CDVInvokedUrlCommand*)command { + [[[SensorControlForegroundDelegate alloc] initWithDelegate:self.commandDelegate + forCommand:command] checkAndPromptLocationPermissions]; } - (void)isValidLocationPermissions:(CDVInvokedUrlCommand*)command diff --git a/src/ios/Location/TripDiaryStateMachine.h b/src/ios/Location/TripDiaryStateMachine.h index 0d65e64..2dd99de 100644 --- a/src/ios/Location/TripDiaryStateMachine.h +++ b/src/ios/Location/TripDiaryStateMachine.h @@ -60,6 +60,7 @@ typedef void(^GeofenceStatusCallback)(NSString* geofenceStatus); @interface TripDiaryStateMachine : NSObject + (TripDiaryStateMachine*) instance; ++ (id) delegate; -(void)registerForNotifications; diff --git a/src/ios/Location/TripDiaryStateMachine.m b/src/ios/Location/TripDiaryStateMachine.m index 83ceb56..8eccb67 100644 --- a/src/ios/Location/TripDiaryStateMachine.m +++ b/src/ios/Location/TripDiaryStateMachine.m @@ -62,6 +62,11 @@ + (TripDiaryStateMachine*) instance { return sharedInstance; } ++ (TripDiaryDelegate*) delegate { + return [self instance]->_locDelegate; +} + + - (id) init { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; diff --git a/src/ios/Verification/SensorControlBackgroundChecker.m b/src/ios/Verification/SensorControlBackgroundChecker.m index 2940bd9..51aad2a 100644 --- a/src/ios/Verification/SensorControlBackgroundChecker.m +++ b/src/ios/Verification/SensorControlBackgroundChecker.m @@ -42,19 +42,19 @@ +(void)checkAppState { [LocalNotificationManager cancelNotification:OPEN_APP_STATUS_PAGE_ID]; - NSArray* allChecks = @[ + NSArray* allChecks = @[ @([TripDiarySensorControlChecks checkLocationSettings]), @([TripDiarySensorControlChecks checkLocationPermissions]), @([TripDiarySensorControlChecks checkMotionActivitySettings]), @([TripDiarySensorControlChecks checkMotionActivityPermissions]), @([TripDiarySensorControlChecks checkNotificationsEnabled]) ]; - BOOL allChecksPass = true; - for (id check in allChecks) { - allChecksPass = allChecksPass && check; + BOOL allChecksPass = TRUE; + for (NSNumber* check in allChecks) { + allChecksPass = allChecksPass && check.boolValue; } - BOOL locChecksPass = allChecks[0] && allChecks[1]; + BOOL locChecksPass = allChecks[0].boolValue && allChecks[1].boolValue; if (allChecksPass) { [LocalNotificationManager addNotification:[NSString stringWithFormat:@"All settings valid, nothing to prompt"]]; diff --git a/src/ios/Verification/SensorControlForegroundDelegate.h b/src/ios/Verification/SensorControlForegroundDelegate.h index 70c2d50..4276d9d 100644 --- a/src/ios/Verification/SensorControlForegroundDelegate.h +++ b/src/ios/Verification/SensorControlForegroundDelegate.h @@ -3,6 +3,7 @@ #import #import #import +#import "TripDiaryDelegate.h" @interface SensorControlForegroundDelegate: NSObject @@ -15,4 +16,12 @@ - (void) checkAndPromptLocationSettings; - (void) checkAndPromptLocationPermissions; +- (void) didChangeAuthorizationStatus:(CLAuthorizationStatus)status; +@end + +@interface TripDiaryDelegate (TripDiaryDelegatePermissions) +- (void)registerForegroundDelegate:(SensorControlForegroundDelegate*) foregroundDelegate; +- (void)locationManager:(CLLocationManager *)manager + didChangeAuthorizationStatus:(CLAuthorizationStatus)status; + @end diff --git a/src/ios/Verification/SensorControlForegroundDelegate.m b/src/ios/Verification/SensorControlForegroundDelegate.m index 540532c..eda5e54 100644 --- a/src/ios/Verification/SensorControlForegroundDelegate.m +++ b/src/ios/Verification/SensorControlForegroundDelegate.m @@ -115,4 +115,128 @@ -(void) checkAndPromptLocationSettings [self sendCheckResult:result errorKey:@"location-turned-off-problem"]; } + +-(void) checkAndPromptLocationPermissions +{ + NSString* callbackId = [command callbackId]; + @try { + BOOL result = [TripDiarySensorControlChecks checkLocationPermissions]; + + [LocalNotificationManager addNotification:[NSString stringWithFormat:@"in checkLocationSettingsAndPermissions, locationService is enabled, but the permission is %d", [CLLocationManager authorizationStatus]]]; + + if (result) { + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_OK]; + [commandDelegate sendPluginResult:result callbackId:callbackId]; + } else { + [self promptForPermission:[TripDiaryStateMachine instance].locMgr]; + } + } + @catch (NSException *exception) { + NSString* msg = [NSString stringWithFormat: @"While getting settings, error %@", exception]; + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_ERROR + messageAsString:msg]; + [commandDelegate sendPluginResult:result callbackId:callbackId]; + } +} + +- (void) didChangeAuthorizationStatus:(CLAuthorizationStatus)status +{ + NSString* callbackId = [command callbackId]; + @try { + if (status == kCLAuthorizationStatusAuthorizedAlways) { + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_OK]; + [commandDelegate sendPluginResult:result callbackId:callbackId]; + } else { + NSString* msg = NSLocalizedStringFromTable(@"location_permission_off_app_open", @"DCLocalizable", nil); + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_ERROR + messageAsString:msg]; + [commandDelegate sendPluginResult:result callbackId:callbackId]; + } + } + @catch (NSException *exception) { + NSString* msg = [NSString stringWithFormat: @"While handling auth callback, error %@", exception]; + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_ERROR + messageAsString:msg]; + [commandDelegate sendPluginResult:result callbackId:callbackId]; + } + +} + +-(void)promptForPermission:(CLLocationManager*)locMgr { + if (IsAtLeastiOSVersion(@"13.0")) { + NSLog(@"iOS 13+ detected, launching UI settings to easily enable always"); + // we want to leave the registration in the prompt for permission, since we don't want to register callbacks when we open the app settings for other reasons + [[TripDiaryStateMachine delegate] registerForegroundDelegate:self]; + [self openAppSettings]; + } + else { + if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined) { + if ([CLLocationManager instancesRespondToSelector:@selector(requestAlwaysAuthorization)]) { + NSLog(@"Current location authorization = %d, always = %d, requesting always", + [CLLocationManager authorizationStatus], kCLAuthorizationStatusAuthorizedAlways); + [locMgr requestAlwaysAuthorization]; + } else { + // TODO: should we remove this? Not sure when it will ever be called, given that + // requestAlwaysAuthorization is available in iOS8+ + [LocalNotificationManager addNotification:@"Don't need to request authorization, system will automatically prompt for it"]; + } + } else { + // we want to leave the registration in the prompt for permission, since we don't want to register callbacks when we open the app settings for other reasons + [[TripDiaryStateMachine delegate] registerForegroundDelegate:self]; + [self openAppSettings]; + } + } +} + +-(void) openAppSettings { + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:^(BOOL success) { + if (success) { + NSLog(@"Opened url"); + } else { + NSLog(@"Failed open"); + }}]; +} + +@end + +@implementation TripDiaryDelegate (TripDiaryDelegatePermissions) + +NSMutableArray* foregroundDelegateList; + +/* + * This is a bit tricky since this function is called whenever the authorization is changed + * Design decisions are at: + * https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1035972636 + * https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1035976420 + * https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1035984060 + */ + +- (void)registerForegroundDelegate:(SensorControlForegroundDelegate*) foregroundDelegate +{ + if (foregroundDelegateList == nil) { + foregroundDelegateList = [NSMutableArray new]; + } + [foregroundDelegateList addObject:foregroundDelegate]; +} + +- (void)locationManager:(CLLocationManager *)manager + didChangeAuthorizationStatus:(CLAuthorizationStatus)status { + [LocalNotificationManager addNotification:[NSString stringWithFormat:@"In checker's didChangeAuthorizationStatus, new authorization status = %d, always = %d", status, kCLAuthorizationStatusAuthorizedAlways]]; + + [LocalNotificationManager addNotification:[NSString stringWithFormat:@"Calling TripDiarySettingsCheck from didChangeAuthorizationStatus to verify location service status and permission"]]; + if (foregroundDelegateList.count > 0) { + for (id currDelegate in foregroundDelegateList) { + [currDelegate didChangeAuthorizationStatus:(CLAuthorizationStatus)status]; + } + [foregroundDelegateList removeAllObjects]; + } else { + [SensorControlBackgroundChecker checkAppState]; + } +} + @end diff --git a/src/ios/Verification/TripDiarySensorControlChecks.m b/src/ios/Verification/TripDiarySensorControlChecks.m index 522de69..b053210 100644 --- a/src/ios/Verification/TripDiarySensorControlChecks.m +++ b/src/ios/Verification/TripDiarySensorControlChecks.m @@ -48,65 +48,13 @@ +(UIUserNotificationSettings*) REQUESTED_NOTIFICATION_TYPES { categories:nil]; } + /* +(void)checkSettingsAndPermission { [TripDiarySettingsCheck checkLocationSettingsAndPermission:TRUE]; [TripDiarySettingsCheck checkMotionSettingsAndPermission:TRUE]; } -+(void)checkLocationSettingsAndPermission:(BOOL)inBackground { - if (![CLLocationManager locationServicesEnabled]) { - // first, check to see if location services are enabled - [LocalNotificationManager addNotification:[NSString stringWithFormat:@"in checkLocationSettingsAndPermissions, locationService is not enabled"]]; - - NSString* errorDescription = NSLocalizedStringFromTable(@"location-turned-off-problem", @"DCLocalizable", nil); - if (inBackground) { - [LocalNotificationManager showNotificationAfterSecs:errorDescription withUserInfo:NULL secsLater:60]; - } - } else { - // next, check to see if it is "always" - if ([CLLocationManager authorizationStatus] != kCLAuthorizationStatusAuthorizedAlways) { - [LocalNotificationManager addNotification:[NSString stringWithFormat:@"in checkLocationSettingsAndPermissions, locationService is enabled, but the permission is %d", [CLLocationManager authorizationStatus]]]; - - NSString* errorDescription = NSLocalizedStringFromTable(@"location-permission-problem", @"DCLocalizable", nil); - if (inBackground) { - [LocalNotificationManager showNotificationAfterSecs:errorDescription withUserInfo:NULL secsLater:60]; - } - [TripDiarySettingsCheck showLaunchSettingsAlert:@"permission-problem-title" withMessage:@"location-permission-problem" button:@"fix-permission-action-button"]; - } else { - // finally, check to see if it is "precise" - // we currently check these in a cascade, since generating multiple alerts results in - // "Attempt to present on (from ) which is already presenting ." - CLLocationManager* currLocMgr = [TripDiaryStateMachine instance].locMgr; - if (@available(iOS 14.0, *)) { - CLAccuracyAuthorization preciseOrNot = [currLocMgr accuracyAuthorization]; - [LocalNotificationManager addNotification:[NSString stringWithFormat:@"in checkLocationSettingsAndPermissions, locationService is enabled, permission is 'always', accuracy status is %ld", preciseOrNot]]; - if (preciseOrNot != CLAccuracyAuthorizationFullAccuracy) { - [TripDiarySettingsCheck showLaunchSettingsAlert:@"permission-problem-title" withMessage:@"precise-location-problem" button:@"fix-permission-action-button"]; - } - } else { - [LocalNotificationManager addNotification:[NSString stringWithFormat:@"No precise location check needed for iOS < 14"]]; - } - } - } -} - -+(void)promptForPermission:(CLLocationManager*)locMgr { - if (IsAtLeastiOSVersion(@"13.0")) { - NSLog(@"iOS 13+ detected, launching UI settings to easily enable always"); - [TripDiarySettingsCheck openAppSettings]; - } - else if ([CLLocationManager instancesRespondToSelector:@selector(requestAlwaysAuthorization)]) { - NSLog(@"Current location authorization = %d, always = %d, requesting always", - [CLLocationManager authorizationStatus], kCLAuthorizationStatusAuthorizedAlways); - [locMgr requestAlwaysAuthorization]; - } else { - // TODO: should we remove this? Not sure when it will ever be called, given that - // requestAlwaysAuthorization is available in iOS8+ - [LocalNotificationManager addNotification:@"Don't need to request authorization, system will automatically prompt for it"]; - } -} - +(void)checkMotionSettingsAndPermission:(BOOL)inBackground { if ([CMMotionActivityManager isActivityAvailable] == YES) { [LocalNotificationManager addNotification:@"Motion activity available, checking auth status"]; From c2c58bef0815d50e5e766aaaf13cdc061788ea2b Mon Sep 17 00:00:00 2001 From: Shankari Date: Tue, 15 Feb 2022 10:35:22 -0800 Subject: [PATCH 30/35] Avoid infinite loop by adding a parameter on whether or not the status should be recomputed The current code has an infinite loop if there is a problem with the location settings or permission. In that case, we generate a `CFCTransitionGeofenceCreationError` ``` else { /* Log.i(ctxt, TAG, "location settings are valid, but location permission is not, generating tracking error and visible notification"); Log.i(ctxt, TAG, "curr status check results = " + " loc permission, motion permission, notification, unused apps "+ Arrays.toString(allOtherChecks)); */ // Should replace with TRACKING_ERROR but looks like we // don't have any [[NSNotificationCenter defaultCenter] postNotificationName:CFCTransitionNotificationName object:CFCTransitionGeofenceCreationError]; [self generateOpenAppSettingsNotification]; ``` which should cause the app go to into the start state. ``` } else if ([transition isEqualToString:CFCTransitionGeofenceCreationError]) { [self setState:kStartState]; ``` However, with 2fc078b93c443e03d15712bef81835fb8665bed2, we now check the app state in `setState`. ``` -(void)setState:(TripDiaryStates) newState { ... [SensorControlBackgroundChecker checkAppState]; ``` So we end up with an infinite loop: - check app settings - permission checks fail - geofence creation error - set state to start - check app settings - permission checks fail - geofence creation error - set state to start - check app settings ... On android, we handle this by including a parameter that indicates whether the app status should be recomputed or not (in 9560859eda4bf39d6d92c1156eb08c4a3589d93b) This commit makes similar changes to iOS. On android, we would avoid recomputing only once and that would be the case for iOS as well. ``` Log.i(this, TAG, "Already in the start state, so going to stay there"); - setNewState(mCurrState); + setNewState(mCurrState, false); return; ``` Testing done: - No infinite loop on start (when we have no permissions) --- src/ios/Location/TripDiaryStateMachine.m | 32 +++++++++++++----------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/ios/Location/TripDiaryStateMachine.m b/src/ios/Location/TripDiaryStateMachine.m index 8eccb67..a49b643 100644 --- a/src/ios/Location/TripDiaryStateMachine.m +++ b/src/ios/Location/TripDiaryStateMachine.m @@ -143,7 +143,7 @@ - (id) init { it easier for us to ignore silent push as well as the transitions generated from here. */ [TripDiaryActions oneTimeInitTracking:CFCTransitionInitialize withLocationMgr:self.locMgr]; - [self setState:kOngoingTripState]; + [self setState:kOngoingTripState withChecks:TRUE]; [TripDiaryActions startTracking:CFCTransitionExitedGeofence withLocationMgr:self.locMgr]; } @@ -211,7 +211,7 @@ -(void)handleTransition:(NSString*) transition withUserInfo:(NSDictionary*) user } } --(void)setState:(TripDiaryStates) newState { +-(void)setState:(TripDiaryStates) newState withChecks:(BOOL) doChecks { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; [defaults setInteger:newState forKey:kCurrState]; @@ -221,8 +221,10 @@ -(void)setState:(TripDiaryStates) newState { [TripDiaryStateMachine getStateName:newState]]]; self.currState = newState; + if (doChecks) { [SensorControlBackgroundChecker checkAppState]; } +} /* * BEGIN: State transition handlers @@ -251,7 +253,7 @@ -(void) handleStart:(NSString*) transition withUserInfo:(NSDictionary*) userInfo // but if tracking is stopped, we can skip that, and then if we turn it on again, we // need to turn everything on here as well [TripDiaryActions oneTimeInitTracking:transition withLocationMgr:self.locMgr]; - [self setState:kOngoingTripState]; + [self setState:kOngoingTripState withChecks:TRUE]; [TripDiaryActions startTracking:transition withLocationMgr:self.locMgr]; } } else if ([transition isEqualToString:CFCTransitionRecievedSilentPush]) { @@ -260,26 +262,26 @@ -(void) handleStart:(NSString*) transition withUserInfo:(NSDictionary*) userInfo } else if ([transition isEqualToString:CFCTransitionInitComplete]) { // Geofence has been successfully created and we are inside it so we are about to move to // the WAITING_FOR_TRIP_START state. - [self setState:kWaitingForTripStartState]; + [self setState:kWaitingForTripStartState withChecks:TRUE]; } else if ([transition isEqualToString:CFCTransitionGeofenceCreationError]) { // if we get a geofence creation error, we stay in the start state. NSLog(@"Got transition %@ in state %@, staying in %@ state", transition, [TripDiaryStateMachine getStateName:self.currState], [TripDiaryStateMachine getStateName:self.currState]); - [SensorControlBackgroundChecker checkAppState]; + [self setState:kStartState withChecks:FALSE]; } else if ([transition isEqualToString:CFCTransitionExitedGeofence]) { [TripDiaryActions startTracking:transition withLocationMgr:self.locMgr]; [TripDiaryActions deleteGeofence:self.locMgr]; [[NSNotificationCenter defaultCenter] postNotificationName:CFCTransitionNotificationName object:CFCTransitionTripStarted]; } else if ([transition isEqualToString:CFCTransitionTripStarted]) { - [self setState:kOngoingTripState]; + [self setState:kOngoingTripState withChecks:TRUE]; } else if ([transition isEqualToString:CFCTransitionForceStopTracking]) { // One would think that we don't need to deal with anything other than starting from the start // state, but we can be stuck in the start state for a while if it turns out that the geofence is // not created correctly. If the user forces us to stop tracking then, we still need to do it. - [self setState:kTrackingStoppedState]; + [self setState:kTrackingStoppedState withChecks:TRUE]; } else if ([transition isEqualToString:CFCTransitionStartTracking]) { // One would think that we don't need to deal with anything other than starting from the start // state, but we can be stuck in the start state for a while if it turns out that the geofence is @@ -317,7 +319,7 @@ - (void) handleWaitingForTripStart:(NSString*) transition withUserInfo:(NSDicti object:CFCTransitionTripStarted]; } } else if ([transition isEqualToString:CFCTransitionTripStarted]) { - [self setState:kOngoingTripState]; + [self setState:kOngoingTripState withChecks:TRUE]; } else if ([transition isEqualToString:CFCTransitionRecievedSilentPush]) { // Let's push any pending changes since we know that the trip has ended [[TripDiaryActions pushTripToServer] continueWithBlock:^id(BFTask *task) { @@ -335,7 +337,7 @@ - (void) handleWaitingForTripStart:(NSString*) transition withUserInfo:(NSDicti } else if ([transition isEqualToString:CFCTransitionForceStopTracking]) { [TripDiaryActions resetFSM:transition withLocationMgr:self.locMgr]; } else if ([transition isEqualToString:CFCTransitionTrackingStopped]) { - [self setState:kTrackingStoppedState]; + [self setState:kTrackingStoppedState withChecks:TRUE]; } else { NSLog(@"Got unexpected transition %@ in state %@, ignoring", transition, @@ -405,7 +407,7 @@ - (void) handleOngoingTrip:(NSString*) transition withUserInfo:(NSDictionary*) u // so we can use visits for the trip start detection, so we are also // about to move to the WAITING_FOR_TRIP_START state // TODO: Should this be here, or in EndTripTracking - [self setState:kWaitingForTripStartState]; + [self setState:kWaitingForTripStartState withChecks:TRUE]; [[BEMServerSyncCommunicationHelper backgroundSync] continueWithBlock:^id(BFTask *task) { [[BuiltinUserCache database] checkAfterPull]; [LocalNotificationManager addNotification:[NSString stringWithFormat: @@ -417,11 +419,11 @@ - (void) handleOngoingTrip:(NSString*) transition withUserInfo:(NSDictionary*) u }]; } else if ([transition isEqualToString:CFCTransitionGeofenceCreationError]) { // setState will call SensorControlBackgroundChecker checkAppState by default - [self setState:kStartState]; + [self setState:kStartState withChecks:FALSE]; } else if ([transition isEqualToString:CFCTransitionForceStopTracking]) { [TripDiaryActions resetFSM:transition withLocationMgr:self.locMgr]; } else if ([transition isEqualToString:CFCTransitionTrackingStopped]) { - [self setState:kTrackingStoppedState]; + [self setState:kTrackingStoppedState withChecks:TRUE]; } else { NSLog(@"Got unexpected transition %@ in state %@, ignoring", transition, @@ -441,14 +443,14 @@ - (void) handleTrackingStopped:(NSString*) transition withUserInfo:(NSDictionary [LocalNotificationManager addNotification:[NSString stringWithFormat: @"Found unexpected VISIT_STARTED transition while tracking was stopped, stop everything again"] showUI:TRUE]; - [self setState:kTrackingStoppedState]; + [self setState:kTrackingStoppedState withChecks:TRUE]; [TripDiaryActions resetFSM:transition withLocationMgr:self.locMgr]; } else if ([transition isEqualToString:CFCTransitionForceStopTracking]) { [TripDiaryActions resetFSM:transition withLocationMgr:self.locMgr]; } else if ([transition isEqualToString:CFCTransitionTrackingStopped]) { - [self setState:kTrackingStoppedState]; + [self setState:kTrackingStoppedState withChecks:TRUE]; } else if ([transition isEqualToString:CFCTransitionStartTracking]) { - [self setState:kStartState]; + [self setState:kStartState withChecks:TRUE]; // This will run the one time init tracking as well as try to create a geofence // if we are moving, then we will be outside the geofence... the existing state machine will take over. [[NSNotificationCenter defaultCenter] postNotificationName:CFCTransitionNotificationName From f3c673a9a3dda2c65802cb9656fdd751d68af8eb Mon Sep 17 00:00:00 2001 From: Shankari Date: Tue, 15 Feb 2022 21:42:19 -0800 Subject: [PATCH 31/35] Implement the `fixFitnessPermissions` method High level changes: - create a new method in the foreground delegate - checks and returns appropriate plugin results - moved from `TripDiarySensorControlChecks` - create a new interface for the async operation to read the activity results, which is the only way to generate the prompt - moved this from the `cordova-server-sync` repo https://github.com/e-mission/cordova-server-sync/pull/50 - stored callback functions and invoked them asynchronously, similar to the location changes in ca979bd85f585161cdb387687bbd2acba4f3cb85 - ensured that the simulator always returns TRUE to allow easy testing of other features - changed the error messages to improve user instructions, including: - if motion activity is not supported on the device, uninstall - if the permission is off, fix in app settings - if the setting is off, fix and restart the app Related testing: https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1040713690 https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1040948835 --- res/ios/en.lproj/DCLocalizable.strings | 6 +- src/ios/BEMDataCollection.m | 2 + .../SensorControlForegroundDelegate.h | 10 ++ .../SensorControlForegroundDelegate.m | 127 ++++++++++++++++++ .../TripDiarySensorControlChecks.m | 91 ++----------- 5 files changed, 150 insertions(+), 86 deletions(-) diff --git a/res/ios/en.lproj/DCLocalizable.strings b/res/ios/en.lproj/DCLocalizable.strings index 790ae99..abd55b0 100644 --- a/res/ios/en.lproj/DCLocalizable.strings +++ b/res/ios/en.lproj/DCLocalizable.strings @@ -2,9 +2,9 @@ "new-data-collections-terms" = "New data collection terms - collection paused until consent"; "error-reading-activities" = "Error while reading activities"; "travel-mode-unavailable" = "Travel mode detection may be unavailable."; -"activity-detection-unsupported" = "Activity detection unsupported"; -"activity-permission-problem" = "No 'Motion & Fitness' permission - automatic mode detection will not work. Turn it on (Settings -> app)"; -"activity-turned-off-problem" = "Motion & Fitness Service disabled - automatic mode detection will not work. Turn it on (Settings -> Privacy)"; +"activity-detection-unsupported" = "Activity detection unsupported. Please uninstall."; +"activity-permission-problem" = "'Motion & Fitness' permission off, please fix in app settings"; +"activity-turned-off-problem" = "Motion & Fitness Service disabled. Turn it on (Phone Settings -> Privacy) and *restart the app*"; "travel-mode-unknown" = "Travel mode detection unavailable - all trips will be UNKNOWN."; "bad-loc-tracking-problem" = "Background location accuracy is consistently poor - trip tracking may not work. Report problem."; "location-turned-off-problem" = "Location Services are turned off. Turn it on (Settings -> Privacy)"; diff --git a/src/ios/BEMDataCollection.m b/src/ios/BEMDataCollection.m index becf412..88f36ab 100644 --- a/src/ios/BEMDataCollection.m +++ b/src/ios/BEMDataCollection.m @@ -108,6 +108,8 @@ - (void)isValidLocationPermissions:(CDVInvokedUrlCommand*)command - (void)fixFitnessPermissions:(CDVInvokedUrlCommand*)command { + [[[SensorControlForegroundDelegate alloc] initWithDelegate:self.commandDelegate + forCommand:command] checkAndPromptFitnessPermissions]; } - (void)isValidFitnessPermissions:(CDVInvokedUrlCommand*)command diff --git a/src/ios/Verification/SensorControlForegroundDelegate.h b/src/ios/Verification/SensorControlForegroundDelegate.h index 4276d9d..476baed 100644 --- a/src/ios/Verification/SensorControlForegroundDelegate.h +++ b/src/ios/Verification/SensorControlForegroundDelegate.h @@ -17,6 +17,9 @@ - (void) checkAndPromptLocationSettings; - (void) checkAndPromptLocationPermissions; - (void) didChangeAuthorizationStatus:(CLAuthorizationStatus)status; + +- (void) checkAndPromptFitnessPermissions; +- (void) didRecieveFitnessPermission:(BOOL)isPermitted; @end @interface TripDiaryDelegate (TripDiaryDelegatePermissions) @@ -25,3 +28,10 @@ didChangeAuthorizationStatus:(CLAuthorizationStatus)status; @end + +@interface MotionActivityPermissionDelegate: NSObject ++ (void)registerForegroundDelegate:(SensorControlForegroundDelegate*) foregroundDelegate; ++ (void)readAndPromptForPermission; + +@end + diff --git a/src/ios/Verification/SensorControlForegroundDelegate.m b/src/ios/Verification/SensorControlForegroundDelegate.m index eda5e54..ff83bca 100644 --- a/src/ios/Verification/SensorControlForegroundDelegate.m +++ b/src/ios/Verification/SensorControlForegroundDelegate.m @@ -167,6 +167,80 @@ - (void) didChangeAuthorizationStatus:(CLAuthorizationStatus)status } +- (void)checkAndPromptFitnessPermissions { + NSString* callbackId = [command callbackId]; +#if TARGET_OS_SIMULATOR + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_OK]; + [commandDelegate sendPluginResult:result callbackId:callbackId]; +#else + @try { + if ([CMMotionActivityManager isActivityAvailable] == YES) { + [LocalNotificationManager addNotification:@"Motion activity available, checking auth status"]; + CMAuthorizationStatus currAuthStatus = [CMMotionActivityManager authorizationStatus]; + [LocalNotificationManager addNotification:[NSString stringWithFormat:@"Auth status = %ld", currAuthStatus]]; + + if (currAuthStatus == CMAuthorizationStatusAuthorized) { + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_OK]; + [commandDelegate sendPluginResult:result callbackId:callbackId]; + } + + if (currAuthStatus == CMAuthorizationStatusNotDetermined) { + [LocalNotificationManager addNotification:[NSString stringWithFormat:@"Activity status not determined, initializing to get regular prompt"]]; + [MotionActivityPermissionDelegate registerForegroundDelegate:self]; + [MotionActivityPermissionDelegate readAndPromptForPermission]; + } + + if (currAuthStatus == CMAuthorizationStatusRestricted) { + /* + It looked like this status is read when the app starts and cached after that. This is not resolvable from the code, so we just change the resulting message to highlight that the app needs to be restarted. + Gory details at: https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1040948835 + */ + [LocalNotificationManager addNotification:[NSString stringWithFormat:@"Activity detection not enabled, prompting user to change Settings"]]; + NSString* msg = NSLocalizedStringFromTable(@"activity-turned-off-problem", @"DCLocalizable", nil); + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_ERROR + messageAsString:msg]; + [commandDelegate sendPluginResult:result callbackId:callbackId]; + } + + + if ([CMMotionActivityManager authorizationStatus] == CMAuthorizationStatusDenied) { + [LocalNotificationManager addNotification:[NSString stringWithFormat:@"Activity status denied, opening app settings to enable"]]; + + NSString* msg = NSLocalizedStringFromTable(@"activity-permission-problem", @"DCLocalizable", nil); + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_ERROR + messageAsString:msg]; + [commandDelegate sendPluginResult:result callbackId:callbackId]; + [self openAppSettings]; + } + } else { + [LocalNotificationManager addNotification:[NSString stringWithFormat:@"Activity detection unsupported, all trips will be UNKNOWN"]]; + NSString* msg = NSLocalizedStringFromTable(@"activity-detection-unsupported", @"DCLocalizable", nil); + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_ERROR + messageAsString:msg]; + [commandDelegate sendPluginResult:result callbackId:callbackId]; + } + } + @catch (NSException *exception) { + NSString* msg = [NSString stringWithFormat: @"While getting settings, error %@", exception]; + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_ERROR + messageAsString:msg]; + [commandDelegate sendPluginResult:result callbackId:callbackId]; + } +#endif +} + +-(void) didRecieveFitnessPermission:(BOOL)isPermitted +{ + [self sendCheckResult:isPermitted + errorKey:@"activity-permission-problem"]; +} + -(void)promptForPermission:(CLLocationManager*)locMgr { if (IsAtLeastiOSVersion(@"13.0")) { NSLog(@"iOS 13+ detected, launching UI settings to easily enable always"); @@ -179,6 +253,7 @@ -(void)promptForPermission:(CLLocationManager*)locMgr { if ([CLLocationManager instancesRespondToSelector:@selector(requestAlwaysAuthorization)]) { NSLog(@"Current location authorization = %d, always = %d, requesting always", [CLLocationManager authorizationStatus], kCLAuthorizationStatusAuthorizedAlways); + [[TripDiaryStateMachine delegate] registerForegroundDelegate:self]; [locMgr requestAlwaysAuthorization]; } else { // TODO: should we remove this? Not sure when it will ever be called, given that @@ -238,5 +313,57 @@ - (void)locationManager:(CLLocationManager *)manager [SensorControlBackgroundChecker checkAppState]; } } +@end + +@implementation MotionActivityPermissionDelegate +NSMutableArray* foregroundDelegateList; + +/* + * This is a bit tricky since this function is called whenever the authorization is changed + * Design decisions are at: + * https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1035972636 + * https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1035976420 + * https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1035984060 + */ + ++(void)registerForegroundDelegate:(SensorControlForegroundDelegate*) foregroundDelegate +{ + if (foregroundDelegateList == nil) { + foregroundDelegateList = [NSMutableArray new]; + } + [foregroundDelegateList addObject:foregroundDelegate]; +} + ++(void)readAndPromptForPermission { + CMMotionActivityManager* activityMgr = [[CMMotionActivityManager alloc] init]; + NSOperationQueue* mq = [NSOperationQueue mainQueue]; + NSDate* startDate = [NSDate new]; + NSTimeInterval dayAgoSecs = 24 * 60 * 60; + NSDate* endDate = [NSDate dateWithTimeIntervalSinceNow:-(dayAgoSecs)]; + /* This queryActivity call is the one that prompt the user for permission */ + [activityMgr queryActivityStartingFromDate:startDate toDate:endDate toQueue:mq withHandler:^(NSArray *activities, NSError *error) { + if (error == nil) { + [LocalNotificationManager addNotification:@"activity recognition works fine"]; + if (foregroundDelegateList.count > 0) { + for (id currDelegate in foregroundDelegateList) { + [currDelegate didRecieveFitnessPermission:TRUE]; + } + [foregroundDelegateList removeAllObjects]; + } else { + [LocalNotificationManager addNotification:[NSString stringWithFormat:@"no foreground delegate callbacks found for fitness sensors, ignoring success..."]]; + } + } else { + [LocalNotificationManager addNotification:[NSString stringWithFormat:@"Error %@ while reading activities, travel mode detection may be unavailable", error]]; + if (foregroundDelegateList.count > 0) { + for (id currDelegate in foregroundDelegateList) { + [currDelegate didRecieveFitnessPermission:FALSE]; + } + [foregroundDelegateList removeAllObjects]; + } else { + [LocalNotificationManager addNotification:[NSString stringWithFormat:@"no foreground delegate callbacks found for fitness sensor error %@, ignoring...", error]]; + } + } + }]; +} @end diff --git a/src/ios/Verification/TripDiarySensorControlChecks.m b/src/ios/Verification/TripDiarySensorControlChecks.m index b053210..402c148 100644 --- a/src/ios/Verification/TripDiarySensorControlChecks.m +++ b/src/ios/Verification/TripDiarySensorControlChecks.m @@ -28,12 +28,20 @@ +(BOOL)checkLocationPermissions { } +(BOOL)checkMotionActivitySettings { +#if TARGET_OS_SIMULATOR + return TRUE; +#else return [CMMotionActivityManager isActivityAvailable] == YES; +#endif } +(BOOL)checkMotionActivityPermissions { +#if TARGET_OS_SIMULATOR + return TRUE; +#else CMAuthorizationStatus currAuthStatus = [CMMotionActivityManager authorizationStatus]; return currAuthStatus == CMAuthorizationStatusAuthorized; +#endif } +(BOOL)checkNotificationsEnabled { @@ -48,87 +56,4 @@ +(UIUserNotificationSettings*) REQUESTED_NOTIFICATION_TYPES { categories:nil]; } - -/* -+(void)checkSettingsAndPermission { - [TripDiarySettingsCheck checkLocationSettingsAndPermission:TRUE]; - [TripDiarySettingsCheck checkMotionSettingsAndPermission:TRUE]; -} - -+(void)checkMotionSettingsAndPermission:(BOOL)inBackground { - if ([CMMotionActivityManager isActivityAvailable] == YES) { - [LocalNotificationManager addNotification:@"Motion activity available, checking auth status"]; - CMAuthorizationStatus currAuthStatus = [CMMotionActivityManager authorizationStatus]; - [LocalNotificationManager addNotification:[NSString stringWithFormat:@"Auth status = %ld", currAuthStatus]]; - - if (currAuthStatus == CMAuthorizationStatusRestricted) { - [LocalNotificationManager addNotification:[NSString stringWithFormat:@"Activity detection not enabled, prompting user to change Settings"]]; - if (inBackground) { - NSString* errorDescription = NSLocalizedStringFromTable(@"activity-turned-off-problem", @"DCLocalizable", nil); - [LocalNotificationManager showNotificationAfterSecs:errorDescription withUserInfo:NULL secsLater:60]; - } - [TripDiarySettingsCheck showLaunchSettingsAlert:@"activity-detection-unsupported" withMessage:@"activity-turned-off-problem" button:@"fix-service-action-button"]; - } - if (currAuthStatus == CMAuthorizationStatusNotDetermined) { - [LocalNotificationManager addNotification:[NSString stringWithFormat:@"Activity status not determined, initializing to get regular prompt"]]; - [BEMActivitySync initWithConsent]; - } - if ([CMMotionActivityManager authorizationStatus] == CMAuthorizationStatusDenied) { - [LocalNotificationManager addNotification:[NSString stringWithFormat:@"Activity status denied, opening app settings to enable"]]; - NSString* errorDescription = NSLocalizedStringFromTable(@"activity-permission-problem", @"DCLocalizable", nil); - if (inBackground) { - [LocalNotificationManager showNotificationAfterSecs:errorDescription withUserInfo:NULL secsLater:60]; - } - [TripDiarySettingsCheck showLaunchSettingsAlert:@"permission-problem-title" withMessage:@"activity-permission-problem" button:@"fix-permission-action-button"]; - } - } else { - [LocalNotificationManager addNotification:[NSString stringWithFormat:@"Activity detection unsupported, all trips will be UNKNOWN"]]; - NSString* title = NSLocalizedStringFromTable(@"activity-detection-unsupported", @"DCLocalizable", nil); - NSString* message = NSLocalizedStringFromTable(@"travel-mode-unknown", @"DCLocalizable", nil); - - UIAlertController* alert = [UIAlertController alertControllerWithTitle:title - message:message - preferredStyle:UIAlertControllerStyleAlert]; - - UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - }]; - [alert addAction:defaultAction]; - [TripDiarySettingsCheck showSettingsAlert:alert]; - } -} - -+(void)showLaunchSettingsAlert:(NSString*)titleTag withMessage:(NSString*)messageTag button:(NSString*)buttonTag { - NSString* title = NSLocalizedStringFromTable(titleTag, @"DCLocalizable", nil); - NSString* message = NSLocalizedStringFromTable(messageTag, @"DCLocalizable", nil); - NSString* errorAction = NSLocalizedStringFromTable(buttonTag, @"DCLocalizable", nil); - - UIAlertController* alert = [UIAlertController alertControllerWithTitle:title - message:message - preferredStyle:UIAlertControllerStyleAlert]; - - UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:errorAction style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - [TripDiarySettingsCheck openAppSettings]; - }]; - [alert addAction:defaultAction]; - [TripDiarySettingsCheck showSettingsAlert:alert]; -} - -+(void) openAppSettings { - [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:^(BOOL success) { - if (success) { - NSLog(@"Opened url"); - } else { - NSLog(@"Failed open"); - }}]; -} - -+(void) showSettingsAlert:(UIAlertController*)alert { - CDVAppDelegate *ad = [[UIApplication sharedApplication] delegate]; - CDVViewController *vc = ad.viewController; - [vc presentViewController:alert animated:YES completion:nil]; -} -*/ - @end From 4c681c8a95fca6ea0fba35509f057eddbf19c840 Mon Sep 17 00:00:00 2001 From: Shankari Date: Fri, 18 Feb 2022 11:53:19 -0800 Subject: [PATCH 32/35] Finish the notification callback This should be the last of the permissions required for iOS. There is still a whole lot of cleanup that needs to be done, but at least we have it working end to end. Changes: - Copy over notification code from 9d31e1008566f1563995484300e76b5f32636b16 that were originally made in the plugin directly - Use the callback/delegate model for dealing with permission callbacks, similar to the location and fitness permissions - Change the messages as needed --- res/ios/en.lproj/DCLocalizable.strings | 2 +- src/ios/BEMAppDelegate.m | 9 --- src/ios/BEMDataCollection.m | 32 +-------- .../SensorControlForegroundDelegate.h | 8 +++ .../SensorControlForegroundDelegate.m | 70 +++++++++++++++++++ .../TripDiarySensorControlChecks.h | 9 +-- 6 files changed, 83 insertions(+), 47 deletions(-) diff --git a/res/ios/en.lproj/DCLocalizable.strings b/res/ios/en.lproj/DCLocalizable.strings index abd55b0..cf5144d 100644 --- a/res/ios/en.lproj/DCLocalizable.strings +++ b/res/ios/en.lproj/DCLocalizable.strings @@ -14,7 +14,7 @@ "fix-permission-action-button" = "Fix permission"; "precise-location-problem" = "The app does not have permission to read 'precise' location - background trip tracking will not work."; "notifications_blocked" = "Notifications blocked, please enable"; -"notifications_blocked_app_open" = "Notifications blocked, please fix in app settings"; +"notifications_blocked_app_open" = "Notifications blocked, please fix in app settings and then refresh"; "location_not_enabled" = "Location turned off, please turn on"; "location_permission_off" = "Insufficient location permissions, please fix"; "location_permission_off_app_open" = "Insufficient location permissions, please fix in app settings"; diff --git a/src/ios/BEMAppDelegate.m b/src/ios/BEMAppDelegate.m index 6025d05..1960fbf 100644 --- a/src/ios/BEMAppDelegate.m +++ b/src/ios/BEMAppDelegate.m @@ -42,15 +42,6 @@ + (BOOL)didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { return YES; } --(void) application:(UIApplication*)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings -{ - [LocalNotificationManager addNotification:[NSString stringWithFormat: - @"Received callback from user notification settings "] - showUI:FALSE]; - [[NSNotificationCenter defaultCenter] postNotificationName:NotificationCallback - object:notificationSettings]; - -} - (void)applicationWillResignActive:(UIApplication *)application { // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. diff --git a/src/ios/BEMDataCollection.m b/src/ios/BEMDataCollection.m index 88f36ab..ba5510a 100644 --- a/src/ios/BEMDataCollection.m +++ b/src/ios/BEMDataCollection.m @@ -121,36 +121,8 @@ - (void)isValidFitnessPermissions:(CDVInvokedUrlCommand*)command - (void)fixShowNotifications:(CDVInvokedUrlCommand*)command { - /* - NSString* callbackId = [command callbackId]; - @try { - if ([UIApplication instancesRespondToSelector:@selector(registerUserNotificationSettings:)]) { - UIUserNotificationSettings* requestedSettings = [self REQUESTED_NOTIFICATION_TYPES]; - [[UIApplication sharedApplication] registerUserNotificationSettings:requestedSettings]; - [[NSNotificationCenter defaultCenter] addObserverForName:NotificationCallback object:nil queue:nil - usingBlock:^(NSNotification* note) { - if (requestedSettings.types == ((UIUserNotificationSettings*)note.object).types) { - CDVPluginResult* result = [CDVPluginResult - resultWithStatus:CDVCommandStatus_OK]; - [self.commandDelegate sendPluginResult:result callbackId:callbackId]; - } else { - NSString* msg = NSLocalizedStringFromTable(@"notifications_blocked", @"DCLocalizable", nil); - CDVPluginResult* result = [CDVPluginResult - resultWithStatus:CDVCommandStatus_ERROR - messageAsString:msg]; - [self.commandDelegate sendPluginResult:result callbackId:callbackId]; - } - }]; - } - } - @catch (NSException *exception) { - NSString* msg = [NSString stringWithFormat: @"While getting settings, error %@", exception]; - CDVPluginResult* result = [CDVPluginResult - resultWithStatus:CDVCommandStatus_ERROR - messageAsString:msg]; - [self.commandDelegate sendPluginResult:result callbackId:callbackId]; -} - */ + [[[SensorControlForegroundDelegate alloc] initWithDelegate:self.commandDelegate + forCommand:command] checkAndPromptNotificationPermission]; } diff --git a/src/ios/Verification/SensorControlForegroundDelegate.h b/src/ios/Verification/SensorControlForegroundDelegate.h index 476baed..0a359ea 100644 --- a/src/ios/Verification/SensorControlForegroundDelegate.h +++ b/src/ios/Verification/SensorControlForegroundDelegate.h @@ -4,6 +4,7 @@ #import #import #import "TripDiaryDelegate.h" +#import "AppDelegate.h" @interface SensorControlForegroundDelegate: NSObject @@ -20,6 +21,9 @@ - (void) checkAndPromptFitnessPermissions; - (void) didRecieveFitnessPermission:(BOOL)isPermitted; + +- (void) checkAndPromptNotificationPermission; +- (void) didRegisterUserNotificationSettings:(UIUserNotificationSettings*)isPermitted; @end @interface TripDiaryDelegate (TripDiaryDelegatePermissions) @@ -32,6 +36,10 @@ @interface MotionActivityPermissionDelegate: NSObject + (void)registerForegroundDelegate:(SensorControlForegroundDelegate*) foregroundDelegate; + (void)readAndPromptForPermission; +@end +@interface AppDelegate (AppDelegate) ++ (void)registerForegroundDelegate:(SensorControlForegroundDelegate*) foregroundDelegate; ++ (void)readAndPromptForPermission; @end diff --git a/src/ios/Verification/SensorControlForegroundDelegate.m b/src/ios/Verification/SensorControlForegroundDelegate.m index ff83bca..894317e 100644 --- a/src/ios/Verification/SensorControlForegroundDelegate.m +++ b/src/ios/Verification/SensorControlForegroundDelegate.m @@ -241,6 +241,41 @@ -(void) didRecieveFitnessPermission:(BOOL)isPermitted errorKey:@"activity-permission-problem"]; } +- (void)checkAndPromptNotificationPermission { + NSString* callbackId = [command callbackId]; + @try { + if ([UIApplication instancesRespondToSelector:@selector(registerUserNotificationSettings:)]) { + UIUserNotificationSettings* requestedSettings = [TripDiarySensorControlChecks REQUESTED_NOTIFICATION_TYPES]; + [AppDelegate registerForegroundDelegate:self]; + [[UIApplication sharedApplication] registerUserNotificationSettings:requestedSettings]; + } + } + @catch (NSException *exception) { + NSString* msg = [NSString stringWithFormat: @"While getting settings, error %@", exception]; + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_ERROR + messageAsString:msg]; + [commandDelegate sendPluginResult:result callbackId:callbackId]; + } +} + +- (void) didRegisterUserNotificationSettings:(UIUserNotificationSettings*)newSettings { + NSString* callbackId = [command callbackId]; + UIUserNotificationSettings* requestedSettings = [TripDiarySensorControlChecks REQUESTED_NOTIFICATION_TYPES]; + if (requestedSettings.types == newSettings.types) { + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_OK]; + [commandDelegate sendPluginResult:result callbackId:callbackId]; + } else { + NSString* msg = NSLocalizedStringFromTable(@"notifications_blocked_app_open", @"DCLocalizable", nil); + [self openAppSettings]; + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_ERROR + messageAsString:msg]; + [commandDelegate sendPluginResult:result callbackId:callbackId]; + } +} + -(void)promptForPermission:(CLLocationManager*)locMgr { if (IsAtLeastiOSVersion(@"13.0")) { NSLog(@"iOS 13+ detected, launching UI settings to easily enable always"); @@ -367,3 +402,38 @@ +(void)readAndPromptForPermission { } @end + +@implementation AppDelegate(CollectionPermission) +NSMutableArray* foregroundDelegateList; + +/* + * This is a bit tricky since this function is called whenever the authorization is changed + * Design decisions are at: + * https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1035972636 + * https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1035976420 + * https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1035984060 + */ + ++(void)registerForegroundDelegate:(SensorControlForegroundDelegate*) foregroundDelegate +{ + if (foregroundDelegateList == nil) { + foregroundDelegateList = [NSMutableArray new]; + } + [foregroundDelegateList addObject:foregroundDelegate]; +} + +-(void) application:(UIApplication*)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings +{ + [LocalNotificationManager addNotification:[NSString stringWithFormat: + @"Received callback from user notification settings "] + showUI:FALSE]; + if (foregroundDelegateList.count > 0) { + for (id currDelegate in foregroundDelegateList) { + [currDelegate didRegisterUserNotificationSettings:notificationSettings]; + } + [foregroundDelegateList removeAllObjects]; + } else { + [LocalNotificationManager addNotification:[NSString stringWithFormat:@"no foreground delegate callbacks found for notifications, ignoring success..."]]; + } +} +@end diff --git a/src/ios/Verification/TripDiarySensorControlChecks.h b/src/ios/Verification/TripDiarySensorControlChecks.h index e951dea..f31526f 100644 --- a/src/ios/Verification/TripDiarySensorControlChecks.h +++ b/src/ios/Verification/TripDiarySensorControlChecks.h @@ -2,6 +2,7 @@ #import #import #import +#import @interface TripDiarySensorControlChecks: NSObject @@ -11,11 +12,5 @@ +(BOOL)checkMotionActivityPermissions; +(BOOL)checkNotificationsEnabled; -+(void)checkSettingsAndPermission; -+(void)checkLocationSettingsAndPermission:(BOOL)inBackground; -+(void)checkMotionSettingsAndPermission:(BOOL)inBackground; -+(void)promptForPermission:(CLLocationManager*)locMgr; -+(void)openAppSettings; -+(void)showSettingsAlert:(UIAlertController*)alert; - ++(UIUserNotificationSettings*) REQUESTED_NOTIFICATION_TYPES; @end From 392c34e57c0d0b42d51bfd3f41ea9e68cf64de0e Mon Sep 17 00:00:00 2001 From: Shankari Date: Fri, 18 Feb 2022 17:42:30 -0800 Subject: [PATCH 33/35] Fix the issue where the location option didn't show up when we opened the app page It turns out that we need to actually start tracking the location before the location permission would be visible, similar to the motion activity permission (https://github.com/e-mission/e-mission-data-collection/pull/195/commits/f3c673a9a3dda2c65802cb9656fdd751d68af8eb). In the previous version, we would call `markConsented` and then popup permissions as required. So the `startUpdatingLocation` call would happen in parallel with opening up the status screen. However, now we get all permissions upfront and then call `markConsented`. So we need to include a dummy `startUpdatingLocation` call to see the location. We can stop updating the location once the user has given the permission. We didn't see this earlier (in ca979bd85f585161cdb387687bbd2acba4f3cb85) because we tested on a physical phone which was running 12.x and can prompt for permissions inline. For excrutiating detail, see https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1045429690 --- src/ios/Verification/SensorControlForegroundDelegate.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ios/Verification/SensorControlForegroundDelegate.m b/src/ios/Verification/SensorControlForegroundDelegate.m index 894317e..22f9af0 100644 --- a/src/ios/Verification/SensorControlForegroundDelegate.m +++ b/src/ios/Verification/SensorControlForegroundDelegate.m @@ -143,6 +143,7 @@ -(void) checkAndPromptLocationPermissions - (void) didChangeAuthorizationStatus:(CLAuthorizationStatus)status { + [[TripDiaryStateMachine instance].locMgr stopUpdatingLocation]; NSString* callbackId = [command callbackId]; @try { if (status == kCLAuthorizationStatusAuthorizedAlways) { @@ -281,6 +282,7 @@ -(void)promptForPermission:(CLLocationManager*)locMgr { NSLog(@"iOS 13+ detected, launching UI settings to easily enable always"); // we want to leave the registration in the prompt for permission, since we don't want to register callbacks when we open the app settings for other reasons [[TripDiaryStateMachine delegate] registerForegroundDelegate:self]; + [[TripDiaryStateMachine instance].locMgr startUpdatingLocation]; [self openAppSettings]; } else { From 769f8a4cb43c83640653b5781482717dc2599341 Mon Sep 17 00:00:00 2001 From: Shankari Date: Fri, 18 Feb 2022 21:58:23 -0800 Subject: [PATCH 34/35] Bump up the version number to 1.7.0 Bumped up by a whole minor version since the changes are substantial + Fix issue with header vs. source files that was giving problems with building Planning to merge this and handle cleanup later --- plugin.xml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugin.xml b/plugin.xml index 63848d3..2931f91 100644 --- a/plugin.xml +++ b/plugin.xml @@ -1,7 +1,7 @@ + version="1.7.0"> DataCollection Background data collection FTW! This is the part that I really @@ -221,9 +221,9 @@ - - - + + + @@ -243,9 +243,9 @@ - - - + + + From a081e066c01d7f162dd64b303861ae18ec5d3ba3 Mon Sep 17 00:00:00 2001 From: Shankari Date: Tue, 22 Feb 2022 23:42:40 -0800 Subject: [PATCH 35/35] Bump up the version number in the package.json as well --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ad9c227..a955bf8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cordova-plugin-em-datacollection", - "version": "1.6.0", + "version": "1.7.0", "description": "The main tracking for the e-mission platform", "license": "BSD-3-clause", "cordova": {