summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJin Chen <jinjiechen@google.com>2022-06-30 17:30:08 +0000
committerJin Chen <jinjiechen@google.com>2022-07-01 16:49:56 +0000
commita336a1c1eb2112a116cd852b355b0e0308db7995 (patch)
treec18da566f726d82d9864e053a6e12766dfd96269
parentca20892fc39f058d7389a924a51f5c0194a5c7a3 (diff)
downloadUwb-a336a1c1eb2112a116cd852b355b0e0308db7995.tar.gz
[uwb] Implement GattTransportClientProvider
Bug: 197341177 Test: atest ServiceUwbTests Change-Id: I1fbca67303a3393dbe2d2a193e00d72b9d1faf7e
-rw-r--r--service/java/com/android/server/uwb/discovery/TransportClientService.java9
-rw-r--r--service/java/com/android/server/uwb/discovery/ble/GattTransportClientProvider.java586
2 files changed, 594 insertions, 1 deletions
diff --git a/service/java/com/android/server/uwb/discovery/TransportClientService.java b/service/java/com/android/server/uwb/discovery/TransportClientService.java
index 1960e687..bd971ae4 100644
--- a/service/java/com/android/server/uwb/discovery/TransportClientService.java
+++ b/service/java/com/android/server/uwb/discovery/TransportClientService.java
@@ -22,6 +22,7 @@ import android.util.Log;
import androidx.annotation.WorkerThread;
import com.android.server.uwb.discovery.TransportClientProvider.TransportClientCallback;
+import com.android.server.uwb.discovery.ble.GattTransportClientProvider;
import com.android.server.uwb.discovery.info.DiscoveryInfo;
import com.android.server.uwb.discovery.info.FiraConnectorCapabilities;
import com.android.server.uwb.discovery.info.FiraConnectorMessage;
@@ -51,7 +52,13 @@ public class TransportClientService {
+ " transportClientInfo in discoveryInfo:"
+ discoveryInfo);
}
- mTransportClientProvider = null;
+ mTransportClientProvider =
+ new GattTransportClientProvider(
+ attributionSource,
+ context,
+ executor,
+ discoveryInfo.transportClientInfo.get(),
+ transportClientCallback);
break;
default:
throw new AssertionError(
diff --git a/service/java/com/android/server/uwb/discovery/ble/GattTransportClientProvider.java b/service/java/com/android/server/uwb/discovery/ble/GattTransportClientProvider.java
new file mode 100644
index 00000000..84cdbfd6
--- /dev/null
+++ b/service/java/com/android/server/uwb/discovery/ble/GattTransportClientProvider.java
@@ -0,0 +1,586 @@
+/*
+ * Copyright (C) 2022 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.uwb.discovery.ble;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCallback;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothStatusCodes;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.content.ContextParams;
+import android.util.Log;
+
+import androidx.annotation.WorkerThread;
+
+import com.android.server.uwb.discovery.TransportClientProvider;
+import com.android.server.uwb.discovery.TransportClientProvider.TerminationReason;
+import com.android.server.uwb.discovery.TransportClientProvider.TransportClientCallback;
+import com.android.server.uwb.discovery.info.FiraConnectorCapabilities;
+import com.android.server.uwb.discovery.info.FiraConnectorDataPacket;
+import com.android.server.uwb.discovery.info.FiraConnectorMessage;
+import com.android.server.uwb.discovery.info.SecureComponentInfo;
+import com.android.server.uwb.discovery.info.TransportClientInfo;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.util.ArrayDeque;
+import java.util.concurrent.Executor;
+
+/**
+ * Class for UWB transport client provider using Bluetooth GATT.
+ *
+ * <p>The GATT client is responsible for the entire Service discovery procedure. Once the device
+ * discovery phase passed, the client establishes the Bluetooth connection and perform GATT service
+ * and GATT characterstics discovery. When discovery complete, the client writes it's FiRa Connector
+ * Capabilities into the Capabilities characteristic and enable Value Notifications on the "OUT"
+ * Control Point characteristic on the remote GATT server. Afterwards, the client can starts
+ * exchange data with the server over the "IN" and "OUT" Control Point characteristic. When any
+ * unrecoverable event occurred, GATT client will be terminated.
+ */
+@WorkerThread
+public class GattTransportClientProvider extends TransportClientProvider {
+ private static final String TAG = GattTransportClientProvider.class.getSimpleName();
+
+ private final Executor mCallbackExecutor;
+ private final Context mContext;
+ private TransportClientCallback mTransportClientCallback;
+ private BluetoothDevice mRemoteBluetoothDevice;
+ private BluetoothGatt mBluetoothGatt;
+
+ private FiraConnectorCapabilities mCapabilities;
+
+ private boolean mConnected;
+ private boolean mServiceDiscovered;
+ private boolean mCapabilitiesWritten;
+ private boolean mNotificationEnabled;
+ private boolean mIsProcessing;
+
+ private BluetoothGattCharacteristic mInControlPointCharacteristic;
+ private BluetoothGattCharacteristic mOutControlPointCharacteristic;
+ private BluetoothGattCharacteristic mCapabilitiesCharacteristic;
+
+ private BluetoothGattDescriptor mOutControlPointCccdDescriptor;
+
+ /* Queue of Fira Connector Data Packets from the mOutControlPointCharacteristic that are
+ * incomplete to be constructed as FiRa Connector Message.
+ */
+ private ArrayDeque<FiraConnectorDataPacket> mIncompleteOutDataPacketQueue;
+
+ /* Wraps Fira Connector Message byte array and the associated SECID.
+ */
+ private static class MessagePacket {
+ public final int secid;
+ public ByteBuffer messageBytes;
+
+ MessagePacket(int secid, ByteBuffer messageBytes) {
+ this.secid = secid;
+ this.messageBytes = messageBytes;
+ }
+ }
+
+ /* Queue of Fira Connector Message wrapped as MessagePacket to be sent via the
+ * mInControlPointCharacteristic.
+ */
+ private ArrayDeque<MessagePacket> mInMessageQueue;
+
+ /** GATT callbacks responsible for handling events from the remote device GATT server. */
+ private BluetoothGattCallback mBluetoothGattCallback =
+ new BluetoothGattCallback() {
+
+ @Override
+ public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
+ Log.i(TAG, "onConnectionStateChange state:" + newState);
+ mBluetoothGatt = gatt;
+ if (status != BluetoothGatt.GATT_SUCCESS) {
+ Log.w(TAG, "onConnectionStateChange failed");
+ terminateOnError(TerminationReason.REMOTE_DISCONNECTED);
+ return;
+ }
+ if (newState == BluetoothProfile.STATE_DISCONNECTED) {
+ mConnected = false;
+ terminateOnError(TerminationReason.REMOTE_DISCONNECTED);
+ mCallbackExecutor.execute(() -> startProcessing());
+ } else if (newState == BluetoothProfile.STATE_CONNECTED) {
+ mConnected = true;
+ mCallbackExecutor.execute(() -> startProcessing());
+ }
+ }
+
+ @Override
+ public void onServiceChanged(BluetoothGatt gatt) {
+ Log.d(TAG, "onServiceChanged");
+ // Service changed event, call to re-discover the services.
+ gatt.discoverServices();
+ }
+
+ @Override
+ public void onServicesDiscovered(BluetoothGatt gatt, int status) {
+ mBluetoothGatt = gatt;
+ if (status != BluetoothGatt.GATT_SUCCESS) {
+ Log.w(TAG, "onServicesDiscovered failed");
+ terminateOnError(TerminationReason.SERVICE_DISCOVERY_FAILURE);
+ return;
+ }
+
+ BluetoothGattService service =
+ gatt.getService(UuidConstants.FIRA_CP_PARCEL_UUID.getUuid());
+ if (service == null) {
+ Log.w(
+ TAG,
+ "onServicesDiscovered FiRa CP Gatt service not found on remote"
+ + " device.");
+ terminateOnError(TerminationReason.SERVICE_DISCOVERY_FAILURE);
+ return;
+ }
+ mInControlPointCharacteristic =
+ service.getCharacteristic(
+ UuidConstants.CP_IN_CONTROL_POINT_UUID.getUuid());
+ mOutControlPointCharacteristic =
+ service.getCharacteristic(
+ UuidConstants.CP_OUT_CONTROL_POINT_UUID.getUuid());
+ mCapabilitiesCharacteristic =
+ service.getCharacteristic(
+ UuidConstants.CP_FIRA_CONNECTOR_CAPABILITIES_UUID.getUuid());
+ mOutControlPointCccdDescriptor =
+ mOutControlPointCharacteristic.getDescriptor(
+ UuidConstants.CCCD_UUID.getUuid());
+
+ if (mInControlPointCharacteristic == null
+ || mOutControlPointCharacteristic == null
+ || mCapabilitiesCharacteristic == null
+ || mOutControlPointCccdDescriptor == null) {
+ Log.w(
+ TAG,
+ "onServicesDiscovered FiRa CP Gatt service characteristics and/or"
+ + " descriptor not found.");
+ terminateOnError(TerminationReason.SERVICE_DISCOVERY_FAILURE);
+ return;
+ }
+ mServiceDiscovered = true;
+ mCallbackExecutor.execute(() -> startProcessing());
+ }
+
+ @Override
+ public void onCharacteristicChanged(
+ BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic,
+ byte[] value) {
+ if (characteristic.getUuid().equals(mOutControlPointCharacteristic.getUuid())) {
+ mBluetoothGatt = gatt;
+ mCallbackExecutor.execute(() -> readOutControlPointCharacteristic());
+ }
+ }
+
+ @Override
+ public void onCharacteristicRead(
+ BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic,
+ byte[] value,
+ int status) {
+ mBluetoothGatt = gatt;
+ if (status != BluetoothGatt.GATT_SUCCESS) {
+ Log.e(TAG, "onCharacteristicRead failed uuid:" + characteristic.getUuid());
+ terminateOnError(TerminationReason.CHARACTERSTIC_READ_FAILURE);
+ return;
+ }
+ if (characteristic.getUuid().equals(mOutControlPointCharacteristic.getUuid())) {
+ mCallbackExecutor.execute(() -> processOutDataPacket(value));
+ }
+ }
+
+ @Override
+ public void onCharacteristicWrite(
+ BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic,
+ int status) {
+ mBluetoothGatt = gatt;
+ if (status != BluetoothGatt.GATT_SUCCESS) {
+ Log.e(TAG, "onCharacteristicWrite failed uuid:" + characteristic.getUuid());
+ terminateOnError(TerminationReason.CHARACTERSTIC_WRITE_FAILURE);
+ return;
+ }
+ if (characteristic.getUuid().equals(mCapabilitiesCharacteristic.getUuid())) {
+ mCapabilitiesWritten = true;
+ mCallbackExecutor.execute(() -> startProcessing());
+ } else if (characteristic
+ .getUuid()
+ .equals(mInControlPointCharacteristic.getUuid())) {
+ mCallbackExecutor.execute(() -> processInDataPacket());
+ }
+ }
+
+ @Override
+ public void onDescriptorWrite(
+ BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
+ mBluetoothGatt = gatt;
+ if (status != BluetoothGatt.GATT_SUCCESS) {
+ Log.e(TAG, "onDescriptorWrite failed uuid:" + descriptor.getUuid());
+ terminateOnError(TerminationReason.DESCRIPTOR_WRITE_FAILURE);
+ return;
+ }
+ if (descriptor.getUuid().equals(mOutControlPointCccdDescriptor.getUuid())) {
+ mNotificationEnabled = true;
+ mCallbackExecutor.execute(() -> startProcessing());
+ }
+ }
+
+ @Override
+ public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
+ mBluetoothGatt = gatt;
+ if (status != BluetoothGatt.GATT_SUCCESS) {
+ Log.e(TAG, "onMtuChanged failed");
+ return;
+ }
+ Log.d(TAG, "onMtuChanged new mtu=" + mtu);
+ // Update Capabilities if changed, and sent to server.
+ int newDataPacketSize = mtu - 3;
+ if (newDataPacketSize == mCapabilities.optimizedDataPacketSize) {
+ return;
+ }
+ FiraConnectorCapabilities.Builder builder =
+ new FiraConnectorCapabilities.Builder()
+ .setProtocolVersion(mCapabilities.protocolVersion)
+ .setMaxMessageBufferSize(mCapabilities.maxMessageBufferSize)
+ .setMaxConcurrentFragmentedMessageSessionSupported(
+ mCapabilities
+ .maxConcurrentFragmentedMessageSessionSupported)
+ .setOptimizedDataPacketSize(newDataPacketSize);
+ for (SecureComponentInfo info : mCapabilities.secureComponentInfos) {
+ builder.addSecureComponentInfo(info);
+ }
+ mCallbackExecutor.execute(() -> setCapabilites(builder.build()));
+ }
+ };
+
+ public GattTransportClientProvider(
+ AttributionSource attributionSource,
+ Context context,
+ Executor executor,
+ TransportClientInfo transportClientInfo,
+ TransportClientCallback transportServerCallback) {
+ mCallbackExecutor = executor;
+ mContext =
+ context.createContext(
+ new ContextParams.Builder()
+ .setNextAttributionSource(attributionSource)
+ .build());
+ mTransportClientCallback = transportServerCallback;
+ mRemoteBluetoothDevice = transportClientInfo.scanResult.getDevice();
+
+ // Using FiRa defined default connector capabilities.
+ mCapabilities = new FiraConnectorCapabilities.Builder().build();
+
+ mIncompleteOutDataPacketQueue = new ArrayDeque();
+ mInMessageQueue = new ArrayDeque();
+ }
+
+ @Override
+ public boolean start() {
+ if (mRemoteBluetoothDevice == null) {
+ Log.w(TAG, "start failed due to BluetoothDevice is null.");
+ return false;
+ }
+ boolean succeed = true;
+ if (mBluetoothGatt == null) {
+ // Connects to the GATT server on the remote device.
+ mBluetoothGatt =
+ mRemoteBluetoothDevice.connectGatt(
+ mContext,
+ /*autoConnect=*/ false,
+ mBluetoothGattCallback,
+ BluetoothDevice.TRANSPORT_LE);
+ } else {
+ succeed = mBluetoothGatt.connect();
+ }
+
+ mStarted = succeed;
+ return succeed;
+ }
+
+ @Override
+ public boolean stop() {
+ if (mBluetoothGatt == null) {
+ Log.w(TAG, "stop failed due to BluetoothGatt is null.");
+ return false;
+ }
+ mBluetoothGatt.disconnect();
+
+ // Clear in/out message queue.
+ mIncompleteOutDataPacketQueue.clear();
+ mInMessageQueue.clear();
+
+ mStarted = false;
+ return true;
+ }
+
+ @Override
+ public boolean sendMessage(int secid, FiraConnectorMessage message) {
+ if (!isProcessing()) {
+ Log.w(TAG, "Sent request failed due to server not ready for processing.");
+ return false;
+ }
+ byte[] messageBytes = message.toBytes();
+ if (messageBytes.length > mCapabilities.maxMessageBufferSize) {
+ Log.w(TAG, "Sent request failed due to message size exceeded device capabilities.");
+ return false;
+ }
+ mInMessageQueue.add(new MessagePacket(secid, ByteBuffer.wrap(messageBytes)));
+
+ // No existing meesage in progress, sent this message immediately.
+ if (mInMessageQueue.size() == 1) {
+ return processInDataPacket();
+ }
+ return true;
+ }
+
+ @Override
+ public boolean setCapabilites(FiraConnectorCapabilities capabilities) {
+ if (capabilities == null) {
+ Log.e(TAG, "setCapabilites failed null capabilities.");
+ return false;
+ }
+ Log.d(TAG, "setCapabilites new capabilities:" + capabilities);
+ mCapabilities = capabilities;
+ if (!mStarted || !mServiceDiscovered) {
+ Log.w(
+ TAG,
+ "setCapabilites only updated locally since client hasn't started or service not"
+ + " discovered.");
+ return false;
+ }
+ return writeCapabilitiesCharacteristic();
+ }
+
+ /**
+ * Process the next in control data packet from the queue. Write new data packet to {@link
+ * mInControlPointCharacteristic}.
+ *
+ * @return indicate if next in data packet was process successfully.
+ */
+ private boolean processInDataPacket() {
+ if (!isProcessing()) {
+ Log.w(TAG, "processInDataPacket failed due to client not ready for processing.");
+ return false;
+ }
+ if (mInMessageQueue.isEmpty()) {
+ Log.w(TAG, "processInDataPacket skipped due to empty queue.");
+ return false;
+ }
+ MessagePacket messagePacket = mInMessageQueue.peek();
+ ByteBuffer byteBuffer = messagePacket.messageBytes;
+ byte[] nextPayload =
+ new byte
+ [Math.min(
+ byteBuffer.remaining(),
+ mCapabilities.optimizedDataPacketSize
+ - FiraConnectorDataPacket.HEADER_SIZE)];
+ byteBuffer.get(nextPayload);
+
+ final FiraConnectorDataPacket dataPacket =
+ new FiraConnectorDataPacket(
+ /*lastChainingPacket=*/ !byteBuffer.hasRemaining(),
+ messagePacket.secid,
+ nextPayload);
+
+ if (!byteBuffer.hasRemaining()) {
+ mInMessageQueue.pop();
+ }
+ final int status =
+ mBluetoothGatt.writeCharacteristic(
+ mInControlPointCharacteristic,
+ dataPacket.toBytes(),
+ BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
+ if (status != BluetoothStatusCodes.SUCCESS) {
+ Log.w(TAG, "processInDataPacket failed due to fail to writeCharacteristic.");
+ terminateOnError(TerminationReason.CHARACTERSTIC_WRITE_FAILURE);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Process the new out control data packet. Construct the FiraConnectorMessage if data is
+ * complete, and notify callback with the constructed message.
+ *
+ * @return indicate if new out data packet was process successfully.
+ */
+ private boolean processOutDataPacket(byte[] bytes) {
+ if (!isProcessing()) {
+ Log.w(TAG, "processOutDataPacket failed due to server not ready for processing.");
+ return false;
+ }
+ FiraConnectorDataPacket latestDataPacket = FiraConnectorDataPacket.fromBytes(bytes);
+ if (latestDataPacket == null) {
+ Log.w(
+ TAG,
+ "processOutDataPacket failed due to latest FiraConnectorDataPacket cannot be"
+ + " constructed from bytes.");
+ return false;
+ }
+ if (!mIncompleteOutDataPacketQueue.isEmpty()
+ && latestDataPacket.secid != mIncompleteOutDataPacketQueue.peek().secid) {
+ Log.w(
+ TAG,
+ "processOutDataPacket failed due to latest FiraConnectorDataPacket's SECID"
+ + " doesn't match previous data packet.");
+ return false;
+ }
+ mIncompleteOutDataPacketQueue.add(latestDataPacket);
+ if (!latestDataPacket.lastChainingPacket) {
+ return true;
+ }
+ // All data packets of the message has been received. Constructing the message.
+ ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
+ for (FiraConnectorDataPacket dataPacket : mIncompleteOutDataPacketQueue) {
+ byteStream.write(dataPacket.payload, /*off=*/ 0, dataPacket.payload.length);
+ }
+ mIncompleteOutDataPacketQueue.clear();
+
+ FiraConnectorMessage message = FiraConnectorMessage.fromBytes(byteStream.toByteArray());
+ if (message == null) {
+ Log.w(
+ TAG,
+ "processOutDataPacket failed due to FiraConnectorMessage cannot be constructed"
+ + " from bytes.");
+ return false;
+ }
+
+ mTransportClientCallback.onMessageReceived(latestDataPacket.secid, message);
+ return true;
+ }
+
+ /**
+ * Start processing of the FiRa Connector Data Packets and the FiRa Connector Messages through
+ * the In/Out control point characterstic on the remote GATT server once the setup procedure
+ * defined by FiRa OOB spec is complete,
+ */
+ private void startProcessing() {
+ Log.d(
+ TAG,
+ "startProcessing: isProcessing="
+ + isProcessing()
+ + " (connected="
+ + mConnected
+ + ", service discovered="
+ + mServiceDiscovered
+ + ", capabilities written="
+ + mCapabilitiesWritten
+ + ", notification enabled="
+ + mNotificationEnabled
+ + ")");
+
+ if (mConnected) {
+ if (!mServiceDiscovered) {
+ mBluetoothGatt.discoverServices();
+ } else if (!mCapabilitiesWritten) {
+ writeCapabilitiesCharacteristic();
+ } else if (!mNotificationEnabled) {
+ enableNotification();
+ }
+ }
+
+ boolean isProcessing =
+ mConnected && mServiceDiscovered && mCapabilitiesWritten && mNotificationEnabled;
+ if (isProcessing == mIsProcessing) {
+ return;
+ }
+ mIsProcessing = isProcessing;
+ if (mIsProcessing) {
+ mTransportClientCallback.onProcessingStarted();
+ } else {
+ mTransportClientCallback.onProcessingStopped();
+ }
+ }
+
+ /**
+ * Check if processing has started.
+ *
+ * @return indicate if server has started processing.
+ */
+ private boolean isProcessing() {
+ return mIsProcessing;
+ }
+
+ private boolean writeCapabilitiesCharacteristic() {
+ if (mBluetoothGatt == null) {
+ Log.e(TAG, "writeCapabilitiesCharacteristic failed due to Gatt is null.");
+ terminateOnError(TerminationReason.CHARACTERSTIC_WRITE_FAILURE);
+ return false;
+ }
+ if (mCapabilitiesCharacteristic == null) {
+ Log.e(TAG, "writeCapabilitiesCharacteristic failed due to characteristic is null.");
+ terminateOnError(TerminationReason.CHARACTERSTIC_WRITE_FAILURE);
+ return false;
+ }
+ final int status =
+ mBluetoothGatt.writeCharacteristic(
+ mCapabilitiesCharacteristic,
+ mCapabilities.toBytes(),
+ BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
+ if (status != BluetoothStatusCodes.SUCCESS) {
+ terminateOnError(TerminationReason.CHARACTERSTIC_WRITE_FAILURE);
+ return false;
+ }
+ return true;
+ }
+
+ private void enableNotification() {
+ if (mBluetoothGatt == null) {
+ Log.e(TAG, "enableNotification failed due to Gatt is null.");
+ terminateOnError(TerminationReason.DESCRIPTOR_WRITE_FAILURE);
+ return;
+ }
+ if (mOutControlPointCccdDescriptor == null) {
+ Log.e(TAG, "enableNotification failed due to descriptor is null.");
+ terminateOnError(TerminationReason.DESCRIPTOR_WRITE_FAILURE);
+ return;
+ }
+ final int status =
+ mBluetoothGatt.writeDescriptor(
+ mOutControlPointCccdDescriptor,
+ BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
+ if (status != BluetoothStatusCodes.SUCCESS) {
+ terminateOnError(TerminationReason.DESCRIPTOR_WRITE_FAILURE);
+ }
+ }
+
+ private void readOutControlPointCharacteristic() {
+ if (mBluetoothGatt == null) {
+ Log.e(TAG, "readOutControlPointCharacteristic failed due to Gatt is null.");
+ terminateOnError(TerminationReason.CHARACTERSTIC_READ_FAILURE);
+ return;
+ }
+ if (mOutControlPointCharacteristic == null) {
+ Log.e(TAG, "readOutControlPointCharacteristic failed due to descriptor is null.");
+ terminateOnError(TerminationReason.CHARACTERSTIC_READ_FAILURE);
+ return;
+ }
+ if (!mBluetoothGatt.readCharacteristic(mOutControlPointCharacteristic)) {
+ terminateOnError(TerminationReason.CHARACTERSTIC_READ_FAILURE);
+ }
+ }
+
+ private void terminateOnError(TerminationReason reason) {
+ Log.e(TAG, "GattTransportClient terminated with reason:" + reason);
+ stop();
+ mTransportClientCallback.onTerminated(reason);
+ }
+}