Skip to content

Commit

Permalink
👷🔧 🪠 Plumb through support for requesting bluetooth scan permissions …
Browse files Browse the repository at this point in the history
…on android

As far as we can tell, iOS does not need any additional permissions to use the
iBeacon API since we already have "always" location permissions.
However, android requires the BLUETOOTH_SCAN permission
https://developer.android.com/develop/connectivity/bluetooth/bt-permissions#declare-android12-or-higher

This change includes:
- removing the hack popup that shows up when the app is launched
- adding check and checkAndFix methods in the javascript -> plugin -> foreground checker
- adding a new check method to the list of checks
- invoking the method from the background checker so that we can prompt people
  to provide it if they don't already do so
- ensure that the background checker method is only run for fleet configs

Testing done:
- See screenshots in the related UI PR
- Without the fleet check, simulated trip start/end on a non-fleet config and
  got a notification saying that permissions were incorrect
- With the fleet check, retried, and did not get the notification
  • Loading branch information
shankari committed Apr 15, 2024
1 parent 1cb376c commit 32a9b92
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 55 deletions.
16 changes: 8 additions & 8 deletions src/android/DataCollectionPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,6 @@ public void pluginInitialize() {
new StatsEvent(myActivity, R.string.app_launched));

TripDiaryStateMachineReceiver.initOnUpgrade(myActivity);

// Ask for bluetooth permissions
// We will change this with future releases, we just ran out of time implementing this into the front end
mControlDelegate.checkAndPromptBluetoothScanPermissions();
}

