Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Support API 31 (Android 12) new Bluetooth permissions #762

Merged
merged 12 commits into from
Oct 6, 2021
Merged
36 changes: 27 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,27 +53,45 @@ maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
```

### Permissions
Android since API 23 (6.0 / Marshmallow) requires location permissions declared in the manifest for an app to run a BLE scan. RxAndroidBle already provides all the necessary bluetooth permissions for you in AndroidManifest.
Android since API 23 (6.0 / Marshmallow) requires additional permissions declared in the manifest for an app to run a BLE scan. RxAndroidBle provides minimal required bluetooth permissions for you in AndroidManifest — it assumes to be used in the foreground and not deriving actual user location from BLE signal.

#### Scanning
Runtime permissions required for running a BLE scan:

| from API | to API (inclusive) | Acceptable runtime permissions |
|:---:|:---:| --- |
| 18 | 22 | (No runtime permissions needed) |
| 23 | 28 | One of below:<br>- `android.permission.ACCESS_COARSE_LOCATION`<br>- `android.permission.ACCESS_FINE_LOCATION` |
| 29 | current | - `android.permission.ACCESS_FINE_LOCATION` |
| 29 | 30 | - `android.permission.ACCESS_FINE_LOCATION`<br>- `android.permission.ACCESS_BACKGROUND_LOCATION`\* |
| 31 | current | - `android.permission.BLUETOOTH_SCAN`\*\*<br>- `android.permission.ACCESS_FINE_LOCATION`\*\*\* |

\* Needed if [scan is performed in background](https://developer.android.com/about/versions/10/privacy/changes#app-access-device-location)
\*\* It is assumed in [AndroidManifest](https://github.com/Polidea/RxAndroidBle/blob/master/rxandroidble/src/main/AndroidManifest.xml) that the application is trying to derive user's location from BLE signal. If that is not the case look below into [Potential permission issues](https://github.com/Polidea/RxAndroidBle#potential-permission-issues).
\*\*\* Needed if `BLUETOOTH_SCAN` is not using `neverForLocation` flag

#### Connecting
Runtime permissions required for connecting to a BLE peripheral:
| from API | to API (inclusive) | Acceptable runtime permissions |
|:---:|:---:| --- |
| 18 | 30 | (No runtime permissions needed) |
| 31 | current | - `android.permission.BLUETOOTH_CONNECT` |

#### Potential permission issues
Google is checking `AndroidManifest` for declaring permissions when releasing to the Play Store. If you have `ACCESS_COARSE_LOCATION` or `ACCESS_FINE_LOCATION` set manually using tag `uses-permission` (as opposed to `uses-permission-sdk-23`) you may run into an issue where your manifest does not merge with RxAndroidBle's, resulting in a failure to upload to the Play Store. These permissions are only required on SDK 23+. If you need any of these permissions on a lower version of Android replace your statement with:
Google is checking `AndroidManifest` for declaring permissions when releasing to the Play Store. If you have `ACCESS_COARSE_LOCATION` or `ACCESS_FINE_LOCATION` set manually using tag `uses-permission` (as opposed to `uses-permission-sdk-23`) you may run into an issue where your manifest does not merge with [RxAndroidBle's AndroidManifest.xml](https://github.com/Polidea/RxAndroidBle/blob/master/rxandroidble/src/main/AndroidManifest.xml), resulting in a failure to upload to the Play Store. These permissions are required only on APIs 23-30 assuming your app is not accessing location otherwise. If you need any of these permissions other versions of Android replace your statement with:
```xml
<uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION" tools:node="remove" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
```
If you only want to scan BLE peripherals and do not access location otherwise you can restrict location permissions to only the required range by using [Manifest Merger tool directives](https://developer.android.com/studio/build/manifest-merge.html#marker_selector):
```xml
<uses-permission
android:name="android.permission.ACCESS_COARSE_LOCATION"
android:maxSdkVersion="22"/>
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION" tools:node="remove" />
<uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION" tools:node="remove" />
<uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
```
After API 31 (Android 12) there are new Bluetooth permissions. One of them comes in a flavour that restricts deriving user's location from BLE signal acquired while scanning — this is assumed by the library. If you need to locate user by scanning BLE use below but keep in mind that you will still need `ACCESS_FINE_LOCATION` then:
```xml
<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="22"/>
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" tools:node="remove" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
```

## Usage
Expand Down
4 changes: 2 additions & 2 deletions mockrxandroidble/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ apply plugin: 'groovyx.android'
apply from: rootProject.file('gradle/gradle-mvn-push.gradle')

android {
compileSdkVersion 29
compileSdkVersion 31

defaultConfig {
minSdkVersion 18
targetSdkVersion 29
targetSdkVersion 31
versionCode 1
versionName VERSION_NAME
}
Expand Down
4 changes: 2 additions & 2 deletions rxandroidble/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ apply plugin: 'groovyx.android'
apply from: rootProject.file('gradle/gradle-mvn-push.gradle')

android {
compileSdkVersion 29
compileSdkVersion 31

defaultConfig {
minSdkVersion 18
targetSdkVersion 29
targetSdkVersion 31
versionCode 1
versionName VERSION_NAME
}
Expand Down
27 changes: 18 additions & 9 deletions rxandroidble/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
<manifest
package="com.polidea.rxandroidble2"
xmlns:android="http://schemas.android.com/apk/res/android">

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

<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION"/>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.polidea.rxandroidble2">
<!-- required for API 18 - 30 -->
<uses-permission
android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<!-- required for API 23 - 30, no android:maxSdkVersion because of a potential breaking change -->
<!-- TODO: add android:maxSdkVersion on 2.0.0 -->
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- API 31+ -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@
import android.bluetooth.BluetoothManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.location.LocationManager;
import android.os.Build;

import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;

import com.polidea.rxandroidble2.helpers.LocationServicesOkObservable;
import com.polidea.rxandroidble2.internal.DeviceComponent;
import com.polidea.rxandroidble2.internal.RxBleLog;
import com.polidea.rxandroidble2.internal.scan.BackgroundScannerImpl;
import com.polidea.rxandroidble2.internal.scan.InternalToExternalScanResultConverter;
import com.polidea.rxandroidble2.internal.scan.RxBleInternalScanResult;
Expand All @@ -32,6 +35,7 @@
import com.polidea.rxandroidble2.internal.util.LocationServicesStatus;
import com.polidea.rxandroidble2.internal.util.LocationServicesStatusApi18;
import com.polidea.rxandroidble2.internal.util.LocationServicesStatusApi23;
import com.polidea.rxandroidble2.internal.util.LocationServicesStatusApi31;
import com.polidea.rxandroidble2.internal.util.ObservableUtil;
import com.polidea.rxandroidble2.scan.BackgroundScanner;
import com.polidea.rxandroidble2.scan.ScanResult;
Expand Down Expand Up @@ -60,6 +64,7 @@ class NamedExecutors {

public static final String BLUETOOTH_INTERACTION = "executor_bluetooth_interaction";
public static final String CONNECTION_QUEUE = "executor_connection_queue";

private NamedExecutors() {

}
Expand All @@ -71,6 +76,7 @@ class NamedSchedulers {
public static final String TIMEOUT = "timeout";
public static final String BLUETOOTH_INTERACTION = "bluetooth_interaction";
public static final String BLUETOOTH_CALLBACKS = "bluetooth_callbacks";

private NamedSchedulers() {

}
Expand All @@ -81,7 +87,9 @@ class PlatformConstants {
public static final String INT_TARGET_SDK = "target-sdk";
public static final String INT_DEVICE_SDK = "device-sdk";
public static final String BOOL_IS_ANDROID_WEAR = "android-wear";
public static final String BOOL_IS_NEARBY_PERMISSION_NEVER_FOR_LOCATION = "nearby-permission-never-for-location";
public static final String STRING_ARRAY_SCAN_PERMISSIONS = "scan-permissions";

private PlatformConstants() {

}
Expand All @@ -90,6 +98,7 @@ private PlatformConstants() {
class NamedBooleanObservables {

public static final String LOCATION_SERVICES_OK = "location-ok-boolean-observable";

private NamedBooleanObservables() {

}
Expand All @@ -100,6 +109,7 @@ class BluetoothConstants {
public static final String ENABLE_NOTIFICATION_VALUE = "enable-notification-value";
public static final String ENABLE_INDICATION_VALUE = "enable-indication-value";
public static final String DISABLE_NOTIFICATION_VALUE = "disable-notification-value";

private BluetoothConstants() {

}
Expand Down Expand Up @@ -141,24 +151,40 @@ static int provideDeviceSdk() {

@Provides
@Named(PlatformConstants.STRING_ARRAY_SCAN_PERMISSIONS)
static String[] provideRecommendedScanRuntimePermissionNames(
static String[][] provideRecommendedScanRuntimePermissionNames(
@Named(PlatformConstants.INT_DEVICE_SDK) int deviceSdk,
@Named(PlatformConstants.INT_TARGET_SDK) int targetSdk
@Named(PlatformConstants.INT_TARGET_SDK) int targetSdk,
@Named(PlatformConstants.BOOL_IS_NEARBY_PERMISSION_NEVER_FOR_LOCATION) boolean isNearbyServicesNeverForLocation
) {
int sdkVersion = Math.min(deviceSdk, targetSdk);
if (sdkVersion < 23 /* pre Android M */) {
// Before API 23 (Android M) no runtime permissions are needed
return new String[]{};
return new String[][]{};
}
if (sdkVersion < 29 /* pre Android 10 */) {
// Since API 23 (Android M) ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION allows for getting scan results
return new String[]{
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION
return new String[][]{
new String[]{Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION}
};
}
// Since API 29 (Android 10) only ACCESS_FINE_LOCATION allows for getting scan results
return new String[]{Manifest.permission.ACCESS_FINE_LOCATION};
if (sdkVersion < 31 /* pre Android 12 */) {
// Since API 29 (Android 10) only ACCESS_FINE_LOCATION allows for getting scan results
return new String[][]{
new String[]{Manifest.permission.ACCESS_FINE_LOCATION}
};
}
// Since API 31 (Android 12) only BLUETOOTH_SCAN allows for getting scan results
if (isNearbyServicesNeverForLocation) {
// if neverForLocation flag is used on BLUETOOTH_SCAN then it is the only permission needed
return new String[][]{
new String[]{Manifest.permission.BLUETOOTH_SCAN}
};
}
// otherwise ACCESS_FINE_LOCATION is needed as well
return new String[][]{
new String[]{Manifest.permission.BLUETOOTH_SCAN},
new String[]{Manifest.permission.ACCESS_FINE_LOCATION}
};
}

@Provides
Expand All @@ -170,11 +196,17 @@ static ContentResolver provideContentResolver(Context context) {
static LocationServicesStatus provideLocationServicesStatus(
@Named(PlatformConstants.INT_DEVICE_SDK) int deviceSdk,
Provider<LocationServicesStatusApi18> locationServicesStatusApi18Provider,
Provider<LocationServicesStatusApi23> locationServicesStatusApi23Provider
Provider<LocationServicesStatusApi23> locationServicesStatusApi23Provider,
Provider<LocationServicesStatusApi31> locationServicesStatusApi31Provider
) {
return deviceSdk < Build.VERSION_CODES.M
? locationServicesStatusApi18Provider.get()
: locationServicesStatusApi23Provider.get();
if (deviceSdk < 23 /* Build.VERSION_CODES.M */) {
return locationServicesStatusApi18Provider.get();
}
if (deviceSdk < 31 /* Build.VERSION_CODES.S */) {
return locationServicesStatusApi23Provider.get();
}
/* deviceSdk >= Build.VERSION_CODES.S */
return locationServicesStatusApi31Provider.get();
}

@Provides
Expand Down Expand Up @@ -255,6 +287,28 @@ static boolean provideIsAndroidWear(Context context, @Named(PlatformConstants.IN
&& context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
}

@Provides
@Named(PlatformConstants.BOOL_IS_NEARBY_PERMISSION_NEVER_FOR_LOCATION)
@ClientScope
static boolean provideIsNearbyPermissionNeverForLocation(Context context) {
try {
PackageInfo packageInfo = context.getPackageManager().getPackageInfo(
context.getPackageName(),
PackageManager.GET_PERMISSIONS
);
for (int i = 0; i < packageInfo.requestedPermissions.length; i++) {
if (!Manifest.permission.BLUETOOTH_SCAN.equals(packageInfo.requestedPermissions[i])) {
continue;
}
return (packageInfo.requestedPermissionsFlags[i] & PackageInfo.REQUESTED_PERMISSION_NEVER_FOR_LOCATION) != 0;
}
} catch (PackageManager.NameNotFoundException e) {
RxBleLog.e(e, "Could not find application PackageInfo");
}
// default to a safe value
return false;
}

@Provides
@ClientScope
static ScanSetupBuilder provideScanSetupProvider(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,9 @@ public static void updateLogOptions(LogOptions logOptions) {
/**
* Returns permission strings needed by the application to run a BLE scan or an empty array if no runtime permissions are needed. Since
* Android 6.0 runtime permissions were introduced. To run a BLE scan a runtime permission is needed ever since. Since Android 10.0
* a different (finer) permission is needed. Only a single permission returned by this function is needed to perform a scan. It is up
* to the user to decide which one. The result array is sorted with the least permissive values first.
* a different (finer) permission is needed. Prior to Android 12.0 only a single permission returned by this function is needed to
* perform a scan. It is up to the user to decide which one. The result array is sorted with the least permissive values first. Since
* Android 12 all permissions returned by this function are needed.
* <p>
* Returned values:
* <p>
Expand All @@ -208,8 +209,12 @@ public static void updateLogOptions(LogOptions logOptions) {
* {@link android.Manifest.permission#ACCESS_COARSE_LOCATION}
* {@link android.Manifest.permission#ACCESS_FINE_LOCATION}
* <p>
* case: 29 <= API<p>
* case: 29 <= API < 31<p>
* {@link android.Manifest.permission#ACCESS_FINE_LOCATION}
* <p>
* case: 31 <= API<p>
* {@link android.Manifest.permission#BLUETOOTH_SCAN}
* optionally {@link android.Manifest.permission#ACCESS_FINE_LOCATION} if BLUETOOTH_SCAN does not have a "neverForLocation" flag
*
* @return an ordered array of possible scan permissions
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import com.polidea.rxandroidble2.internal.scan.ScanSetup;
import com.polidea.rxandroidble2.internal.scan.ScanSetupBuilder;
import com.polidea.rxandroidble2.internal.serialization.ClientOperationQueue;
import com.polidea.rxandroidble2.internal.util.CheckerLocationPermission;
import com.polidea.rxandroidble2.internal.util.CheckerScanPermission;
import com.polidea.rxandroidble2.internal.util.ClientStateObservable;
import com.polidea.rxandroidble2.internal.util.LocationServicesStatus;
import com.polidea.rxandroidble2.internal.util.RxBleAdapterWrapper;
Expand Down Expand Up @@ -66,7 +66,7 @@ class RxBleClientImpl extends RxBleClient {
private final LocationServicesStatus locationServicesStatus;
private final Lazy<ClientStateObservable> lazyClientStateObservable;
private final BackgroundScanner backgroundScanner;
private final CheckerLocationPermission checkerLocationPermission;
private final CheckerScanPermission checkerScanPermission;

@Inject
RxBleClientImpl(RxBleAdapterWrapper rxBleAdapterWrapper,
Expand All @@ -82,7 +82,7 @@ class RxBleClientImpl extends RxBleClient {
@Named(ClientComponent.NamedSchedulers.BLUETOOTH_INTERACTION) Scheduler bluetoothInteractionScheduler,
ClientComponent.ClientComponentFinalizer clientComponentFinalizer,
BackgroundScanner backgroundScanner,
CheckerLocationPermission checkerLocationPermission) {
CheckerScanPermission checkerScanPermission) {
this.operationQueue = operationQueue;
this.rxBleAdapterWrapper = rxBleAdapterWrapper;
this.rxBleAdapterStateObservable = adapterStateObservable;
Expand All @@ -96,7 +96,7 @@ class RxBleClientImpl extends RxBleClient {
this.bluetoothInteractionScheduler = bluetoothInteractionScheduler;
this.clientComponentFinalizer = clientComponentFinalizer;
this.backgroundScanner = backgroundScanner;
this.checkerLocationPermission = checkerLocationPermission;
this.checkerScanPermission = checkerScanPermission;
}

@Override
Expand Down Expand Up @@ -272,11 +272,11 @@ public State getState() {

@Override
public boolean isScanRuntimePermissionGranted() {
return checkerLocationPermission.isScanRuntimePermissionGranted();
return checkerScanPermission.isScanRuntimePermissionGranted();
}

@Override
public String[] getRecommendedScanRuntimePermissions() {
return checkerLocationPermission.getRecommendedScanRuntimePermissions();
return checkerScanPermission.getRecommendedScanRuntimePermissions();
}
}