Android : Permissions check for BLE [closed]

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.

Leave a Comment