diff --git a/mockrxandroidble/src/main/java/com/polidea/rxandroidble/mockrxandroidble/RxBleConnectionMock.java b/mockrxandroidble/src/main/java/com/polidea/rxandroidble/mockrxandroidble/RxBleConnectionMock.java index a89ef7081..f2de7c09d 100644 --- a/mockrxandroidble/src/main/java/com/polidea/rxandroidble/mockrxandroidble/RxBleConnectionMock.java +++ b/mockrxandroidble/src/main/java/com/polidea/rxandroidble/mockrxandroidble/RxBleConnectionMock.java @@ -2,7 +2,9 @@ import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; +import android.os.Build; import android.support.annotation.NonNull; +import android.support.annotation.RequiresApi; import com.polidea.rxandroidble.NotificationSetupMode; import com.polidea.rxandroidble.RxBleConnection; @@ -61,6 +63,11 @@ public RxBleConnectionMock(RxBleDeviceServices rxBleDeviceServices, this.characteristicNotificationSources = characteristicNotificationSources; } + @Override + public Completable requestConnectionPriority(int connectionPriority, long delay, @NonNull TimeUnit timeUnit) { + return Completable.complete(); + } + @Override public Observable requestMtu(final int mtu) { return Observable.fromCallable(new Callable() { diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble/RxBleConnection.java b/rxandroidble/src/main/java/com/polidea/rxandroidble/RxBleConnection.java index eaa631c30..bfcdccccb 100644 --- a/rxandroidble/src/main/java/com/polidea/rxandroidble/RxBleConnection.java +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble/RxBleConnection.java @@ -4,6 +4,8 @@ import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; import android.os.Build; +import android.support.annotation.IntDef; +import android.support.annotation.IntRange; import android.support.annotation.NonNull; import android.support.annotation.RequiresApi; @@ -15,9 +17,12 @@ import com.polidea.rxandroidble.exceptions.BleGattOperationType; import com.polidea.rxandroidble.internal.connection.RxBleGattCallback; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.UUID; import java.util.concurrent.TimeUnit; +import rx.Completable; import rx.Observable; import rx.Scheduler; @@ -41,6 +46,16 @@ public interface RxBleConnection { */ int GATT_READ_MTU_OVERHEAD = 1; + /** + * Description of correct values of connection priority + */ + @Retention(RetentionPolicy.SOURCE) + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @IntDef({BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER, + BluetoothGatt.CONNECTION_PRIORITY_BALANCED, + BluetoothGatt.CONNECTION_PRIORITY_HIGH}) + @interface ConnectionPriority { } + interface Connector { Observable prepareConnection(boolean autoConnect); @@ -411,6 +426,46 @@ Observable> setupIndication(@NonNull BluetoothGattCharacteris */ Observable writeDescriptor(BluetoothGattDescriptor descriptor, byte[] data); + + /** + * Performs a GATT request connection priority operation, which requests a connection parameter + * update on the remote device. NOTE: peripheral may silently decline request. + *

+ * Tells Android to request an update of connection interval and slave latency parameters. + * Using {@link BluetoothGatt#CONNECTION_PRIORITY_HIGH} will increase transmission speed and + * battery drainage, if accepted by the device, compared to {@link BluetoothGatt#CONNECTION_PRIORITY_BALANCED}, + * while using {@link BluetoothGatt#CONNECTION_PRIORITY_LOW_POWER} will cause higher latencies + * and save battery, if accepted by the device, compared to {@link BluetoothGatt#CONNECTION_PRIORITY_BALANCED}. + *

+ * By default connection is balanced. + *

+ * NOTE: Due to lack of support for `BluetoothGattCallback.onConnectionPriorityChanged()` or similar it + * is not possible to know if the request was successful (accepted by the peripheral). This also causes + * the need of specifying when the request is considered finished (parameter delay and timeUnit). + *

+ * As of Lollipop the connection parameters are: + * * {@link BluetoothGatt#CONNECTION_PRIORITY_BALANCED}: min interval 30 ms, max interval 50 ms, slave latency 0 + * * {@link BluetoothGatt#CONNECTION_PRIORITY_HIGH}: min interval 7.5 ms, max interval 10ms, slave latency 0 + * * {@link BluetoothGatt#CONNECTION_PRIORITY_LOW_POWER}: min interval 100ms, max interval 125 ms, slave latency 2 + *

+ * Returned completable completes after the specified delay if and only if + * {@link BluetoothGatt#requestConnectionPriority(int)} has returned true. + * + * @param connectionPriority requested connection priority + * @param delay delay after which operation is assumed to be successful (must be shorter than 30 seconds) + * @param timeUnit time unit of the delay + * @return Completable which finishes after calling the request and the specified delay + * @throws BleGattCannotStartException with {@link BleGattOperationType#CONNECTION_PRIORITY_CHANGE} type + * if requested operation returned false or threw exception + * @throws IllegalArgumentException in case of invalid connection priority or delay + */ + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + Completable requestConnectionPriority( + @ConnectionPriority int connectionPriority, + @IntRange(from = 1) long delay, + @NonNull TimeUnit timeUnit + ); + /** * Performs GATT read rssi operation. * diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble/RxBleDeviceServices.java b/rxandroidble/src/main/java/com/polidea/rxandroidble/RxBleDeviceServices.java index 89faeefc4..483ee259c 100644 --- a/rxandroidble/src/main/java/com/polidea/rxandroidble/RxBleDeviceServices.java +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble/RxBleDeviceServices.java @@ -16,7 +16,7 @@ import rx.functions.Func1; /** - * Service discovery result containing list of services and characteristics withing the services. + * Service discovery result containing list of services and characteristics within the services. */ public class RxBleDeviceServices { diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble/exceptions/BleGattOperationType.java b/rxandroidble/src/main/java/com/polidea/rxandroidble/exceptions/BleGattOperationType.java index 3b7d6faa8..19aa359ed 100644 --- a/rxandroidble/src/main/java/com/polidea/rxandroidble/exceptions/BleGattOperationType.java +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble/exceptions/BleGattOperationType.java @@ -13,6 +13,7 @@ public class BleGattOperationType { public static final BleGattOperationType RELIABLE_WRITE_COMPLETED = new BleGattOperationType("RELIABLE_WRITE_COMPLETED"); public static final BleGattOperationType READ_RSSI = new BleGattOperationType("READ_RSSI"); public static final BleGattOperationType ON_MTU_CHANGED = new BleGattOperationType("ON_MTU_CHANGED"); + public static final BleGattOperationType CONNECTION_PRIORITY_CHANGE = new BleGattOperationType("CONNECTION_PRIORITY_CHANGE"); private final String description; private BleGattOperationType(String description) { diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble/internal/connection/RxBleConnectionImpl.java b/rxandroidble/src/main/java/com/polidea/rxandroidble/internal/connection/RxBleConnectionImpl.java index e7278111f..cfc5d7053 100644 --- a/rxandroidble/src/main/java/com/polidea/rxandroidble/internal/connection/RxBleConnectionImpl.java +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble/internal/connection/RxBleConnectionImpl.java @@ -27,6 +27,7 @@ import javax.inject.Named; import javax.inject.Provider; +import rx.Completable; import rx.Observable; import rx.Scheduler; import rx.functions.Action1; @@ -75,6 +76,29 @@ public LongWriteOperationBuilder createNewLongWriteBuilder() { return longWriteOperationBuilderProvider.get(); } + @Override + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public Completable requestConnectionPriority(int connectionPriority, long delay, @NonNull TimeUnit timeUnit) { + if (connectionPriority != BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER + && connectionPriority != BluetoothGatt.CONNECTION_PRIORITY_BALANCED + && connectionPriority != BluetoothGatt.CONNECTION_PRIORITY_HIGH) { + return Completable.error( + new IllegalArgumentException( + "Connection priority must have valid value from BluetoothGatt (received " + + connectionPriority + ")" + ) + ); + } + + if (delay <= 0) { + return Completable.error(new IllegalArgumentException("Delay must be bigger than 0")); + } + + return rxBleRadio + .queue(operationsProvider.provideConnectionPriorityChangeOperation(connectionPriority, delay, timeUnit)) + .toCompletable(); + } + @Override @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public Observable requestMtu(int mtu) { diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble/internal/operations/OperationsProvider.java b/rxandroidble/src/main/java/com/polidea/rxandroidble/internal/operations/OperationsProvider.java index 61ec0e02a..ea3e2ba4e 100644 --- a/rxandroidble/src/main/java/com/polidea/rxandroidble/internal/operations/OperationsProvider.java +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble/internal/operations/OperationsProvider.java @@ -32,4 +32,10 @@ RxBleRadioOperationCharacteristicLongWrite provideLongWriteOperation( RxBleRadioOperationCharacteristicWrite provideWriteCharacteristic(BluetoothGattCharacteristic characteristic, byte[] data); RxBleRadioOperationDescriptorWrite provideWriteDescriptor(BluetoothGattDescriptor bluetoothGattDescriptor, byte[] data); + + RxBleRadioOperationConnectionPriorityRequest provideConnectionPriorityChangeOperation( + int connectionPriority, + long delay, + TimeUnit timeUnit + ); } diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble/internal/operations/OperationsProviderImpl.java b/rxandroidble/src/main/java/com/polidea/rxandroidble/internal/operations/OperationsProviderImpl.java index 6d2312971..13b1d620b 100644 --- a/rxandroidble/src/main/java/com/polidea/rxandroidble/internal/operations/OperationsProviderImpl.java +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble/internal/operations/OperationsProviderImpl.java @@ -99,4 +99,13 @@ public RxBleRadioOperationDescriptorWrite provideWriteDescriptor(BluetoothGattDe return new RxBleRadioOperationDescriptorWrite(rxBleGattCallback, bluetoothGatt, timeoutConfiguration, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT, bluetoothGattDescriptor, data); } + + @Override + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public RxBleRadioOperationConnectionPriorityRequest provideConnectionPriorityChangeOperation(int connectionPriority, + long delay, + TimeUnit timeUnit) { + return new RxBleRadioOperationConnectionPriorityRequest(rxBleGattCallback, bluetoothGatt, timeoutConfiguration, + connectionPriority, delay, timeUnit, timeoutScheduler); + } } diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble/internal/operations/RxBleRadioOperationConnectionPriorityRequest.java b/rxandroidble/src/main/java/com/polidea/rxandroidble/internal/operations/RxBleRadioOperationConnectionPriorityRequest.java new file mode 100644 index 000000000..f17e7cf58 --- /dev/null +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble/internal/operations/RxBleRadioOperationConnectionPriorityRequest.java @@ -0,0 +1,52 @@ +package com.polidea.rxandroidble.internal.operations; + +import android.bluetooth.BluetoothGatt; +import android.os.Build; +import android.support.annotation.RequiresApi; + +import com.polidea.rxandroidble.exceptions.BleGattCannotStartException; +import com.polidea.rxandroidble.exceptions.BleGattOperationType; +import com.polidea.rxandroidble.internal.RxBleSingleGattRadioOperation; +import com.polidea.rxandroidble.internal.connection.RxBleGattCallback; + +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; + +import rx.Observable; +import rx.Scheduler; + +public class RxBleRadioOperationConnectionPriorityRequest extends RxBleSingleGattRadioOperation { + + private final int connectionPriority; + private final long operationTimeout; + private final TimeUnit timeUnit; + private final Scheduler delayScheduler; + + @Inject + RxBleRadioOperationConnectionPriorityRequest( + RxBleGattCallback rxBleGattCallback, + BluetoothGatt bluetoothGatt, + TimeoutConfiguration timeoutConfiguration, + int connectionPriority, + long operationTimeout, + TimeUnit timeUnit, + Scheduler delayScheduler) { + super(bluetoothGatt, rxBleGattCallback, BleGattOperationType.CONNECTION_PRIORITY_CHANGE, timeoutConfiguration); + this.connectionPriority = connectionPriority; + this.operationTimeout = operationTimeout; + this.timeUnit = timeUnit; + this.delayScheduler = delayScheduler; + } + + @Override + protected Observable getCallback(RxBleGattCallback rxBleGattCallback) { + return Observable.timer(operationTimeout, timeUnit, delayScheduler); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + protected boolean startOperation(BluetoothGatt bluetoothGatt) throws IllegalArgumentException, BleGattCannotStartException { + return bluetoothGatt.requestConnectionPriority(connectionPriority); + } +} diff --git a/rxandroidble/src/test/groovy/com/polidea/rxandroidble/internal/operations/RxBleRadioOperationConnectionPriorityRequestTest.groovy b/rxandroidble/src/test/groovy/com/polidea/rxandroidble/internal/operations/RxBleRadioOperationConnectionPriorityRequestTest.groovy new file mode 100644 index 000000000..4b5c32936 --- /dev/null +++ b/rxandroidble/src/test/groovy/com/polidea/rxandroidble/internal/operations/RxBleRadioOperationConnectionPriorityRequestTest.groovy @@ -0,0 +1,78 @@ +package com.polidea.rxandroidble.internal.operations + +import android.bluetooth.BluetoothGatt +import com.polidea.rxandroidble.exceptions.BleGattCannotStartException +import com.polidea.rxandroidble.internal.connection.RxBleGattCallback +import com.polidea.rxandroidble.internal.util.MockOperationTimeoutConfiguration +import rx.observers.TestSubscriber +import rx.schedulers.TestScheduler +import spock.lang.Specification + +import java.util.concurrent.Semaphore +import java.util.concurrent.TimeUnit + +class RxBleRadioOperationConnectionPriorityRequestTest extends Specification { + + static long timeout = 10 + int completedDelay = 500L + TimeUnit delayUnit = TimeUnit.MILLISECONDS + BluetoothGatt mockBluetoothGatt = Mock BluetoothGatt + RxBleGattCallback mockGattCallback = Mock RxBleGattCallback + Semaphore mockSemaphore = Mock Semaphore + TestSubscriber testSubscriber = new TestSubscriber() + TestScheduler testScheduler = new TestScheduler() + TimeoutConfiguration mockTimeoutConfiguration = new MockOperationTimeoutConfiguration( + timeout.intValue(), + testScheduler + ) + RxBleRadioOperationConnectionPriorityRequest objectUnderTest + + def setup() { + prepareObjectUnderTest(BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER) + } + + def "should call BluetoothGatt.requestConnectionPriority(int) exactly once when run()"() { + when: + objectUnderTest.run() + + then: + 1 * mockBluetoothGatt.requestConnectionPriority(_) >> true + } + + def "should complete after specified time if BluetoothGatt.requestConnectionPriority() will return true"() { + given: + mockBluetoothGatt.requestConnectionPriority(_) >> true + objectUnderTest.run() + + when: + testScheduler.advanceTimeBy(completedDelay + 500, delayUnit) + + then: + testSubscriber.assertCompleted() + } + + def "should throw exception if operation failed"() { + given: + mockBluetoothGatt.requestConnectionPriority(_) >> false + + when: + objectUnderTest.run() + + then: + testSubscriber.assertError BleGattCannotStartException + } + + def prepareObjectUnderTest(int connectionPriority) { + objectUnderTest = new RxBleRadioOperationConnectionPriorityRequest( + mockGattCallback, + mockBluetoothGatt, + mockTimeoutConfiguration, + connectionPriority, + completedDelay, + delayUnit, + testScheduler + ) + objectUnderTest.setRadioBlockingSemaphore(mockSemaphore) + objectUnderTest.asObservable().subscribe(testSubscriber) + } +}