@Override
Expand Down Expand Up @@ -125,6 +121,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("fixBluetoothPermissions")) {
Log.d(cordova.getActivity(), TAG, "fixing bluetooth permissions");
mControlDelegate.checkAndPromptBluetoothScanPermissions(callbackContext);
return true;
} else if (action.equals("isValidBluetoothPermissions")) {
Log.d(cordova.getActivity(), TAG, "checking bluetooth permissions");
mControlDelegate.checkBluetoothPermissions(callbackContext);
return true;
} else if (action.equals("fixShowNotifications")) {
Log.d(cordova.getActivity(), TAG, "fixing notification enable");
mControlDelegate.checkAndPromptShowNotificationsEnabled(callbackContext);
Expand Down Expand Up @@ -153,10 +157,6 @@ public boolean execute(String action, JSONArray data, final CallbackContext call
Log.d(cordova.getActivity(), TAG, "checking ignored battery optimizations");
mControlDelegate.checkIgnoreBatteryOptimizations(callbackContext);
return true;
} else if (action.equals("bluetoothScanPermissions")) {
Log.d(cordova.getActivity(), TAG, "requesting bluetooth scan permissions");
mControlDelegate.checkAndPromptBluetoothScanPermissions(callbackContext);
return true;
} else if (action.equals("storeBatteryLevel")) {
Context ctxt = cordova.getActivity();
TripDiaryStateMachineReceiver.saveBatteryAndSimulateUser(ctxt);
Expand Down
22 changes: 19 additions & 3 deletions src/android/verification/SensorControlBackgroundChecker.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package edu.berkeley.eecs.emission.cordova.tracker.verification;
// Auto fixed by post-plugin hook
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

// Auto fixed by post-plugin hook
import android.app.PendingIntent;
import android.content.Context;
Expand Down Expand Up @@ -35,6 +37,7 @@
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.usercache.UserCacheFactory;



Expand Down Expand Up @@ -89,6 +92,15 @@ public static void checkAppState(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);
boolean isFleet = false;
try {
JSONObject config = (JSONObject) UserCacheFactory.getUserCache(ctxt).getDocument("config/app_ui_config", false);
isFleet = (config != null && config.has("tracking") && config.getJSONObject("tracking").getBoolean("bluetooth_only"));
} catch (JSONException e) {
Log.d(ctxt, TAG, "Error reading config! " + e);
// TODO: Need to figure out what to do about the fleet flag when the config is invalid
// Original implementation by @louisg1337 had isFleet = true in that case (location tracking would not stop)
}

// Now that we know that the location settings are correct, we start the permission checks
boolean[] allOtherChecks = new boolean[]{
Expand All @@ -100,7 +112,11 @@ public static void checkAppState(final Context ctxt) {
boolean allOtherChecksPass = true;
for (boolean check: allOtherChecks) {
allOtherChecksPass = allOtherChecksPass && check;
}
}
if (isFleet) {
allOtherChecksPass = allOtherChecksPass &&
SensorControlChecks.checkBluetoothPermissions(ctxt);
}

/*
Using index-based iteration since we need to start from index 1 instead of 0 and array slices
Expand Down
10 changes: 10 additions & 0 deletions src/android/verification/SensorControlChecks.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ public static boolean checkMotionActivityPermissions(final Context ctxt) {
return version29Check || permCheck;
}

// 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 checkBluetoothPermissions(final Context ctxt) {
// apps before version 31 did not need to prompt for bluetooth permissions
boolean version32Check = Build.VERSION.SDK_INT < Build.VERSION_CODES.S;
boolean permCheck = ContextCompat.checkSelfPermission(ctxt, SensorControlConstants.BLUETOOTH_SCAN) == PermissionChecker.PERMISSION_GRANTED;
Log.i(ctxt, TAG, "version32Check "+version32Check+" permCheck "+permCheck+" retVal = "+(version32Check || permCheck));
return version32Check || permCheck;
}

public static boolean checkNotificationsEnabled(final Context ctxt) {
NotificationManagerCompat nMgr = NotificationManagerCompat.from(ctxt);
boolean appDisabled = nMgr.areNotificationsEnabled();
Expand Down
60 changes: 21 additions & 39 deletions src/android/verification/SensorControlForegroundDelegate.java
Original file line number Diff line number Diff line change
Expand Up @@ -420,17 +420,24 @@ public void checkAndPromptLocationPermissions(CallbackContext cordovaCallback) {
}
}

public void checkBluetoothPermissions(CallbackContext cordovaCallback) {
boolean validPerms = SensorControlChecks.checkBluetoothPermissions(cordova.getActivity());
if(validPerms) {
cordovaCallback.success();
} else {
cordovaCallback.error(cordova.getActivity().getString(R.string.activity_permission_off));
}
}

/**
* Check to see if the user has the ability to scan for bluetooth devices, if not prompt them asking for it.
*
* @param cordovaCallback
*/
public void checkAndPromptBluetoothScanPermissions(CallbackContext cordovaCallback) {
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.S){
Log.d(cordova.getActivity(), TAG, "Older build version than API 31, return success!");
cordovaCallback.success();
} else if (cordova.hasPermission(SensorControlConstants.BLUETOOTH_SCAN)){
Log.d(cordova.getActivity(), TAG, "User has already enabled bluetooth scan!");
boolean validPerms = SensorControlChecks.checkBluetoothPermissions(cordova.getActivity());
if(validPerms) {
SensorControlBackgroundChecker.restartFSMIfStartState(cordova.getActivity());
cordovaCallback.success();
} else {
Log.d(cordova.getActivity(), TAG, "User has not enabled bluetooth scan, requesting now...");
Expand All @@ -444,25 +451,6 @@ public void checkAndPromptBluetoothScanPermissions(CallbackContext cordovaCallba
}
}

/**
* Overloaded version of function aboe so we can use on native side.
*/
public void checkAndPromptBluetoothScanPermissions() {
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.S){
Log.d(cordova.getActivity(), TAG, "Older build version than API 31, return success!");
} else if (cordova.hasPermission(SensorControlConstants.BLUETOOTH_SCAN)){
Log.d(cordova.getActivity(), TAG, "User has already enabled bluetooth scan!");
} else {
Log.d(cordova.getActivity(), TAG, "User has not enabled bluetooth scan, requesting now...");
this.permissionChecker = getPermissionChecker(
SensorControlConstants.ENABLE_BLUETOOTH_SCAN,
SensorControlConstants.BLUETOOTH_SCAN,
"Please enable \'Nearby devices\' permission to use the scanner.",
"Please enable \'Nearby devices\' permission to use the scanner.");
this.permissionChecker.requestPermission();
}
}

public void checkMotionActivityPermissions(CallbackContext cordovaCallback) {
boolean validPerms = SensorControlChecks.checkMotionActivityPermissions(cordova.getActivity());
if(validPerms) {
Expand Down Expand Up @@ -775,6 +763,15 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) {
}
permissionChecker = null;
break;
case SensorControlConstants.ENABLE_BLUETOOTH_SCAN:
if (SensorControlChecks.checkBluetoothPermissions(cordova.getActivity())) {
SensorControlBackgroundChecker.restartFSMIfStartState(cordova.getActivity());
cordovaCallback.success();
} else {
permissionChecker.generateErrorCallback();
}
permissionChecker = null;
break;
case SensorControlConstants.ENABLE_NOTIFICATIONS:
Log.d(mAct, TAG, requestCode + " is our code, handling callback");
Log.d(mAct, TAG, "Got notification callback from launching app settings");
Expand Down Expand Up @@ -814,21 +811,6 @@ public void run() {
}
}
});
case SensorControlConstants.ENABLE_BLUETOOTH_SCAN:
if (cordovaCallback == null) {
break;
}

Log.d(mAct, TAG, requestCode + " is our code, handling callback");
Log.d(mAct, TAG, "Got bluetooth callback from launching app settings");
if (cordova.hasPermission(SensorControlConstants.BLUETOOTH_SCAN)) {
Log.d(mAct, TAG, "Bluetooth permissions are allowed after settings page opened!");
cordovaCallback.success();
} else {
Log.d(mAct, TAG, "Bluetooth permissions are NOT allowed after settings page opened!");
cordovaCallback.error("Please enable \'Nearby devices\' permission to use the scanner.");
}
break;
default:
Log.d(cordova.getActivity(), TAG, "Got unsupported request code " + requestCode + " , ignoring...");
}
Expand Down
15 changes: 10 additions & 5 deletions www/datacollection.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,6 @@ var DataCollection = {
exec(resolve, reject, "DataCollection", "isValidLocationPermissions", []);
});
},
bluetoothScanPermissions: function () {
return new Promise(function(resolve, reject) {
exec(resolve, reject, "DataCollection", "bluetoothScanPermissions", []);
});
},
fixFitnessPermissions: function () {
return new Promise(function(resolve, reject) {
exec(resolve, reject, "DataCollection", "fixFitnessPermissions", []);
Expand All @@ -56,6 +51,16 @@ var DataCollection = {
exec(resolve, reject, "DataCollection", "isValidFitnessPermissions", []);
});
},
fixBluetoothPermissions: function () {
return new Promise(function(resolve, reject) {
exec(resolve, reject, "DataCollection", "fixBluetoothPermissions", []);
});
},
isValidBluetoothPermissions: function () {
return new Promise(function(resolve, reject) {
exec(resolve, reject, "DataCollection", "isValidBluetoothPermissions", []);
});
},
fixShowNotifications: function () {
return new Promise(function(resolve, reject) {
exec(resolve, reject, "DataCollection", "fixShowNotifications", []);
Expand Down

0 comments on commit 32a9b92

Please sign in to comment.