Add Android build switch for checking bluetooth permissions

Refactored method 'hasPermission' to 'hasNoBluetoothPermission'. The new method
respect the in SKD 31 introduced permission BLUETOOTH_CONNECT. For older SDK
versions the permission BLUETOOTH will be used.

Additionaly make methods private which need bluetooth permissions and only
called locally. For public methods which need bluetooth permissions the method
'hasNoBluetoothPermission' is called to check them.
Also add suppress lint annotation for 'MissingPermission'.

From SDK 30 to SDK 31 the bluetooth related permissions changed and it
is suggested to add the attribute 'android:maxSdkVersion="30"' to the
legacy BLUETOOTH permission in the 'AndroidManifest.xml' [1]:

> For your legacy Bluetooth-related permission declarations, set
> android:maxSdkVersion to 30. This app compatibility step helps the system
> grant your app only the Bluetooth permissions that it needs when installed
> on devices that run Android 12 or higher.

This is explicitly not done here!

During runtime (on Android 12) while starting the 'MagicBluetoothManger' the
following part in 'android.bluetooth.BluetootHeadset' constructor will be
executed and results in the previous exception:

  // Preserve legacy compatibility where apps were depending on
  // registerStateChangeCallback() performing a permissions check which
  // has been relaxed in modern platform versions
  if (context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.R
           && context.checkSelfPermission(android.Manifest.permission.BLUETOOTH)
                   != PackageManager.PERMISSION_GRANTED) {
     throw new SecurityException("Need BLUETOOTH permission");
 }

In the 'build.gradle' the 'targetSdkVersion 30' and 'compileSdkVersion 31' is
configured. Because the 'MagicBluetoothManager' checks for the 'targetSdkVersion'
instead of 'Build.VERSION.SDK_INT' also on a Android 12 (SDK 31) the 'BLUETOOTH'
permission is needed.

So the solution is to don't set `android:maxSdkVersion="30"' for the BLUETOOTH
permission and request the BLUETOOTH_CONNECT permission.

Resolves: #2132
See:
  [1] https://web.archive.org/web/20220416121005/https://developer.android.com/guide/topics/connectivity/bluetooth/permissions#declare-android12-or-higher

Signed-off-by: Tim Krüger <t@timkrueger.me>
This commit is contained in:
Tim Krüger 2022-06-09 17:39:09 +02:00
parent 45c33de776
commit 4821b02729
No known key found for this signature in database
GPG Key ID: FECE3A7222C52A4E

View File

@ -31,6 +31,7 @@
package com.nextcloud.talk.webrtc;
import android.Manifest;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
@ -47,11 +48,14 @@ import android.os.Handler;
import android.os.Looper;
import android.os.Process;
import android.util.Log;
import org.webrtc.ThreadUtils;
import java.util.List;
import java.util.Set;
import androidx.core.app.ActivityCompat;
public class MagicBluetoothManager {
private static final String TAG = "MagicBluetoothManager";
@ -73,12 +77,7 @@ public class MagicBluetoothManager {
// Runs when the Bluetooth timeout expires. We use that timeout after calling
// startScoAudio() or stopScoAudio() because we're not guaranteed to get a
// callback after those calls.
private final Runnable bluetoothTimeoutRunnable = new Runnable() {
@Override
public void run() {
bluetoothTimeout();
}
};
private final Runnable bluetoothTimeoutRunnable = this::bluetoothTimeout;
protected MagicBluetoothManager(Context context, MagicAudioManager audioManager) {
Log.d(TAG, "ctor");
@ -122,11 +121,11 @@ public class MagicBluetoothManager {
* Note that the MagicAudioManager is also involved in driving this state
* change.
*/
@SuppressLint("MissingPermission")
public void start() {
ThreadUtils.checkIsOnMainThread();
Log.d(TAG, "start");
if (!hasPermission(apprtcContext, android.Manifest.permission.BLUETOOTH)) {
Log.w(TAG, "Process (pid=" + Process.myPid() + ") lacks BLUETOOTH permission");
if(hasNoBluetoothPermission()){
return;
}
if (bluetoothState != State.UNINITIALIZED) {
@ -262,8 +261,12 @@ public class MagicBluetoothManager {
* HEADSET_AVAILABLE and |bluetoothDevice| will be mapped to the connected
* device if available.
*/
@SuppressLint("MissingPermission")
public void updateDevice() {
if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
boolean hasNoBluetoothPermissions = hasNoBluetoothPermission();
if (hasNoBluetoothPermissions ||
bluetoothState == State.UNINITIALIZED ||
bluetoothHeadset == null) {
return;
}
Log.d(TAG, "updateDevice");
@ -307,16 +310,28 @@ public class MagicBluetoothManager {
return bluetoothAdapter.getProfileProxy(context, listener, profile);
}
protected boolean hasPermission(Context context, String permission) {
return apprtcContext.checkPermission(permission, Process.myPid(), Process.myUid())
== PackageManager.PERMISSION_GRANTED;
private boolean hasNoBluetoothPermission() {
String permission;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
permission = Manifest.permission.BLUETOOTH_CONNECT;
} else {
permission = Manifest.permission.BLUETOOTH;
}
boolean hasPermission =
ActivityCompat.checkSelfPermission(apprtcContext, permission) == PackageManager.PERMISSION_GRANTED;
if(!hasPermission) {
Log.w(TAG, "Process (pid=" + Process.myPid() + ") lacks \"" + permission + "\" permission");
}
return !hasPermission;
}
/**
* Logs the state of the local Bluetooth adapter.
*/
@SuppressLint("HardwareIds")
protected void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) {
@SuppressLint({"HardwareIds", "MissingPermission"})
private void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) {
Log.d(TAG, "BluetoothAdapter: "
+ "enabled=" + localAdapter.isEnabled() + ", "
+ "state=" + stateToString(localAdapter.getState()) + ", "
@ -362,9 +377,13 @@ public class MagicBluetoothManager {
* Called when start of the BT SCO channel takes too long time. Usually
* happens when the BT device has been turned on during an ongoing call.
*/
@SuppressLint("MissingPermission")
private void bluetoothTimeout() {
ThreadUtils.checkIsOnMainThread();
if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
boolean hasNoBluetoothPermissions = hasNoBluetoothPermission();
if (hasNoBluetoothPermissions ||
bluetoothState == State.UNINITIALIZED ||
bluetoothHeadset == null) {
return;
}
Log.d(TAG, "bluetoothTimeout: BT state=" + bluetoothState + ", "