Android 12 bluetooth permissions confusion

Viewed 4949

So in API 31 there are new Bluetooth permissions. I want to turn bluetooth on or off using this:

private void changeBluetoothState(boolean status) {
    BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    if (status)
        mBluetoothAdapter.enable();
    else mBluetoothAdapter.disable();
}

and in manifest I already have this:

<uses-permission
    android:name="android.permission.WRITE_SETTINGS"
    tools:ignore="ProtectedPermissions" />

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

<uses-feature
    android:name="android.hardware.bluetooth"
    android:required="false" />

Android documentations says to add android:maxSdkVersion="30" to the above bluetooth permissions if targeting API 31 or higher. I also get an error in Android Studio that the enable() and disable() functions need "android.permission.BLUETOOTH_CONNECT".

  1. If adding android:maxSdkVersion="30" to tell the system to ignore the statement on higher APIs is optional rather than obligatory, does that mean that not adding it will allow the statement to work on higher APIs?

  2. If "android.permission.BLUETOOTH_CONNECT" is to allow my app to interact with other bluetooth devices then why is it needed to enable or disable the bluetooth adapter on the original device?

  3. If the BLUETOOTH_CONNECT permission needs to be requested at runtime what is the correct full way to do it? Meaning checking if it's already granted then requesting it if it's not. I have no Android 12 device so no way to test this code.

2 Answers

Targeting Android 12 my working solution is to declare the permissions in this way:

<!--Before Android 12 (but still needed location, even if not requested)-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<!--From Android 12-->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

Like you said, BLUETOOTH_SCAN is not sufficient and you need BLUETOOTH_CONNECT (also if you decide, like me, to ask to the user to enable Bluetooth starting a new startActivityForResult with action BluetoothAdapter.ACTION_REQUEST_ENABLE)

If the BLUETOOTH_CONNECT permission needs to be requested at runtime what is the correct full way to do it? Meaning checking if it's already granted then requesting it if it's not. I have no Android 12 device so no way to test this code.

Yes, in the same way of asking location permission on Android < 12 (not need anymore), but asking both BLUETOOTH_SCAN and BLUETOOTH_CONNECT

To improve @AndreasGobs answer, below the code to test if the connection with a device is viable or not based on current available permissions. In the manifest I've set that the COARSE and FINE location permissions must be limited to max API 30. Tested on Android 6, 8.1, 11 and 12 devices. I hope this will be useful.

/**
 * - API < S
 *   - Check ACCESS_COARSE_LOCATION and ACCESS_FINE_LOCATION permissions
 *   - API < O
 *     - Check has GPS
 *     - Check GPS enabled
 * - API >= S
 *   - Check BLUETOOTH_SCAN permission
 *   - Check BLUETOOTH_CONNECT permission
 * - Check Bluetooth enabled
 */
private boolean canConnect(){
    Timber.d("canConnect called");
    List<String> deniedPermissions = new ArrayList<>();
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
        if (!checkPermission(Manifest.permission.ACCESS_COARSE_LOCATION))
            deniedPermissions.add(Manifest.permission.ACCESS_COARSE_LOCATION);
        if (!checkPermission(Manifest.permission.ACCESS_FINE_LOCATION))
            deniedPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
        if(deniedPermissions.isEmpty()){
            if (!MmcDeviceCapabilities.hasLocationGps() //check if the device has GPS
                    || Build.VERSION.SDK_INT < Build.VERSION_CODES.O
                    || MmcDeviceCapabilities.isGpsEnabled()){ //check if the GPS is enabled
                if(MmcDeviceCapabilities.bluetoothEnabled()) //check if bluetooth is enabled
                    return true;
                else {
                    requestEnableBluetooth(); //method to request enable bluetooth
                    return false;
                }
            }
            else {
                Timber.d("Request enable GPS");
                requestEnableGps(); //method to request enable GPS (improving devices scan)
                return false;
            }
        }
        else {
            Timber.d("Request GPS permissions");
            requestRuntimePermissions(
                    "Bluetooth GPS request",
                    "GPS permissions request rationale",
                    GPS_PERMISSIONS_CODE,
                    deniedPermissions.toArray(new String[0]));
            return false;
        }
    }
    else { // Build.VERSION_CODES.S or later
        if(!checkPermission(Manifest.permission.BLUETOOTH_SCAN))
            deniedPermissions.add(Manifest.permission.BLUETOOTH_SCAN);
        if(!checkPermission(Manifest.permission.BLUETOOTH_CONNECT))
            deniedPermissions.add(Manifest.permission.BLUETOOTH_CONNECT);
        if(deniedPermissions.isEmpty())
            if(MmcDeviceCapabilities.bluetoothEnabled()) //check if bluetooth is enabled
                return true;
            else {
                requestEnableBluetooth(); //method to request enable bluetooth
                return false;
            }
        else {
            Timber.d("Request bluetooth permissions");
            requestRuntimePermissions(
                    "Bluetooth permissions request",
                    "Bluetooth permissions request rationale",
                    CONNECT_PERMISSIONS_CODE,
                    deniedPermissions.toArray(new String[0]));
            return false;
        }
    }
}
/**
 * This method checks if a runtime permission has been granted.
 * @param permission The permission to check.
 * @return <code>TRUE</code> if the permission has been granted, <code>FALSE</code> otherwise.
 */
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private boolean checkPermission(@NonNull String permission){
    return ActivityCompat.checkSelfPermission(this, permission)
            == PackageManager.PERMISSION_GRANTED;
}
private void requestRuntimePermissions(@NonNull String title, @NonNull String description, int requestCode, @NonNull String... permissions){
    if (ActivityCompat.shouldShowRequestPermissionRationale(this, permissions[0])) {
        AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
        builder
                .setTitle(title)
                .setMessage(description)
                .setCancelable(false)
                .setNegativeButton(android.R.string.no, (dialog, id) -> {
                    //do nothing
                })
                .setPositiveButton(android.R.string.ok, (dialog, id) -> ActivityCompat.requestPermissions(this, permissions, requestCode));
        showDialog(builder); //method to show a dialog
    }
    else ActivityCompat.requestPermissions(this, permissions, requestCode);
}
Related