Issue
While trying to restore the functionality of my Bluetooth Low Energy App I ran into problems with the newly introduced permissions requirements
The first issue is while there are 6 possible permissions in the Manifest not all are required or will be granted depending on the Android Version
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
Hence the app must only check for permissions required by the particular version of Android
Big thank you to @Kozmotronik for pointing this out!
private fun checkAndRequestMissingPermissions() {
// check required permissions - request those which have not already been granted
val missingPermissionsToBeRequested = ArrayList<String>()
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH) != PackageManager.PERMISSION_GRANTED)
missingPermissionsToBeRequested.add(Manifest.permission.BLUETOOTH)
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_ADMIN) != PackageManager.PERMISSION_GRANTED)
missingPermissionsToBeRequested.add(Manifest.permission.BLUETOOTH_ADMIN)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// For Android 12 and above require both BLUETOOTH_CONNECT and BLUETOOTH_SCAN
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED)
missingPermissionsToBeRequested.add(Manifest.permission.BLUETOOTH_CONNECT)
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED)
missingPermissionsToBeRequested.add(Manifest.permission.BLUETOOTH_SCAN)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// FINE_LOCATION is needed for Android 10 and above
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED)
missingPermissionsToBeRequested.add(Manifest.permission.ACCESS_FINE_LOCATION)
} else {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED)
missingPermissionsToBeRequested.add(Manifest.permission.ACCESS_COARSE_LOCATION)
}
if (missingPermissionsToBeRequested.isNotEmpty()) {
waitingForPermission = true
writeToLog("Missing the following permissions: $missingPermissionsToBeRequested")
ActivityCompat.requestPermissions(this, missingPermissionsToBeRequested.toArray(arrayOfNulls<String>(0)), REQUEST_MULTIPLE_PERMISSIONS)
} else {
writeToLog("All required permissions GRANTED !")
waitingForPermission = false
}
}
UPDATE (although unclear if it makes a material difference)
Rather than checking for == PERMISSION_DENIED the code now checks != PERMISSION_GRANTED
Responses to requestPermissions() are then checked
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_MULTIPLE_PERMISSIONS) {
for (idx in permissions.indices) {
var result = if (grantResults[idx] == PackageManager.PERMISSION_GRANTED) "Granted" else "Denied"
writeToLog("REQUEST_MULTIPLE_PERMISSIONS Permission ${permissions[idx]} $result}")
}
}
}
Android still requires a permission check before calling protected activities, however Android Studio tells you which permission to check and allows the Android version to be included:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
writeToLog("Scan permission denied")
} else {
bluetoothScanner.startScan(filters, scanSettings, scanCallback)
writeToLog("Bluetooth Scan Started")
}
Android Studio also suggests an alternative to the permission check which involves catching any exception (NOTE: while it compiles I was never able to get this to work)
try {
bluetoothScanner.startScan(filters, scanSettings, scanCallback)
writeToLog("Bluetooth Scan Started")
} catch (e: Throwable) {
writeToLog("Error bluetoothScanner.startScan() $e")
}
Hope this helps some others
Solution
Google made Android more strict by adding the new BLUETOOTH_CONNECT
and BLUETOOTH_SCAN
permissions. You will get SecurityException
in runtime if you attempt to access any BLE API that requires these permissions. So we need to check the permissions in the activity which is set to android.intent.action.MAIN
in the manifest file. I call that as MainActivity
. Unfortunately I don't code in Kotlin yet, so I will write the examples in Java.
MainActivity
public class MainActivity extends AppCompatActivity {
private static int BLE_PERMISSIONS_REQUEST_CODE = 0x55; // Could be any other positive integer value
private int permissionsCount;
//...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Other codes...
checkBlePermissions();
// Maybe some more codes...
}
// Maybe some other codes...
private String getMissingLocationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
// COARSE is needed for Android 6 to Android 10
return Manifest.permission.ACCESS_COARSE_LOCATION;
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// FINE is needed for Android 10 and above
return Manifest.permission.ACCESS_FINE_LOCATION;
}
// No location permission is needed for Android 6 and below
return null;
}
private boolean hasLocationPermission(String locPermission) {
if(locPermission == null) return true; // An Android version that doesn't need a location permission
return ContextCompat.checkSelfPermission(getApplicationContext(), locPermission) ==
PackageManager.PERMISSION_GRANTED;
}
private String[] getMissingBlePermissions() {
String[] missingPermissions = null;
String locationPermission = getMissingLocationPermission();
// For Android 12 and above
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if(ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.BLUETOOTH_SCAN)
!= PackageManager.PERMISSION_GRANTED) {
missingPermissions = new String[1];
missingPermissions[0] = Manifest.permission.BLUETOOTH_SCAN;
}
if(ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.BLUETOOTH_CONNECT)
!= PackageManager.PERMISSION_GRANTED) {
if (missingPermissions == null) {
missingPermissions = new String[1];
missingPermissions[0] = Manifest.permission.BLUETOOTH_CONNECT;
} else {
missingPermissions = Arrays.copyOf(missingPermissions, missingPermissions.length + 1);
missingPermissions[missingPermissions.length-1] = Manifest.permission.BLUETOOTH_CONNECT;
}
}
}
else if(!hasLocationPermission(locationPermission)) {
missingPermissions = new String[1];
missingPermissions[0] = getMissingLocationPermission();
}
return missingPermissions;
}
private void checkBlePermissions() {
String[] missingPermissions = getMissingBlePermissions();
if(missingPermissions == null || missingPermissions.length == 0) {
Log.i(TAG, "checkBlePermissions: Permissions is already granted");
return;
}
for(String perm : missingPermissions)
Log.d(TAG, "checkBlePermissions: missing permissions "+perm);
permissionsCount = missingPermissions.length;
requestPermissions(missingPermissions, BLE_PERMISSIONS_REQUEST_CODE);
}
// Maybe some other codes...
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if(requestCode == BLE_PERMISSIONS_REQUEST_CODE) {
int index = 0;
for(int result: grantResults) {
if(result == PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Permission granted for "+permissions[index]);
if(permissionsCount > 0) permissionsCount--;
if(permissionsCount == 0) {
// All permissions have been granted from user.
// Here you can notify other parts of the app ie. using a custom callback or a viewmodel so on.
}
} else {
Log.d(TAG, "Permission denied for "+permissions[index]);
// TODO handle user denial i.e. show an informing dialog
}
}
} else {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
}
We are done with requesting the all needed permissions depending on the device's SDK so far. Now we need to block the API call pathways to be able to check the permissions. Somewhere where you implement bluetooth scanning put a function like startScanning()
as in the code example below, instead of using the BleScanner.scan()
API directly. All the following functions must be in the same activity or fragment.
Another activity or fragment where scanning is implemented
private String getMissingLocationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
// COARSE is needed for Android 6 to Android 10
return Manifest.permission.ACCESS_COARSE_LOCATION;
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// FINE is needed for Android 10 and above
return Manifest.permission.ACCESS_FINE_LOCATION;
}
// No location permission is needed for Android 6 and below
return null;
}
private boolean hasLocationPermission() {
String missingLocationPermission = getMissingLocationPermission();
if(missingLocationPermission == null) return true; // No permissions needed
return ContextCompat.checkSelfPermission(requireContext(), missingLocationPermission) ==
PackageManager.PERMISSION_GRANTED;
}
private boolean checkLocationService(@Nullable Runnable r) {
boolean locationServiceState = isLocationServiceEnabled();
String stateVerbose = locationServiceState ? "Location is on" : "Location is off";
Log.d(TAG, stateVerbose);
if(!locationServiceState){
new MaterialAlertDialogBuilder(requireContext())
.setCancelable(false)
.setTitle("Location Service Off")
.setView("Location service must be enabled in order to scan the bluetooth devices.")
.setPositiveButton(android.R.string.ok, (dialog, which) ->
startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)))
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
if(r != null) r.run();
})
.create().show();
}
return locationServiceState;
}
private boolean isLocationServiceEnabled(){
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P){
// This is provided as of API 28
LocationManager lm = (LocationManager) requireContext().getSystemService(Context.LOCATION_SERVICE);
return lm.isLocationServiceEnabled();
} else {
// This is deprecated as of API 28
int mod = Settings.Secure.getInt(requireContext().getContentResolver(), Settings.Secure.LOCATION_MODE,
Settings.Secure.LOCATION_MODE_OFF);
return (mod != Settings.Secure.LOCATION_MODE_OFF);
}
}
private void startScanning() {
// Here we intervene the scanning process and check whether the user allowed us to use location.
if(!hasLocationPermission()) {
// Here you have to request the approprite location permission similar to that main activity class
return;
}
// Location service must be enabled
if(!checkLocationService(() -> // Pass a Runnable that starts scanning)) return;
// Everything is good, CAN START SCANNING
}
This logic is a little confusing by nature but it is robust and have been running in a real application that presents in Google Play Store. However It is not 100% complete code since you need to adapt the idea behind it to your application.
Answered By - Kozmotronik
Answer Checked By - Senaida (JavaFixing Volunteer)