diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/ClientComponent.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/ClientComponent.java index 7586cd988..f18a7938d 100644 --- a/rxandroidble/src/main/java/com/polidea/rxandroidble2/ClientComponent.java +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/ClientComponent.java @@ -24,6 +24,7 @@ import com.polidea.rxandroidble2.internal.scan.ScanPreconditionsVerifier; import com.polidea.rxandroidble2.internal.scan.ScanPreconditionsVerifierApi18; import com.polidea.rxandroidble2.internal.scan.ScanPreconditionsVerifierApi24; +import com.polidea.rxandroidble2.internal.scan.ScanPreconditionsVerifierApi31; import com.polidea.rxandroidble2.internal.scan.ScanSetupBuilder; import com.polidea.rxandroidble2.internal.scan.ScanSetupBuilderImplApi18; import com.polidea.rxandroidble2.internal.scan.ScanSetupBuilderImplApi21; @@ -89,6 +90,7 @@ class PlatformConstants { 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"; + public static final String PACKAGE_INFO = "package-info"; private PlatformConstants() { @@ -187,6 +189,18 @@ static String[][] provideRecommendedScanRuntimePermissionNames( }; } + @Provides + @Named(PlatformConstants.PACKAGE_INFO) + static PackageInfo providePackageInfo( + Context context + ) { + try { + return context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS); + } catch (Exception e) { + return new PackageInfo(); + } + } + @Provides static ContentResolver provideContentResolver(Context context) { return context.getContentResolver(); @@ -347,12 +361,15 @@ static byte[] provideDisableNotificationValue() { static ScanPreconditionsVerifier provideScanPreconditionVerifier( @Named(PlatformConstants.INT_DEVICE_SDK) int deviceSdk, Provider scanPreconditionVerifierForApi18, - Provider scanPreconditionVerifierForApi24 + Provider scanPreconditionVerifierForApi24, + Provider scanPreconditionVerifierForApi31 ) { if (deviceSdk < Build.VERSION_CODES.N) { return scanPreconditionVerifierForApi18.get(); - } else { + } else if (deviceSdk < Build.VERSION_CODES.S) { return scanPreconditionVerifierForApi24.get(); + } else { + return scanPreconditionVerifierForApi31.get(); } } diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/RxBleClientImpl.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/RxBleClientImpl.java index 838bed9ec..a645acee0 100644 --- a/rxandroidble/src/main/java/com/polidea/rxandroidble2/RxBleClientImpl.java +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/RxBleClientImpl.java @@ -115,6 +115,7 @@ public RxBleDevice getBleDevice(@NonNull String macAddress) { public Set getBondedDevices() { guardBluetoothAdapterAvailable(); Set rxBleDevices = new HashSet<>(); + // TODO: check BLUETOOTH_CONNECT permission Set bluetoothDevices = rxBleAdapterWrapper.getBondedDevices(); for (BluetoothDevice bluetoothDevice : bluetoothDevices) { rxBleDevices.add(getBleDevice(bluetoothDevice.getAddress())); diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/exceptions/BleScanException.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/exceptions/BleScanException.java index 82c9f957c..d72d0dc93 100644 --- a/rxandroidble/src/main/java/com/polidea/rxandroidble2/exceptions/BleScanException.java +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/exceptions/BleScanException.java @@ -15,7 +15,8 @@ public class BleScanException extends BleException { @IntDef({BLUETOOTH_CANNOT_START, BLUETOOTH_DISABLED, BLUETOOTH_NOT_AVAILABLE, LOCATION_PERMISSION_MISSING, LOCATION_SERVICES_DISABLED, SCAN_FAILED_ALREADY_STARTED, SCAN_FAILED_APPLICATION_REGISTRATION_FAILED, SCAN_FAILED_INTERNAL_ERROR, - SCAN_FAILED_FEATURE_UNSUPPORTED, SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES, UNDOCUMENTED_SCAN_THROTTLE, UNKNOWN_ERROR_CODE}) + SCAN_FAILED_FEATURE_UNSUPPORTED, SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES, SCAN_PERMISSION_MISSING, UNDOCUMENTED_SCAN_THROTTLE, + UNKNOWN_ERROR_CODE}) @Retention(RetentionPolicy.SOURCE) public @interface Reason { @@ -76,6 +77,11 @@ public class BleScanException extends BleException { */ public static final int SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES = 9; + /** + * The BLUETOOTH_SCAN permission has not been granted. Only on API >=31. + */ + public static final int SCAN_PERMISSION_MISSING = 10; + /** * On API >=25 there is an undocumented scan throttling mechanism. If 5 scans were started by the app during a 30 second window * the next scan in that window will be silently skipped with only a log warning. In this situation there should be diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/RxBleDeviceImpl.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/RxBleDeviceImpl.java index 0b8aa421d..e02aeb4b4 100644 --- a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/RxBleDeviceImpl.java +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/RxBleDeviceImpl.java @@ -72,6 +72,7 @@ public Observable establishConnection(final ConnectionSetup opt return Observable.defer(new Callable>() { @Override public ObservableSource call() { + // TODO: Check BLUETOOTH_CONNECT permission if (isConnected.compareAndSet(false, true)) { return connector.prepareConnection(options) .doFinally(new Action() { diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/scan/ScanPreconditionsVerifierApi31.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/scan/ScanPreconditionsVerifierApi31.java new file mode 100644 index 000000000..a5f617702 --- /dev/null +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/scan/ScanPreconditionsVerifierApi31.java @@ -0,0 +1,108 @@ +package com.polidea.rxandroidble2.internal.scan; + + +import android.Manifest; +import android.content.pm.PackageInfo; + +import com.polidea.rxandroidble2.ClientComponent; +import com.polidea.rxandroidble2.exceptions.BleScanException; +import com.polidea.rxandroidble2.internal.util.LocationServicesStatus; +import com.polidea.rxandroidble2.internal.util.RxBleAdapterWrapper; + +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import bleshadow.javax.inject.Inject; +import bleshadow.javax.inject.Named; +import io.reactivex.Scheduler; + +public class ScanPreconditionsVerifierApi31 implements ScanPreconditionsVerifier { + + /* + * default values taken from + * https://android.googlesource.com/platform/packages/apps/Bluetooth/+/android-7.0.0_r1/src/com/android/bluetooth/gatt/AppScanStats.java + */ + private static final int SCANS_LENGTH = 5; + private static final long EXCESSIVE_SCANNING_PERIOD = TimeUnit.SECONDS.toMillis(30); + private final long[] previousChecks = new long[SCANS_LENGTH]; + private final RxBleAdapterWrapper rxBleAdapterWrapper; + private final LocationServicesStatus locationServicesStatus; + private final Scheduler timeScheduler; + private final PackageInfo packageInfo; + + @Inject + public ScanPreconditionsVerifierApi31( + RxBleAdapterWrapper rxBleAdapterWrapper, + LocationServicesStatus locationServicesStatus, + @Named(ClientComponent.NamedSchedulers.COMPUTATION) Scheduler timeScheduler, + @Named(ClientComponent.PlatformConstants.PACKAGE_INFO) PackageInfo packageInfo + ) { + this.rxBleAdapterWrapper = rxBleAdapterWrapper; + this.locationServicesStatus = locationServicesStatus; + this.timeScheduler = timeScheduler; + this.packageInfo = packageInfo; + } + + @Override + public void verify(boolean checkLocationProviderState) { + // determine if we really need to check location + if (checkLocationProviderState + && this.packageInfo != null + && this.packageInfo.requestedPermissions != null + && this.packageInfo.requestedPermissionsFlags != null) { + // On API 31 we only need to check location here if the scan permission requests it + for (int i = 0; i < this.packageInfo.requestedPermissions.length; i++) { + if (Manifest.permission.BLUETOOTH_SCAN.equals(this.packageInfo.requestedPermissions[i])) { + if ((this.packageInfo.requestedPermissionsFlags[i] & PackageInfo.REQUESTED_PERMISSION_NEVER_FOR_LOCATION) != 0) { + // BLUETOOTH_SCAN is neverForLocation + checkLocationProviderState = false; + } + break; + } + } + } + + if (!rxBleAdapterWrapper.hasBluetoothAdapter()) { + throw new BleScanException(BleScanException.BLUETOOTH_NOT_AVAILABLE); + } else if (!rxBleAdapterWrapper.isBluetoothEnabled()) { + throw new BleScanException(BleScanException.BLUETOOTH_DISABLED); + } else if (checkLocationProviderState && !locationServicesStatus.isLocationPermissionOk()) { + throw new BleScanException(BleScanException.LOCATION_PERMISSION_MISSING); + } else if (checkLocationProviderState && !locationServicesStatus.isLocationProviderOk()) { + throw new BleScanException(BleScanException.LOCATION_SERVICES_DISABLED); + } else if (!locationServicesStatus.isScanPermissionOk()) { + throw new BleScanException(BleScanException.SCAN_PERMISSION_MISSING); + } + + /* + * Android 7.0 (API 24) introduces an undocumented scan throttle for applications that try to scan more than 5 times during + * a 30 second window. More on the topic: https://blog.classycode.com/undocumented-android-7-ble-behavior-changes-d1a9bd87d983 + */ + + // TODO: [DS] 27.06.2017 Think if persisting this information through Application close is needed + final int oldestCheckTimestampIndex = getOldestCheckTimestampIndex(); + final long oldestCheckTimestamp = previousChecks[oldestCheckTimestampIndex]; + final long currentCheckTimestamp = timeScheduler.now(TimeUnit.MILLISECONDS); + + if (currentCheckTimestamp - oldestCheckTimestamp < EXCESSIVE_SCANNING_PERIOD) { + throw new BleScanException( + BleScanException.UNDOCUMENTED_SCAN_THROTTLE, + new Date(oldestCheckTimestamp + EXCESSIVE_SCANNING_PERIOD) + ); + } + previousChecks[oldestCheckTimestampIndex] = currentCheckTimestamp; + } + + private int getOldestCheckTimestampIndex() { + long oldestTimestamp = Long.MAX_VALUE; + int index = -1; + for (int i = 0; i < SCANS_LENGTH; i++) { + final long previousCheckTimestamp = previousChecks[i]; + if (previousCheckTimestamp < oldestTimestamp) { + index = i; + oldestTimestamp = previousCheckTimestamp; + } + } + return index; + } +} diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/CheckerScanPermission.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/CheckerScanPermission.java index 9d58509ce..58ba24d77 100644 --- a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/CheckerScanPermission.java +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/CheckerScanPermission.java @@ -1,6 +1,7 @@ package com.polidea.rxandroidble2.internal.util; +import android.Manifest; import android.content.Context; import android.content.pm.PackageManager; import android.os.Process; @@ -34,6 +35,15 @@ public boolean isScanRuntimePermissionGranted() { return allNeededPermissionsGranted; } + public boolean isLocationRuntimePermissionGranted() { + return isPermissionGranted(Manifest.permission.ACCESS_COARSE_LOCATION) + || isPermissionGranted(Manifest.permission.ACCESS_FINE_LOCATION); + } + + public boolean isConnectRuntimePermissionGranted() { + return isPermissionGranted(Manifest.permission.BLUETOOTH_CONNECT); + } + private boolean isAnyPermissionGranted(String[] acceptablePermissions) { for (String acceptablePermission : acceptablePermissions) { if (isPermissionGranted(acceptablePermission)) { diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/LocationServicesStatus.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/LocationServicesStatus.java index e1da71da3..bbf8ff6d5 100644 --- a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/LocationServicesStatus.java +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/LocationServicesStatus.java @@ -5,4 +5,6 @@ public interface LocationServicesStatus { boolean isLocationPermissionOk(); boolean isLocationProviderOk(); + boolean isScanPermissionOk(); + boolean isConnectPermissionOk(); } diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/LocationServicesStatusApi18.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/LocationServicesStatusApi18.java index 413e22e06..c32545544 100644 --- a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/LocationServicesStatusApi18.java +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/LocationServicesStatusApi18.java @@ -16,4 +16,12 @@ public boolean isLocationPermissionOk() { public boolean isLocationProviderOk() { return true; } + + public boolean isScanPermissionOk() { + return true; + } + + public boolean isConnectPermissionOk() { + return true; + } } 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 21e13ed41..6193f737f 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 @@ -36,6 +36,14 @@ public boolean isLocationProviderOk() { return !isLocationProviderEnabledRequired() || checkerLocationProvider.isLocationProviderEnabled(); } + public boolean isScanPermissionOk() { + return true; + } + + public boolean isConnectPermissionOk() { + return true; + } + /** * 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. 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 index a94c23d0d..5fc465fcc 100644 --- a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/LocationServicesStatusApi31.java +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/LocationServicesStatusApi31.java @@ -29,13 +29,22 @@ public class LocationServicesStatusApi31 implements LocationServicesStatus { } public boolean isLocationPermissionOk() { - return checkerScanPermission.isScanRuntimePermissionGranted(); + return checkerScanPermission.isLocationRuntimePermissionGranted(); } public boolean isLocationProviderOk() { return !isLocationProviderEnabledRequired() || checkerLocationProvider.isLocationProviderEnabled(); } + @Override + public boolean isScanPermissionOk() { + return checkerScanPermission.isScanRuntimePermissionGranted(); + } + + public boolean isConnectPermissionOk() { + return checkerScanPermission.isConnectRuntimePermissionGranted(); + } + /** * 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. diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/RxBleAdapterWrapper.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/RxBleAdapterWrapper.java index c8217379f..5eda116f3 100644 --- a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/RxBleAdapterWrapper.java +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/util/RxBleAdapterWrapper.java @@ -115,6 +115,7 @@ public Set getBondedDevices() { if (bluetoothAdapter == null) { throw nullBluetoothAdapter; } + // TODO: check BLUETOOTH_CONNECT permission return bluetoothAdapter.getBondedDevices(); } }