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
@@ -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(
diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/RxBleClient.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/RxBleClient.java
index 64ef1f274..1a46badd2 100644
--- a/rxandroidble/src/main/java/com/polidea/rxandroidble2/RxBleClient.java
+++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/RxBleClient.java
@@ -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.
*
* Returned values:
*
@@ -208,8 +209,12 @@ public static void updateLogOptions(LogOptions logOptions) {
* {@link android.Manifest.permission#ACCESS_COARSE_LOCATION}
* {@link android.Manifest.permission#ACCESS_FINE_LOCATION}
*
- * case: 29 <= API
+ * case: 29 <= API < 31
* {@link android.Manifest.permission#ACCESS_FINE_LOCATION}
+ *
+ * case: 31 <= API
+ * {@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
*/
diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/RxBleClientImpl.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/RxBleClientImpl.java
index 98535b8ce..838bed9ec 100644
--- a/rxandroidble/src/main/java/com/polidea/rxandroidble2/RxBleClientImpl.java
+++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/RxBleClientImpl.java
@@ -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;
@@ -66,7 +66,7 @@ class RxBleClientImpl extends RxBleClient {
private final LocationServicesStatus locationServicesStatus;
private final Lazy lazyClientStateObservable;
private final BackgroundScanner backgroundScanner;
- private final CheckerLocationPermission checkerLocationPermission;
+ private final CheckerScanPermission checkerScanPermission;
@Inject
RxBleClientImpl(RxBleAdapterWrapper rxBleAdapterWrapper,
@@ -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;
@@ -96,7 +96,7 @@ class RxBleClientImpl extends RxBleClient {
this.bluetoothInteractionScheduler = bluetoothInteractionScheduler;
this.clientComponentFinalizer = clientComponentFinalizer;
this.backgroundScanner = backgroundScanner;
- this.checkerLocationPermission = checkerLocationPermission;
+ this.checkerScanPermission = checkerScanPermission;
}
@Override
@@ -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();
}
}
diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/CheckerLocationPermission.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/CheckerScanPermission.java
similarity index 52%
rename from rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/CheckerLocationPermission.java
rename to rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/CheckerScanPermission.java
index e7dab33f1..9d58509ce 100644
--- a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/CheckerLocationPermission.java
+++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/CheckerScanPermission.java
@@ -12,31 +12,50 @@
import com.polidea.rxandroidble2.ClientScope;
@ClientScope
-public class CheckerLocationPermission {
+public class CheckerScanPermission {
private final Context context;
- private final String[] scanPermissions;
+ private final String[][] scanPermissions;
@Inject
- CheckerLocationPermission(
+ CheckerScanPermission(
Context context,
- @Named(ClientComponent.PlatformConstants.STRING_ARRAY_SCAN_PERMISSIONS) String[] scanPermissions
+ @Named(ClientComponent.PlatformConstants.STRING_ARRAY_SCAN_PERMISSIONS) String[][] scanPermissions
) {
this.context = context;
this.scanPermissions = scanPermissions;
}
public boolean isScanRuntimePermissionGranted() {
- for (String locationPermission : scanPermissions) {
- if (isPermissionGranted(locationPermission)) {
+ boolean allNeededPermissionsGranted = true;
+ for (String[] neededPermissions : scanPermissions) {
+ allNeededPermissionsGranted &= isAnyPermissionGranted(neededPermissions);
+ }
+ return allNeededPermissionsGranted;
+ }
+
+ private boolean isAnyPermissionGranted(String[] acceptablePermissions) {
+ for (String acceptablePermission : acceptablePermissions) {
+ if (isPermissionGranted(acceptablePermission)) {
return true;
}
}
- return scanPermissions.length == 0;
+ return false;
}
public String[] getRecommendedScanRuntimePermissions() {
- return scanPermissions;
+ int allPermissionsCount = 0;
+ for (String[] permissionsArray : scanPermissions) {
+ allPermissionsCount += permissionsArray.length;
+ }
+ String[] resultPermissions = new String[allPermissionsCount];
+ int i = 0;
+ for (String[] permissionsArray : scanPermissions) {
+ for (String permission : permissionsArray) {
+ resultPermissions[i++] = permission;
+ }
+ }
+ return resultPermissions;
}
/**
diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/LocationServicesStatusApi23.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/LocationServicesStatusApi23.java
index 834746599..21e13ed41 100644
--- a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/LocationServicesStatusApi23.java
+++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/LocationServicesStatusApi23.java
@@ -8,7 +8,7 @@
public class LocationServicesStatusApi23 implements LocationServicesStatus {
private final CheckerLocationProvider checkerLocationProvider;
- private final CheckerLocationPermission checkerLocationPermission;
+ private final CheckerScanPermission checkerScanPermission;
private final boolean isAndroidWear;
private final int targetSdk;
private final int deviceSdk;
@@ -16,20 +16,20 @@ public class LocationServicesStatusApi23 implements LocationServicesStatus {
@Inject
LocationServicesStatusApi23(
CheckerLocationProvider checkerLocationProvider,
- CheckerLocationPermission checkerLocationPermission,
+ CheckerScanPermission checkerScanPermission,
@Named(ClientComponent.PlatformConstants.INT_TARGET_SDK) int targetSdk,
@Named(ClientComponent.PlatformConstants.INT_DEVICE_SDK) int deviceSdk,
@Named(ClientComponent.PlatformConstants.BOOL_IS_ANDROID_WEAR) boolean isAndroidWear
) {
this.checkerLocationProvider = checkerLocationProvider;
- this.checkerLocationPermission = checkerLocationPermission;
+ this.checkerScanPermission = checkerScanPermission;
this.targetSdk = targetSdk;
this.deviceSdk = deviceSdk;
this.isAndroidWear = isAndroidWear;
}
public boolean isLocationPermissionOk() {
- return checkerLocationPermission.isScanRuntimePermissionGranted();
+ return checkerScanPermission.isScanRuntimePermissionGranted();
}
public boolean isLocationProviderOk() {
diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/LocationServicesStatusApi31.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/LocationServicesStatusApi31.java
new file mode 100644
index 000000000..a94c23d0d
--- /dev/null
+++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/LocationServicesStatusApi31.java
@@ -0,0 +1,52 @@
+package com.polidea.rxandroidble2.internal.util;
+
+import android.annotation.TargetApi;
+
+import com.polidea.rxandroidble2.ClientComponent;
+
+import bleshadow.javax.inject.Inject;
+import bleshadow.javax.inject.Named;
+
+@TargetApi(31 /* Build.VERSION_CODES.S */)
+public class LocationServicesStatusApi31 implements LocationServicesStatus {
+
+ private final CheckerLocationProvider checkerLocationProvider;
+ private final CheckerScanPermission checkerScanPermission;
+ private final boolean isAndroidWear;
+ private final boolean isNearbyPermissionNeverForLoc;
+
+ @Inject
+ LocationServicesStatusApi31(
+ CheckerLocationProvider checkerLocationProvider,
+ CheckerScanPermission checkerScanPermission,
+ @Named(ClientComponent.PlatformConstants.BOOL_IS_ANDROID_WEAR) boolean isAndroidWear,
+ @Named(ClientComponent.PlatformConstants.BOOL_IS_NEARBY_PERMISSION_NEVER_FOR_LOCATION) boolean isNearbyPermissionNeverForLoc
+ ) {
+ this.checkerLocationProvider = checkerLocationProvider;
+ this.checkerScanPermission = checkerScanPermission;
+ this.isAndroidWear = isAndroidWear;
+ this.isNearbyPermissionNeverForLoc = isNearbyPermissionNeverForLoc;
+ }
+
+ public boolean isLocationPermissionOk() {
+ return checkerScanPermission.isScanRuntimePermissionGranted();
+ }
+
+ public boolean isLocationProviderOk() {
+ return !isLocationProviderEnabledRequired() || checkerLocationProvider.isLocationProviderEnabled();
+ }
+
+ /**
+ * A function that returns true if the location services may be needed to be turned ON. Since there are no official guidelines
+ * for Android Wear check is disabled.
+ *
+ * @return true if Location Services need to be turned ON
+ * @see Google Groups Discussion
+ */
+ private boolean isLocationProviderEnabledRequired() {
+ if (isAndroidWear) {
+ return false;
+ }
+ return !isNearbyPermissionNeverForLoc;
+ }
+}
diff --git a/rxandroidble/src/test/groovy/com/polidea/rxandroidble2/RxBleClientTest.groovy b/rxandroidble/src/test/groovy/com/polidea/rxandroidble2/RxBleClientTest.groovy
index 6a8772d5f..c9f4dfdd7 100644
--- a/rxandroidble/src/test/groovy/com/polidea/rxandroidble2/RxBleClientTest.groovy
+++ b/rxandroidble/src/test/groovy/com/polidea/rxandroidble2/RxBleClientTest.groovy
@@ -9,7 +9,7 @@ import com.polidea.rxandroidble2.internal.RxBleDeviceProvider
import com.polidea.rxandroidble2.internal.operations.Operation
import com.polidea.rxandroidble2.internal.scan.*
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.ScanRecordParser
import com.polidea.rxandroidble2.scan.BackgroundScanner
@@ -53,7 +53,7 @@ class RxBleClientTest extends ElectricSpecification {
ScanSetup mockScanSetup = new ScanSetup(mockOperationScan, mockObservableTransformer)
ScanPreconditionsVerifier mockScanPreconditionVerifier = Mock ScanPreconditionsVerifier
InternalToExternalScanResultConverter mockMapper = Mock InternalToExternalScanResultConverter
- CheckerLocationPermission mockCheckerLocationPermission = Mock CheckerLocationPermission
+ CheckerScanPermission mockCheckerLocationPermission = Mock CheckerScanPermission
private static someUUID = UUID.randomUUID()
private static otherUUID = UUID.randomUUID()
private static Date suggestedDateToRetry = new Date()
diff --git a/rxandroidble/src/test/groovy/com/polidea/rxandroidble2/internal/util/CheckerScanPermissionTest.groovy b/rxandroidble/src/test/groovy/com/polidea/rxandroidble2/internal/util/CheckerScanPermissionTest.groovy
new file mode 100644
index 000000000..262e3d7df
--- /dev/null
+++ b/rxandroidble/src/test/groovy/com/polidea/rxandroidble2/internal/util/CheckerScanPermissionTest.groovy
@@ -0,0 +1,38 @@
+package com.polidea.rxandroidble2.internal.util
+
+
+import spock.lang.Specification
+import spock.lang.Unroll
+
+class CheckerScanPermissionTest extends Specification {
+ CheckerScanPermission objectUnderTest
+
+ @Unroll
+ def "return input data concatenated if needed"() {
+
+ given:
+ objectUnderTest = new CheckerScanPermission(null, repackToArray(permissions))
+
+ expect:
+ objectUnderTest.getRecommendedScanRuntimePermissions() == (expectedPermissions.toArray(new String[0]))
+
+ where:
+ permissions | expectedPermissions
+ [] | []
+ [[], []] | []
+ [["p0", "p1"]] | ["p0", "p1"]
+ [["p0", "p1"], []] | ["p0", "p1"]
+ [["p0"], ["p1"]] | ["p0", "p1"]
+ [["p0", "p1"], ["p2"]] | ["p0", "p1", "p2"]
+ [["p0", "p1"], ["p2"], ["p3"]] | ["p0", "p1", "p2", "p3"]
+ }
+
+ String[][] repackToArray(ArrayList> permissions) {
+ def result = new String[permissions.size()][]
+ for (i in 0.. permissionsArrayInner = permissions.get(i)
+ result[i] = permissionsArrayInner.toArray(new String[0])
+ }
+ return result
+ }
+}
diff --git a/rxandroidble/src/test/groovy/com/polidea/rxandroidble2/internal/util/LocationServicesStatusApi23Test.groovy b/rxandroidble/src/test/groovy/com/polidea/rxandroidble2/internal/util/LocationServicesStatusApi23Test.groovy
index ca42cf82c..a294a0002 100644
--- a/rxandroidble/src/test/groovy/com/polidea/rxandroidble2/internal/util/LocationServicesStatusApi23Test.groovy
+++ b/rxandroidble/src/test/groovy/com/polidea/rxandroidble2/internal/util/LocationServicesStatusApi23Test.groovy
@@ -8,14 +8,14 @@ import spock.lang.Unroll
class LocationServicesStatusApi23Test extends Specification {
def mockCheckerLocationProvider = Mock CheckerLocationProvider
- def mockCheckerLocationPermission = Mock CheckerLocationPermission
+ def mockCheckerScanPermission = Mock CheckerScanPermission
int mockApplicationTargetSdk
int mockApplicationDeviceSdk
boolean mockIsAndroidWear
LocationServicesStatusApi23 objectUnderTest
private prepareObjectUnderTest() {
- objectUnderTest = new LocationServicesStatusApi23(mockCheckerLocationProvider, mockCheckerLocationPermission, mockApplicationTargetSdk, mockApplicationDeviceSdk, mockIsAndroidWear)
+ objectUnderTest = new LocationServicesStatusApi23(mockCheckerLocationProvider, mockCheckerScanPermission, mockApplicationTargetSdk, mockApplicationDeviceSdk, mockIsAndroidWear)
}
// API 18 is the first version with official AOSP BLE, API 21 is the last w/o need of location
@@ -37,7 +37,7 @@ class LocationServicesStatusApi23Test extends Specification {
given:
prepareObjectUnderTest()
- mockCheckerLocationPermission.isScanRuntimePermissionGranted() >> permissionGranted
+ mockCheckerScanPermission.isScanRuntimePermissionGranted() >> permissionGranted
expect:
objectUnderTest.isLocationPermissionOk() == permissionGranted
diff --git a/sample-kotlin/build.gradle b/sample-kotlin/build.gradle
index 5848238ef..f42dd3847 100644
--- a/sample-kotlin/build.gradle
+++ b/sample-kotlin/build.gradle
@@ -3,12 +3,12 @@ apply plugin: 'org.jetbrains.kotlin.android'
apply plugin: 'org.jetbrains.kotlin.android.extensions'
android {
- compileSdkVersion 29
+ compileSdkVersion 31
defaultConfig {
applicationId "com.polidea.rxandroidble.sample"
minSdkVersion 18
- targetSdkVersion 29
+ targetSdkVersion 31
versionCode 1
versionName "1.0"
}
diff --git a/sample-kotlin/src/main/AndroidManifest.xml b/sample-kotlin/src/main/AndroidManifest.xml
index 7db7e7515..392bdc4dd 100644
--- a/sample-kotlin/src/main/AndroidManifest.xml
+++ b/sample-kotlin/src/main/AndroidManifest.xml
@@ -14,7 +14,8 @@
+ android:label="@string/title_example1"
+ android:exported="true">
diff --git a/sample/build.gradle b/sample/build.gradle
index b1b245385..74ab034a9 100644
--- a/sample/build.gradle
+++ b/sample/build.gradle
@@ -12,12 +12,12 @@ repositories {
}
android {
- compileSdkVersion 29
+ compileSdkVersion 31
defaultConfig {
applicationId "com.polidea.rxandroidble.sample"
minSdkVersion 18
- targetSdkVersion 29
+ targetSdkVersion 31
versionCode 1
versionName "1.0"
}
diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml
index ff03e0c82..bc0c11746 100644
--- a/sample/src/main/AndroidManifest.xml
+++ b/sample/src/main/AndroidManifest.xml
@@ -11,10 +11,10 @@
android:theme="@style/AppTheme">
+ android:label="@string/title_example1"
+ android:exported="true">
-