1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.android.tv.btservices.remote; 18 19 import android.bluetooth.BluetoothDevice; 20 import android.bluetooth.BluetoothGatt; 21 import android.bluetooth.BluetoothGattCallback; 22 import android.bluetooth.BluetoothGattCharacteristic; 23 import android.bluetooth.BluetoothGattDescriptor; 24 import android.bluetooth.BluetoothProfile; 25 import android.content.Context; 26 import android.util.Log; 27 import java.util.concurrent.LinkedBlockingQueue; 28 import java.util.concurrent.atomic.AtomicReference; 29 import java.util.function.Consumer; 30 31 /** 32 * An object that manages Bluetooth LE connection. 33 * 34 * <p>A {@code BleConnection} manages Bluetooth LE connection with a {@code 35 * BluetoothDevice}. The lifecycle of an instance begins with a connect event 36 * and ends when disconnect happens. A new instance should be created on device 37 * reconnect. 38 * 39 * <p>For GATT requests, {@code BleConnection} maintains a request queue that is 40 * built on Android's {@code BluetoothGatt} API. Methods like {@code 41 * writeCharacteristic} are provided and can be called anytime after {@code 42 * Callback#onGattReady} is called. All methods also takes a callback function 43 * that is called when the response from GATT server is received. 44 * 45 * <p>Example usage: 46 * 47 * Create a new {@code BleConnection}: 48 * <pre> 49 * BluetoothDevice device; 50 * 51 * private class Callback implements BleConnection.Callback { 52 * @Override 53 * public void onGattReady(BluetoothGatt gatt) { 54 * // Initialize with GATT requests here. 55 * } 56 * 57 * @Override 58 * public void onNotification(BluetoothGattCharacteristic characteristic, byte[] data) { 59 * // Handle GATT notification. 60 * } 61 * 62 * @Override 63 * public void onDisconnect(BluetoothGatt gatt, int status) { 64 * // Handle disconnect. 65 * } 66 * } 67 * 68 * BleConnection bleConnection = new BleConnection(new Callback()); 69 * bleConnection.connect(context, device); 70 * </pre> 71 * 72 * Write to GATT characteristic (must be called only after {@code 73 * Callback#onGattReady} is called): 74 * <pre>{@code 75 * BluetoothGattCharacteristic characteristic; 76 * 77 * bleConnection.writeCharacteristic( 78 * characteristic, 79 * data, // byte array 80 * (BluetoothGattCharacteristic characteristic, int status) -> { 81 * if (status != BluetoothGatt.GATT_SUCCESS) { 82 * Log.w(TAG, "GATT_FAILURE: " + status); 83 * } 84 * // write process completed 85 * }); 86 * }</pre> 87 * 88 */ 89 public class BleConnection { 90 private static final String TAG = "Atv.BleConnection"; 91 private static final boolean DEBUG = true; 92 93 private enum ConnectionState { 94 UNINITIALIZED, 95 GATT_CONNECTING, 96 SERVICE_DISCOVERING, 97 READY, 98 PENDING_RESPONSE, 99 DISCONNECTED, 100 } 101 102 @FunctionalInterface 103 public interface CharacteristicWriteResultCallback { run(BluetoothGattCharacteristic characteristic, int status)104 void run(BluetoothGattCharacteristic characteristic, int status); 105 } 106 107 @FunctionalInterface 108 public interface CharacteristicReadResultCallback { run(BluetoothGattCharacteristic characteristic, int status)109 void run(BluetoothGattCharacteristic characteristic, int status); 110 } 111 112 @FunctionalInterface 113 public interface DescriptorWriteResultCallback { run(BluetoothGattDescriptor descriptor, int status)114 void run(BluetoothGattDescriptor descriptor, int status); 115 } 116 117 private interface GattRequest { processRequest(BluetoothGatt gatt)118 boolean processRequest(BluetoothGatt gatt); 119 } 120 121 private CharacteristicWriteResultCallback lastCharacteristicWriteCallback; 122 private CharacteristicReadResultCallback lastCharacteristicReadCallback; 123 private DescriptorWriteResultCallback lastDescriptorWriteCallback; 124 private Consumer<Boolean> lastRequestMtuCallback; 125 126 private class CharacteristicWriteRequest implements GattRequest { 127 final BluetoothGattCharacteristic characteristic; 128 final byte[] data; 129 final CharacteristicWriteResultCallback callback; 130 CharacteristicWriteRequest( BluetoothGattCharacteristic characteristic, byte[] data, CharacteristicWriteResultCallback callback)131 CharacteristicWriteRequest( 132 BluetoothGattCharacteristic characteristic, 133 byte[] data, 134 CharacteristicWriteResultCallback callback) { 135 this.characteristic = characteristic; 136 this.data = data; 137 this.callback = callback; 138 } 139 140 @Override processRequest(BluetoothGatt gatt)141 public boolean processRequest(BluetoothGatt gatt) { 142 lastCharacteristicWriteCallback = this.callback; 143 characteristic.setValue(data); 144 return gatt.writeCharacteristic(characteristic); 145 } 146 } 147 148 private class CharacteristicReadRequest implements GattRequest { 149 final BluetoothGattCharacteristic characteristic; 150 final CharacteristicReadResultCallback callback; 151 CharacteristicReadRequest( BluetoothGattCharacteristic characteristic, CharacteristicReadResultCallback callback)152 CharacteristicReadRequest( 153 BluetoothGattCharacteristic characteristic, 154 CharacteristicReadResultCallback callback) { 155 this.characteristic = characteristic; 156 this.callback = callback; 157 } 158 159 @Override processRequest(BluetoothGatt gatt)160 public boolean processRequest(BluetoothGatt gatt) { 161 lastCharacteristicReadCallback = this.callback; 162 return gatt.readCharacteristic(characteristic); 163 } 164 } 165 166 private class DescriptorWriteRequest implements GattRequest { 167 final BluetoothGattDescriptor descriptor; 168 final byte[] data; 169 final DescriptorWriteResultCallback callback; 170 DescriptorWriteRequest( BluetoothGattDescriptor descriptor, byte[] data, DescriptorWriteResultCallback callback)171 DescriptorWriteRequest( 172 BluetoothGattDescriptor descriptor, 173 byte[] data, 174 DescriptorWriteResultCallback callback) { 175 this.descriptor = descriptor; 176 this.data = data; 177 this.callback = callback; 178 } 179 180 @Override processRequest(BluetoothGatt gatt)181 public boolean processRequest(BluetoothGatt gatt) { 182 lastDescriptorWriteCallback = this.callback; 183 descriptor.setValue(data); 184 return gatt.writeDescriptor(descriptor); 185 } 186 } 187 188 private final AtomicReference<ConnectionState> state; 189 private final Callback bleConnectionCallback; 190 // Contains the following types of request: CharacteristicWriteRequest, 191 // CharacteristicReadRequest, DescriptorWriteRequest. 192 private final LinkedBlockingQueue<GattRequest> requestQueue = new LinkedBlockingQueue<>(); 193 private BluetoothGatt gatt; 194 BleConnection(Callback bleConnectionCallback)195 public BleConnection(Callback bleConnectionCallback) { 196 this.bleConnectionCallback = bleConnectionCallback; 197 state = new AtomicReference<>(ConnectionState.UNINITIALIZED); 198 } 199 connect(Context context, BluetoothDevice device)200 public boolean connect(Context context, BluetoothDevice device) { 201 if (state.compareAndSet( 202 ConnectionState.UNINITIALIZED, 203 ConnectionState.GATT_CONNECTING)) { 204 synchronized (state) { 205 gatt = device.connectGatt(context, false, new GattCallback()); 206 } 207 208 if (gatt != null) { 209 return true; 210 } 211 } 212 state.set(ConnectionState.DISCONNECTED); 213 return false; 214 } 215 216 /** 217 * Sends the next command from {@link #requestQueue} if exists. 218 * 219 * <p>The main purpose of this method is to handle simultaneous write requests without an 220 * explicit synchronization. 221 */ maybeSendNextCommand(BluetoothGatt gatt)222 private void maybeSendNextCommand(BluetoothGatt gatt) { 223 GattRequest upcomingRequest; 224 while ((upcomingRequest = requestQueue.poll()) == null) { 225 if (!state.compareAndSet(ConnectionState.PENDING_RESPONSE, ConnectionState.READY) 226 || requestQueue.isEmpty() 227 || !state.compareAndSet(ConnectionState.READY, 228 ConnectionState.PENDING_RESPONSE)) { 229 return; 230 } 231 } 232 233 if (!upcomingRequest.processRequest(gatt)) { 234 closeGatt(); 235 } 236 } 237 writeCharacteristic( BluetoothGattCharacteristic characteristic, byte[] data, CharacteristicWriteResultCallback callback)238 public void writeCharacteristic( 239 BluetoothGattCharacteristic characteristic, 240 byte[] data, 241 CharacteristicWriteResultCallback callback) { 242 if (characteristic == null) { 243 callback.run(characteristic, BluetoothGatt.GATT_FAILURE); 244 return; 245 } 246 247 CharacteristicWriteRequest request = 248 new CharacteristicWriteRequest(characteristic, data, callback); 249 requestQueue.add(request); 250 251 if (state.compareAndSet(ConnectionState.READY, ConnectionState.PENDING_RESPONSE)) { 252 maybeSendNextCommand(gatt); 253 } else { 254 Log.i(TAG, "Queueing write request to " + gatt.getDevice()); 255 } 256 } 257 readCharacteristic( BluetoothGattCharacteristic characteristic, CharacteristicReadResultCallback callback)258 public void readCharacteristic( 259 BluetoothGattCharacteristic characteristic, 260 CharacteristicReadResultCallback callback) { 261 if (characteristic == null) { 262 callback.run(characteristic, BluetoothGatt.GATT_FAILURE); 263 return; 264 } 265 266 CharacteristicReadRequest request = new CharacteristicReadRequest(characteristic, callback); 267 requestQueue.add(request); 268 269 if (state.compareAndSet(ConnectionState.READY, ConnectionState.PENDING_RESPONSE)) { 270 maybeSendNextCommand(gatt); 271 } else { 272 Log.i(TAG, "Queueing read request to " + gatt.getDevice()); 273 } 274 } 275 writeDescriptor( BluetoothGattDescriptor descriptor, byte[] data, DescriptorWriteResultCallback callback)276 public void writeDescriptor( 277 BluetoothGattDescriptor descriptor, 278 byte[] data, 279 DescriptorWriteResultCallback callback) { 280 if (descriptor == null) { 281 callback.run(descriptor, BluetoothGatt.GATT_FAILURE); 282 return; 283 } 284 285 DescriptorWriteRequest request = new DescriptorWriteRequest(descriptor, data, callback); 286 requestQueue.add(request); 287 288 if (state.compareAndSet(ConnectionState.READY, ConnectionState.PENDING_RESPONSE)) { 289 maybeSendNextCommand(gatt); 290 } else { 291 Log.i(TAG, "Queueing write request to " + gatt.getDevice()); 292 } 293 } 294 setCharacteristicNotification( BluetoothGattCharacteristic characteristic, boolean enable)295 public boolean setCharacteristicNotification( 296 BluetoothGattCharacteristic characteristic, boolean enable) { 297 return gatt.setCharacteristicNotification(characteristic, enable); 298 } 299 requestMtu(int mtu, Consumer<Boolean> callback)300 public boolean requestMtu(int mtu, Consumer<Boolean> callback) { 301 lastRequestMtuCallback = callback; 302 303 return gatt.requestMtu(mtu); 304 } 305 refreshGattCache()306 public void refreshGattCache() { 307 gatt.discoverServices(); 308 } 309 closeGatt()310 private void closeGatt() { 311 disconnect(() -> bleConnectionCallback.onDisconnect(gatt, BluetoothGatt.GATT_FAILURE)); 312 } 313 disconnect(Runnable callback)314 private boolean disconnect(Runnable callback) { 315 ConnectionState prevState = state.getAndSet(ConnectionState.DISCONNECTED); 316 317 if (prevState != ConnectionState.UNINITIALIZED && 318 prevState != ConnectionState.DISCONNECTED) { 319 synchronized (state) { 320 // Acts as a barrier 321 322 callback.run(); 323 324 gatt.close(); 325 } 326 return true; 327 } 328 return false; 329 } 330 331 public interface Callback { 332 /** Called when GATT connection is set up and ready to handle requests. */ onGattReady(BluetoothGatt gatt)333 void onGattReady(BluetoothGatt gatt); 334 335 /** 336 * Notifies about GATT notification from the server. 337 * 338 * @param characteristic Characteristic to which notification is sent. 339 * @param data Payload sent by GATT server. 340 */ onNotification(BluetoothGattCharacteristic characteristic, byte[] data)341 void onNotification(BluetoothGattCharacteristic characteristic, byte[] data); 342 343 /** 344 * Notifies disconnection and corresponding status code. 345 * 346 * @param status Status code for disconnection reason. 347 */ onDisconnect(BluetoothGatt gatt, int status)348 void onDisconnect(BluetoothGatt gatt, int status); 349 } 350 351 private class GattCallback extends BluetoothGattCallback { 352 @Override onConnectionStateChange(BluetoothGatt gatt, int status, int newState)353 public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { 354 if (DEBUG) { 355 Log.d(TAG, 356 "Device " + gatt.getDevice() + " newState: " + newState + 357 ", status: " + status); 358 } 359 if (newState == BluetoothProfile.STATE_CONNECTED) { 360 if (state.compareAndSet( 361 ConnectionState.GATT_CONNECTING, 362 ConnectionState.SERVICE_DISCOVERING)) { 363 if (gatt.discoverServices()) { 364 return; 365 } 366 closeGatt(); 367 } 368 } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { 369 disconnect(() -> bleConnectionCallback.onDisconnect(gatt, status)); 370 } 371 } 372 373 @Override onServicesDiscovered(BluetoothGatt gatt, int status)374 public void onServicesDiscovered(BluetoothGatt gatt, int status) { 375 // state == SERVICE_DISCOVERING 376 if (status == BluetoothGatt.GATT_SUCCESS && 377 state.compareAndSet(ConnectionState.SERVICE_DISCOVERING, 378 ConnectionState.READY)) { 379 bleConnectionCallback.onGattReady(gatt); 380 return; 381 } 382 Log.w(TAG, "Failed to discover services for " + gatt.getDevice() + ": " + status); 383 closeGatt(); 384 } 385 386 @Override onDescriptorWrite( BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status)387 public void onDescriptorWrite( 388 BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { 389 if (lastDescriptorWriteCallback != null) { 390 lastDescriptorWriteCallback.run(descriptor, status); 391 lastDescriptorWriteCallback = null; 392 } 393 394 maybeSendNextCommand(gatt); 395 396 if (status != BluetoothGatt.GATT_SUCCESS) { 397 Log.w(TAG, 398 "Error response to write descriptor " + descriptor.getUuid() + 399 " to " + gatt.getDevice() + ": " + status); 400 } 401 } 402 403 @Override onCharacteristicWrite( BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status)404 public void onCharacteristicWrite( 405 BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { 406 if (lastCharacteristicWriteCallback != null) { 407 lastCharacteristicWriteCallback.run(characteristic, status); 408 lastCharacteristicWriteCallback = null; 409 } 410 411 maybeSendNextCommand(gatt); 412 413 if (status != BluetoothGatt.GATT_SUCCESS) { 414 Log.w(TAG, 415 "Error response to write characteristic " + characteristic.getUuid() + 416 " to " + gatt.getDevice() + ": " + status); 417 } 418 } 419 420 @Override onCharacteristicRead( BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status)421 public void onCharacteristicRead( 422 BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { 423 if (lastCharacteristicReadCallback != null) { 424 lastCharacteristicReadCallback.run(characteristic, status); 425 lastCharacteristicReadCallback = null; 426 } 427 428 maybeSendNextCommand(gatt); 429 430 if (status != BluetoothGatt.GATT_SUCCESS) { 431 Log.w(TAG, 432 "Error response to read characteristic " + characteristic.getUuid() + 433 " to " + gatt.getDevice() + ": " + status); 434 } 435 } 436 437 @Override onCharacteristicChanged( BluetoothGatt gatt, BluetoothGattCharacteristic characteristic)438 public void onCharacteristicChanged( 439 BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { 440 byte[] data = characteristic.getValue(); 441 bleConnectionCallback.onNotification(characteristic, data); 442 } 443 444 @Override onMtuChanged( BluetoothGatt gatt, int mtu, int status)445 public void onMtuChanged( 446 BluetoothGatt gatt, int mtu, int status) { 447 if (status == BluetoothGatt.GATT_SUCCESS) { 448 Log.i(TAG, "MTU size updated to " + mtu + " for device: " + gatt.getDevice()); 449 450 if (lastRequestMtuCallback != null) { 451 lastRequestMtuCallback.accept(true); 452 lastRequestMtuCallback = null; 453 } 454 } else { 455 if (lastRequestMtuCallback != null) { 456 lastRequestMtuCallback.accept(false); 457 lastRequestMtuCallback = null; 458 } 459 } 460 } 461 } 462 } 463