From e9e45cc63f77ace94c761c5c0ca2f419932a09d6 Mon Sep 17 00:00:00 2001 From: James Dougherty <42870850+JamesDougherty@users.noreply.github.com> Date: Thu, 4 Jan 2024 15:18:21 -0500 Subject: [PATCH] Functionality to set preferred PHY and to read the set PHY (#840) --- .../mockrxandroidble/RxBleConnectionMock.java | 32 +++- .../RxBleConnectionMockTest.groovy | 19 +- .../com/polidea/rxandroidble2/PhyPair.java | 23 +++ .../rxandroidble2/RxBleConnection.java | 27 +++ .../com/polidea/rxandroidble2/RxBlePhy.java | 40 ++++ .../polidea/rxandroidble2/RxBlePhyOption.java | 29 +++ .../exceptions/BleGattOperationType.java | 2 + .../rxandroidble2/internal/PhyPairImpl.java | 54 ++++++ .../rxandroidble2/internal/RxBlePhyImpl.java | 175 ++++++++++++++++++ .../internal/RxBlePhyOptionImpl.java | 77 ++++++++ .../connection/NativeCallbackDispatcher.java | 14 ++ .../connection/RxBleConnectionImpl.java | 36 +++- .../connection/RxBleGattCallback.java | 38 ++++ .../internal/logger/LoggerUtil.java | 8 + .../operations/OperationsProvider.java | 10 + .../operations/OperationsProviderImpl.java | 17 +- .../internal/operations/PhyReadOperation.java | 45 +++++ .../operations/PhyUpdateOperation.java | 66 +++++++ .../connection/RxBleGattCallbackTest.groovy | 41 ++++ .../operations/OperationPhyReadTest.groovy | 80 ++++++++ .../operations/OperationPhyUpdateTest.groovy | 93 ++++++++++ 21 files changed, 913 insertions(+), 13 deletions(-) create mode 100644 rxandroidble/src/main/java/com/polidea/rxandroidble2/PhyPair.java create mode 100644 rxandroidble/src/main/java/com/polidea/rxandroidble2/RxBlePhy.java create mode 100644 rxandroidble/src/main/java/com/polidea/rxandroidble2/RxBlePhyOption.java create mode 100644 rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/PhyPairImpl.java create mode 100644 rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/RxBlePhyImpl.java create mode 100644 rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/RxBlePhyOptionImpl.java create mode 100644 rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/operations/PhyReadOperation.java create mode 100644 rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/operations/PhyUpdateOperation.java create mode 100644 rxandroidble/src/test/groovy/com/polidea/rxandroidble2/internal/operations/OperationPhyReadTest.groovy create mode 100644 rxandroidble/src/test/groovy/com/polidea/rxandroidble2/internal/operations/OperationPhyUpdateTest.groovy diff --git a/mockrxandroidble/src/main/java/com/polidea/rxandroidble2/mockrxandroidble/RxBleConnectionMock.java b/mockrxandroidble/src/main/java/com/polidea/rxandroidble2/mockrxandroidble/RxBleConnectionMock.java index c1fa843ad..0e793d193 100644 --- a/mockrxandroidble/src/main/java/com/polidea/rxandroidble2/mockrxandroidble/RxBleConnectionMock.java +++ b/mockrxandroidble/src/main/java/com/polidea/rxandroidble2/mockrxandroidble/RxBleConnectionMock.java @@ -2,35 +2,41 @@ import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; -import androidx.annotation.NonNull; - import android.bluetooth.BluetoothGattService; import android.util.Log; +import androidx.annotation.NonNull; + import com.polidea.rxandroidble2.ConnectionParameters; import com.polidea.rxandroidble2.NotificationSetupMode; +import com.polidea.rxandroidble2.PhyPair; import com.polidea.rxandroidble2.RxBleConnection; import com.polidea.rxandroidble2.RxBleCustomOperation; import com.polidea.rxandroidble2.RxBleDeviceServices; +import com.polidea.rxandroidble2.RxBlePhy; +import com.polidea.rxandroidble2.RxBlePhyOption; import com.polidea.rxandroidble2.exceptions.BleConflictingNotificationAlreadySetException; import com.polidea.rxandroidble2.exceptions.BleDisconnectedException; import com.polidea.rxandroidble2.exceptions.BleGattCharacteristicException; import com.polidea.rxandroidble2.exceptions.BleGattDescriptorException; import com.polidea.rxandroidble2.exceptions.BleGattOperationType; +import com.polidea.rxandroidble2.internal.PhyPairImpl; import com.polidea.rxandroidble2.internal.Priority; import com.polidea.rxandroidble2.internal.connection.ImmediateSerializedBatchAckStrategy; import com.polidea.rxandroidble2.internal.util.ObservableUtil; -import com.polidea.rxandroidble2.mockrxandroidble.callbacks.results.RxBleGattReadResultMock; -import com.polidea.rxandroidble2.mockrxandroidble.callbacks.results.RxBleGattWriteResultMock; import com.polidea.rxandroidble2.mockrxandroidble.callbacks.RxBleCharacteristicReadCallback; import com.polidea.rxandroidble2.mockrxandroidble.callbacks.RxBleCharacteristicWriteCallback; import com.polidea.rxandroidble2.mockrxandroidble.callbacks.RxBleDescriptorReadCallback; import com.polidea.rxandroidble2.mockrxandroidble.callbacks.RxBleDescriptorWriteCallback; +import com.polidea.rxandroidble2.mockrxandroidble.callbacks.results.RxBleGattReadResultMock; +import com.polidea.rxandroidble2.mockrxandroidble.callbacks.results.RxBleGattWriteResultMock; import java.util.ArrayList; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; @@ -73,6 +79,7 @@ public class RxBleConnectionMock implements RxBleConnection { private RxBleDeviceServices rxBleDeviceServices; private int rssi; private int currentMtu = 23; + private PhyPair phy = new PhyPairImpl(RxBlePhy.PHY_1M, RxBlePhy.PHY_1M); private Map> characteristicNotificationSources; private Map characteristicReadCallbacks; private Map characteristicWriteCallbacks; @@ -123,6 +130,23 @@ public int getMtu() { return currentMtu; } + @Override + public Single readPhy() { + return Single.fromCallable(() -> phy); + } + + @Override + public Single setPreferredPhy(Set txPhy, Set rxPhy, RxBlePhyOption phyOptions) { + return Single.fromCallable(() -> { + final Iterator txPhyIterator = txPhy.iterator(); + final Iterator rxPhyIterator = rxPhy.iterator(); + phy = new PhyPairImpl( + txPhyIterator.hasNext() ? txPhyIterator.next() : RxBlePhy.PHY_1M, + rxPhyIterator.hasNext() ? rxPhyIterator.next() : RxBlePhy.PHY_1M); + return phy; + }); + } + public int getRssi() { return rssi; } diff --git a/mockrxandroidble/src/test/groovy/com/polidea/rxandroidble2/mockrxandroidble/RxBleConnectionMockTest.groovy b/mockrxandroidble/src/test/groovy/com/polidea/rxandroidble2/mockrxandroidble/RxBleConnectionMockTest.groovy index 43d42e8c0..8ca3aa73e 100644 --- a/mockrxandroidble/src/test/groovy/com/polidea/rxandroidble2/mockrxandroidble/RxBleConnectionMockTest.groovy +++ b/mockrxandroidble/src/test/groovy/com/polidea/rxandroidble2/mockrxandroidble/RxBleConnectionMockTest.groovy @@ -1,13 +1,16 @@ package com.polidea.rxandroidble2.mockrxandroidble - +import com.polidea.rxandroidble2.RxBlePhy import com.polidea.rxandroidble2.RxBleClient +import com.polidea.rxandroidble2.RxBlePhyOption import com.polidea.rxandroidble2.exceptions.BleDisconnectedException import com.polidea.rxandroidble2.exceptions.BleGattCharacteristicException import com.polidea.rxandroidble2.exceptions.BleGattDescriptorException +import com.polidea.rxandroidble2.internal.PhyPairImpl import com.polidea.rxandroidble2.mockrxandroidble.callbacks.results.RxBleGattReadResultMock import com.polidea.rxandroidble2.mockrxandroidble.callbacks.results.RxBleGattWriteResultMock import io.reactivex.Observable +import io.reactivex.functions.Predicate import io.reactivex.subjects.PublishSubject import spock.lang.Specification @@ -113,6 +116,20 @@ public class RxBleConnectionMockTest extends Specification { testSubscriber.assertValue(72) } + def "should return the BluetoothDevice PHY"() { + when: + def testSubscriber = rxBleConnectionMock + .setPreferredPhy( + new LinkedHashSet(List.of(RxBlePhy.PHY_2M, RxBlePhy.PHY_1M)), + new LinkedHashSet(List.of(RxBlePhy.PHY_2M, RxBlePhy.PHY_1M)), + RxBlePhyOption.PHY_OPTION_NO_PREFERRED + ) + .test() + + then: + testSubscriber.assertValue ({ new PhyPairImpl(RxBlePhy.PHY_2M, RxBlePhy.PHY_2M) == it } as Predicate) + } + def "should return services list"() { when: def testSubscriber = rxBleConnectionMock diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/PhyPair.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/PhyPair.java new file mode 100644 index 000000000..03d90880d --- /dev/null +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/PhyPair.java @@ -0,0 +1,23 @@ +package com.polidea.rxandroidble2; + + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Set; + +/** + * The interface used for results of {@link RxBleConnection#readPhy()} and {@link RxBleConnection#setPreferredPhy(Set, Set, RxBlePhyOption)} + */ +public interface PhyPair { + + @NonNull + RxBlePhy getTxPhy(); + + @NonNull + RxBlePhy getRxPhy(); + + int hashCode(); + + boolean equals(@Nullable Object obj); +} diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/RxBleConnection.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/RxBleConnection.java index 06e7314d9..67594d7d9 100644 --- a/rxandroidble/src/main/java/com/polidea/rxandroidble2/RxBleConnection.java +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/RxBleConnection.java @@ -3,6 +3,7 @@ import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; + import androidx.annotation.IntDef; import androidx.annotation.IntRange; import androidx.annotation.NonNull; @@ -21,6 +22,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -582,6 +584,31 @@ Completable requestConnectionPriority( */ int getMtu(); + /** + * Performs GATT read PHY operation. + * + * @return Observable emitting the read Tx and Rx values. + */ + @RequiresApi(26 /* Build.VERSION_CODES.O */) + Single readPhy(); + + /** + * Performs set preferred PHY request. + * + * @param txPhy Sets the preferred transmitter (Tx) PHY. Use static values defined by the library, e.g. {@link RxBlePhy#PHY_1M}. + * @param rxPhy Sets the preferred receiver (Rx) PHY. Use static values defined by the library, e.g. {@link RxBlePhy#PHY_1M}. + * @param phyOptions Sets the preferred coding to use when transmitting on the LE Coded PHY. Use static values defined by the library, + * e.g. {@link RxBlePhyOption}. + * @return Observable emitting negotiated PHY values pair. + * @throws BleGattException in case of GATT operation error with {@link BleGattOperationType#PHY_UPDATE} type. + * @implNote In case the library is outdated and does not implement expected pre-defined static objects to use, one can implement their + * own objects and pass as parameters which should unblock the use-case. In this case please consider making a PR to the + * library. Please keep in mind that passing custom implementations of RxBlePhy or RxBlePhyOption is permitted for unblocking, + * it is also considered an undefined behaviour and may break with subsequent releases. + */ + @RequiresApi(26 /* Build.VERSION_CODES.O */) + Single setPreferredPhy(Set txPhy, Set rxPhy, RxBlePhyOption phyOptions); + /** * This method requires deep knowledge of RxAndroidBLE internals. Use it only as a last resort if you know * what your are doing. diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/RxBlePhy.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/RxBlePhy.java new file mode 100644 index 000000000..325927676 --- /dev/null +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/RxBlePhy.java @@ -0,0 +1,40 @@ +package com.polidea.rxandroidble2; + +import android.bluetooth.BluetoothDevice; + +import com.polidea.rxandroidble2.internal.RxBlePhyImpl; + +import java.util.Set; + +/** + * The interface used in {@link Set} for requesting PHY when calling {@link RxBleConnection#setPreferredPhy(Set, Set, RxBlePhyOption)} and + * inside {@link PhyPair} as results of {@link RxBleConnection#readPhy()} and + * {@link RxBleConnection#setPreferredPhy(Set, Set, RxBlePhyOption)} + */ +public interface RxBlePhy { + + /** + * Bluetooth LE 1M PHY. + */ + RxBlePhy PHY_1M = RxBlePhyImpl.PHY_1M; + + /** + * Bluetooth LE 2M PHY. + */ + RxBlePhy PHY_2M = RxBlePhyImpl.PHY_2M; + + /** + * Bluetooth LE Coded PHY. + */ + RxBlePhy PHY_CODED = RxBlePhyImpl.PHY_CODED; + + /** + * Corresponds to e.g. {@link BluetoothDevice#PHY_LE_CODED_MASK} + */ + int getMask(); + + /** + * Corresponds to e.g. {@link BluetoothDevice#PHY_LE_CODED} + */ + int getValue(); +} diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/RxBlePhyOption.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/RxBlePhyOption.java new file mode 100644 index 000000000..aa7dfeab2 --- /dev/null +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/RxBlePhyOption.java @@ -0,0 +1,29 @@ +package com.polidea.rxandroidble2; + +import com.polidea.rxandroidble2.internal.RxBlePhyOptionImpl; + +/** + * Coding to be used when transmitting on the LE Coded PHY. + */ +public interface RxBlePhyOption { + /** + * No preferred coding. + */ + RxBlePhyOption PHY_OPTION_NO_PREFERRED = RxBlePhyOptionImpl.PHY_OPTION_NO_PREFERRED; + + /** + * Prefer the S=2 coding. + */ + RxBlePhyOption PHY_OPTION_S2 = RxBlePhyOptionImpl.PHY_OPTION_S2; + + /** + * Prefer the S=8 coding. + */ + RxBlePhyOption PHY_OPTION_S8 = RxBlePhyOptionImpl.PHY_OPTION_S8; + + /** + * + * @return integer value representing PHY option, e.g. {@link android.bluetooth.BluetoothDevice#PHY_OPTION_S2} + */ + int getValue(); +} diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/exceptions/BleGattOperationType.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/exceptions/BleGattOperationType.java index bffbc80c4..c48b9e58d 100644 --- a/rxandroidble/src/main/java/com/polidea/rxandroidble2/exceptions/BleGattOperationType.java +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/exceptions/BleGattOperationType.java @@ -13,6 +13,8 @@ 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 PHY_READ = new BleGattOperationType("PHY_READ"); + public static final BleGattOperationType PHY_UPDATE = new BleGattOperationType("PHY_UPDATE"); public static final BleGattOperationType CONNECTION_PRIORITY_CHANGE = new BleGattOperationType("CONNECTION_PRIORITY_CHANGE"); private final String description; diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/PhyPairImpl.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/PhyPairImpl.java new file mode 100644 index 000000000..8ba9628e7 --- /dev/null +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/PhyPairImpl.java @@ -0,0 +1,54 @@ +package com.polidea.rxandroidble2.internal; + + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.polidea.rxandroidble2.PhyPair; +import com.polidea.rxandroidble2.RxBlePhy; + +import java.util.Objects; + +public class PhyPairImpl implements PhyPair { + public final RxBlePhy txPhy; + public final RxBlePhy rxPhy; + + public PhyPairImpl(@NonNull final RxBlePhy txPhy, @NonNull final RxBlePhy rxPhy) { + this.txPhy = txPhy; + this.rxPhy = rxPhy; + } + + @NonNull + @Override + public RxBlePhy getTxPhy() { + return txPhy; + } + + @NonNull + @Override + public RxBlePhy getRxPhy() { + return rxPhy; + } + + @Override + public int hashCode() { + return Objects.hash(rxPhy, txPhy); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj == this) return true; + if (!(obj instanceof PhyPair)) return false; + PhyPair phyPair = (PhyPair) obj; + return txPhy.equals(phyPair.getTxPhy()) && rxPhy.equals(phyPair.getRxPhy()); + } + + @NonNull + @Override + public String toString() { + return "PhyPair{" + + "txPhy=" + txPhy + + ", rxPhy=" + rxPhy + + '}'; + } +} diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/RxBlePhyImpl.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/RxBlePhyImpl.java new file mode 100644 index 000000000..4aac07365 --- /dev/null +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/RxBlePhyImpl.java @@ -0,0 +1,175 @@ +package com.polidea.rxandroidble2.internal; + +import android.bluetooth.BluetoothDevice; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.polidea.rxandroidble2.PhyPair; +import com.polidea.rxandroidble2.RxBlePhy; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Objects; +import java.util.Set; + +public final class RxBlePhyImpl implements RxBlePhy { + + /** + * Bluetooth LE 1M PHY. + */ + public static final RxBlePhyImpl PHY_1M = new RxBlePhyImpl("PHY_1M", 1, 1); + + /** + * Bluetooth LE 2M PHY. + */ + public static final RxBlePhyImpl PHY_2M = new RxBlePhyImpl("PHY_2M", 1 << 1, 2); + + /** + * Bluetooth LE Coded PHY. + */ + public static final RxBlePhyImpl PHY_CODED = new RxBlePhyImpl("PHY_CODED", 1 << 2, 3); + + private static final Set BUILTIN_VALUES; + + static { + HashSet builtinValues = new HashSet<>(); + builtinValues.add(PHY_1M); + builtinValues.add(PHY_2M); + builtinValues.add(PHY_CODED); + BUILTIN_VALUES = Collections.unmodifiableSet(builtinValues); + } + + /** + * Used for user-friendly object toString() + */ + final String toStringOverride; + + /** + * Corresponds to e.g. {@link BluetoothDevice#PHY_LE_CODED_MASK} + */ + final int mask; + + /** + * Corresponds to e.g. {@link BluetoothDevice#PHY_LE_CODED} + */ + final int value; + + private RxBlePhyImpl(final String builtInToString, final int mask, final int value) { + this.toStringOverride = builtInToString; + this.mask = mask; + this.value = value; + } + + private RxBlePhyImpl(final int mask, final int value) { + this.toStringOverride = null; + this.mask = mask; + this.value = value; + } + + public int getMask() { + return mask; + } + + public int getValue() { + return value; + } + + @NonNull + @Override + public String toString() { + if (toStringOverride != null) { + return toStringOverride; + } + return "RxBlePhy{[CUSTOM] " + + "mask=" + mask + + ", value=" + value + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof RxBlePhy)) return false; + RxBlePhy rxBlePhy = (RxBlePhy) o; + return mask == rxBlePhy.getMask() && value == rxBlePhy.getValue(); + } + + @Override + public int hashCode() { + return Objects.hash(mask, value); + } + + /** + * Function used to get the PHY static object from an integer. + * + * @param value The integer value to try to get he PHY enum value for. + * + * @return The PHY value. + */ + @NonNull + private static RxBlePhy fromValue(final int value) { + for (final RxBlePhy entry : RxBlePhyImpl.BUILTIN_VALUES) { + if (entry.getValue() == value) { + return entry; + } + } + RxBleLog.e("Encountered an unexpected PHY value=%d. Please consider making a PR to the library.", value); + return new RxBlePhyImpl(0, value); + } + + /** + * Function used to convert the specified enum set to the equivalent values mask. + * + * @param set The enum set to compute the values mask for. + * + * @return If the set is NULL, empty, then the default value mask, RxBlePhy.PHY_1M, is returned. + * Otherwise, the resulting values mask is returned. + */ + public static int enumSetToValuesMask(@Nullable final Set set) { + if (set == null || set.size() == 0) { + return RxBlePhyImpl.PHY_1M.getMask(); + } + + final Iterator iterator = set.iterator(); + + int result = 0; + + while (iterator.hasNext()) { + final int requestedValue = iterator.next().getMask(); + result |= requestedValue; + } + + return result; + } + + @NonNull + public static PhyPair toPhyPair(int txPhy, int rxPhy) { + // GATT callbacks do not use the same LE Coded value as it does for the setPreferredPhy function. GATT + // callbacks use the unmasked value (BluetoothDevice.PHY_LE_CODED) and setPreferredPhy uses the mask value + // (BluetoothDevice.PHY_LE_CODED_MASK). Explicitly check for BluetoothDevice.PHY_LE_CODED here and manually + // set it to RxBlePhy.PHY_CODED since it too uses the masked version. This will abstract that confusion + // away from the user and make it unified. + RxBlePhy tx = RxBlePhyImpl.fromValue(txPhy); + RxBlePhy rx = RxBlePhyImpl.fromValue(rxPhy); + + return new PhyPairImpl(tx, rx); + } + + public static Set fromInterface(Set phys) { + Set result = new HashSet<>(); + for (RxBlePhy phy : phys) { + int value = phy.getValue(); + int mask = phy.getMask(); + if (phy.getClass() == RxBlePhyImpl.class && BUILTIN_VALUES.contains(phy)) { + result.add((RxBlePhyImpl) phy); + } else { + RxBleLog.w("Using a custom RxBlePhy with value=%d, mask=%d. Please consider making a PR to the library.", value, mask); + result.add(new RxBlePhyImpl(mask, value)); + } + } + return result; + } + +} diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/RxBlePhyOptionImpl.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/RxBlePhyOptionImpl.java new file mode 100644 index 000000000..07dd2fd82 --- /dev/null +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/RxBlePhyOptionImpl.java @@ -0,0 +1,77 @@ +package com.polidea.rxandroidble2.internal; + +import androidx.annotation.NonNull; + +import com.polidea.rxandroidble2.RxBlePhyOption; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Coding to be used when transmitting on the LE Coded PHY. + */ +public final class RxBlePhyOptionImpl implements RxBlePhyOption { + /** + * No preferred coding. + */ + public static final RxBlePhyOption PHY_OPTION_NO_PREFERRED = new RxBlePhyOptionImpl("PHY_OPTION_NO_PREFERRED", 0); + + /** + * Prefer the S=2 coding. + */ + public static final RxBlePhyOption PHY_OPTION_S2 = new RxBlePhyOptionImpl("PHY_OPTION_S2", 1); + + /** + * Prefer the S=8 coding. + */ + public static final RxBlePhyOption PHY_OPTION_S8 = new RxBlePhyOptionImpl("PHY_OPTION_S8", 2); + + private static final Set BUILTIN_VALUES; + + static { + HashSet builtinValues = new HashSet<>(); + builtinValues.add(PHY_OPTION_NO_PREFERRED); + builtinValues.add(PHY_OPTION_S2); + builtinValues.add(PHY_OPTION_S8); + BUILTIN_VALUES = Collections.unmodifiableSet(builtinValues); + } + + /** + * Used for user-friendly object toString() + */ + private final String toStringOverride; + + private final int value; + + public RxBlePhyOptionImpl(String toStringOverride, final int value) { + this.toStringOverride = toStringOverride; + this.value = value; + } + + @Override + public int getValue() { + return value; + } + + @NonNull + @Override + public String toString() { + if (toStringOverride != null) { + return toStringOverride; + } + return "RxBlePhyOption{[CUSTOM] " + + " value=" + value + + '}'; + } + + public static RxBlePhyOptionImpl fromInterface(RxBlePhyOption phyOption) { + int phyOptionsValue = phyOption.getValue(); + if (phyOption.getClass() != RxBlePhyOptionImpl.class || !BUILTIN_VALUES.contains(phyOption)) { + RxBleLog.w("Using a custom RxBlePhyOption with value=%d. Please consider making a PR to the library.", phyOptionsValue); + } + return phyOption.getClass() == RxBlePhyOptionImpl.class + ? (RxBlePhyOptionImpl) phyOption + : new RxBlePhyOptionImpl(null, phyOptionsValue); + } +} diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/connection/NativeCallbackDispatcher.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/connection/NativeCallbackDispatcher.java index ec50bb573..da7b80d8b 100644 --- a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/connection/NativeCallbackDispatcher.java +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/connection/NativeCallbackDispatcher.java @@ -50,6 +50,20 @@ void notifyNativeMtuChangedCallback(BluetoothGatt gatt, int mtu, int status) { } } + @TargetApi(26 /* Build.VERSION_CODES.O */) + void notifyNativePhyReadCallback(BluetoothGatt gatt, int txPhy, int rxPhy, int status) { + if (nativeCallback != null) { + nativeCallback.onPhyRead(gatt, txPhy, rxPhy, status); + } + } + + @TargetApi(26 /* Build.VERSION_CODES.O */) + void notifyNativePhyUpdateCallback(BluetoothGatt gatt, int txPhy, int rxPhy, int status) { + if (nativeCallback != null) { + nativeCallback.onPhyUpdate(gatt, txPhy, rxPhy, status); + } + } + void notifyNativeReadRssiCallback(BluetoothGatt gatt, int rssi, int status) { if (nativeCallback != null) { nativeCallback.onReadRemoteRssi(gatt, rssi, status); diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/connection/RxBleConnectionImpl.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/connection/RxBleConnectionImpl.java index e58932739..10764a987 100644 --- a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/connection/RxBleConnectionImpl.java +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/connection/RxBleConnectionImpl.java @@ -1,28 +1,42 @@ package com.polidea.rxandroidble2.internal.connection; +import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_INDICATE; +import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_NOTIFY; +import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_READ; +import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE; +import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_WRITE; +import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE; + import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; import android.os.DeadObjectException; + import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import com.polidea.rxandroidble2.ClientComponent; import com.polidea.rxandroidble2.ConnectionParameters; import com.polidea.rxandroidble2.NotificationSetupMode; +import com.polidea.rxandroidble2.PhyPair; import com.polidea.rxandroidble2.RxBleConnection; import com.polidea.rxandroidble2.RxBleCustomOperation; import com.polidea.rxandroidble2.RxBleDeviceServices; +import com.polidea.rxandroidble2.RxBlePhy; +import com.polidea.rxandroidble2.RxBlePhyOption; import com.polidea.rxandroidble2.exceptions.BleDisconnectedException; import com.polidea.rxandroidble2.exceptions.BleException; import com.polidea.rxandroidble2.internal.Priority; import com.polidea.rxandroidble2.internal.QueueOperation; +import com.polidea.rxandroidble2.internal.RxBlePhyImpl; +import com.polidea.rxandroidble2.internal.RxBlePhyOptionImpl; import com.polidea.rxandroidble2.internal.operations.OperationsProvider; import com.polidea.rxandroidble2.internal.serialization.ConnectionOperationQueue; import com.polidea.rxandroidble2.internal.serialization.QueueReleaseInterface; import com.polidea.rxandroidble2.internal.util.ByteAssociation; import com.polidea.rxandroidble2.internal.util.QueueReleasingEmitterWrapper; +import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -40,13 +54,6 @@ import io.reactivex.functions.Action; import io.reactivex.functions.Function; -import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_INDICATE; -import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_NOTIFY; -import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_READ; -import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE; -import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_WRITE; -import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE; - @ConnectionScope public class RxBleConnectionImpl implements RxBleConnection { @@ -128,6 +135,21 @@ public int getMtu() { return mtuProvider.getMtu(); } + @Override + @RequiresApi(26 /* Build.VERSION_CODES.O */) + public Single readPhy() { + return operationQueue.queue(operationsProvider.providePhyReadOperation()).firstOrError(); + } + + @Override + @RequiresApi(26 /* Build.VERSION_CODES.O */) + public Single setPreferredPhy(Set txPhy, Set rxPhy, RxBlePhyOption phyOptions) { + Set txPhyImpls = RxBlePhyImpl.fromInterface(txPhy); + Set rxPhyImpls = RxBlePhyImpl.fromInterface(rxPhy); + RxBlePhyOptionImpl phyOptionImpl = RxBlePhyOptionImpl.fromInterface(phyOptions); + return operationQueue.queue(operationsProvider.providePhyRequestOperation(txPhyImpls, rxPhyImpls, phyOptionImpl)).firstOrError(); + } + @Override public Single discoverServices() { return serviceDiscoveryManager.getDiscoverServicesSingle(20L, TimeUnit.SECONDS); diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/connection/RxBleGattCallback.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/connection/RxBleGattCallback.java index 9326e3478..26be2ce33 100644 --- a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/connection/RxBleGattCallback.java +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/connection/RxBleGattCallback.java @@ -10,6 +10,7 @@ import com.polidea.rxandroidble2.ConnectionParameters; import com.polidea.rxandroidble2.HiddenBluetoothGattCallback; import com.polidea.rxandroidble2.ClientComponent; +import com.polidea.rxandroidble2.PhyPair; import com.polidea.rxandroidble2.RxBleConnection.RxBleConnectionState; import com.polidea.rxandroidble2.RxBleDeviceServices; import com.polidea.rxandroidble2.exceptions.BleDisconnectedException; @@ -17,6 +18,7 @@ import com.polidea.rxandroidble2.exceptions.BleGattDescriptorException; import com.polidea.rxandroidble2.exceptions.BleGattException; import com.polidea.rxandroidble2.exceptions.BleGattOperationType; +import com.polidea.rxandroidble2.internal.RxBlePhyImpl; import com.polidea.rxandroidble2.internal.logger.LoggerUtil; import com.polidea.rxandroidble2.internal.util.ByteAssociation; import com.polidea.rxandroidble2.internal.util.CharacteristicChangedEvent; @@ -49,6 +51,8 @@ public class RxBleGattCallback { final Output> writeDescriptorOutput = new Output<>(); final Output readRssiOutput = new Output<>(); final Output changedMtuOutput = new Output<>(); + final Output phyReadOutput = new Output<>(); + final Output phyUpdateOutput = new Output<>(); final Output updatedConnectionOutput = new Output<>(); private final Function> errorMapper = new Function>() { @Override @@ -207,6 +211,32 @@ public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { } } + @Override + public void onPhyRead(BluetoothGatt gatt, int txPhy, int rxPhy, int status) { + LoggerUtil.logCallback("onPhyRead", gatt, status, txPhy, rxPhy); + nativeCallbackDispatcher.notifyNativePhyReadCallback(gatt, txPhy, rxPhy, status); + super.onPhyRead(gatt, txPhy, rxPhy, status); + + if (phyReadOutput.hasObservers() + && !propagateErrorIfOccurred(phyReadOutput, gatt, status, BleGattOperationType.PHY_READ)) { + PhyPair phyPair = RxBlePhyImpl.toPhyPair(txPhy, rxPhy); + phyReadOutput.valueRelay.accept(phyPair); + } + } + + @Override + public void onPhyUpdate(BluetoothGatt gatt, int txPhy, int rxPhy, int status) { + LoggerUtil.logCallback("onPhyUpdate", gatt, status, txPhy, rxPhy); + nativeCallbackDispatcher.notifyNativePhyUpdateCallback(gatt, txPhy, rxPhy, status); + super.onPhyUpdate(gatt, txPhy, rxPhy, status); + + if (phyUpdateOutput.hasObservers() + && !propagateErrorIfOccurred(phyUpdateOutput, gatt, status, BleGattOperationType.PHY_UPDATE)) { + PhyPair phyPair = RxBlePhyImpl.toPhyPair(txPhy, rxPhy); + phyUpdateOutput.valueRelay.accept(phyPair); + } + } + // This callback first appeared in Android 8.0 (android-8.0.0_r1/core/java/android/bluetooth/BluetoothGattCallback.java) // It is hidden since @SuppressWarnings("unused") @@ -315,6 +345,14 @@ public Observable getOnMtuChanged() { return withDisconnectionHandling(changedMtuOutput).delay(0, TimeUnit.SECONDS, callbackScheduler); } + public Observable getOnPhyRead() { + return withDisconnectionHandling(phyReadOutput).delay(0, TimeUnit.SECONDS, callbackScheduler); + } + + public Observable getOnPhyUpdate() { + return withDisconnectionHandling(phyUpdateOutput).delay(0, TimeUnit.SECONDS, callbackScheduler); + } + public Observable> getOnCharacteristicRead() { return withDisconnectionHandling(readCharacteristicOutput).delay(0, TimeUnit.SECONDS, callbackScheduler); } diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/logger/LoggerUtil.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/logger/LoggerUtil.java index 605bdd776..8d1239311 100644 --- a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/logger/LoggerUtil.java +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/logger/LoggerUtil.java @@ -137,6 +137,14 @@ public static void logCallback(String callbackName, BluetoothGatt gatt, int stat callbackName, status, value); } + public static void logCallback(String callbackName, BluetoothGatt gatt, int status, int valueA, int valueB) { + if (!RxBleLog.isAtLeast(LogConstants.INFO)) { + return; + } + RxBleLog.i(commonMacMessage(gatt) + commonCallbackMessage() + commonStatusMessage() + commonValueMessage() + commonValueMessage(), + callbackName, status, valueA, valueB); + } + public static void logConnectionUpdateCallback(String callbackName, BluetoothGatt gatt, int status, int interval, int latency, int timeout) { if (!RxBleLog.isAtLeast(LogConstants.INFO)) { diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/operations/OperationsProvider.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/operations/OperationsProvider.java index 6f8d4ff8a..0ee23cfe9 100644 --- a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/operations/OperationsProvider.java +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/operations/OperationsProvider.java @@ -2,11 +2,15 @@ import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; + import androidx.annotation.RequiresApi; import com.polidea.rxandroidble2.RxBleConnection; +import com.polidea.rxandroidble2.internal.RxBlePhyImpl; +import com.polidea.rxandroidble2.internal.RxBlePhyOptionImpl; import com.polidea.rxandroidble2.internal.connection.PayloadSizeLimitProvider; +import java.util.Set; import java.util.concurrent.TimeUnit; public interface OperationsProvider { @@ -21,6 +25,12 @@ CharacteristicLongWriteOperation provideLongWriteOperation( @RequiresApi(21 /* Build.VERSION_CODES.LOLLIPOP */) MtuRequestOperation provideMtuChangeOperation(int requestedMtu); + @RequiresApi(26 /* Build.VERSION_CODES.O */) + PhyReadOperation providePhyReadOperation(); + + @RequiresApi(26 /* Build.VERSION_CODES.O */) + PhyUpdateOperation providePhyRequestOperation(Set txPhy, Set rxPhy, RxBlePhyOptionImpl phyOptions); + CharacteristicReadOperation provideReadCharacteristic(BluetoothGattCharacteristic characteristic); DescriptorReadOperation provideReadDescriptor(BluetoothGattDescriptor descriptor); diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/operations/OperationsProviderImpl.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/operations/OperationsProviderImpl.java index 3daa5c6ff..e2bc9dc2b 100644 --- a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/operations/OperationsProviderImpl.java +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/operations/OperationsProviderImpl.java @@ -3,21 +3,24 @@ import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; + import androidx.annotation.RequiresApi; import com.polidea.rxandroidble2.ClientComponent; import com.polidea.rxandroidble2.RxBleConnection; +import com.polidea.rxandroidble2.internal.RxBlePhyImpl; +import com.polidea.rxandroidble2.internal.RxBlePhyOptionImpl; import com.polidea.rxandroidble2.internal.connection.ConnectionModule; import com.polidea.rxandroidble2.internal.connection.PayloadSizeLimitProvider; import com.polidea.rxandroidble2.internal.connection.RxBleGattCallback; import com.polidea.rxandroidble2.internal.logger.LoggerUtilBluetoothServices; +import java.util.Set; import java.util.concurrent.TimeUnit; import bleshadow.javax.inject.Inject; import bleshadow.javax.inject.Named; import bleshadow.javax.inject.Provider; - import io.reactivex.Scheduler; public class OperationsProviderImpl implements OperationsProvider { @@ -73,6 +76,18 @@ public MtuRequestOperation provideMtuChangeOperation(int requestedMtu) { return new MtuRequestOperation(rxBleGattCallback, bluetoothGatt, timeoutConfiguration, requestedMtu); } + @Override + @RequiresApi(26 /* Build.VERSION_CODES.O */) + public PhyReadOperation providePhyReadOperation() { + return new PhyReadOperation(rxBleGattCallback, bluetoothGatt, timeoutConfiguration); + } + + @Override + @RequiresApi(26 /* Build.VERSION_CODES.O */) + public PhyUpdateOperation providePhyRequestOperation(Set txPhy, Set rxPhy, RxBlePhyOptionImpl phyOptions) { + return new PhyUpdateOperation(rxBleGattCallback, bluetoothGatt, timeoutConfiguration, txPhy, rxPhy, phyOptions); + } + @Override public CharacteristicReadOperation provideReadCharacteristic(BluetoothGattCharacteristic characteristic) { return new CharacteristicReadOperation(rxBleGattCallback, bluetoothGatt, timeoutConfiguration, characteristic); diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/operations/PhyReadOperation.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/operations/PhyReadOperation.java new file mode 100644 index 000000000..f81d712cb --- /dev/null +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/operations/PhyReadOperation.java @@ -0,0 +1,45 @@ +package com.polidea.rxandroidble2.internal.operations; + +import android.bluetooth.BluetoothGatt; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.annotation.RequiresPermission; + +import com.polidea.rxandroidble2.PhyPair; +import com.polidea.rxandroidble2.exceptions.BleGattOperationType; +import com.polidea.rxandroidble2.internal.SingleResponseOperation; +import com.polidea.rxandroidble2.internal.connection.ConnectionModule; +import com.polidea.rxandroidble2.internal.connection.RxBleGattCallback; + +import bleshadow.javax.inject.Inject; +import bleshadow.javax.inject.Named; +import io.reactivex.Single; + +@RequiresApi(26 /* Build.VERSION_CODES.O */) +public class PhyReadOperation extends SingleResponseOperation { + + @Inject + PhyReadOperation(RxBleGattCallback bleGattCallback, BluetoothGatt bluetoothGatt, + @Named(ConnectionModule.OPERATION_TIMEOUT) TimeoutConfiguration timeoutConfiguration) { + super(bluetoothGatt, bleGattCallback, BleGattOperationType.PHY_READ, timeoutConfiguration); + } + + @Override + protected Single getCallback(RxBleGattCallback rxBleGattCallback) { + return rxBleGattCallback.getOnPhyRead().firstOrError(); + } + + @Override + @RequiresPermission("android.permission.BLUETOOTH_CONNECT") + protected boolean startOperation(BluetoothGatt bluetoothGatt) { + bluetoothGatt.readPhy(); + return true; + } + + @NonNull + @Override + public String toString() { + return "PhyReadOperation{" + super.toString() + '}'; + } +} diff --git a/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/operations/PhyUpdateOperation.java b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/operations/PhyUpdateOperation.java new file mode 100644 index 000000000..c92183c5f --- /dev/null +++ b/rxandroidble/src/main/java/com/polidea/rxandroidble2/internal/operations/PhyUpdateOperation.java @@ -0,0 +1,66 @@ +package com.polidea.rxandroidble2.internal.operations; + +import android.annotation.SuppressLint; +import android.bluetooth.BluetoothGatt; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; + +import com.polidea.rxandroidble2.PhyPair; +import com.polidea.rxandroidble2.RxBlePhyOption; +import com.polidea.rxandroidble2.exceptions.BleGattOperationType; +import com.polidea.rxandroidble2.internal.RxBlePhyImpl; +import com.polidea.rxandroidble2.internal.SingleResponseOperation; +import com.polidea.rxandroidble2.internal.connection.RxBleGattCallback; + +import java.util.Set; + +import bleshadow.javax.inject.Inject; +import io.reactivex.Single; + +@RequiresApi(26 /* Build.VERSION_CODES.O */) +public class PhyUpdateOperation extends SingleResponseOperation { + + Set txPhy; + Set rxPhy; + RxBlePhyOption phyOptions; + + @Inject + PhyUpdateOperation( + RxBleGattCallback rxBleGattCallback, + BluetoothGatt bluetoothGatt, + TimeoutConfiguration timeoutConfiguration, + Set txPhy, Set rxPhy, RxBlePhyOption phyOptions) { + super(bluetoothGatt, rxBleGattCallback, BleGattOperationType.PHY_UPDATE, timeoutConfiguration); + this.txPhy = txPhy; + this.rxPhy = rxPhy; + this.phyOptions = phyOptions; + } + + @Override + protected Single getCallback(RxBleGattCallback rxBleGattCallback) { + return rxBleGattCallback.getOnPhyUpdate().firstOrError(); + } + + @Override + @SuppressLint("MissingPermission") + protected boolean startOperation(BluetoothGatt bluetoothGatt) { + bluetoothGatt.setPreferredPhy( + RxBlePhyImpl.enumSetToValuesMask(txPhy), + RxBlePhyImpl.enumSetToValuesMask(rxPhy), + phyOptions.getValue() + ); + return true; + } + + @NonNull + @Override + public String toString() { + return "PhyUpdateOperation{" + + super.toString() + + ", txPhy=" + txPhy + + ", rxPhy=" + rxPhy + + ", phyOptions=" + phyOptions + + '}'; + } +} diff --git a/rxandroidble/src/test/groovy/com/polidea/rxandroidble2/internal/connection/RxBleGattCallbackTest.groovy b/rxandroidble/src/test/groovy/com/polidea/rxandroidble2/internal/connection/RxBleGattCallbackTest.groovy index ef29ee4ca..99b7ec250 100644 --- a/rxandroidble/src/test/groovy/com/polidea/rxandroidble2/internal/connection/RxBleGattCallbackTest.groovy +++ b/rxandroidble/src/test/groovy/com/polidea/rxandroidble2/internal/connection/RxBleGattCallbackTest.groovy @@ -1,5 +1,8 @@ package com.polidea.rxandroidble2.internal.connection +import com.polidea.rxandroidble2.PhyPair +import com.polidea.rxandroidble2.RxBlePhy + import static com.polidea.rxandroidble2.RxBleConnection.RxBleConnectionState.CONNECTED import android.bluetooth.* @@ -104,6 +107,8 @@ class RxBleGattCallbackTest extends Specification { { return (it as RxBleGattCallback).getOnDescriptorRead() }, { return (it as RxBleGattCallback).getOnDescriptorWrite() }, { return (it as RxBleGattCallback).getOnRssiRead() }, + { return (it as RxBleGattCallback).getOnPhyRead() }, + { return (it as RxBleGattCallback).getOnPhyUpdate() }, { return (it as RxBleGattCallback).getConnectionParametersUpdates() } ] callbackCaller << [ @@ -115,6 +120,8 @@ class RxBleGattCallbackTest extends Specification { { (it as BluetoothGattCallback).onDescriptorRead(mockBluetoothGatt, mockBluetoothGattDescriptor, GATT_SUCCESS) }, { (it as BluetoothGattCallback).onDescriptorWrite(mockBluetoothGatt, mockBluetoothGattDescriptor, GATT_SUCCESS) }, { (it as BluetoothGattCallback).onReadRemoteRssi(mockBluetoothGatt, 1, GATT_SUCCESS) }, + { (it as BluetoothGattCallback).onPhyRead(mockBluetoothGatt, 1, 1, GATT_SUCCESS) }, + { (it as BluetoothGattCallback).onPhyUpdate(mockBluetoothGatt, 1, 1, GATT_SUCCESS) }, { (it as HiddenBluetoothGattCallback).onConnectionUpdated(mockBluetoothGatt, 1, 1, 1, GATT_SUCCESS) } ] } @@ -182,6 +189,8 @@ class RxBleGattCallbackTest extends Specification { { (it as BluetoothGattCallback).onDescriptorRead(mockBluetoothGatt, mockBluetoothGattDescriptor, GATT_FAILURE) }, { (it as BluetoothGattCallback).onDescriptorWrite(mockBluetoothGatt, mockBluetoothGattDescriptor, GATT_FAILURE) }, { (it as BluetoothGattCallback).onReadRemoteRssi(mockBluetoothGatt, 1, GATT_FAILURE) }, + { (it as BluetoothGattCallback).onPhyRead(mockBluetoothGatt, 1, 1, GATT_FAILURE) }, + { (it as BluetoothGattCallback).onPhyUpdate(mockBluetoothGatt, 1, 1, GATT_FAILURE) }, { (it as HiddenBluetoothGattCallback).onConnectionUpdated(mockBluetoothGatt, 1, 1, 1, GATT_FAILURE) } ] } @@ -228,6 +237,8 @@ class RxBleGattCallbackTest extends Specification { { return (it as RxBleGattCallback).getOnDescriptorRead() }, { return (it as RxBleGattCallback).getOnDescriptorWrite() }, { return (it as RxBleGattCallback).getOnRssiRead() }, + { return (it as RxBleGattCallback).getOnPhyRead() }, + { return (it as RxBleGattCallback).getOnPhyUpdate() }, { return (it as RxBleGattCallback).getConnectionParametersUpdates() } ] } @@ -252,6 +263,8 @@ class RxBleGattCallbackTest extends Specification { { (it as RxBleGattCallback).getOnDescriptorRead() }, { (it as RxBleGattCallback).getOnDescriptorWrite() }, { (it as RxBleGattCallback).getOnRssiRead() }, + { (it as RxBleGattCallback).getOnPhyRead() }, + { (it as RxBleGattCallback).getOnPhyUpdate() }, { (it as RxBleGattCallback).getConnectionParametersUpdates() } ] callbackCaller << [ @@ -261,6 +274,8 @@ class RxBleGattCallbackTest extends Specification { { (it as BluetoothGattCallback).onDescriptorRead(mockBluetoothGatt, mockBluetoothGattDescriptor, GATT_FAILURE) }, { (it as BluetoothGattCallback).onDescriptorWrite(mockBluetoothGatt, mockBluetoothGattDescriptor, GATT_FAILURE) }, { (it as BluetoothGattCallback).onReadRemoteRssi(mockBluetoothGatt, 1, GATT_FAILURE) }, + { (it as BluetoothGattCallback).onPhyRead(mockBluetoothGatt, 1, 1, GATT_FAILURE) }, + { (it as BluetoothGattCallback).onPhyUpdate(mockBluetoothGatt, 1, 1, GATT_FAILURE) }, { (it as HiddenBluetoothGattCallback).onConnectionUpdated(mockBluetoothGatt, 1, 1, 1, GATT_FAILURE) } ] errorAssertion << [ @@ -294,6 +309,16 @@ class RxBleGattCallbackTest extends Specification { it instanceof BleGattException && it.getMacAddress() == mockBluetoothDeviceMacAddress } }, + { + (it as TestObserver).assertError { + it instanceof BleGattException && it.getMacAddress() == mockBluetoothDeviceMacAddress + } + }, + { + (it as TestObserver).assertError { + it instanceof BleGattException && it.getMacAddress() == mockBluetoothDeviceMacAddress + } + }, { (it as TestObserver).assertError { it instanceof BleGattException && it.getMacAddress() == mockBluetoothDeviceMacAddress @@ -328,6 +353,8 @@ class RxBleGattCallbackTest extends Specification { { RxBleGattCallback objectUnderTest -> objectUnderTest.getOnDescriptorRead() }, { RxBleGattCallback objectUnderTest -> objectUnderTest.getOnDescriptorWrite() }, { RxBleGattCallback objectUnderTest -> objectUnderTest.getOnRssiRead() }, + { RxBleGattCallback objectUnderTest -> objectUnderTest.getOnPhyRead() }, + { RxBleGattCallback objectUnderTest -> objectUnderTest.getOnPhyUpdate() }, { RxBleGattCallback objectUnderTest -> objectUnderTest.getOnServicesDiscovered() }, { RxBleGattCallback objectUnderTest -> objectUnderTest.getConnectionParametersUpdates() } ] @@ -337,6 +364,8 @@ class RxBleGattCallbackTest extends Specification { { BluetoothGattCallback callback, int status -> callback.onDescriptorRead(mockBluetoothGatt, Mock(BluetoothGattDescriptor), status) }, { BluetoothGattCallback callback, int status -> callback.onDescriptorWrite(mockBluetoothGatt, Mock(BluetoothGattDescriptor), status) }, { BluetoothGattCallback callback, int status -> callback.onReadRemoteRssi(mockBluetoothGatt, 0, status) }, + { BluetoothGattCallback callback, int status -> callback.onPhyRead(mockBluetoothGatt, 0, 0, status) }, + { BluetoothGattCallback callback, int status -> callback.onPhyUpdate(mockBluetoothGatt, 0, 0, status) }, { BluetoothGattCallback callback, int status -> callback.onServicesDiscovered(mockBluetoothGatt, status) }, { BluetoothGattCallback callback, int status -> (callback as HiddenBluetoothGattCallback).onConnectionUpdated(mockBluetoothGatt, 1, 1, 1, status) } ] @@ -421,6 +450,18 @@ class RxBleGattCallbackTest extends Specification { { BluetoothGattCallback bgc -> bgc.onReadRemoteRssi(mockBluetoothGatt, 13373, GATT_SUCCESS) }, { it instanceof Integer && it == 13373 } ), + new CallbackTestCase( + "PhyRead", + { RxBleGattCallback out -> out.getOnPhyRead() }, + { BluetoothGattCallback bgc -> bgc.onPhyRead(mockBluetoothGatt, 1, 1, GATT_SUCCESS) }, + { it instanceof PhyPair && (it as PhyPair).txPhy == RxBlePhy.PHY_1M && it.rxPhy == RxBlePhy.PHY_1M } + ), + new CallbackTestCase( + "PhyUpdate", + { RxBleGattCallback out -> out.getOnPhyUpdate() }, + { BluetoothGattCallback bgc -> bgc.onPhyUpdate(mockBluetoothGatt, 1, 1, GATT_SUCCESS) }, + { it instanceof PhyPair && (it as PhyPair).txPhy == RxBlePhy.PHY_1M && it.rxPhy == RxBlePhy.PHY_1M } + ), new CallbackTestCase( "ServiceDiscovery", { RxBleGattCallback out -> out.getOnServicesDiscovered() }, diff --git a/rxandroidble/src/test/groovy/com/polidea/rxandroidble2/internal/operations/OperationPhyReadTest.groovy b/rxandroidble/src/test/groovy/com/polidea/rxandroidble2/internal/operations/OperationPhyReadTest.groovy new file mode 100644 index 000000000..ce1497398 --- /dev/null +++ b/rxandroidble/src/test/groovy/com/polidea/rxandroidble2/internal/operations/OperationPhyReadTest.groovy @@ -0,0 +1,80 @@ +package com.polidea.rxandroidble2.internal.operations + +import android.bluetooth.BluetoothGatt +import com.polidea.rxandroidble2.PhyPair +import com.polidea.rxandroidble2.RxBlePhy +import com.polidea.rxandroidble2.RxBlePhyOption +import com.polidea.rxandroidble2.exceptions.BleGattCallbackTimeoutException +import com.polidea.rxandroidble2.exceptions.BleGattOperationType +import com.polidea.rxandroidble2.internal.connection.RxBleGattCallback +import com.polidea.rxandroidble2.internal.serialization.QueueReleaseInterface +import com.polidea.rxandroidble2.internal.util.MockOperationTimeoutConfiguration +import io.reactivex.schedulers.TestScheduler +import io.reactivex.subjects.PublishSubject +import spock.lang.Specification + +import java.util.concurrent.TimeUnit + +public class OperationPhyReadTest extends Specification { + + static long timeout = 10 + static TimeUnit timeoutTimeUnit = TimeUnit.SECONDS + QueueReleaseInterface mockQueueReleaseInterface = Mock QueueReleaseInterface + BluetoothGatt mockBluetoothGatt = Mock BluetoothGatt + RxBleGattCallback mockGattCallback = Mock RxBleGattCallback + TestScheduler testScheduler = new TestScheduler() + PublishSubject readPhyPublishSubject = PublishSubject.create() + PhyReadOperation objectUnderTest + + def setup() { + mockGattCallback.getOnPhyRead() >> readPhyPublishSubject + prepareObjectUnderTest() + } + + def "should call BluetoothGatt.readPhy() exactly once when run()"() { + + when: + objectUnderTest.run(mockQueueReleaseInterface).test() + + then: + 1 * mockBluetoothGatt.readPhy() + } + + def "should emit an error if RxBleGattCallback will emit error on RxBleGattCallback.getOnPhyRead() and release queue"() { + + given: + def testSubscriber = objectUnderTest.run(mockQueueReleaseInterface).test() + def testException = new Exception("test") + + when: + readPhyPublishSubject.onError(testException) + + then: + testSubscriber.assertError(testException) + + and: + (1.._) * mockQueueReleaseInterface.release() // technically it's not an error to call it more than once + } + + def "should timeout if will not response after 10 seconds "() { + + given: + def testSubscriber = objectUnderTest.run(mockQueueReleaseInterface).test() + + when: + testScheduler.advanceTimeTo(timeout + 5, timeoutTimeUnit) + + then: + testSubscriber.assertError(BleGattCallbackTimeoutException) + + and: + testSubscriber.assertError { + ((BleGattCallbackTimeoutException)it).getBleGattOperationType() == BleGattOperationType.PHY_READ + } + } + + private prepareObjectUnderTest() { + objectUnderTest = new PhyReadOperation(mockGattCallback, mockBluetoothGatt, + new MockOperationTimeoutConfiguration(timeout.intValue(), testScheduler)) + } +} diff --git a/rxandroidble/src/test/groovy/com/polidea/rxandroidble2/internal/operations/OperationPhyUpdateTest.groovy b/rxandroidble/src/test/groovy/com/polidea/rxandroidble2/internal/operations/OperationPhyUpdateTest.groovy new file mode 100644 index 000000000..efd6898c0 --- /dev/null +++ b/rxandroidble/src/test/groovy/com/polidea/rxandroidble2/internal/operations/OperationPhyUpdateTest.groovy @@ -0,0 +1,93 @@ +package com.polidea.rxandroidble2.internal.operations + +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import com.polidea.rxandroidble2.PhyPair +import com.polidea.rxandroidble2.RxBlePhy +import com.polidea.rxandroidble2.RxBlePhyOption +import com.polidea.rxandroidble2.exceptions.BleGattCallbackTimeoutException +import com.polidea.rxandroidble2.exceptions.BleGattOperationType +import com.polidea.rxandroidble2.internal.RxBlePhyImpl +import com.polidea.rxandroidble2.internal.connection.RxBleGattCallback +import com.polidea.rxandroidble2.internal.serialization.QueueReleaseInterface +import com.polidea.rxandroidble2.internal.util.MockOperationTimeoutConfiguration +import io.reactivex.schedulers.TestScheduler +import io.reactivex.subjects.PublishSubject +import spock.lang.Specification + +import java.util.concurrent.TimeUnit + +public class OperationPhyUpdateTest extends Specification { + + static long timeout = 10 + static TimeUnit timeoutTimeUnit = TimeUnit.SECONDS + QueueReleaseInterface mockQueueReleaseInterface = Mock QueueReleaseInterface + BluetoothGatt mockBluetoothGatt = Mock BluetoothGatt + BluetoothDevice mockBluetoothDevice = Mock BluetoothDevice + RxBleGattCallback mockGattCallback = Mock RxBleGattCallback + TestScheduler testScheduler = new TestScheduler() + PublishSubject updatedPhyPublishSubject = PublishSubject.create() + PhyUpdateOperation objectUnderTest + Set rxSet = Set.of(RxBlePhy.PHY_1M) + Set txSet = Set.of(RxBlePhy.PHY_1M, RxBlePhy.PHY_2M) + RxBlePhyOption phyOption = RxBlePhyOption.PHY_OPTION_S8 + + def setup() { + mockGattCallback.getOnPhyUpdate() >> updatedPhyPublishSubject + mockBluetoothGatt.getDevice() >> mockBluetoothDevice + mockBluetoothDevice.getAddress() >> "AA:BB:CC:DD:EE:FF" + prepareObjectUnderTest() + } + + def "should call BluetoothGatt.setPreferredPhy(int, int, int) exactly once when run()"() { + + when: + objectUnderTest.run(mockQueueReleaseInterface).test() + + then: + 1 * mockBluetoothGatt.setPreferredPhy( + RxBlePhyImpl.enumSetToValuesMask(txSet), + RxBlePhyImpl.enumSetToValuesMask(rxSet), + phyOption.value + ) + } + + def "should emit an error if RxBleGattCallback will emit error on RxBleGattCallback.getOnPhyUpdate() and release queue"() { + + given: + def testSubscriber = objectUnderTest.run(mockQueueReleaseInterface).test() + def testException = new Exception("test") + + when: + updatedPhyPublishSubject.onError(testException) + + then: + testSubscriber.assertError(testException) + + and: + (1.._) * mockQueueReleaseInterface.release() // technically it's not an error to call it more than once + } + + def "should timeout if will not response after 10 seconds "() { + + given: + println(objectUnderTest.toString()) + def testSubscriber = objectUnderTest.run(mockQueueReleaseInterface).test() + + when: + testScheduler.advanceTimeTo(timeout + 5, timeoutTimeUnit) + + then: + testSubscriber.assertError(BleGattCallbackTimeoutException) + + and: + testSubscriber.assertError { + ((BleGattCallbackTimeoutException)it).getBleGattOperationType() == BleGattOperationType.PHY_UPDATE + } + } + + private prepareObjectUnderTest() { + objectUnderTest = new PhyUpdateOperation(mockGattCallback, mockBluetoothGatt, + new MockOperationTimeoutConfiguration(timeout.intValue(), testScheduler), txSet, rxSet, phyOption) + } +}