diff --git a/README.md b/README.md index f3ba2189a..9be704bd3 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,8 @@ RxBleClient.setLogLevel(RxBleLog.DEBUG); ### Error handling Every error you may encounter is provided via onError callback. Each public method has JavaDoc explaining possible errors. +### Helpers +We encourage you to check the package `com.polidea.rxandroidble.helpers` which contains handy reactive wrappers for some typical use-cases. ## More examples diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble/helpers/LocationServicesOkObservable.java b/rxandroidble/src/main/java/com/polidea/rxandroidble/helpers/LocationServicesOkObservable.java new file mode 100644 index 000000000..ac4bdcc39 --- /dev/null +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble/helpers/LocationServicesOkObservable.java @@ -0,0 +1,79 @@ +package com.polidea.rxandroidble.helpers; + + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.location.LocationManager; +import android.support.annotation.NonNull; +import com.polidea.rxandroidble.internal.util.CheckerLocationPermission; +import com.polidea.rxandroidble.internal.util.CheckerLocationProvider; +import com.polidea.rxandroidble.internal.util.LocationServicesStatus; +import com.polidea.rxandroidble.internal.util.ProviderApplicationTargetSdk; +import com.polidea.rxandroidble.internal.util.ProviderDeviceSdk; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; +import rx.Emitter; +import rx.Observable; +import rx.functions.Action1; +import rx.functions.Cancellable; +import rx.internal.operators.OnSubscribeFromEmitter; + +/** + * An Observable that emits true when {@link com.polidea.rxandroidble.RxBleClient#scanBleDevices(UUID...)} would not + * emit {@link com.polidea.rxandroidble.exceptions.BleScanException} with a reason + * {@link com.polidea.rxandroidble.exceptions.BleScanException#LOCATION_SERVICES_DISABLED} + */ +public class LocationServicesOkObservable extends Observable { + + public static LocationServicesOkObservable createInstance(@NonNull Context context) { + final Context applicationContext = context.getApplicationContext(); + final LocationManager locationManager = (LocationManager) applicationContext.getSystemService(Context.LOCATION_SERVICE); + final ProviderDeviceSdk providerDeviceSdk = new ProviderDeviceSdk(); + final ProviderApplicationTargetSdk providerApplicationTargetSdk = new ProviderApplicationTargetSdk(applicationContext); + final CheckerLocationPermission checkerLocationPermission = new CheckerLocationPermission(applicationContext); + final CheckerLocationProvider checkerLocationProvider = new CheckerLocationProvider(locationManager); + final LocationServicesStatus locationServicesStatus = new LocationServicesStatus( + checkerLocationProvider, + checkerLocationPermission, + providerDeviceSdk, + providerApplicationTargetSdk + ); + return new LocationServicesOkObservable(applicationContext, locationServicesStatus); + } + + LocationServicesOkObservable(@NonNull Context context, @NonNull LocationServicesStatus locationServicesStatus) { + super(new OnSubscribeFromEmitter<>( + new Action1>() { + @Override + public void call(Emitter emitter) { + final boolean locationProviderOk = locationServicesStatus.isLocationProviderOk(); + final AtomicBoolean locationProviderOkAtomicBoolean = new AtomicBoolean(locationProviderOk); + emitter.onNext(locationProviderOk); + + BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final boolean newLocationProviderOkValue = locationServicesStatus.isLocationProviderOk(); + final boolean valueChanged = locationProviderOkAtomicBoolean + .compareAndSet(!newLocationProviderOkValue, newLocationProviderOkValue); + if (valueChanged) { + emitter.onNext(newLocationProviderOkValue); + } + } + }; + + context.registerReceiver(broadcastReceiver, new IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION)); + emitter.setCancellation(new Cancellable() { + @Override + public void cancel() throws Exception { + context.unregisterReceiver(broadcastReceiver); + } + }); + } + }, + Emitter.BackpressureMode.LATEST + )); + } +} diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble/internal/util/CheckerLocationPermission.java b/rxandroidble/src/main/java/com/polidea/rxandroidble/internal/util/CheckerLocationPermission.java index 664b5e068..20212c805 100644 --- a/rxandroidble/src/main/java/com/polidea/rxandroidble/internal/util/CheckerLocationPermission.java +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble/internal/util/CheckerLocationPermission.java @@ -4,8 +4,7 @@ import android.Manifest; import android.content.Context; import android.content.pm.PackageManager; -import android.os.Build; -import android.support.annotation.RequiresApi; +import android.os.Process; public class CheckerLocationPermission { @@ -15,14 +14,21 @@ public CheckerLocationPermission(Context context) { this.context = context; } - @RequiresApi(api = Build.VERSION_CODES.M) - public boolean isLocationPermissionGranted() { + boolean isLocationPermissionGranted() { return isPermissionGranted(Manifest.permission.ACCESS_COARSE_LOCATION) || isPermissionGranted(Manifest.permission.ACCESS_FINE_LOCATION); } - @RequiresApi(api = Build.VERSION_CODES.M) + /** + * Copied from android.support.v4.content.ContextCompat for backwards compatibility + * @param permission the permission to check + * @return true is granted + */ private boolean isPermissionGranted(String permission) { - return context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED; + if (permission == null) { + throw new IllegalArgumentException("permission is null"); + } + + return context.checkPermission(permission, android.os.Process.myPid(), Process.myUid()) == PackageManager.PERMISSION_GRANTED; } } diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble/internal/util/LocationServicesStatus.java b/rxandroidble/src/main/java/com/polidea/rxandroidble/internal/util/LocationServicesStatus.java index 50cf607f4..c00250be2 100644 --- a/rxandroidble/src/main/java/com/polidea/rxandroidble/internal/util/LocationServicesStatus.java +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble/internal/util/LocationServicesStatus.java @@ -1,6 +1,5 @@ package com.polidea.rxandroidble.internal.util; -import android.annotation.TargetApi; import android.os.Build; public class LocationServicesStatus { @@ -26,20 +25,11 @@ public LocationServicesStatus( } public boolean isLocationPermissionOk() { - return !isLocationPermissionGrantedRequired() || isLocationPermissionGranted(); + return !isLocationPermissionGrantedRequired() || checkerLocationPermission.isLocationPermissionGranted(); } public boolean isLocationProviderOk() { - return !isLocationProviderEnabledRequired() || isLocationProviderEnabled(); - } - - @TargetApi(Build.VERSION_CODES.M) - private boolean isLocationPermissionGranted() { - return checkerLocationPermission.isLocationPermissionGranted(); - } - - private boolean isLocationProviderEnabled() { - return checkerLocationProvider.isLocationProviderEnabled(); + return !isLocationProviderEnabledRequired() || checkerLocationProvider.isLocationProviderEnabled(); } private boolean isLocationPermissionGrantedRequired() { diff --git a/rxandroidble/src/test/groovy/com/polidea/rxandroidble/helpers/LocationServicesOkObservableTest.groovy b/rxandroidble/src/test/groovy/com/polidea/rxandroidble/helpers/LocationServicesOkObservableTest.groovy new file mode 100644 index 000000000..dddbd13e8 --- /dev/null +++ b/rxandroidble/src/test/groovy/com/polidea/rxandroidble/helpers/LocationServicesOkObservableTest.groovy @@ -0,0 +1,133 @@ +package com.polidea.rxandroidble.helpers + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.location.LocationManager +import com.polidea.rxandroidble.internal.util.LocationServicesStatus +import org.robolectric.annotation.Config +import org.robospock.RoboSpecification +import rx.Subscription +import rx.observers.TestSubscriber + +@Config(manifest = Config.NONE) +class LocationServicesOkObservableTest extends RoboSpecification { + def contextMock = Mock Context + def mockLocationServicesStatus = Mock LocationServicesStatus + def objectUnderTest = new LocationServicesOkObservable(contextMock, mockLocationServicesStatus) + BroadcastReceiver registeredReceiver + + def setup() { + contextMock.getApplicationContext() >> contextMock + } + + def "should register to correct receiver on subscribe"() { + + given: + mockLocationServicesStatus.isLocationProviderOk() >> true + + when: + objectUnderTest.subscribe() + + then: + 1 * contextMock.registerReceiver(!null, { + it.hasAction(LocationManager.PROVIDERS_CHANGED_ACTION) + }) + } + + def "should unregister after observable was unsubscribed"() { + + given: + mockLocationServicesStatus.isLocationProviderOk() >> true + shouldCaptureRegisteredReceiver() + Subscription subscription = objectUnderTest.subscribe() + + when: + subscription.unsubscribe() + + then: + 1 * contextMock.unregisterReceiver(registeredReceiver) + } + + def "should emit what LocationServicesStatus.isLocationProviderOk() returns on subscribe and on next broadcasts"() { + + given: + shouldCaptureRegisteredReceiver() + mockLocationServicesStatus.isLocationProviderOk() >>> [true, false, true] + TestSubscriber testSubscriber = new TestSubscriber<>() + + when: + objectUnderTest.subscribe(testSubscriber) + + then: + testSubscriber.assertValue(true) + + when: + postStateChangeBroadcast() + + then: + testSubscriber.assertValues(true, false) + + when: + postStateChangeBroadcast() + + then: + testSubscriber.assertValues(true, false, true) + } + + def "should not emit what LocationServicesStatus.isLocationProviderOk() returns on next broadcasts if the value does not change"() { + + given: + shouldCaptureRegisteredReceiver() + mockLocationServicesStatus.isLocationProviderOk() >>> [false, false, true, true, false, false] + TestSubscriber testSubscriber = new TestSubscriber<>() + + when: + objectUnderTest.subscribe(testSubscriber) + + then: + testSubscriber.assertValue(false) + + when: + postStateChangeBroadcast() + + then: + testSubscriber.assertValue(false) + + when: + postStateChangeBroadcast() + + then: + testSubscriber.assertValues(false, true) + + when: + postStateChangeBroadcast() + + then: + testSubscriber.assertValues(false, true) + + when: + postStateChangeBroadcast() + + then: + testSubscriber.assertValues(false, true, false) + + when: + postStateChangeBroadcast() + + then: + testSubscriber.assertValues(false, true, false) + } + + public postStateChangeBroadcast() { + def intent = new Intent(LocationManager.PROVIDERS_CHANGED_ACTION) + registeredReceiver.onReceive(contextMock, intent) + } + + public BroadcastReceiver shouldCaptureRegisteredReceiver() { + _ * contextMock.registerReceiver({ + BroadcastReceiver receiver -> + this.registeredReceiver = receiver + }, _) + } +}