Add BluetoothGattHelper, BluetoothGattConnection.
Test: unit test
Bug: 200231384
Change-Id: Ia4c9dec00c81aa398fffc5af9477682f96682168
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattConnection.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattConnection.java
new file mode 100644
index 0000000..8e66ff2
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattConnection.java
@@ -0,0 +1,782 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.gatt;
+
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattService;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.server.nearby.common.bluetooth.BluetoothConsts;
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+import com.android.server.nearby.common.bluetooth.BluetoothGattException;
+import com.android.server.nearby.common.bluetooth.BluetoothTimeoutException;
+import com.android.server.nearby.common.bluetooth.ReservedUuids;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattHelper.ConnectionOptions;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattHelper.OperationType;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothDevice;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothGatt;
+import com.android.server.nearby.common.bluetooth.util.BluetoothGattUtils;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.Operation;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.SynchronousOperation;
+
+import com.google.common.base.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.BlockingDeque;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Gatt connection to a Bluetooth device.
+ */
+public class BluetoothGattConnection implements AutoCloseable {
+
+ private static final String TAG = BluetoothGattConnection.class.getSimpleName();
+
+ @VisibleForTesting
+ static final long OPERATION_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(1);
+ @VisibleForTesting
+ static final long SLOW_OPERATION_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10);
+
+ @VisibleForTesting
+ static final int GATT_INTERNAL_ERROR = 129;
+ @VisibleForTesting
+ static final int GATT_ERROR = 133;
+
+ private final BluetoothGatt mGatt;
+ private final BluetoothOperationExecutor mBluetoothOperationExecutor;
+ private final ConnectionOptions mConnectionOptions;
+
+ private volatile boolean mServicesDiscovered = false;
+
+ private volatile boolean mIsConnected = false;
+
+ private volatile int mMtu = BluetoothConsts.DEFAULT_MTU;
+
+ private final ConcurrentMap<BluetoothGattCharacteristic, ChangeObserver> mChangeObservers =
+ new ConcurrentHashMap<>();
+
+ private final List<ConnectionCloseListener> mCloseListeners = new ArrayList<>();
+
+ private long mOperationTimeoutMillis = OPERATION_TIMEOUT_MILLIS;
+
+ BluetoothGattConnection(
+ BluetoothGatt gatt,
+ BluetoothOperationExecutor bluetoothOperationExecutor,
+ ConnectionOptions connectionOptions) {
+ mGatt = gatt;
+ mBluetoothOperationExecutor = bluetoothOperationExecutor;
+ mConnectionOptions = connectionOptions;
+ }
+
+ /**
+ * Set operation timeout.
+ */
+ public void setOperationTimeout(long timeoutMillis) {
+ Preconditions.checkArgument(timeoutMillis > 0, "invalid time out value");
+ mOperationTimeoutMillis = timeoutMillis;
+ }
+
+ /**
+ * Returns connected device.
+ */
+ public BluetoothDevice getDevice() {
+ return mGatt.getDevice();
+ }
+
+ public ConnectionOptions getConnectionOptions() {
+ return mConnectionOptions;
+ }
+
+ public boolean isConnected() {
+ return mIsConnected;
+ }
+
+ /**
+ * Get service.
+ */
+ public BluetoothGattService getService(UUID uuid) throws BluetoothException {
+ Log.d(TAG, String.format("Getting service %s.", uuid));
+ if (!mServicesDiscovered) {
+ discoverServices();
+ }
+ BluetoothGattService match = null;
+ for (BluetoothGattService service : mGatt.getServices()) {
+ if (service.getUuid().equals(uuid)) {
+ if (match != null) {
+ throw new BluetoothException(
+ String.format("More than one service %s found on device %s.",
+ uuid,
+ mGatt.getDevice()));
+ }
+ match = service;
+ }
+ }
+ if (match == null) {
+ throw new BluetoothException(String.format("Service %s not found on device %s.",
+ uuid,
+ mGatt.getDevice()));
+ }
+ Log.d(TAG, "Service found.");
+ return match;
+ }
+
+ /**
+ * Returns a list of all characteristics under a given service UUID.
+ */
+ private List<BluetoothGattCharacteristic> getCharacteristics(UUID serviceUuid)
+ throws BluetoothException {
+ if (!mServicesDiscovered) {
+ discoverServices();
+ }
+ ArrayList<BluetoothGattCharacteristic> characteristics = new ArrayList<>();
+ for (BluetoothGattService service : mGatt.getServices()) {
+ // Add all characteristics under this service if its service UUID matches.
+ if (service.getUuid().equals(serviceUuid)) {
+ characteristics.addAll(service.getCharacteristics());
+ }
+ }
+ return characteristics;
+ }
+
+ /**
+ * Get characteristic.
+ */
+ public BluetoothGattCharacteristic getCharacteristic(UUID serviceUuid,
+ UUID characteristicUuid) throws BluetoothException {
+ Log.d(TAG, String.format("Getting characteristic %s on service %s.", characteristicUuid,
+ serviceUuid));
+ BluetoothGattCharacteristic match = null;
+ for (BluetoothGattCharacteristic characteristic : getCharacteristics(serviceUuid)) {
+ if (characteristic.getUuid().equals(characteristicUuid)) {
+ if (match != null) {
+ throw new BluetoothException(String.format(
+ "More than one characteristic %s found on service %s on device %s.",
+ characteristicUuid,
+ serviceUuid,
+ mGatt.getDevice()));
+ }
+ match = characteristic;
+ }
+ }
+ if (match == null) {
+ throw new BluetoothException(String.format(
+ "Characteristic %s not found on service %s of device %s.",
+ characteristicUuid,
+ serviceUuid,
+ mGatt.getDevice()));
+ }
+ Log.d(TAG, "Characteristic found.");
+ return match;
+ }
+
+ /**
+ * Get descriptor.
+ */
+ public BluetoothGattDescriptor getDescriptor(UUID serviceUuid,
+ UUID characteristicUuid, UUID descriptorUuid) throws BluetoothException {
+ Log.d(TAG, String.format("Getting descriptor %s on characteristic %s on service %s.",
+ descriptorUuid, characteristicUuid, serviceUuid));
+ BluetoothGattDescriptor match = null;
+ for (BluetoothGattDescriptor descriptor :
+ getCharacteristic(serviceUuid, characteristicUuid).getDescriptors()) {
+ if (descriptor.getUuid().equals(descriptorUuid)) {
+ if (match != null) {
+ throw new BluetoothException(String.format("More than one descriptor %s found "
+ + "on characteristic %s service %s on device %s.",
+ descriptorUuid,
+ characteristicUuid,
+ serviceUuid,
+ mGatt.getDevice()));
+ }
+ match = descriptor;
+ }
+ }
+ if (match == null) {
+ throw new BluetoothException(String.format(
+ "Descriptor %s not found on characteristic %s on service %s of device %s.",
+ descriptorUuid,
+ characteristicUuid,
+ serviceUuid,
+ mGatt.getDevice()));
+ }
+ Log.d(TAG, "Descriptor found.");
+ return match;
+ }
+
+ /**
+ * Discover services.
+ */
+ public void discoverServices() throws BluetoothException {
+ mBluetoothOperationExecutor.execute(
+ new SynchronousOperation<Void>(OperationType.DISCOVER_SERVICES) {
+ @Nullable
+ @Override
+ public Void call() throws BluetoothException {
+ if (mServicesDiscovered) {
+ return null;
+ }
+ boolean forceRefresh = false;
+ try {
+ discoverServicesInternal();
+ } catch (BluetoothException e) {
+ if (!(e instanceof BluetoothGattException)) {
+ throw e;
+ }
+ int errorCode = ((BluetoothGattException) e).getGattErrorCode();
+ if (errorCode != GATT_ERROR && errorCode != GATT_INTERNAL_ERROR) {
+ throw e;
+ }
+ Log.e(TAG, e.getMessage()
+ + "\n Ignore the gatt error for post MNC apis and force "
+ + "a refresh");
+ forceRefresh = true;
+ }
+
+ forceRefreshServiceCacheIfNeeded(forceRefresh);
+
+ mServicesDiscovered = true;
+
+ return null;
+ }
+ });
+ }
+
+ private void discoverServicesInternal() throws BluetoothException {
+ Log.i(TAG, "Starting services discovery.");
+ long startTimeMillis = System.currentTimeMillis();
+ try {
+ mBluetoothOperationExecutor.execute(
+ new Operation<Void>(OperationType.DISCOVER_SERVICES_INTERNAL, mGatt) {
+ @Override
+ public void run() throws BluetoothException {
+ boolean success = mGatt.discoverServices();
+ if (!success) {
+ throw new BluetoothException(
+ "gatt.discoverServices returned false.");
+ }
+ }
+ },
+ SLOW_OPERATION_TIMEOUT_MILLIS);
+ Log.i(TAG, String.format("Services discovered successfully in %s ms.",
+ System.currentTimeMillis() - startTimeMillis));
+ } catch (BluetoothException e) {
+ if (e instanceof BluetoothGattException) {
+ throw new BluetoothGattException(String.format(
+ "Failed to discover services on device: %s.",
+ mGatt.getDevice()), ((BluetoothGattException) e).getGattErrorCode(), e);
+ } else {
+ throw new BluetoothException(String.format(
+ "Failed to discover services on device: %s.",
+ mGatt.getDevice()), e);
+ }
+ }
+ }
+
+ private boolean hasDynamicServices() {
+ BluetoothGattService gattService =
+ mGatt.getService(ReservedUuids.Services.GENERIC_ATTRIBUTE);
+ if (gattService != null) {
+ BluetoothGattCharacteristic serviceChange =
+ gattService.getCharacteristic(ReservedUuids.Characteristics.SERVICE_CHANGE);
+ if (serviceChange != null) {
+ return true;
+ }
+ }
+
+ // Check whether the server contains a self defined service dynamic characteristic.
+ gattService = mGatt.getService(BluetoothConsts.SERVICE_DYNAMIC_SERVICE);
+ if (gattService != null) {
+ BluetoothGattCharacteristic serviceChange =
+ gattService.getCharacteristic(BluetoothConsts.SERVICE_DYNAMIC_CHARACTERISTIC);
+ if (serviceChange != null) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private void forceRefreshServiceCacheIfNeeded(boolean forceRefresh) throws BluetoothException {
+ if (mGatt.getDevice().getBondState() != BluetoothDevice.BOND_BONDED) {
+ // Device is not bonded, so services should not have been cached.
+ return;
+ }
+
+ if (!forceRefresh && !hasDynamicServices()) {
+ return;
+ }
+ Log.i(TAG, "Forcing a refresh of local cache of GATT services");
+ boolean success = mGatt.refresh();
+ if (!success) {
+ throw new BluetoothException("gatt.refresh returned false.");
+ }
+ discoverServicesInternal();
+ }
+
+ /**
+ * Read characteristic.
+ */
+ public byte[] readCharacteristic(UUID serviceUuid, UUID characteristicUuid)
+ throws BluetoothException {
+ return readCharacteristic(getCharacteristic(serviceUuid, characteristicUuid));
+ }
+
+ /**
+ * Read characteristic.
+ */
+ public byte[] readCharacteristic(final BluetoothGattCharacteristic characteristic)
+ throws BluetoothException {
+ try {
+ return mBluetoothOperationExecutor.executeNonnull(
+ new Operation<byte[]>(OperationType.READ_CHARACTERISTIC, mGatt,
+ characteristic) {
+ @Override
+ public void run() throws BluetoothException {
+ boolean success = mGatt.readCharacteristic(characteristic);
+ if (!success) {
+ throw new BluetoothException(
+ "gatt.readCharacteristic returned false.");
+ }
+ }
+ },
+ mOperationTimeoutMillis);
+ } catch (BluetoothException e) {
+ throw new BluetoothException(String.format(
+ "Failed to read %s on device %s.",
+ BluetoothGattUtils.toString(characteristic),
+ mGatt.getDevice()), e);
+ }
+ }
+
+ /**
+ * Writes Characteristic.
+ */
+ public void writeCharacteristic(UUID serviceUuid, UUID characteristicUuid, byte[] value)
+ throws BluetoothException {
+ writeCharacteristic(getCharacteristic(serviceUuid, characteristicUuid), value);
+ }
+
+ /**
+ * Writes Characteristic.
+ */
+ public void writeCharacteristic(final BluetoothGattCharacteristic characteristic,
+ final byte[] value) throws BluetoothException {
+ Log.d(TAG, String.format("Writing %d bytes on %s on device %s.",
+ value.length,
+ BluetoothGattUtils.toString(characteristic),
+ mGatt.getDevice()));
+ if ((characteristic.getProperties() & (BluetoothGattCharacteristic.PROPERTY_WRITE
+ | BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE)) == 0) {
+ throw new BluetoothException(String.format("%s is not writable!", characteristic));
+ }
+ try {
+ mBluetoothOperationExecutor.execute(
+ new Operation<Void>(OperationType.WRITE_CHARACTERISTIC, mGatt, characteristic) {
+ @Override
+ public void run() throws BluetoothException {
+ BluetoothGattCharacteristic characteristiClone =
+ BluetoothGattUtils.clone(characteristic);
+ characteristiClone.setValue(value);
+ boolean success = mGatt.writeCharacteristic(characteristiClone);
+ if (!success) {
+ throw new BluetoothException(
+ "gatt.writeCharacteristic returned false.");
+ }
+ }
+ },
+ mOperationTimeoutMillis);
+ } catch (BluetoothException e) {
+ throw new BluetoothException(String.format(
+ "Failed to write %s on device %s.",
+ BluetoothGattUtils.toString(characteristic),
+ mGatt.getDevice()), e);
+ }
+ Log.d(TAG, "Writing characteristic done.");
+ }
+
+ /**
+ * Reads descriptor.
+ */
+ public byte[] readDescriptor(UUID serviceUuid, UUID characteristicUuid, UUID descriptorUuid)
+ throws BluetoothException {
+ return readDescriptor(getDescriptor(serviceUuid, characteristicUuid, descriptorUuid));
+ }
+
+ /**
+ * Reads descriptor.
+ */
+ public byte[] readDescriptor(final BluetoothGattDescriptor descriptor)
+ throws BluetoothException {
+ try {
+ return mBluetoothOperationExecutor.executeNonnull(
+ new Operation<byte[]>(OperationType.READ_DESCRIPTOR, mGatt, descriptor) {
+ @Override
+ public void run() throws BluetoothException {
+ boolean success = mGatt.readDescriptor(descriptor);
+ if (!success) {
+ throw new BluetoothException("gatt.readDescriptor returned false.");
+ }
+ }
+ },
+ mOperationTimeoutMillis);
+ } catch (BluetoothException e) {
+ throw new BluetoothException(String.format(
+ "Failed to read %s on %s on device %s.",
+ descriptor.getUuid(),
+ BluetoothGattUtils.toString(descriptor),
+ mGatt.getDevice()), e);
+ }
+ }
+
+ /**
+ * Writes descriptor.
+ */
+ public void writeDescriptor(UUID serviceUuid, UUID characteristicUuid, UUID descriptorUuid,
+ byte[] value) throws BluetoothException {
+ writeDescriptor(getDescriptor(serviceUuid, characteristicUuid, descriptorUuid), value);
+ }
+
+ /**
+ * Writes descriptor.
+ */
+ public void writeDescriptor(final BluetoothGattDescriptor descriptor, final byte[] value)
+ throws BluetoothException {
+ Log.d(TAG, String.format(
+ "Writing %d bytes on %s on device %s.",
+ value.length,
+ BluetoothGattUtils.toString(descriptor),
+ mGatt.getDevice()));
+ long startTimeMillis = System.currentTimeMillis();
+ try {
+ mBluetoothOperationExecutor.execute(
+ new Operation<Void>(OperationType.WRITE_DESCRIPTOR, mGatt, descriptor) {
+ @Override
+ public void run() throws BluetoothException {
+ BluetoothGattDescriptor descriptorClone =
+ BluetoothGattUtils.clone(descriptor);
+ descriptorClone.setValue(value);
+ boolean success = mGatt.writeDescriptor(descriptorClone);
+ if (!success) {
+ throw new BluetoothException(
+ "gatt.writeDescriptor returned false.");
+ }
+ }
+ },
+ mOperationTimeoutMillis);
+ Log.d(TAG, String.format("Writing descriptor done in %s ms.",
+ System.currentTimeMillis() - startTimeMillis));
+ } catch (BluetoothException e) {
+ throw new BluetoothException(String.format(
+ "Failed to write %s on device %s.",
+ BluetoothGattUtils.toString(descriptor),
+ mGatt.getDevice()), e);
+ }
+ }
+
+ /**
+ * Reads remote Rssi.
+ */
+ public int readRemoteRssi() throws BluetoothException {
+ try {
+ return mBluetoothOperationExecutor.executeNonnull(
+ new Operation<Integer>(OperationType.READ_RSSI, mGatt) {
+ @Override
+ public void run() throws BluetoothException {
+ boolean success = mGatt.readRemoteRssi();
+ if (!success) {
+ throw new BluetoothException("gatt.readRemoteRssi returned false.");
+ }
+ }
+ },
+ mOperationTimeoutMillis);
+ } catch (BluetoothException e) {
+ throw new BluetoothException(
+ String.format("Failed to read rssi on device %s.", mGatt.getDevice()), e);
+ }
+ }
+
+ public int getMtu() {
+ return mMtu;
+ }
+
+ /**
+ * Get max data packet size.
+ */
+ public int getMaxDataPacketSize() {
+ // Per BT specs (3.2.9), only MTU - 3 bytes can be used to transmit data
+ return mMtu - 3;
+ }
+
+ /** Set notification enabled or disabled. */
+ @VisibleForTesting
+ public void setNotificationEnabled(BluetoothGattCharacteristic characteristic, boolean enabled)
+ throws BluetoothException {
+ boolean isIndication;
+ int properties = characteristic.getProperties();
+ if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) {
+ isIndication = false;
+ } else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0) {
+ isIndication = true;
+ } else {
+ throw new BluetoothException(String.format(
+ "%s on device %s supports neither notifications nor indications.",
+ BluetoothGattUtils.toString(characteristic),
+ mGatt.getDevice()));
+ }
+ BluetoothGattDescriptor clientConfigDescriptor =
+ characteristic.getDescriptor(
+ ReservedUuids.Descriptors.CLIENT_CHARACTERISTIC_CONFIGURATION);
+ if (clientConfigDescriptor == null) {
+ throw new BluetoothException(String.format(
+ "%s on device %s is missing client config descriptor.",
+ BluetoothGattUtils.toString(characteristic),
+ mGatt.getDevice()));
+ }
+ long startTime = System.currentTimeMillis();
+ Log.d(TAG, String.format("%s %s on characteristic %s.", enabled ? "Enabling" : "Disabling",
+ isIndication ? "indication" : "notification", characteristic.getUuid()));
+
+ if (enabled) {
+ mGatt.setCharacteristicNotification(characteristic, enabled);
+ }
+ writeDescriptor(clientConfigDescriptor,
+ enabled
+ ? (isIndication
+ ? BluetoothGattDescriptor.ENABLE_INDICATION_VALUE :
+ BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)
+ : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
+ if (!enabled) {
+ mGatt.setCharacteristicNotification(characteristic, enabled);
+ }
+
+ Log.d(TAG, String.format("Done in %d ms.", System.currentTimeMillis() - startTime));
+ }
+
+ /**
+ * Enables notification.
+ */
+ public ChangeObserver enableNotification(UUID serviceUuid, UUID characteristicUuid)
+ throws BluetoothException {
+ return enableNotification(getCharacteristic(serviceUuid, characteristicUuid));
+ }
+
+ /**
+ * Enables notification.
+ */
+ public ChangeObserver enableNotification(final BluetoothGattCharacteristic characteristic)
+ throws BluetoothException {
+ return mBluetoothOperationExecutor.executeNonnull(
+ new SynchronousOperation<ChangeObserver>(
+ OperationType.NOTIFICATION_CHANGE,
+ characteristic) {
+ @Override
+ public ChangeObserver call() throws BluetoothException {
+ ChangeObserver changeObserver = new ChangeObserver();
+ mChangeObservers.put(characteristic, changeObserver);
+ setNotificationEnabled(characteristic, true);
+ return changeObserver;
+ }
+ });
+ }
+
+ /**
+ * Disables notification.
+ */
+ public void disableNotification(UUID serviceUuid, UUID characteristicUuid)
+ throws BluetoothException {
+ disableNotification(getCharacteristic(serviceUuid, characteristicUuid));
+ }
+
+ /**
+ * Disables notification.
+ */
+ public void disableNotification(final BluetoothGattCharacteristic characteristic)
+ throws BluetoothException {
+ mBluetoothOperationExecutor.execute(
+ new SynchronousOperation<Void>(
+ OperationType.NOTIFICATION_CHANGE,
+ characteristic) {
+ @Nullable
+ @Override
+ public Void call() throws BluetoothException {
+ setNotificationEnabled(characteristic, false);
+ mChangeObservers.remove(characteristic);
+ return null;
+ }
+ });
+ }
+
+ /**
+ * Adds a close listener.
+ */
+ public void addCloseListener(ConnectionCloseListener listener) {
+ mCloseListeners.add(listener);
+ if (!mIsConnected) {
+ listener.onClose();
+ return;
+ }
+ }
+
+ /**
+ * Removes a close listener.
+ */
+ public void removeCloseListener(ConnectionCloseListener listener) {
+ mCloseListeners.remove(listener);
+ }
+
+ /** onCharacteristicChanged callback. */
+ public void onCharacteristicChanged(BluetoothGattCharacteristic characteristic, byte[] value) {
+ ChangeObserver observer = mChangeObservers.get(characteristic);
+ if (observer == null) {
+ return;
+ }
+ observer.onValueChange(value);
+ }
+
+ @Override
+ public void close() throws BluetoothException {
+ Log.d(TAG, "close");
+ try {
+ if (!mIsConnected) {
+ // Don't call disconnect on a closed connection, since Android framework won't
+ // provide any feedback.
+ return;
+ }
+ mBluetoothOperationExecutor.execute(
+ new Operation<Void>(OperationType.DISCONNECT, mGatt.getDevice()) {
+ @Override
+ public void run() throws BluetoothException {
+ mGatt.disconnect();
+ }
+ }, mOperationTimeoutMillis);
+ } finally {
+ mGatt.close();
+ }
+ }
+
+ /** onConnected callback. */
+ public void onConnected() {
+ Log.d(TAG, "onConnected");
+ mIsConnected = true;
+ }
+
+ /** onClosed callback. */
+ public void onClosed() {
+ Log.d(TAG, "onClosed");
+ if (!mIsConnected) {
+ return;
+ }
+ mIsConnected = false;
+ for (ConnectionCloseListener listener : mCloseListeners) {
+ listener.onClose();
+ }
+ mGatt.close();
+ }
+
+ /**
+ * Observer to wait or be called back when value change.
+ */
+ public static class ChangeObserver {
+
+ private final BlockingDeque<byte[]> mValues = new LinkedBlockingDeque<byte[]>();
+
+ @GuardedBy("mValues")
+ @Nullable
+ private volatile CharacteristicChangeListener mListener;
+
+ /**
+ * Set listener.
+ */
+ public void setListener(@Nullable CharacteristicChangeListener listener) {
+ synchronized (mValues) {
+ mListener = listener;
+ if (listener != null) {
+ byte[] value;
+ while ((value = mValues.poll()) != null) {
+ listener.onValueChange(value);
+ }
+ }
+ }
+ }
+
+ /**
+ * onValueChange callback.
+ */
+ public void onValueChange(byte[] newValue) {
+ synchronized (mValues) {
+ CharacteristicChangeListener listener = mListener;
+ if (listener == null) {
+ mValues.add(newValue);
+ } else {
+ listener.onValueChange(newValue);
+ }
+ }
+ }
+
+ /**
+ * Waits for update for a given time.
+ */
+ public byte[] waitForUpdate(long timeoutMillis) throws BluetoothException {
+ byte[] result;
+ try {
+ result = mValues.poll(timeoutMillis, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new BluetoothException("Operation interrupted.");
+ }
+ if (result == null) {
+ throw new BluetoothTimeoutException(
+ String.format("Operation timed out after %dms", timeoutMillis));
+ }
+ return result;
+ }
+ }
+
+ /**
+ * Listener for characteristic data changes over notifications or indications.
+ */
+ public interface CharacteristicChangeListener {
+
+ /**
+ * onValueChange callback.
+ */
+ void onValueChange(byte[] newValue);
+ }
+
+ /**
+ * Listener for connection close events.
+ */
+ public interface ConnectionCloseListener {
+
+ /**
+ * onClose callback.
+ */
+ void onClose();
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattHelper.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattHelper.java
new file mode 100644
index 0000000..8eb96ff
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattHelper.java
@@ -0,0 +1,598 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.gatt;
+
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanSettings;
+import android.content.Context;
+import android.os.ParcelUuid;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothAdapter;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothDevice;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothGatt;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothGattCallback;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le.BluetoothLeScanner;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le.ScanCallback;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le.ScanResult;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.BluetoothOperationTimeoutException;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.Operation;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Wrapper of {@link BluetoothGatt} that provides blocking methods, errors and timeout handling.
+ */
+@SuppressWarnings("Guava") // java.util.Optional is not available until API 24
+public class BluetoothGattHelper {
+
+ private static final String TAG = BluetoothGattHelper.class.getSimpleName();
+
+ @VisibleForTesting
+ static final long LOW_LATENCY_SCAN_MILLIS = TimeUnit.SECONDS.toMillis(5);
+ private static final long POLL_INTERVAL_MILLIS = 5L /* milliseconds */;
+
+ /**
+ * BT operation types that can be in flight.
+ */
+ public enum OperationType {
+ SCAN,
+ CONNECT,
+ DISCOVER_SERVICES,
+ DISCOVER_SERVICES_INTERNAL,
+ NOTIFICATION_CHANGE,
+ READ_CHARACTERISTIC,
+ WRITE_CHARACTERISTIC,
+ READ_DESCRIPTOR,
+ WRITE_DESCRIPTOR,
+ READ_RSSI,
+ WRITE_RELIABLE,
+ CHANGE_MTU,
+ DISCONNECT
+ }
+
+ @VisibleForTesting
+ final ScanCallback mScanCallback = new InternalScanCallback();
+ @VisibleForTesting
+ final BluetoothGattCallback mBluetoothGattCallback =
+ new InternalBluetoothGattCallback();
+ @VisibleForTesting
+ final ConcurrentMap<BluetoothGatt, BluetoothGattConnection> mConnections =
+ new ConcurrentHashMap<>();
+
+ private final Context mApplicationContext;
+ private final BluetoothAdapter mBluetoothAdapter;
+ private final BluetoothOperationExecutor mBluetoothOperationExecutor;
+
+ @VisibleForTesting
+ BluetoothGattHelper(
+ Context applicationContext,
+ BluetoothAdapter bluetoothAdapter,
+ BluetoothOperationExecutor bluetoothOperationExecutor) {
+ mApplicationContext = applicationContext;
+ mBluetoothAdapter = bluetoothAdapter;
+ mBluetoothOperationExecutor = bluetoothOperationExecutor;
+ }
+
+ public BluetoothGattHelper(Context applicationContext, BluetoothAdapter bluetoothAdapter) {
+ this(
+ Preconditions.checkNotNull(applicationContext),
+ Preconditions.checkNotNull(bluetoothAdapter),
+ new BluetoothOperationExecutor(5));
+ }
+
+ /**
+ * Auto-connects a serice Uuid.
+ */
+ public BluetoothGattConnection autoConnect(final UUID serviceUuid) throws BluetoothException {
+ Log.d(TAG, String.format("Starting autoconnection to a device advertising service %s.",
+ serviceUuid));
+ BluetoothDevice device = null;
+ int retries = 3;
+ final BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner();
+ if (scanner == null) {
+ throw new BluetoothException("Bluetooth is disabled or LE is not supported.");
+ }
+ final ScanFilter serviceFilter = new ScanFilter.Builder()
+ .setServiceUuid(new ParcelUuid(serviceUuid))
+ .build();
+ ScanSettings.Builder scanSettingsBuilder = new ScanSettings.Builder()
+ .setReportDelay(0);
+ final ScanSettings scanSettingsLowLatency = scanSettingsBuilder
+ .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
+ .build();
+ final ScanSettings scanSettingsLowPower = scanSettingsBuilder
+ .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
+ .build();
+ while (true) {
+ long startTimeMillis = System.currentTimeMillis();
+ try {
+ Log.d(TAG, "Starting low latency scanning.");
+ device =
+ mBluetoothOperationExecutor.executeNonnull(
+ new Operation<BluetoothDevice>(OperationType.SCAN) {
+ @Override
+ public void run() throws BluetoothException {
+ scanner.startScan(Arrays.asList(serviceFilter),
+ scanSettingsLowLatency, mScanCallback);
+ }
+ }, LOW_LATENCY_SCAN_MILLIS);
+ } catch (BluetoothOperationTimeoutException e) {
+ Log.d(TAG, String.format(
+ "Cannot find a nearby device in low latency scanning after %s ms.",
+ LOW_LATENCY_SCAN_MILLIS));
+ } finally {
+ scanner.stopScan(mScanCallback);
+ }
+ if (device == null) {
+ Log.d(TAG, "Starting low power scanning.");
+ try {
+ device = mBluetoothOperationExecutor.executeNonnull(
+ new Operation<BluetoothDevice>(OperationType.SCAN) {
+ @Override
+ public void run() throws BluetoothException {
+ scanner.startScan(Arrays.asList(serviceFilter),
+ scanSettingsLowPower, mScanCallback);
+ }
+ });
+ } finally {
+ scanner.stopScan(mScanCallback);
+ }
+ }
+ Log.d(TAG, String.format("Scanning done in %d ms. Found device %s.",
+ System.currentTimeMillis() - startTimeMillis, device));
+
+ try {
+ return connect(device);
+ } catch (BluetoothException e) {
+ retries--;
+ if (retries == 0) {
+ throw e;
+ } else {
+ Log.d(TAG, String.format(
+ "Connection failed: %s. Retrying %d more times.", e, retries));
+ }
+ }
+ }
+ }
+
+ /**
+ * Connects to a device using default connection options.
+ */
+ public BluetoothGattConnection connect(BluetoothDevice bluetoothDevice)
+ throws BluetoothException {
+ return connect(bluetoothDevice, ConnectionOptions.builder().build());
+ }
+
+ /**
+ * Connects to a device using specifies connection options.
+ */
+ public BluetoothGattConnection connect(
+ BluetoothDevice bluetoothDevice, ConnectionOptions options) throws BluetoothException {
+ Log.d(TAG, String.format("Connecting to device %s.", bluetoothDevice));
+ long startTimeMillis = System.currentTimeMillis();
+
+ Operation<BluetoothGattConnection> connectOperation =
+ new Operation<BluetoothGattConnection>(OperationType.CONNECT, bluetoothDevice) {
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private boolean mIsCanceled = false;
+
+ @GuardedBy("mLock")
+ @Nullable(/* null before operation is executed */)
+ private BluetoothGatt mBluetoothGatt;
+
+ @Override
+ public void run() throws BluetoothException {
+ synchronized (mLock) {
+ if (mIsCanceled) {
+ return;
+ }
+ BluetoothGatt bluetoothGatt;
+ Log.d(TAG, "Use LE transport");
+ bluetoothGatt =
+ bluetoothDevice.connectGatt(
+ mApplicationContext,
+ options.autoConnect(),
+ mBluetoothGattCallback,
+ android.bluetooth.BluetoothDevice.TRANSPORT_LE);
+ if (bluetoothGatt == null) {
+ throw new BluetoothException("connectGatt() returned null.");
+ }
+
+ try {
+ // Set connection priority without waiting for connection callback.
+ // Per code, btif_gatt_client.c, when priority is set before
+ // connection, this sets preferred connection parameters that will
+ // be used during the connection establishment.
+ Optional<Integer> connectionPriorityOption =
+ options.connectionPriority();
+ if (connectionPriorityOption.isPresent()) {
+ // requestConnectionPriority can only be called when
+ // BluetoothGatt is connected to the system BluetoothGatt
+ // service (see android/bluetooth/BluetoothGatt.java code).
+ // However, there is no callback to the app to inform when this
+ // is done. requestConnectionPriority will returns false with no
+ // side-effect before the service is connected, so we just poll
+ // here until true is returned.
+ int connectionPriority = connectionPriorityOption.get();
+ long startTimeMillis = System.currentTimeMillis();
+ while (!bluetoothGatt.requestConnectionPriority(
+ connectionPriority)) {
+ if (System.currentTimeMillis() - startTimeMillis
+ > options.connectionTimeoutMillis()) {
+ throw new BluetoothException(
+ String.format(
+ Locale.US,
+ "Failed to set connectionPriority "
+ + "after %dms.",
+ options.connectionTimeoutMillis()));
+ }
+ try {
+ Thread.sleep(POLL_INTERVAL_MILLIS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new BluetoothException(
+ "connect() operation interrupted.");
+ }
+ }
+ }
+ } catch (Exception e) {
+ // Make sure to clean connection.
+ bluetoothGatt.disconnect();
+ bluetoothGatt.close();
+ throw e;
+ }
+
+ BluetoothGattConnection connection = new BluetoothGattConnection(
+ bluetoothGatt, mBluetoothOperationExecutor, options);
+ mConnections.put(bluetoothGatt, connection);
+ mBluetoothGatt = bluetoothGatt;
+ }
+ }
+
+ @Override
+ public void cancel() {
+ // Clean connection if connection times out.
+ synchronized (mLock) {
+ if (mIsCanceled) {
+ return;
+ }
+ mIsCanceled = true;
+ BluetoothGatt bluetoothGatt = mBluetoothGatt;
+ if (bluetoothGatt == null) {
+ return;
+ }
+ mConnections.remove(bluetoothGatt);
+ bluetoothGatt.disconnect();
+ bluetoothGatt.close();
+ }
+ }
+ };
+ BluetoothGattConnection result;
+ if (options.autoConnect()) {
+ result = mBluetoothOperationExecutor.executeNonnull(connectOperation);
+ } else {
+ result =
+ mBluetoothOperationExecutor.executeNonnull(
+ connectOperation, options.connectionTimeoutMillis());
+ }
+ Log.d(TAG, String.format("Connection success in %d ms.",
+ System.currentTimeMillis() - startTimeMillis));
+ return result;
+ }
+
+ private BluetoothGattConnection getConnectionByGatt(BluetoothGatt gatt)
+ throws BluetoothException {
+ BluetoothGattConnection connection = mConnections.get(gatt);
+ if (connection == null) {
+ throw new BluetoothException("Receive callback on unexpected device: " + gatt);
+ }
+ return connection;
+ }
+
+ private class InternalBluetoothGattCallback extends BluetoothGattCallback {
+
+ @Override
+ public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
+ BluetoothGattConnection connection;
+ BluetoothDevice device = gatt.getDevice();
+ switch (newState) {
+ case BluetoothGatt.STATE_CONNECTED: {
+ connection = mConnections.get(gatt);
+ if (connection == null) {
+ Log.w(TAG, String.format(
+ "Received unexpected successful connection for dev %s! Ignoring.",
+ device));
+ break;
+ }
+
+ Operation<BluetoothGattConnection> operation =
+ new Operation<>(OperationType.CONNECT, device);
+ if (status != BluetoothGatt.GATT_SUCCESS) {
+ mConnections.remove(gatt);
+ gatt.disconnect();
+ gatt.close();
+ mBluetoothOperationExecutor.notifyCompletion(operation, status, null);
+ break;
+ }
+
+ // Process connection options
+ ConnectionOptions options = connection.getConnectionOptions();
+ Optional<Integer> mtuOption = options.mtu();
+ if (mtuOption.isPresent()) {
+ // Requesting MTU and waiting for MTU callback.
+ boolean success = gatt.requestMtu(mtuOption.get());
+ if (!success) {
+ mBluetoothOperationExecutor.notifyFailure(operation,
+ new BluetoothException(String.format(Locale.US,
+ "Failed to request MTU of %d for dev %s: "
+ + "returned false.",
+ mtuOption.get(), device)));
+ // Make sure to clean connection.
+ mConnections.remove(gatt);
+ gatt.disconnect();
+ gatt.close();
+ }
+ break;
+ }
+
+ // Connection successful
+ connection.onConnected();
+ mBluetoothOperationExecutor.notifyCompletion(operation, status, connection);
+ break;
+ }
+ case BluetoothGatt.STATE_DISCONNECTED: {
+ connection = mConnections.remove(gatt);
+ if (connection == null) {
+ Log.w(TAG, String.format("Received unexpected disconnection"
+ + " for device %s! Ignoring.", device));
+ break;
+ }
+ if (!connection.isConnected()) {
+ // This is a failed connection attempt
+ if (status == BluetoothGatt.GATT_SUCCESS) {
+ // This is weird... considering this as a failure
+ Log.w(TAG, String.format(
+ "Received a success for a failed connection "
+ + "attempt for device %s! Ignoring.", device));
+ status = BluetoothGatt.GATT_FAILURE;
+ }
+ mBluetoothOperationExecutor
+ .notifyCompletion(new Operation<BluetoothGattConnection>(
+ OperationType.CONNECT, device), status, null);
+ // Clean Gatt object in every case.
+ gatt.disconnect();
+ gatt.close();
+ break;
+ }
+ connection.onClosed();
+ mBluetoothOperationExecutor.notifyCompletion(
+ new Operation<>(OperationType.DISCONNECT, device), status);
+ break;
+ }
+ default:
+ Log.e(TAG, "Unexpected connection state: " + newState);
+ }
+ }
+
+ @Override
+ public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
+ BluetoothGattConnection connection = mConnections.get(gatt);
+ BluetoothDevice device = gatt.getDevice();
+ if (connection == null) {
+ Log.w(TAG, String.format(
+ "Received unexpected MTU change for device %s! Ignoring.", device));
+ return;
+ }
+ if (connection.isConnected()) {
+ // This is the callback for the deprecated BluetoothGattConnection.requestMtu.
+ mBluetoothOperationExecutor.notifyCompletion(
+ new Operation<>(OperationType.CHANGE_MTU, gatt), status, mtu);
+ } else {
+ // This is the callback when requesting MTU right after connecting.
+ connection.onConnected();
+ mBluetoothOperationExecutor.notifyCompletion(
+ new Operation<>(OperationType.CONNECT, device), status, connection);
+ if (status != BluetoothGatt.GATT_SUCCESS) {
+ Log.w(TAG, String.format(
+ "%s responds MTU change failed, status %s.", device, status));
+ // Clean connection if it's failed.
+ mConnections.remove(gatt);
+ gatt.disconnect();
+ gatt.close();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void onServicesDiscovered(BluetoothGatt gatt, int status) {
+ mBluetoothOperationExecutor.notifyCompletion(
+ new Operation<Void>(OperationType.DISCOVER_SERVICES_INTERNAL, gatt), status);
+ }
+
+ @Override
+ public void onCharacteristicRead(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic, int status) {
+ mBluetoothOperationExecutor.notifyCompletion(
+ new Operation<byte[]>(OperationType.READ_CHARACTERISTIC, gatt, characteristic),
+ status, characteristic.getValue());
+ }
+
+ @Override
+ public void onCharacteristicWrite(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic, int status) {
+ mBluetoothOperationExecutor.notifyCompletion(new Operation<Void>(
+ OperationType.WRITE_CHARACTERISTIC, gatt, characteristic), status);
+ }
+
+ @Override
+ public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
+ int status) {
+ mBluetoothOperationExecutor.notifyCompletion(
+ new Operation<byte[]>(OperationType.READ_DESCRIPTOR, gatt, descriptor), status,
+ descriptor.getValue());
+ }
+
+ @Override
+ public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
+ int status) {
+ Log.d(TAG, String.format("onDescriptorWrite %s, %s, %d",
+ gatt.getDevice(), descriptor.getUuid(), status));
+ mBluetoothOperationExecutor.notifyCompletion(
+ new Operation<Void>(OperationType.WRITE_DESCRIPTOR, gatt, descriptor), status);
+ }
+
+ @Override
+ public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
+ mBluetoothOperationExecutor.notifyCompletion(
+ new Operation<Integer>(OperationType.READ_RSSI, gatt), status, rssi);
+ }
+
+ @Override
+ public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
+ mBluetoothOperationExecutor.notifyCompletion(
+ new Operation<Void>(OperationType.WRITE_RELIABLE, gatt), status);
+ }
+
+ @Override
+ public void onCharacteristicChanged(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic) {
+ byte[] value = characteristic.getValue();
+ if (value == null) {
+ // Value is not supposed to be null, but just to be safe...
+ value = new byte[0];
+ }
+ Log.d(TAG, String.format("Characteristic %s changed, Gatt device: %s",
+ characteristic.getUuid(), gatt.getDevice()));
+ try {
+ getConnectionByGatt(gatt).onCharacteristicChanged(characteristic, value);
+ } catch (BluetoothException e) {
+ Log.e(TAG, "Error in onCharacteristicChanged", e);
+ }
+ }
+ }
+
+ private class InternalScanCallback extends ScanCallback {
+
+ @Override
+ public void onScanFailed(int errorCode) {
+ String errorMessage;
+ switch (errorCode) {
+ case ScanCallback.SCAN_FAILED_ALREADY_STARTED:
+ errorMessage = "SCAN_FAILED_ALREADY_STARTED";
+ break;
+ case ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED:
+ errorMessage = "SCAN_FAILED_APPLICATION_REGISTRATION_FAILED";
+ break;
+ case ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED:
+ errorMessage = "SCAN_FAILED_FEATURE_UNSUPPORTED";
+ break;
+ case ScanCallback.SCAN_FAILED_INTERNAL_ERROR:
+ errorMessage = "SCAN_FAILED_INTERNAL_ERROR";
+ break;
+ default:
+ errorMessage = "Unknown error code - " + errorCode;
+ }
+ mBluetoothOperationExecutor.notifyFailure(
+ new Operation<BluetoothDevice>(OperationType.SCAN),
+ new BluetoothException("Scan failed: " + errorMessage));
+ }
+
+ @Override
+ public void onScanResult(int callbackType, ScanResult result) {
+ mBluetoothOperationExecutor.notifySuccess(
+ new Operation<BluetoothDevice>(OperationType.SCAN), result.getDevice());
+ }
+ }
+
+ /**
+ * Options for {@link #connect}.
+ */
+ @AutoValue
+ public abstract static class ConnectionOptions {
+
+ abstract boolean autoConnect();
+
+ abstract long connectionTimeoutMillis();
+
+ abstract Optional<Integer> connectionPriority();
+
+ abstract Optional<Integer> mtu();
+
+ /**
+ * Creates a builder of ConnectionOptions.
+ */
+ public static Builder builder() {
+ return new AutoValue_BluetoothGattHelper_ConnectionOptions.Builder()
+ .setAutoConnect(false)
+ .setConnectionTimeoutMillis(TimeUnit.SECONDS.toMillis(5));
+ }
+
+ /**
+ * Builder for {@link ConnectionOptions}.
+ */
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ /**
+ * See {@link android.bluetooth.BluetoothDevice#connectGatt}.
+ */
+ public abstract Builder setAutoConnect(boolean autoConnect);
+
+ /**
+ * See {@link android.bluetooth.BluetoothGatt#requestConnectionPriority(int)}.
+ */
+ public abstract Builder setConnectionPriority(int connectionPriority);
+
+ /**
+ * See {@link android.bluetooth.BluetoothGatt#requestMtu(int)}.
+ */
+ public abstract Builder setMtu(int mtu);
+
+ /**
+ * Sets the timeout for the GATT connection.
+ */
+ public abstract Builder setConnectionTimeoutMillis(long connectionTimeoutMillis);
+
+ /**
+ * Builds ConnectionOptions.
+ */
+ public abstract ConnectionOptions build();
+ }
+ }
+}
diff --git a/nearby/tests/src/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattConnectionTest.java b/nearby/tests/src/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattConnectionTest.java
new file mode 100644
index 0000000..9776836
--- /dev/null
+++ b/nearby/tests/src/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattConnectionTest.java
@@ -0,0 +1,796 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.gatt;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isA;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattService;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+
+import com.android.server.nearby.common.bluetooth.BluetoothConsts;
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+import com.android.server.nearby.common.bluetooth.BluetoothGattException;
+import com.android.server.nearby.common.bluetooth.ReservedUuids;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection.ChangeObserver;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattHelper.OperationType;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothDevice;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothGatt;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.Operation;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.SynchronousOperation;
+
+import junit.framework.TestCase;
+
+import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Captor;
+import org.mockito.Mock;
+
+import java.util.Arrays;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Unit tests for {@link BluetoothGattConnection}.
+ */
+public class BluetoothGattConnectionTest extends TestCase {
+
+ private static final UUID SERVICE_UUID = UUID.randomUUID();
+ private static final UUID CHARACTERISTIC_UUID = UUID.randomUUID();
+ private static final UUID DESCRIPTOR_UUID = UUID.randomUUID();
+ private static final byte[] DATA = "data".getBytes();
+ private static final int RSSI = -63;
+ private static final int CONNECTION_PRIORITY = 128;
+ private static final int MTU_REQUEST = 512;
+ private static final BluetoothGattHelper.ConnectionOptions CONNECTION_OPTIONS =
+ BluetoothGattHelper.ConnectionOptions.builder().build();
+
+ @Mock
+ private BluetoothGatt mMockBluetoothGatt;
+ @Mock
+ private BluetoothDevice mMockBluetoothDevice;
+ @Mock
+ private BluetoothOperationExecutor mMockBluetoothOperationExecutor;
+ @Mock
+ private BluetoothGattService mMockBluetoothGattService;
+ @Mock
+ private BluetoothGattService mMockBluetoothGattService2;
+ @Mock
+ private BluetoothGattCharacteristic mMockBluetoothGattCharacteristic;
+ @Mock
+ private BluetoothGattCharacteristic mMockBluetoothGattCharacteristic2;
+ @Mock
+ private BluetoothGattDescriptor mMockBluetoothGattDescriptor;
+ @Mock
+ private BluetoothGattConnection.CharacteristicChangeListener mMockCharChangeListener;
+ @Mock
+ private BluetoothGattConnection.ChangeObserver mMockChangeObserver;
+ @Mock
+ private BluetoothGattConnection.ConnectionCloseListener mMockConnectionCloseListener;
+
+ @Captor
+ private ArgumentCaptor<Operation<?>> mOperationCaptor;
+ @Captor
+ private ArgumentCaptor<SynchronousOperation<?>> mSynchronousOperationCaptor;
+ @Captor
+ private ArgumentCaptor<BluetoothGattCharacteristic> mCharacteristicCaptor;
+ @Captor
+ private ArgumentCaptor<BluetoothGattDescriptor> mDescriptorCaptor;
+
+ private BluetoothGattConnection mBluetoothGattConnection;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ initMocks(this);
+
+ mBluetoothGattConnection = new BluetoothGattConnection(
+ mMockBluetoothGatt,
+ mMockBluetoothOperationExecutor,
+ CONNECTION_OPTIONS);
+ mBluetoothGattConnection.onConnected();
+
+ when(mMockBluetoothGatt.getDevice()).thenReturn(mMockBluetoothDevice);
+ when(mMockBluetoothGatt.discoverServices()).thenReturn(true);
+ when(mMockBluetoothGatt.refresh()).thenReturn(true);
+ when(mMockBluetoothGatt.readCharacteristic(mMockBluetoothGattCharacteristic))
+ .thenReturn(true);
+ when(mMockBluetoothGatt
+ .writeCharacteristic(ArgumentMatchers.<BluetoothGattCharacteristic>any()))
+ .thenReturn(true);
+ when(mMockBluetoothGatt.readDescriptor(mMockBluetoothGattDescriptor)).thenReturn(true);
+ when(mMockBluetoothGatt.writeDescriptor(ArgumentMatchers.<BluetoothGattDescriptor>any()))
+ .thenReturn(true);
+ when(mMockBluetoothGatt.readRemoteRssi()).thenReturn(true);
+ when(mMockBluetoothGatt.requestConnectionPriority(CONNECTION_PRIORITY)).thenReturn(true);
+ when(mMockBluetoothGatt.requestMtu(MTU_REQUEST)).thenReturn(true);
+ when(mMockBluetoothGatt.getServices()).thenReturn(Arrays.asList(mMockBluetoothGattService));
+ when(mMockBluetoothGattService.getUuid()).thenReturn(SERVICE_UUID);
+ when(mMockBluetoothGattService.getCharacteristics())
+ .thenReturn(Arrays.asList(mMockBluetoothGattCharacteristic));
+ when(mMockBluetoothGattCharacteristic.getUuid()).thenReturn(CHARACTERISTIC_UUID);
+ when(mMockBluetoothGattCharacteristic.getProperties())
+ .thenReturn(
+ BluetoothGattCharacteristic.PROPERTY_NOTIFY
+ | BluetoothGattCharacteristic.PROPERTY_WRITE);
+ BluetoothGattDescriptor clientConfigDescriptor =
+ new BluetoothGattDescriptor(
+ ReservedUuids.Descriptors.CLIENT_CHARACTERISTIC_CONFIGURATION,
+ BluetoothGattDescriptor.PERMISSION_WRITE);
+ when(mMockBluetoothGattCharacteristic.getDescriptor(
+ ReservedUuids.Descriptors.CLIENT_CHARACTERISTIC_CONFIGURATION))
+ .thenReturn(clientConfigDescriptor);
+ when(mMockBluetoothGattCharacteristic.getDescriptors())
+ .thenReturn(Arrays.asList(mMockBluetoothGattDescriptor, clientConfigDescriptor));
+ when(mMockBluetoothGattDescriptor.getUuid()).thenReturn(DESCRIPTOR_UUID);
+ when(mMockBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
+ }
+
+ public void test_getDevice() {
+ BluetoothDevice result = mBluetoothGattConnection.getDevice();
+
+ assertThat(result).isEqualTo(mMockBluetoothDevice);
+ }
+
+ public void test_getConnectionOptions() {
+ BluetoothGattHelper.ConnectionOptions result = mBluetoothGattConnection
+ .getConnectionOptions();
+
+ assertThat(result).isSameInstanceAs(CONNECTION_OPTIONS);
+ }
+
+ public void test_isConnected_false_beforeConnection() {
+ mBluetoothGattConnection = new BluetoothGattConnection(
+ mMockBluetoothGatt,
+ mMockBluetoothOperationExecutor,
+ CONNECTION_OPTIONS);
+
+ boolean result = mBluetoothGattConnection.isConnected();
+
+ assertThat(result).isFalse();
+ }
+
+ public void test_isConnected_true_afterConnection() {
+ boolean result = mBluetoothGattConnection.isConnected();
+
+ assertThat(result).isTrue();
+ }
+
+ public void test_isConnected_false_afterDisconnection() {
+ mBluetoothGattConnection.onClosed();
+
+ boolean result = mBluetoothGattConnection.isConnected();
+
+ assertThat(result).isFalse();
+ }
+
+ public void test_getService_notDiscovered() throws Exception {
+ BluetoothGattService result = mBluetoothGattConnection.getService(SERVICE_UUID);
+ verify(mMockBluetoothOperationExecutor).execute(mSynchronousOperationCaptor.capture());
+ mSynchronousOperationCaptor.getValue().call();
+ verify(mMockBluetoothOperationExecutor)
+ .execute(
+ mOperationCaptor.capture(),
+ eq(BluetoothGattConnection.SLOW_OPERATION_TIMEOUT_MILLIS));
+ mOperationCaptor.getValue().run();
+
+ assertThat(result).isEqualTo(mMockBluetoothGattService);
+ verify(mMockBluetoothGatt).discoverServices();
+ }
+
+ public void test_getService_alreadyDiscovered() throws Exception {
+ mBluetoothGattConnection.getService(SERVICE_UUID);
+ verify(mMockBluetoothOperationExecutor).execute(mSynchronousOperationCaptor.capture());
+ mSynchronousOperationCaptor.getValue().call();
+ reset(mMockBluetoothOperationExecutor);
+
+ BluetoothGattService result = mBluetoothGattConnection.getService(SERVICE_UUID);
+
+ assertThat(result).isEqualTo(mMockBluetoothGattService);
+ // Verify that service discovery has been done only once
+ verifyNoMoreInteractions(mMockBluetoothOperationExecutor);
+ }
+
+ public void test_getService_notFound() throws Exception {
+ when(mMockBluetoothGatt.getServices()).thenReturn(Arrays.<BluetoothGattService>asList());
+
+ try {
+ mBluetoothGattConnection.getService(SERVICE_UUID);
+ fail("Expected BluetoothException");
+ } catch (BluetoothException expected) {
+ }
+ }
+
+ public void test_getService_moreThanOne() throws Exception {
+ when(mMockBluetoothGatt.getServices())
+ .thenReturn(Arrays.asList(mMockBluetoothGattService, mMockBluetoothGattService));
+
+ try {
+ mBluetoothGattConnection.getService(SERVICE_UUID);
+ fail("Expected BluetoothException");
+ } catch (BluetoothException expected) {
+ }
+ }
+
+ public void test_getCharacteristic() throws Exception {
+ BluetoothGattCharacteristic result =
+ mBluetoothGattConnection.getCharacteristic(SERVICE_UUID, CHARACTERISTIC_UUID);
+
+ assertThat(result).isEqualTo(mMockBluetoothGattCharacteristic);
+ }
+
+ public void test_getCharacteristic_notFound() throws Exception {
+ when(mMockBluetoothGattService.getCharacteristics())
+ .thenReturn(Arrays.<BluetoothGattCharacteristic>asList());
+
+ try {
+ mBluetoothGattConnection.getCharacteristic(SERVICE_UUID, CHARACTERISTIC_UUID);
+ fail("Expected BluetoothException");
+ } catch (BluetoothException expected) {
+ }
+ }
+
+ public void test_getCharacteristic_moreThanOne() throws Exception {
+ when(mMockBluetoothGattService.getCharacteristics())
+ .thenReturn(
+ Arrays.asList(mMockBluetoothGattCharacteristic,
+ mMockBluetoothGattCharacteristic));
+
+ try {
+ mBluetoothGattConnection.getCharacteristic(SERVICE_UUID, CHARACTERISTIC_UUID);
+ fail("Expected BluetoothException");
+ } catch (BluetoothException expected) {
+ }
+ }
+
+ public void test_getCharacteristic_moreThanOneService() throws Exception {
+ // Add a new service with the same service UUID as our existing one, but add a different
+ // characteristic inside of it.
+ when(mMockBluetoothGatt.getServices())
+ .thenReturn(Arrays.asList(mMockBluetoothGattService, mMockBluetoothGattService2));
+ when(mMockBluetoothGattService2.getUuid()).thenReturn(SERVICE_UUID);
+ when(mMockBluetoothGattService2.getCharacteristics())
+ .thenReturn(Arrays.asList(mMockBluetoothGattCharacteristic2));
+ when(mMockBluetoothGattCharacteristic2.getUuid())
+ .thenReturn(
+ new UUID(
+ CHARACTERISTIC_UUID.getMostSignificantBits(),
+ CHARACTERISTIC_UUID.getLeastSignificantBits() + 1));
+ when(mMockBluetoothGattCharacteristic2.getProperties())
+ .thenReturn(
+ BluetoothGattCharacteristic.PROPERTY_NOTIFY
+ | BluetoothGattCharacteristic.PROPERTY_WRITE);
+
+ mBluetoothGattConnection.getCharacteristic(SERVICE_UUID, CHARACTERISTIC_UUID);
+ }
+
+ public void test_getDescriptor() throws Exception {
+ when(mMockBluetoothGattCharacteristic.getDescriptors())
+ .thenReturn(Arrays.asList(mMockBluetoothGattDescriptor));
+
+ BluetoothGattDescriptor result =
+ mBluetoothGattConnection
+ .getDescriptor(SERVICE_UUID, CHARACTERISTIC_UUID, DESCRIPTOR_UUID);
+
+ assertThat(result).isEqualTo(mMockBluetoothGattDescriptor);
+ }
+
+ public void test_getDescriptor_notFound() throws Exception {
+ when(mMockBluetoothGattCharacteristic.getDescriptors())
+ .thenReturn(Arrays.<BluetoothGattDescriptor>asList());
+
+ try {
+ mBluetoothGattConnection
+ .getDescriptor(SERVICE_UUID, CHARACTERISTIC_UUID, DESCRIPTOR_UUID);
+ fail("Expected BluetoothException");
+ } catch (BluetoothException expected) {
+ }
+ }
+
+ public void test_getDescriptor_moreThanOne() throws Exception {
+ when(mMockBluetoothGattCharacteristic.getDescriptors())
+ .thenReturn(
+ Arrays.asList(mMockBluetoothGattDescriptor, mMockBluetoothGattDescriptor));
+
+ try {
+ mBluetoothGattConnection
+ .getDescriptor(SERVICE_UUID, CHARACTERISTIC_UUID, DESCRIPTOR_UUID);
+ fail("Expected BluetoothException");
+ } catch (BluetoothException expected) {
+ }
+ }
+
+ public void test_discoverServices() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new SynchronousOperation<>(
+ mMockBluetoothOperationExecutor, OperationType.NOTIFICATION_CHANGE)))
+ .thenReturn(mMockChangeObserver);
+
+ mBluetoothGattConnection.discoverServices();
+
+ verify(mMockBluetoothOperationExecutor).execute(mSynchronousOperationCaptor.capture());
+ mSynchronousOperationCaptor.getValue().call();
+ verify(mMockBluetoothOperationExecutor)
+ .execute(
+ mOperationCaptor.capture(),
+ eq(BluetoothGattConnection.SLOW_OPERATION_TIMEOUT_MILLIS));
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGatt).discoverServices();
+ verify(mMockBluetoothGatt, never()).refresh();
+ }
+
+ public void test_discoverServices_serviceChange() throws Exception {
+ when(mMockBluetoothGatt.getService(ReservedUuids.Services.GENERIC_ATTRIBUTE))
+ .thenReturn(mMockBluetoothGattService);
+ when(mMockBluetoothGattService
+ .getCharacteristic(ReservedUuids.Characteristics.SERVICE_CHANGE))
+ .thenReturn(mMockBluetoothGattCharacteristic);
+
+ mBluetoothGattConnection.discoverServices();
+
+ verify(mMockBluetoothOperationExecutor).execute(mSynchronousOperationCaptor.capture());
+ mSynchronousOperationCaptor.getValue().call();
+ verify(mMockBluetoothOperationExecutor, times(2))
+ .execute(
+ mOperationCaptor.capture(),
+ eq(BluetoothGattConnection.SLOW_OPERATION_TIMEOUT_MILLIS));
+ verify(mMockBluetoothGatt).refresh();
+ }
+
+ public void test_discoverServices_SelfDefinedServiceDynamic() throws Exception {
+ when(mMockBluetoothGatt.getService(BluetoothConsts.SERVICE_DYNAMIC_SERVICE))
+ .thenReturn(mMockBluetoothGattService);
+ when(mMockBluetoothGattService
+ .getCharacteristic(BluetoothConsts.SERVICE_DYNAMIC_CHARACTERISTIC))
+ .thenReturn(mMockBluetoothGattCharacteristic);
+
+ mBluetoothGattConnection.discoverServices();
+
+ verify(mMockBluetoothOperationExecutor).execute(mSynchronousOperationCaptor.capture());
+ mSynchronousOperationCaptor.getValue().call();
+ verify(mMockBluetoothOperationExecutor, times(2))
+ .execute(
+ mOperationCaptor.capture(),
+ eq(BluetoothGattConnection.SLOW_OPERATION_TIMEOUT_MILLIS));
+ verify(mMockBluetoothGatt).refresh();
+ }
+
+ public void test_discoverServices_refreshWithGattErrorOnMncAbove() throws Exception {
+ if (VERSION.SDK_INT <= VERSION_CODES.LOLLIPOP_MR1) {
+ return;
+ }
+ mBluetoothGattConnection.discoverServices();
+ verify(mMockBluetoothOperationExecutor).execute(mSynchronousOperationCaptor.capture());
+
+ doThrow(new BluetoothGattException("fail", BluetoothGattConnection.GATT_ERROR))
+ .doReturn(null)
+ .when(mMockBluetoothOperationExecutor)
+ .execute(isA(Operation.class),
+ eq(BluetoothGattConnection.SLOW_OPERATION_TIMEOUT_MILLIS));
+ mSynchronousOperationCaptor.getValue().call();
+ verify(mMockBluetoothOperationExecutor, times(2))
+ .execute(
+ mOperationCaptor.capture(),
+ eq(BluetoothGattConnection.SLOW_OPERATION_TIMEOUT_MILLIS));
+ verify(mMockBluetoothGatt).refresh();
+ }
+
+ public void test_discoverServices_refreshWithGattInternalErrorOnMncAbove() throws Exception {
+ if (VERSION.SDK_INT <= VERSION_CODES.LOLLIPOP_MR1) {
+ return;
+ }
+ mBluetoothGattConnection.discoverServices();
+ verify(mMockBluetoothOperationExecutor).execute(mSynchronousOperationCaptor.capture());
+
+ doThrow(new BluetoothGattException("fail", BluetoothGattConnection.GATT_INTERNAL_ERROR))
+ .doReturn(null)
+ .when(mMockBluetoothOperationExecutor)
+ .execute(isA(Operation.class),
+ eq(BluetoothGattConnection.SLOW_OPERATION_TIMEOUT_MILLIS));
+ mSynchronousOperationCaptor.getValue().call();
+ verify(mMockBluetoothOperationExecutor, times(2))
+ .execute(
+ mOperationCaptor.capture(),
+ eq(BluetoothGattConnection.SLOW_OPERATION_TIMEOUT_MILLIS));
+ verify(mMockBluetoothGatt).refresh();
+ }
+
+ public void test_discoverServices_dynamicServices_notBonded() throws Exception {
+ when(mMockBluetoothGatt.getService(ReservedUuids.Services.GENERIC_ATTRIBUTE))
+ .thenReturn(mMockBluetoothGattService);
+ when(mMockBluetoothGattService
+ .getCharacteristic(ReservedUuids.Characteristics.SERVICE_CHANGE))
+ .thenReturn(mMockBluetoothGattCharacteristic);
+ when(mMockBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_NONE);
+
+ mBluetoothGattConnection.discoverServices();
+
+ verify(mMockBluetoothGatt, never()).refresh();
+ }
+
+ public void test_readCharacteristic() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new Operation<byte[]>(
+ OperationType.READ_CHARACTERISTIC,
+ mMockBluetoothGatt,
+ mMockBluetoothGattCharacteristic),
+ BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS))
+ .thenReturn(DATA);
+
+ byte[] result = mBluetoothGattConnection
+ .readCharacteristic(mMockBluetoothGattCharacteristic);
+
+ assertThat(result).isEqualTo(DATA);
+ verify(mMockBluetoothOperationExecutor)
+ .executeNonnull(mOperationCaptor.capture(), anyLong());
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGatt).readCharacteristic(mMockBluetoothGattCharacteristic);
+ }
+
+ public void test_readCharacteristic_by_uuid() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new Operation<byte[]>(
+ OperationType.READ_CHARACTERISTIC,
+ mMockBluetoothGatt,
+ mMockBluetoothGattCharacteristic),
+ BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS))
+ .thenReturn(DATA);
+
+ byte[] result = mBluetoothGattConnection
+ .readCharacteristic(SERVICE_UUID, CHARACTERISTIC_UUID);
+
+ assertThat(result).isEqualTo(DATA);
+ verify(mMockBluetoothOperationExecutor)
+ .executeNonnull(mOperationCaptor.capture(), anyLong());
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGatt).readCharacteristic(mMockBluetoothGattCharacteristic);
+ }
+
+ public void test_writeCharacteristic() throws Exception {
+ BluetoothGattCharacteristic characteristic =
+ new BluetoothGattCharacteristic(
+ CHARACTERISTIC_UUID, BluetoothGattCharacteristic.PROPERTY_WRITE, 0);
+ mBluetoothGattConnection.writeCharacteristic(characteristic, DATA);
+
+ verify(mMockBluetoothOperationExecutor)
+ .execute(mOperationCaptor.capture(),
+ eq(BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS));
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGatt).writeCharacteristic(mCharacteristicCaptor.capture());
+ BluetoothGattCharacteristic writtenCharacteristic = mCharacteristicCaptor.getValue();
+ assertThat(writtenCharacteristic.getValue()).isEqualTo(DATA);
+ assertThat(writtenCharacteristic.getUuid()).isEqualTo(CHARACTERISTIC_UUID);
+ assertThat(writtenCharacteristic).isNotEqualTo(characteristic);
+ }
+
+ public void test_writeCharacteristic_by_uuid() throws Exception {
+ mBluetoothGattConnection.writeCharacteristic(SERVICE_UUID, CHARACTERISTIC_UUID, DATA);
+
+ verify(mMockBluetoothOperationExecutor)
+ .execute(mOperationCaptor.capture(),
+ eq(BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS));
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGatt).writeCharacteristic(mCharacteristicCaptor.capture());
+ BluetoothGattCharacteristic writtenCharacteristic = mCharacteristicCaptor.getValue();
+ assertThat(writtenCharacteristic.getValue()).isEqualTo(DATA);
+ assertThat(writtenCharacteristic.getUuid()).isEqualTo(CHARACTERISTIC_UUID);
+ }
+
+ public void test_readDescriptor() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new Operation<byte[]>(
+ OperationType.READ_DESCRIPTOR, mMockBluetoothGatt,
+ mMockBluetoothGattDescriptor),
+ BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS))
+ .thenReturn(DATA);
+
+ byte[] result = mBluetoothGattConnection.readDescriptor(mMockBluetoothGattDescriptor);
+
+ assertThat(result).isEqualTo(DATA);
+ verify(mMockBluetoothOperationExecutor)
+ .executeNonnull(mOperationCaptor.capture(), anyLong());
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGatt).readDescriptor(mMockBluetoothGattDescriptor);
+ }
+
+ public void test_readDescriptor_by_uuid() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new Operation<byte[]>(
+ OperationType.READ_DESCRIPTOR, mMockBluetoothGatt,
+ mMockBluetoothGattDescriptor),
+ BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS))
+ .thenReturn(DATA);
+
+ byte[] result =
+ mBluetoothGattConnection
+ .readDescriptor(SERVICE_UUID, CHARACTERISTIC_UUID, DESCRIPTOR_UUID);
+
+ assertThat(result).isEqualTo(DATA);
+ verify(mMockBluetoothOperationExecutor)
+ .executeNonnull(mOperationCaptor.capture(), anyLong());
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGatt).readDescriptor(mMockBluetoothGattDescriptor);
+ }
+
+ public void test_writeDescriptor() throws Exception {
+ BluetoothGattDescriptor descriptor = new BluetoothGattDescriptor(DESCRIPTOR_UUID, 0);
+ mBluetoothGattConnection.writeDescriptor(descriptor, DATA);
+
+ verify(mMockBluetoothOperationExecutor)
+ .execute(mOperationCaptor.capture(),
+ eq(BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS));
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGatt).writeDescriptor(mDescriptorCaptor.capture());
+ BluetoothGattDescriptor writtenDescriptor = mDescriptorCaptor.getValue();
+ assertThat(writtenDescriptor.getValue()).isEqualTo(DATA);
+ assertThat(writtenDescriptor.getUuid()).isEqualTo(DESCRIPTOR_UUID);
+ assertThat(writtenDescriptor).isNotEqualTo(descriptor);
+ }
+
+ public void test_writeDescriptor_by_uuid() throws Exception {
+ mBluetoothGattConnection.writeDescriptor(
+ SERVICE_UUID, CHARACTERISTIC_UUID, DESCRIPTOR_UUID, DATA);
+
+ verify(mMockBluetoothOperationExecutor)
+ .execute(mOperationCaptor.capture(),
+ eq(BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS));
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGatt).writeDescriptor(mDescriptorCaptor.capture());
+ BluetoothGattDescriptor writtenDescriptor = mDescriptorCaptor.getValue();
+ assertThat(writtenDescriptor.getValue()).isEqualTo(DATA);
+ assertThat(writtenDescriptor.getUuid()).isEqualTo(DESCRIPTOR_UUID);
+ }
+
+ public void test_readRemoteRssi() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new Operation<Integer>(OperationType.READ_RSSI, mMockBluetoothGatt),
+ BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS))
+ .thenReturn(RSSI);
+
+ int result = mBluetoothGattConnection.readRemoteRssi();
+
+ assertThat(result).isEqualTo(RSSI);
+ verify(mMockBluetoothOperationExecutor)
+ .executeNonnull(
+ mOperationCaptor.capture(),
+ eq(BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS));
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGatt).readRemoteRssi();
+ }
+
+ public void test_getMaxDataPacketSize() throws Exception {
+ int result = mBluetoothGattConnection.getMaxDataPacketSize();
+
+ assertThat(result).isEqualTo(mBluetoothGattConnection.getMtu() - 3);
+ }
+
+ public void testSetNotificationEnabled_indication_enable() throws Exception {
+ when(mMockBluetoothGattCharacteristic.getProperties())
+ .thenReturn(BluetoothGattCharacteristic.PROPERTY_INDICATE);
+
+ mBluetoothGattConnection.setNotificationEnabled(mMockBluetoothGattCharacteristic, true);
+
+ verify(mMockBluetoothGatt)
+ .setCharacteristicNotification(mMockBluetoothGattCharacteristic, true);
+ verify(mMockBluetoothOperationExecutor).execute(mOperationCaptor.capture(), anyLong());
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGatt).writeDescriptor(mDescriptorCaptor.capture());
+ BluetoothGattDescriptor writtenDescriptor = mDescriptorCaptor.getValue();
+ assertThat(writtenDescriptor.getValue())
+ .isEqualTo(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
+ assertThat(writtenDescriptor.getUuid())
+ .isEqualTo(ReservedUuids.Descriptors.CLIENT_CHARACTERISTIC_CONFIGURATION);
+ }
+
+ public void test_getNotificationEnabled_notification_enable() throws Exception {
+ mBluetoothGattConnection.setNotificationEnabled(mMockBluetoothGattCharacteristic, true);
+
+ verify(mMockBluetoothGatt)
+ .setCharacteristicNotification(mMockBluetoothGattCharacteristic, true);
+ verify(mMockBluetoothOperationExecutor).execute(mOperationCaptor.capture(), anyLong());
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGatt).writeDescriptor(mDescriptorCaptor.capture());
+ BluetoothGattDescriptor writtenDescriptor = mDescriptorCaptor.getValue();
+ assertThat(writtenDescriptor.getValue())
+ .isEqualTo(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
+ assertThat(writtenDescriptor.getUuid())
+ .isEqualTo(ReservedUuids.Descriptors.CLIENT_CHARACTERISTIC_CONFIGURATION);
+ }
+
+ public void test_setNotificationEnabled_indication_disable() throws Exception {
+ when(mMockBluetoothGattCharacteristic.getProperties())
+ .thenReturn(BluetoothGattCharacteristic.PROPERTY_INDICATE);
+
+ mBluetoothGattConnection.setNotificationEnabled(mMockBluetoothGattCharacteristic, false);
+
+ verify(mMockBluetoothGatt)
+ .setCharacteristicNotification(mMockBluetoothGattCharacteristic, false);
+ verify(mMockBluetoothOperationExecutor).execute(mOperationCaptor.capture(), anyLong());
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGatt).writeDescriptor(mDescriptorCaptor.capture());
+ BluetoothGattDescriptor writtenDescriptor = mDescriptorCaptor.getValue();
+ assertThat(writtenDescriptor.getValue())
+ .isEqualTo(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
+ assertThat(writtenDescriptor.getUuid())
+ .isEqualTo(ReservedUuids.Descriptors.CLIENT_CHARACTERISTIC_CONFIGURATION);
+ }
+
+ public void test_setNotificationEnabled_notification_disable() throws Exception {
+ mBluetoothGattConnection.setNotificationEnabled(mMockBluetoothGattCharacteristic, false);
+
+ verify(mMockBluetoothGatt)
+ .setCharacteristicNotification(mMockBluetoothGattCharacteristic, false);
+ verify(mMockBluetoothOperationExecutor).execute(mOperationCaptor.capture(), anyLong());
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGatt).writeDescriptor(mDescriptorCaptor.capture());
+ BluetoothGattDescriptor writtenDescriptor = mDescriptorCaptor.getValue();
+ assertThat(writtenDescriptor.getValue())
+ .isEqualTo(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
+ assertThat(writtenDescriptor.getUuid())
+ .isEqualTo(ReservedUuids.Descriptors.CLIENT_CHARACTERISTIC_CONFIGURATION);
+ }
+
+ public void test_setNotificationEnabled_failure() throws Exception {
+ when(mMockBluetoothGattCharacteristic.getProperties())
+ .thenReturn(BluetoothGattCharacteristic.PROPERTY_READ);
+
+ try {
+ mBluetoothGattConnection.setNotificationEnabled(mMockBluetoothGattCharacteristic, true);
+ fail("BluetoothException was expected");
+ } catch (BluetoothException expected) {
+ }
+ }
+
+ public void test_enableNotification_Uuid() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new SynchronousOperation<>(
+ mMockBluetoothOperationExecutor,
+ OperationType.NOTIFICATION_CHANGE,
+ mMockBluetoothGattCharacteristic)))
+ .thenReturn(mMockChangeObserver);
+ mBluetoothGattConnection.enableNotification(SERVICE_UUID, CHARACTERISTIC_UUID);
+
+ verify(mMockBluetoothOperationExecutor)
+ .executeNonnull(mSynchronousOperationCaptor.capture());
+ ((ChangeObserver) mSynchronousOperationCaptor.getValue().call())
+ .setListener(mMockCharChangeListener);
+ mBluetoothGattConnection.onCharacteristicChanged(mMockBluetoothGattCharacteristic, DATA);
+ verify(mMockCharChangeListener).onValueChange(DATA);
+ }
+
+ public void test_enableNotification() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new SynchronousOperation<>(
+ mMockBluetoothOperationExecutor,
+ OperationType.NOTIFICATION_CHANGE,
+ mMockBluetoothGattCharacteristic)))
+ .thenReturn(mMockChangeObserver);
+ mBluetoothGattConnection.enableNotification(mMockBluetoothGattCharacteristic);
+
+ verify(mMockBluetoothOperationExecutor)
+ .executeNonnull(mSynchronousOperationCaptor.capture());
+ ((ChangeObserver) mSynchronousOperationCaptor.getValue().call())
+ .setListener(mMockCharChangeListener);
+
+ mBluetoothGattConnection.onCharacteristicChanged(mMockBluetoothGattCharacteristic, DATA);
+
+ verify(mMockCharChangeListener).onValueChange(DATA);
+ }
+
+ public void test_enableNotification_observe() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new SynchronousOperation<>(
+ mMockBluetoothOperationExecutor,
+ OperationType.NOTIFICATION_CHANGE,
+ mMockBluetoothGattCharacteristic)))
+ .thenReturn(mMockChangeObserver);
+ mBluetoothGattConnection.enableNotification(mMockBluetoothGattCharacteristic);
+
+ verify(mMockBluetoothOperationExecutor)
+ .executeNonnull(mSynchronousOperationCaptor.capture());
+ ChangeObserver changeObserver = (ChangeObserver) mSynchronousOperationCaptor.getValue()
+ .call();
+ mBluetoothGattConnection.onCharacteristicChanged(mMockBluetoothGattCharacteristic, DATA);
+ assertThat(changeObserver.waitForUpdate(TimeUnit.SECONDS.toMillis(1))).isEqualTo(DATA);
+ }
+
+ public void test_disableNotification_Uuid() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new SynchronousOperation<>(
+ OperationType.NOTIFICATION_CHANGE, mMockBluetoothGattCharacteristic)))
+ .thenReturn(mMockChangeObserver);
+ mBluetoothGattConnection
+ .enableNotification(SERVICE_UUID, CHARACTERISTIC_UUID)
+ .setListener(mMockCharChangeListener);
+
+ mBluetoothGattConnection.disableNotification(SERVICE_UUID, CHARACTERISTIC_UUID);
+
+ mBluetoothGattConnection.onCharacteristicChanged(mMockBluetoothGattCharacteristic, DATA);
+ verify(mMockCharChangeListener, never()).onValueChange(DATA);
+ }
+
+ public void test_disableNotification() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new SynchronousOperation<ChangeObserver>(
+ OperationType.NOTIFICATION_CHANGE, mMockBluetoothGattCharacteristic)))
+ .thenReturn(mMockChangeObserver);
+ mBluetoothGattConnection
+ .enableNotification(mMockBluetoothGattCharacteristic)
+ .setListener(mMockCharChangeListener);
+ verify(mMockBluetoothOperationExecutor)
+ .executeNonnull(mSynchronousOperationCaptor.capture());
+ mSynchronousOperationCaptor.getValue().call();
+
+ mBluetoothGattConnection.disableNotification(mMockBluetoothGattCharacteristic);
+ verify(mMockBluetoothOperationExecutor).execute(mSynchronousOperationCaptor.capture());
+ mSynchronousOperationCaptor.getValue().call();
+
+ mBluetoothGattConnection.onCharacteristicChanged(mMockBluetoothGattCharacteristic, DATA);
+ verify(mMockCharChangeListener, never()).onValueChange(DATA);
+ }
+
+ public void test_addCloseListener() throws Exception {
+ mBluetoothGattConnection.addCloseListener(mMockConnectionCloseListener);
+
+ mBluetoothGattConnection.onClosed();
+ verify(mMockConnectionCloseListener).onClose();
+ }
+
+ public void test_removeCloseListener() throws Exception {
+ mBluetoothGattConnection.addCloseListener(mMockConnectionCloseListener);
+
+ mBluetoothGattConnection.removeCloseListener(mMockConnectionCloseListener);
+
+ mBluetoothGattConnection.onClosed();
+ verify(mMockConnectionCloseListener, never()).onClose();
+ }
+
+ public void test_close() throws Exception {
+ mBluetoothGattConnection.close();
+
+ verify(mMockBluetoothOperationExecutor)
+ .execute(mOperationCaptor.capture(),
+ eq(BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS));
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGatt).disconnect();
+ verify(mMockBluetoothGatt).close();
+ }
+
+ public void test_onClosed() throws Exception {
+ mBluetoothGattConnection.onClosed();
+
+ verify(mMockBluetoothOperationExecutor, never())
+ .execute(mOperationCaptor.capture(), anyLong());
+ verify(mMockBluetoothGatt).close();
+ }
+}
diff --git a/nearby/tests/src/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattHelperTest.java b/nearby/tests/src/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattHelperTest.java
new file mode 100644
index 0000000..a60f0b2
--- /dev/null
+++ b/nearby/tests/src/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattHelperTest.java
@@ -0,0 +1,613 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.gatt;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isA;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanSettings;
+import android.content.Context;
+import android.os.ParcelUuid;
+import android.test.mock.MockContext;
+
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattHelper.ConnectionOptions;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattHelper.OperationType;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothAdapter;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothDevice;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothGatt;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le.BluetoothLeScanner;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le.ScanCallback;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le.ScanResult;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.BluetoothOperationTimeoutException;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.Operation;
+
+import junit.framework.TestCase;
+
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+
+import java.util.Arrays;
+import java.util.UUID;
+
+/**
+ * Unit tests for {@link BluetoothGattHelper}.
+ */
+public class BluetoothGattHelperTest extends TestCase {
+
+ private static final UUID SERVICE_UUID = UUID.randomUUID();
+ private static final int GATT_STATUS = 1234;
+ private static final Operation<BluetoothDevice> SCANNING_OPERATION =
+ new Operation<BluetoothDevice>(OperationType.SCAN);
+ private static final byte[] CHARACTERISTIC_VALUE = "characteristic_value".getBytes();
+ private static final byte[] DESCRIPTOR_VALUE = "descriptor_value".getBytes();
+ private static final int RSSI = -63;
+ private static final int MTU = 50;
+ private static final long CONNECT_TIMEOUT_MILLIS = 5000;
+
+ private Context mMockApplicationContext = new MockContext();
+ @Mock
+ private BluetoothAdapter mMockBluetoothAdapter;
+ @Mock
+ private BluetoothLeScanner mMockBluetoothLeScanner;
+ @Mock
+ private BluetoothOperationExecutor mMockBluetoothOperationExecutor;
+ @Mock
+ private BluetoothDevice mMockBluetoothDevice;
+ @Mock
+ private BluetoothGattConnection mMockBluetoothGattConnection;
+ @Mock
+ private BluetoothGatt mMockBluetoothGatt;
+ @Mock
+ private BluetoothGattCharacteristic mMockBluetoothGattCharacteristic;
+ @Mock
+ private BluetoothGattDescriptor mMockBluetoothGattDescriptor;
+ @Mock
+ private ScanResult mMockScanResult;
+
+ @Captor
+ private ArgumentCaptor<Operation<?>> mOperationCaptor;
+ @Captor
+ private ArgumentCaptor<ScanSettings> mScanSettingsCaptor;
+
+ private BluetoothGattHelper mBluetoothGattHelper;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ initMocks(this);
+
+ mBluetoothGattHelper = new BluetoothGattHelper(
+ mMockApplicationContext,
+ mMockBluetoothAdapter,
+ mMockBluetoothOperationExecutor);
+
+ when(mMockBluetoothAdapter.getBluetoothLeScanner()).thenReturn(mMockBluetoothLeScanner);
+ when(mMockBluetoothOperationExecutor.executeNonnull(SCANNING_OPERATION,
+ BluetoothGattHelper.LOW_LATENCY_SCAN_MILLIS)).thenReturn(mMockBluetoothDevice);
+ when(mMockBluetoothOperationExecutor.executeNonnull(SCANNING_OPERATION)).thenReturn(
+ mMockBluetoothDevice);
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new Operation<BluetoothGattConnection>(OperationType.CONNECT, mMockBluetoothDevice),
+ CONNECT_TIMEOUT_MILLIS))
+ .thenReturn(mMockBluetoothGattConnection);
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new Operation<BluetoothGattConnection>(OperationType.CONNECT,
+ mMockBluetoothDevice)))
+ .thenReturn(mMockBluetoothGattConnection);
+ when(mMockBluetoothGattCharacteristic.getValue()).thenReturn(CHARACTERISTIC_VALUE);
+ when(mMockBluetoothGattDescriptor.getValue()).thenReturn(DESCRIPTOR_VALUE);
+ when(mMockScanResult.getDevice()).thenReturn(mMockBluetoothDevice);
+ when(mMockBluetoothGatt.getDevice()).thenReturn(mMockBluetoothDevice);
+ when(mMockBluetoothDevice.connectGatt(eq(mMockApplicationContext), anyBoolean(),
+ eq(mBluetoothGattHelper.mBluetoothGattCallback))).thenReturn(mMockBluetoothGatt);
+ when(mMockBluetoothDevice.connectGatt(eq(mMockApplicationContext), anyBoolean(),
+ eq(mBluetoothGattHelper.mBluetoothGattCallback), anyInt()))
+ .thenReturn(mMockBluetoothGatt);
+ when(mMockBluetoothGattConnection.getConnectionOptions())
+ .thenReturn(ConnectionOptions.builder().build());
+ }
+
+ public void test_autoConnect_uuid_success_lowLatency() throws Exception {
+ BluetoothGattConnection result = mBluetoothGattHelper.autoConnect(SERVICE_UUID);
+
+ assertThat(result).isEqualTo(mMockBluetoothGattConnection);
+ verify(mMockBluetoothOperationExecutor, atLeastOnce())
+ .executeNonnull(mOperationCaptor.capture(),
+ anyLong());
+ for (Operation<?> operation : mOperationCaptor.getAllValues()) {
+ operation.run();
+ }
+ verify(mMockBluetoothLeScanner).startScan(eq(Arrays.asList(
+ new ScanFilter.Builder().setServiceUuid(new ParcelUuid(SERVICE_UUID)).build())),
+ mScanSettingsCaptor.capture(), eq(mBluetoothGattHelper.mScanCallback));
+ assertThat(mScanSettingsCaptor.getValue().getScanMode()).isEqualTo(
+ ScanSettings.SCAN_MODE_LOW_LATENCY);
+ verify(mMockBluetoothLeScanner).stopScan(mBluetoothGattHelper.mScanCallback);
+ verifyNoMoreInteractions(mMockBluetoothLeScanner);
+ }
+
+ public void test_autoConnect_uuid_success_lowPower() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(SCANNING_OPERATION,
+ BluetoothGattHelper.LOW_LATENCY_SCAN_MILLIS)).thenThrow(
+ new BluetoothOperationTimeoutException("Timeout"));
+
+ BluetoothGattConnection result = mBluetoothGattHelper.autoConnect(SERVICE_UUID);
+
+ assertThat(result).isEqualTo(mMockBluetoothGattConnection);
+ verify(mMockBluetoothOperationExecutor).executeNonnull(mOperationCaptor.capture());
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothLeScanner).startScan(eq(Arrays.asList(
+ new ScanFilter.Builder().setServiceUuid(new ParcelUuid(SERVICE_UUID)).build())),
+ mScanSettingsCaptor.capture(), eq(mBluetoothGattHelper.mScanCallback));
+ assertThat(mScanSettingsCaptor.getValue().getScanMode()).isEqualTo(
+ ScanSettings.SCAN_MODE_LOW_POWER);
+ verify(mMockBluetoothLeScanner, times(2)).stopScan(mBluetoothGattHelper.mScanCallback);
+ verifyNoMoreInteractions(mMockBluetoothLeScanner);
+ }
+
+ public void test_autoConnect_uuid_success_afterRetry() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new Operation<BluetoothGattConnection>(OperationType.CONNECT, mMockBluetoothDevice),
+ BluetoothGattHelper.LOW_LATENCY_SCAN_MILLIS))
+ .thenThrow(new BluetoothException("first attempt fails!"))
+ .thenReturn(mMockBluetoothGattConnection);
+
+ BluetoothGattConnection result = mBluetoothGattHelper.autoConnect(SERVICE_UUID);
+
+ assertThat(result).isEqualTo(mMockBluetoothGattConnection);
+ }
+
+ public void test_autoConnect_uuid_failure_scanning() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(SCANNING_OPERATION,
+ BluetoothGattHelper.LOW_LATENCY_SCAN_MILLIS)).thenThrow(
+ new BluetoothException("Scanning failed"));
+
+ try {
+ mBluetoothGattHelper.autoConnect(SERVICE_UUID);
+ fail("BluetoothException expected");
+ } catch (BluetoothException e) {
+ // expected
+ }
+ }
+
+ public void test_autoConnect_uuid_failure_connecting() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new Operation<BluetoothGattConnection>(OperationType.CONNECT, mMockBluetoothDevice),
+ CONNECT_TIMEOUT_MILLIS))
+ .thenThrow(new BluetoothException("Connect failed"));
+
+ try {
+ mBluetoothGattHelper.autoConnect(SERVICE_UUID);
+ fail("BluetoothException expected");
+ } catch (BluetoothException e) {
+ // expected
+ }
+ verify(mMockBluetoothOperationExecutor, times(3))
+ .executeNonnull(
+ new Operation<BluetoothGattConnection>(OperationType.CONNECT,
+ mMockBluetoothDevice),
+ CONNECT_TIMEOUT_MILLIS);
+ }
+
+ public void test_autoConnect_uuid_failure_noBle() throws Exception {
+ when(mMockBluetoothAdapter.getBluetoothLeScanner()).thenReturn(null);
+
+ try {
+ mBluetoothGattHelper.autoConnect(SERVICE_UUID);
+ fail("BluetoothException expected");
+ } catch (BluetoothException e) {
+ // expected
+ }
+ }
+
+ public void test_connect() throws Exception {
+ BluetoothGattConnection result = mBluetoothGattHelper.connect(mMockBluetoothDevice);
+
+ assertThat(result).isEqualTo(mMockBluetoothGattConnection);
+ verify(mMockBluetoothOperationExecutor)
+ .executeNonnull(mOperationCaptor.capture(), eq(CONNECT_TIMEOUT_MILLIS));
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothDevice).connectGatt(mMockApplicationContext, false,
+ mBluetoothGattHelper.mBluetoothGattCallback,
+ android.bluetooth.BluetoothDevice.TRANSPORT_LE);
+ assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGatt).getDevice())
+ .isEqualTo(mMockBluetoothDevice);
+ }
+
+ public void test_connect_withOptionAutoConnect_success() throws Exception {
+ BluetoothGattConnection result = mBluetoothGattHelper
+ .connect(
+ mMockBluetoothDevice,
+ ConnectionOptions.builder()
+ .setAutoConnect(true)
+ .build());
+
+ assertThat(result).isEqualTo(mMockBluetoothGattConnection);
+ verify(mMockBluetoothOperationExecutor).executeNonnull(mOperationCaptor.capture());
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothDevice).connectGatt(mMockApplicationContext, true,
+ mBluetoothGattHelper.mBluetoothGattCallback,
+ android.bluetooth.BluetoothDevice.TRANSPORT_LE);
+ assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGatt).getConnectionOptions())
+ .isEqualTo(ConnectionOptions.builder()
+ .setAutoConnect(true)
+ .build());
+ }
+
+ public void test_connect_withOptionAutoConnect_failure_nullResult() throws Exception {
+ when(mMockBluetoothDevice.connectGatt(eq(mMockApplicationContext), anyBoolean(),
+ eq(mBluetoothGattHelper.mBluetoothGattCallback),
+ eq(android.bluetooth.BluetoothDevice.TRANSPORT_LE))).thenReturn(null);
+
+ try {
+ mBluetoothGattHelper.connect(
+ mMockBluetoothDevice,
+ ConnectionOptions.builder()
+ .setAutoConnect(true)
+ .build());
+ verify(mMockBluetoothOperationExecutor).executeNonnull(mOperationCaptor.capture());
+ mOperationCaptor.getValue().run();
+ fail("BluetoothException expected");
+ } catch (BluetoothException e) {
+ // expected
+ }
+ }
+
+ public void test_connect_withOptionRequestConnectionPriority_success() throws Exception {
+ // Operation succeeds on the 3rd try.
+ when(mMockBluetoothGatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH))
+ .thenReturn(false)
+ .thenReturn(false)
+ .thenReturn(true);
+
+ BluetoothGattConnection result = mBluetoothGattHelper
+ .connect(
+ mMockBluetoothDevice,
+ ConnectionOptions.builder()
+ .setConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)
+ .build());
+
+ assertThat(result).isEqualTo(mMockBluetoothGattConnection);
+ verify(mMockBluetoothOperationExecutor)
+ .executeNonnull(mOperationCaptor.capture(), eq(CONNECT_TIMEOUT_MILLIS));
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothDevice).connectGatt(mMockApplicationContext, false,
+ mBluetoothGattHelper.mBluetoothGattCallback,
+ android.bluetooth.BluetoothDevice.TRANSPORT_LE);
+ assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGatt).getConnectionOptions())
+ .isEqualTo(ConnectionOptions.builder()
+ .setConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)
+ .build());
+ verify(mMockBluetoothGatt, times(3))
+ .requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH);
+ }
+
+ public void test_connect_cancel() throws Exception {
+ mBluetoothGattHelper.connect(mMockBluetoothDevice);
+
+ verify(mMockBluetoothOperationExecutor)
+ .executeNonnull(mOperationCaptor.capture(), eq(CONNECT_TIMEOUT_MILLIS));
+ Operation<?> operation = mOperationCaptor.getValue();
+ operation.run();
+ operation.cancel();
+
+ verify(mMockBluetoothGatt).disconnect();
+ verify(mMockBluetoothGatt).close();
+ assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGatt)).isNull();
+ }
+
+ public void test_BluetoothGattCallback_onConnectionStateChange_connected_success()
+ throws Exception {
+ mBluetoothGattHelper.mConnections.put(mMockBluetoothGatt, mMockBluetoothGattConnection);
+
+ mBluetoothGattHelper.mBluetoothGattCallback.onConnectionStateChange(mMockBluetoothGatt,
+ BluetoothGatt.GATT_SUCCESS, BluetoothGatt.STATE_CONNECTED);
+
+ verify(mMockBluetoothOperationExecutor).notifyCompletion(
+ new Operation<>(OperationType.CONNECT, mMockBluetoothDevice),
+ BluetoothGatt.GATT_SUCCESS,
+ mMockBluetoothGattConnection);
+ verify(mMockBluetoothGattConnection).onConnected();
+ }
+
+ public void test_BluetoothGattCallback_onConnectionStateChange_connected_success_withMtuOption()
+ throws Exception {
+ mBluetoothGattHelper.mConnections.put(mMockBluetoothGatt, mMockBluetoothGattConnection);
+ when(mMockBluetoothGattConnection.getConnectionOptions())
+ .thenReturn(BluetoothGattHelper.ConnectionOptions.builder()
+ .setMtu(MTU)
+ .build());
+ when(mMockBluetoothGatt.requestMtu(MTU)).thenReturn(true);
+
+ mBluetoothGattHelper.mBluetoothGattCallback.onConnectionStateChange(mMockBluetoothGatt,
+ BluetoothGatt.GATT_SUCCESS, BluetoothGatt.STATE_CONNECTED);
+
+ verifyZeroInteractions(mMockBluetoothOperationExecutor);
+ verify(mMockBluetoothGattConnection, never()).onConnected();
+ verify(mMockBluetoothGatt).requestMtu(MTU);
+ }
+
+ public void test_BluetoothGattCallback_onConnectionStateChange_connected_success_failMtuOption()
+ throws Exception {
+ mBluetoothGattHelper.mConnections.put(mMockBluetoothGatt, mMockBluetoothGattConnection);
+ when(mMockBluetoothGattConnection.getConnectionOptions())
+ .thenReturn(BluetoothGattHelper.ConnectionOptions.builder()
+ .setMtu(MTU)
+ .build());
+ when(mMockBluetoothGatt.requestMtu(MTU)).thenReturn(false);
+
+ mBluetoothGattHelper.mBluetoothGattCallback.onConnectionStateChange(mMockBluetoothGatt,
+ BluetoothGatt.GATT_SUCCESS, BluetoothGatt.STATE_CONNECTED);
+
+ verify(mMockBluetoothOperationExecutor).notifyFailure(
+ eq(new Operation<>(OperationType.CONNECT, mMockBluetoothDevice)),
+ any(BluetoothException.class));
+ verify(mMockBluetoothGattConnection, never()).onConnected();
+ verify(mMockBluetoothGatt).disconnect();
+ verify(mMockBluetoothGatt).close();
+ assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGatt)).isNull();
+ }
+
+ public void test_BluetoothGattCallback_onConnectionStateChange_connected_unexpectedSuccess()
+ throws Exception {
+ mBluetoothGattHelper.mBluetoothGattCallback.onConnectionStateChange(mMockBluetoothGatt,
+ BluetoothGatt.GATT_SUCCESS, BluetoothGatt.STATE_CONNECTED);
+
+ verifyZeroInteractions(mMockBluetoothOperationExecutor);
+ }
+
+ public void test_BluetoothGattCallback_onConnectionStateChange_connected_failure()
+ throws Exception {
+ mBluetoothGattHelper.mConnections.put(mMockBluetoothGatt, mMockBluetoothGattConnection);
+
+ mBluetoothGattHelper.mBluetoothGattCallback
+ .onConnectionStateChange(
+ mMockBluetoothGatt,
+ BluetoothGatt.GATT_FAILURE,
+ BluetoothGatt.STATE_CONNECTED);
+
+ verify(mMockBluetoothOperationExecutor)
+ .notifyCompletion(
+ new Operation<>(OperationType.CONNECT, mMockBluetoothDevice),
+ BluetoothGatt.GATT_FAILURE,
+ null);
+ verify(mMockBluetoothGatt).disconnect();
+ verify(mMockBluetoothGatt).close();
+ assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGatt)).isNull();
+ }
+
+ public void test_BluetoothGattCallback_onConnectionStateChange_disconnected_unexpectedSuccess()
+ throws Exception {
+ mBluetoothGattHelper.mBluetoothGattCallback
+ .onConnectionStateChange(
+ mMockBluetoothGatt,
+ BluetoothGatt.GATT_SUCCESS,
+ BluetoothGatt.STATE_DISCONNECTED);
+
+ verifyZeroInteractions(mMockBluetoothOperationExecutor);
+ }
+
+ public void test_BluetoothGattCallback_onConnectionStateChange_disconnected_notConnected()
+ throws Exception {
+ mBluetoothGattHelper.mConnections.put(mMockBluetoothGatt, mMockBluetoothGattConnection);
+ when(mMockBluetoothGattConnection.isConnected()).thenReturn(false);
+
+ mBluetoothGattHelper.mBluetoothGattCallback
+ .onConnectionStateChange(
+ mMockBluetoothGatt,
+ GATT_STATUS,
+ BluetoothGatt.STATE_DISCONNECTED);
+
+ verify(mMockBluetoothOperationExecutor)
+ .notifyCompletion(
+ new Operation<>(OperationType.CONNECT, mMockBluetoothDevice),
+ GATT_STATUS,
+ null);
+ verify(mMockBluetoothGatt).disconnect();
+ verify(mMockBluetoothGatt).close();
+ assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGatt)).isNull();
+ }
+
+ public void test_BluetoothGattCallback_onConnectionStateChange_disconnected_success()
+ throws Exception {
+ mBluetoothGattHelper.mConnections.put(mMockBluetoothGatt, mMockBluetoothGattConnection);
+ when(mMockBluetoothGattConnection.isConnected()).thenReturn(true);
+
+ mBluetoothGattHelper.mBluetoothGattCallback.onConnectionStateChange(mMockBluetoothGatt,
+ BluetoothGatt.GATT_SUCCESS, BluetoothGatt.STATE_DISCONNECTED);
+
+ verify(mMockBluetoothOperationExecutor).notifyCompletion(
+ new Operation<>(OperationType.DISCONNECT, mMockBluetoothDevice),
+ BluetoothGatt.GATT_SUCCESS);
+ verify(mMockBluetoothGattConnection).onClosed();
+ assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGatt)).isNull();
+ }
+
+ public void test_BluetoothGattCallback_onConnectionStateChange_disconnected_failure()
+ throws Exception {
+ mBluetoothGattHelper.mConnections.put(mMockBluetoothGatt, mMockBluetoothGattConnection);
+ when(mMockBluetoothGattConnection.isConnected()).thenReturn(true);
+
+ mBluetoothGattHelper.mBluetoothGattCallback.onConnectionStateChange(mMockBluetoothGatt,
+ BluetoothGatt.GATT_FAILURE, BluetoothGatt.STATE_DISCONNECTED);
+
+ verify(mMockBluetoothOperationExecutor).notifyCompletion(
+ new Operation<>(OperationType.DISCONNECT, mMockBluetoothDevice),
+ BluetoothGatt.GATT_FAILURE);
+ verify(mMockBluetoothGattConnection).onClosed();
+ assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGatt)).isNull();
+ }
+
+ public void test_BluetoothGattCallback_onServicesDiscovered() throws Exception {
+ mBluetoothGattHelper.mBluetoothGattCallback.onServicesDiscovered(mMockBluetoothGatt,
+ GATT_STATUS);
+
+ verify(mMockBluetoothOperationExecutor).notifyCompletion(
+ new Operation<Void>(OperationType.DISCOVER_SERVICES_INTERNAL, mMockBluetoothGatt),
+ GATT_STATUS);
+ }
+
+ public void test_BluetoothGattCallback_onCharacteristicRead() throws Exception {
+ mBluetoothGattHelper.mBluetoothGattCallback.onCharacteristicRead(mMockBluetoothGatt,
+ mMockBluetoothGattCharacteristic, GATT_STATUS);
+
+ verify(mMockBluetoothOperationExecutor).notifyCompletion(new Operation<byte[]>(
+ OperationType.READ_CHARACTERISTIC, mMockBluetoothGatt,
+ mMockBluetoothGattCharacteristic),
+ GATT_STATUS, CHARACTERISTIC_VALUE);
+ }
+
+ public void test_BluetoothGattCallback_onCharacteristicWrite() throws Exception {
+ mBluetoothGattHelper.mBluetoothGattCallback.onCharacteristicWrite(mMockBluetoothGatt,
+ mMockBluetoothGattCharacteristic, GATT_STATUS);
+
+ verify(mMockBluetoothOperationExecutor).notifyCompletion(new Operation<Void>(
+ OperationType.WRITE_CHARACTERISTIC, mMockBluetoothGatt,
+ mMockBluetoothGattCharacteristic),
+ GATT_STATUS);
+ }
+
+ public void test_BluetoothGattCallback_onDescriptorRead() throws Exception {
+ mBluetoothGattHelper.mBluetoothGattCallback.onDescriptorRead(mMockBluetoothGatt,
+ mMockBluetoothGattDescriptor, GATT_STATUS);
+
+ verify(mMockBluetoothOperationExecutor).notifyCompletion(new Operation<byte[]>(
+ OperationType.READ_DESCRIPTOR, mMockBluetoothGatt,
+ mMockBluetoothGattDescriptor),
+ GATT_STATUS,
+ DESCRIPTOR_VALUE);
+ }
+
+ public void test_BluetoothGattCallback_onDescriptorWrite() throws Exception {
+ mBluetoothGattHelper.mBluetoothGattCallback.onDescriptorWrite(mMockBluetoothGatt,
+ mMockBluetoothGattDescriptor, GATT_STATUS);
+
+ verify(mMockBluetoothOperationExecutor).notifyCompletion(new Operation<Void>(
+ OperationType.WRITE_DESCRIPTOR, mMockBluetoothGatt,
+ mMockBluetoothGattDescriptor),
+ GATT_STATUS);
+ }
+
+ public void test_BluetoothGattCallback_onReadRemoteRssi() throws Exception {
+ mBluetoothGattHelper.mBluetoothGattCallback.onReadRemoteRssi(mMockBluetoothGatt, RSSI,
+ GATT_STATUS);
+
+ verify(mMockBluetoothOperationExecutor).notifyCompletion(
+ new Operation<Integer>(OperationType.READ_RSSI, mMockBluetoothGatt), GATT_STATUS,
+ RSSI);
+ }
+
+ public void test_BluetoothGattCallback_onReliableWriteCompleted() throws Exception {
+ mBluetoothGattHelper.mBluetoothGattCallback.onReliableWriteCompleted(mMockBluetoothGatt,
+ GATT_STATUS);
+
+ verify(mMockBluetoothOperationExecutor).notifyCompletion(
+ new Operation<Void>(OperationType.WRITE_RELIABLE, mMockBluetoothGatt), GATT_STATUS);
+ }
+
+ public void test_BluetoothGattCallback_onMtuChanged() throws Exception {
+ mBluetoothGattHelper.mConnections.put(mMockBluetoothGatt, mMockBluetoothGattConnection);
+ when(mMockBluetoothGattConnection.isConnected()).thenReturn(true);
+
+ mBluetoothGattHelper.mBluetoothGattCallback
+ .onMtuChanged(mMockBluetoothGatt, MTU, GATT_STATUS);
+
+ verify(mMockBluetoothOperationExecutor).notifyCompletion(
+ new Operation<>(OperationType.CHANGE_MTU, mMockBluetoothGatt), GATT_STATUS, MTU);
+ }
+
+ public void testBluetoothGattCallback_onMtuChangedDuringConnection_success() throws Exception {
+ mBluetoothGattHelper.mConnections.put(mMockBluetoothGatt, mMockBluetoothGattConnection);
+ when(mMockBluetoothGattConnection.isConnected()).thenReturn(false);
+
+ mBluetoothGattHelper.mBluetoothGattCallback.onMtuChanged(
+ mMockBluetoothGatt, MTU, BluetoothGatt.GATT_SUCCESS);
+
+ verify(mMockBluetoothGattConnection).onConnected();
+ verify(mMockBluetoothOperationExecutor)
+ .notifyCompletion(
+ new Operation<>(OperationType.CONNECT, mMockBluetoothDevice),
+ BluetoothGatt.GATT_SUCCESS,
+ mMockBluetoothGattConnection);
+ }
+
+ public void testBluetoothGattCallback_onMtuChangedDuringConnection_fail() throws Exception {
+ mBluetoothGattHelper.mConnections.put(mMockBluetoothGatt, mMockBluetoothGattConnection);
+ when(mMockBluetoothGattConnection.isConnected()).thenReturn(false);
+
+ mBluetoothGattHelper.mBluetoothGattCallback
+ .onMtuChanged(mMockBluetoothGatt, MTU, GATT_STATUS);
+
+ verify(mMockBluetoothGattConnection).onConnected();
+ verify(mMockBluetoothOperationExecutor)
+ .notifyCompletion(
+ new Operation<>(OperationType.CONNECT, mMockBluetoothDevice),
+ GATT_STATUS,
+ mMockBluetoothGattConnection);
+ verify(mMockBluetoothGatt).disconnect();
+ verify(mMockBluetoothGatt).close();
+ assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGatt)).isNull();
+ }
+
+ public void test_BluetoothGattCallback_onCharacteristicChanged() throws Exception {
+ mBluetoothGattHelper.mConnections.put(mMockBluetoothGatt, mMockBluetoothGattConnection);
+
+ mBluetoothGattHelper.mBluetoothGattCallback.onCharacteristicChanged(mMockBluetoothGatt,
+ mMockBluetoothGattCharacteristic);
+
+ verify(mMockBluetoothGattConnection).onCharacteristicChanged(
+ mMockBluetoothGattCharacteristic,
+ CHARACTERISTIC_VALUE);
+ }
+
+ public void test_ScanCallback_onScanFailed() throws Exception {
+ mBluetoothGattHelper.mScanCallback.onScanFailed(ScanCallback.SCAN_FAILED_INTERNAL_ERROR);
+
+ verify(mMockBluetoothOperationExecutor).notifyFailure(
+ eq(new Operation<BluetoothDevice>(OperationType.SCAN)),
+ isA(BluetoothException.class));
+ }
+
+ public void test_ScanCallback_onScanResult() throws Exception {
+ mBluetoothGattHelper.mScanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_ALL_MATCHES,
+ mMockScanResult);
+
+ verify(mMockBluetoothOperationExecutor).notifySuccess(
+ new Operation<BluetoothDevice>(OperationType.SCAN), mMockBluetoothDevice);
+ }
+}