1 /* 2 * Copyright 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.android.server.nearby.common.bluetooth.gatt; 18 19 import android.bluetooth.BluetoothGattCharacteristic; 20 import android.bluetooth.BluetoothGattDescriptor; 21 import android.bluetooth.BluetoothGattService; 22 import android.bluetooth.BluetoothStatusCodes; 23 import android.util.Log; 24 25 import androidx.annotation.VisibleForTesting; 26 27 import com.android.server.nearby.common.bluetooth.BluetoothConsts; 28 import com.android.server.nearby.common.bluetooth.BluetoothException; 29 import com.android.server.nearby.common.bluetooth.BluetoothGattException; 30 import com.android.server.nearby.common.bluetooth.BluetoothTimeoutException; 31 import com.android.server.nearby.common.bluetooth.ReservedUuids; 32 import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattHelper.ConnectionOptions; 33 import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattHelper.OperationType; 34 import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothDevice; 35 import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothGattWrapper; 36 import com.android.server.nearby.common.bluetooth.util.BluetoothGattUtils; 37 import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor; 38 import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.Operation; 39 import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.SynchronousOperation; 40 41 import com.google.common.base.Preconditions; 42 43 import java.util.ArrayList; 44 import java.util.List; 45 import java.util.UUID; 46 import java.util.concurrent.BlockingDeque; 47 import java.util.concurrent.ConcurrentHashMap; 48 import java.util.concurrent.ConcurrentMap; 49 import java.util.concurrent.LinkedBlockingDeque; 50 import java.util.concurrent.TimeUnit; 51 52 import javax.annotation.Nullable; 53 import javax.annotation.concurrent.GuardedBy; 54 55 /** 56 * Gatt connection to a Bluetooth device. 57 */ 58 public class BluetoothGattConnection implements AutoCloseable { 59 60 private static final String TAG = BluetoothGattConnection.class.getSimpleName(); 61 62 @VisibleForTesting 63 static final long OPERATION_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(1); 64 @VisibleForTesting 65 static final long SLOW_OPERATION_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10); 66 67 @VisibleForTesting 68 static final int GATT_INTERNAL_ERROR = 129; 69 @VisibleForTesting 70 static final int GATT_ERROR = 133; 71 72 private final BluetoothGattWrapper mGatt; 73 private final BluetoothOperationExecutor mBluetoothOperationExecutor; 74 private final ConnectionOptions mConnectionOptions; 75 76 private volatile boolean mServicesDiscovered = false; 77 78 private volatile boolean mIsConnected = false; 79 80 private volatile int mMtu = BluetoothConsts.DEFAULT_MTU; 81 82 private final ConcurrentMap<BluetoothGattCharacteristic, ChangeObserver> mChangeObservers = 83 new ConcurrentHashMap<>(); 84 85 private final List<ConnectionCloseListener> mCloseListeners = new ArrayList<>(); 86 87 private long mOperationTimeoutMillis = OPERATION_TIMEOUT_MILLIS; 88 BluetoothGattConnection( BluetoothGattWrapper gatt, BluetoothOperationExecutor bluetoothOperationExecutor, ConnectionOptions connectionOptions)89 BluetoothGattConnection( 90 BluetoothGattWrapper gatt, 91 BluetoothOperationExecutor bluetoothOperationExecutor, 92 ConnectionOptions connectionOptions) { 93 mGatt = gatt; 94 mBluetoothOperationExecutor = bluetoothOperationExecutor; 95 mConnectionOptions = connectionOptions; 96 } 97 98 /** 99 * Set operation timeout. 100 */ setOperationTimeout(long timeoutMillis)101 public void setOperationTimeout(long timeoutMillis) { 102 Preconditions.checkArgument(timeoutMillis > 0, "invalid time out value"); 103 mOperationTimeoutMillis = timeoutMillis; 104 } 105 106 /** 107 * Returns connected device. 108 */ getDevice()109 public BluetoothDevice getDevice() { 110 return mGatt.getDevice(); 111 } 112 getConnectionOptions()113 public ConnectionOptions getConnectionOptions() { 114 return mConnectionOptions; 115 } 116 isConnected()117 public boolean isConnected() { 118 return mIsConnected; 119 } 120 121 /** 122 * Get service. 123 */ getService(UUID uuid)124 public BluetoothGattService getService(UUID uuid) throws BluetoothException { 125 Log.d(TAG, String.format("Getting service %s.", uuid)); 126 if (!mServicesDiscovered) { 127 discoverServices(); 128 } 129 BluetoothGattService match = null; 130 for (BluetoothGattService service : mGatt.getServices()) { 131 if (service.getUuid().equals(uuid)) { 132 if (match != null) { 133 throw new BluetoothException( 134 String.format("More than one service %s found on device %s.", 135 uuid, 136 mGatt.getDevice())); 137 } 138 match = service; 139 } 140 } 141 if (match == null) { 142 throw new BluetoothException(String.format("Service %s not found on device %s.", 143 uuid, 144 mGatt.getDevice())); 145 } 146 Log.d(TAG, "Service found."); 147 return match; 148 } 149 150 /** 151 * Returns a list of all characteristics under a given service UUID. 152 */ getCharacteristics(UUID serviceUuid)153 private List<BluetoothGattCharacteristic> getCharacteristics(UUID serviceUuid) 154 throws BluetoothException { 155 if (!mServicesDiscovered) { 156 discoverServices(); 157 } 158 ArrayList<BluetoothGattCharacteristic> characteristics = new ArrayList<>(); 159 for (BluetoothGattService service : mGatt.getServices()) { 160 // Add all characteristics under this service if its service UUID matches. 161 if (service.getUuid().equals(serviceUuid)) { 162 characteristics.addAll(service.getCharacteristics()); 163 } 164 } 165 return characteristics; 166 } 167 168 /** 169 * Get characteristic. 170 */ getCharacteristic(UUID serviceUuid, UUID characteristicUuid)171 public BluetoothGattCharacteristic getCharacteristic(UUID serviceUuid, 172 UUID characteristicUuid) throws BluetoothException { 173 Log.d(TAG, String.format("Getting characteristic %s on service %s.", characteristicUuid, 174 serviceUuid)); 175 BluetoothGattCharacteristic match = null; 176 for (BluetoothGattCharacteristic characteristic : getCharacteristics(serviceUuid)) { 177 if (characteristic.getUuid().equals(characteristicUuid)) { 178 if (match != null) { 179 throw new BluetoothException(String.format( 180 "More than one characteristic %s found on service %s on device %s.", 181 characteristicUuid, 182 serviceUuid, 183 mGatt.getDevice())); 184 } 185 match = characteristic; 186 } 187 } 188 if (match == null) { 189 throw new BluetoothException(String.format( 190 "Characteristic %s not found on service %s of device %s.", 191 characteristicUuid, 192 serviceUuid, 193 mGatt.getDevice())); 194 } 195 Log.d(TAG, "Characteristic found."); 196 return match; 197 } 198 199 /** 200 * Get descriptor. 201 */ getDescriptor(UUID serviceUuid, UUID characteristicUuid, UUID descriptorUuid)202 public BluetoothGattDescriptor getDescriptor(UUID serviceUuid, 203 UUID characteristicUuid, UUID descriptorUuid) throws BluetoothException { 204 Log.d(TAG, String.format("Getting descriptor %s on characteristic %s on service %s.", 205 descriptorUuid, characteristicUuid, serviceUuid)); 206 BluetoothGattDescriptor match = null; 207 for (BluetoothGattDescriptor descriptor : 208 getCharacteristic(serviceUuid, characteristicUuid).getDescriptors()) { 209 if (descriptor.getUuid().equals(descriptorUuid)) { 210 if (match != null) { 211 throw new BluetoothException(String.format("More than one descriptor %s found " 212 + "on characteristic %s service %s on device %s.", 213 descriptorUuid, 214 characteristicUuid, 215 serviceUuid, 216 mGatt.getDevice())); 217 } 218 match = descriptor; 219 } 220 } 221 if (match == null) { 222 throw new BluetoothException(String.format( 223 "Descriptor %s not found on characteristic %s on service %s of device %s.", 224 descriptorUuid, 225 characteristicUuid, 226 serviceUuid, 227 mGatt.getDevice())); 228 } 229 Log.d(TAG, "Descriptor found."); 230 return match; 231 } 232 233 /** 234 * Discover services. 235 */ discoverServices()236 public void discoverServices() throws BluetoothException { 237 mBluetoothOperationExecutor.execute( 238 new SynchronousOperation<Void>(OperationType.DISCOVER_SERVICES) { 239 @Nullable 240 @Override 241 public Void call() throws BluetoothException { 242 if (mServicesDiscovered) { 243 return null; 244 } 245 boolean forceRefresh = false; 246 try { 247 discoverServicesInternal(); 248 } catch (BluetoothException e) { 249 if (!(e instanceof BluetoothGattException)) { 250 throw e; 251 } 252 int errorCode = ((BluetoothGattException) e).getGattErrorCode(); 253 if (errorCode != GATT_ERROR && errorCode != GATT_INTERNAL_ERROR) { 254 throw e; 255 } 256 Log.e(TAG, e.getMessage() 257 + "\n Ignore the gatt error for post MNC apis and force " 258 + "a refresh"); 259 forceRefresh = true; 260 } 261 262 forceRefreshServiceCacheIfNeeded(forceRefresh); 263 264 mServicesDiscovered = true; 265 266 return null; 267 } 268 }); 269 } 270 discoverServicesInternal()271 private void discoverServicesInternal() throws BluetoothException { 272 Log.i(TAG, "Starting services discovery."); 273 long startTimeMillis = System.currentTimeMillis(); 274 try { 275 mBluetoothOperationExecutor.execute( 276 new Operation<Void>(OperationType.DISCOVER_SERVICES_INTERNAL, mGatt) { 277 @Override 278 public void run() throws BluetoothException { 279 boolean success = mGatt.discoverServices(); 280 if (!success) { 281 throw new BluetoothException( 282 "gatt.discoverServices returned false."); 283 } 284 } 285 }, 286 SLOW_OPERATION_TIMEOUT_MILLIS); 287 Log.i(TAG, String.format("Services discovered successfully in %s ms.", 288 System.currentTimeMillis() - startTimeMillis)); 289 } catch (BluetoothException e) { 290 if (e instanceof BluetoothGattException) { 291 throw new BluetoothGattException(String.format( 292 "Failed to discover services on device: %s.", 293 mGatt.getDevice()), ((BluetoothGattException) e).getGattErrorCode(), e); 294 } else { 295 throw new BluetoothException(String.format( 296 "Failed to discover services on device: %s.", 297 mGatt.getDevice()), e); 298 } 299 } 300 } 301 hasDynamicServices()302 private boolean hasDynamicServices() { 303 BluetoothGattService gattService = 304 mGatt.getService(ReservedUuids.Services.GENERIC_ATTRIBUTE); 305 if (gattService != null) { 306 BluetoothGattCharacteristic serviceChange = 307 gattService.getCharacteristic(ReservedUuids.Characteristics.SERVICE_CHANGE); 308 if (serviceChange != null) { 309 return true; 310 } 311 } 312 313 // Check whether the server contains a self defined service dynamic characteristic. 314 gattService = mGatt.getService(BluetoothConsts.SERVICE_DYNAMIC_SERVICE); 315 if (gattService != null) { 316 BluetoothGattCharacteristic serviceChange = 317 gattService.getCharacteristic(BluetoothConsts.SERVICE_DYNAMIC_CHARACTERISTIC); 318 if (serviceChange != null) { 319 return true; 320 } 321 } 322 323 return false; 324 } 325 forceRefreshServiceCacheIfNeeded(boolean forceRefresh)326 private void forceRefreshServiceCacheIfNeeded(boolean forceRefresh) throws BluetoothException { 327 if (mGatt.getDevice().getBondState() != BluetoothDevice.BOND_BONDED) { 328 // Device is not bonded, so services should not have been cached. 329 return; 330 } 331 332 if (!forceRefresh && !hasDynamicServices()) { 333 return; 334 } 335 Log.i(TAG, "Forcing a refresh of local cache of GATT services"); 336 boolean success = mGatt.refresh(); 337 if (!success) { 338 throw new BluetoothException("gatt.refresh returned false."); 339 } 340 discoverServicesInternal(); 341 } 342 343 /** 344 * Read characteristic. 345 */ readCharacteristic(UUID serviceUuid, UUID characteristicUuid)346 public byte[] readCharacteristic(UUID serviceUuid, UUID characteristicUuid) 347 throws BluetoothException { 348 return readCharacteristic(getCharacteristic(serviceUuid, characteristicUuid)); 349 } 350 351 /** 352 * Read characteristic. 353 */ readCharacteristic(final BluetoothGattCharacteristic characteristic)354 public byte[] readCharacteristic(final BluetoothGattCharacteristic characteristic) 355 throws BluetoothException { 356 try { 357 return mBluetoothOperationExecutor.executeNonnull( 358 new Operation<byte[]>(OperationType.READ_CHARACTERISTIC, mGatt, 359 characteristic) { 360 @Override 361 public void run() throws BluetoothException { 362 boolean success = mGatt.readCharacteristic(characteristic); 363 if (!success) { 364 throw new BluetoothException( 365 "gatt.readCharacteristic returned false."); 366 } 367 } 368 }, 369 mOperationTimeoutMillis); 370 } catch (BluetoothException e) { 371 throw new BluetoothException(String.format( 372 "Failed to read %s on device %s.", 373 BluetoothGattUtils.toString(characteristic), 374 mGatt.getDevice()), e); 375 } 376 } 377 378 /** 379 * Writes Characteristic. 380 */ 381 public void writeCharacteristic(UUID serviceUuid, UUID characteristicUuid, byte[] value) 382 throws BluetoothException { 383 writeCharacteristic(getCharacteristic(serviceUuid, characteristicUuid), value); 384 } 385 386 /** 387 * Writes Characteristic. 388 */ 389 public void writeCharacteristic(final BluetoothGattCharacteristic characteristic, 390 final byte[] value) throws BluetoothException { 391 Log.d(TAG, String.format("Writing %d bytes on %s on device %s.", 392 value.length, 393 BluetoothGattUtils.toString(characteristic), 394 mGatt.getDevice())); 395 if ((characteristic.getProperties() & (BluetoothGattCharacteristic.PROPERTY_WRITE 396 | BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE)) == 0) { 397 throw new BluetoothException(String.format("%s is not writable!", characteristic)); 398 } 399 try { 400 mBluetoothOperationExecutor.execute( 401 new Operation<Void>(OperationType.WRITE_CHARACTERISTIC, mGatt, characteristic) { 402 @Override 403 public void run() throws BluetoothException { 404 int writeCharacteristicResponseCode = mGatt.writeCharacteristic( 405 characteristic, value, characteristic.getWriteType()); 406 if (writeCharacteristicResponseCode != BluetoothStatusCodes.SUCCESS) { 407 throw new BluetoothException( 408 "gatt.writeCharacteristic returned " 409 + writeCharacteristicResponseCode); 410 } 411 } 412 }, 413 mOperationTimeoutMillis); 414 } catch (BluetoothException e) { 415 throw new BluetoothException(String.format( 416 "Failed to write %s on device %s.", 417 BluetoothGattUtils.toString(characteristic), 418 mGatt.getDevice()), e); 419 } 420 Log.d(TAG, "Writing characteristic done."); 421 } 422 423 /** 424 * Reads descriptor. 425 */ 426 public byte[] readDescriptor(UUID serviceUuid, UUID characteristicUuid, UUID descriptorUuid) 427 throws BluetoothException { 428 return readDescriptor(getDescriptor(serviceUuid, characteristicUuid, descriptorUuid)); 429 } 430 431 /** 432 * Reads descriptor. 433 */ 434 public byte[] readDescriptor(final BluetoothGattDescriptor descriptor) 435 throws BluetoothException { 436 try { 437 return mBluetoothOperationExecutor.executeNonnull( 438 new Operation<byte[]>(OperationType.READ_DESCRIPTOR, mGatt, descriptor) { 439 @Override 440 public void run() throws BluetoothException { 441 boolean success = mGatt.readDescriptor(descriptor); 442 if (!success) { 443 throw new BluetoothException("gatt.readDescriptor returned false."); 444 } 445 } 446 }, 447 mOperationTimeoutMillis); 448 } catch (BluetoothException e) { 449 throw new BluetoothException(String.format( 450 "Failed to read %s on %s on device %s.", 451 descriptor.getUuid(), 452 BluetoothGattUtils.toString(descriptor), 453 mGatt.getDevice()), e); 454 } 455 } 456 457 /** 458 * Writes descriptor. 459 */ 460 public void writeDescriptor(UUID serviceUuid, UUID characteristicUuid, UUID descriptorUuid, 461 byte[] value) throws BluetoothException { 462 writeDescriptor(getDescriptor(serviceUuid, characteristicUuid, descriptorUuid), value); 463 } 464 465 /** 466 * Writes descriptor. 467 */ 468 public void writeDescriptor(final BluetoothGattDescriptor descriptor, final byte[] value) 469 throws BluetoothException { 470 Log.d(TAG, String.format( 471 "Writing %d bytes on %s on device %s.", 472 value.length, 473 BluetoothGattUtils.toString(descriptor), 474 mGatt.getDevice())); 475 long startTimeMillis = System.currentTimeMillis(); 476 try { 477 mBluetoothOperationExecutor.execute( 478 new Operation<Void>(OperationType.WRITE_DESCRIPTOR, mGatt, descriptor) { 479 @Override 480 public void run() throws BluetoothException { 481 int writeDescriptorResponseCode = mGatt.writeDescriptor(descriptor, 482 value); 483 if (writeDescriptorResponseCode != BluetoothStatusCodes.SUCCESS) { 484 throw new BluetoothException( 485 "gatt.writeDescriptor returned " 486 + writeDescriptorResponseCode); 487 } 488 } 489 }, 490 mOperationTimeoutMillis); 491 Log.d(TAG, String.format("Writing descriptor done in %s ms.", 492 System.currentTimeMillis() - startTimeMillis)); 493 } catch (BluetoothException e) { 494 throw new BluetoothException(String.format( 495 "Failed to write %s on device %s.", 496 BluetoothGattUtils.toString(descriptor), 497 mGatt.getDevice()), e); 498 } 499 } 500 501 /** 502 * Reads remote Rssi. 503 */ 504 public int readRemoteRssi() throws BluetoothException { 505 try { 506 return mBluetoothOperationExecutor.executeNonnull( 507 new Operation<Integer>(OperationType.READ_RSSI, mGatt) { 508 @Override 509 public void run() throws BluetoothException { 510 boolean success = mGatt.readRemoteRssi(); 511 if (!success) { 512 throw new BluetoothException("gatt.readRemoteRssi returned false."); 513 } 514 } 515 }, 516 mOperationTimeoutMillis); 517 } catch (BluetoothException e) { 518 throw new BluetoothException( 519 String.format("Failed to read rssi on device %s.", mGatt.getDevice()), e); 520 } 521 } 522 523 public int getMtu() { 524 return mMtu; 525 } 526 527 /** 528 * Get max data packet size. 529 */ 530 public int getMaxDataPacketSize() { 531 // Per BT specs (3.2.9), only MTU - 3 bytes can be used to transmit data 532 return mMtu - 3; 533 } 534 535 /** Set notification enabled or disabled. */ 536 @VisibleForTesting 537 public void setNotificationEnabled(BluetoothGattCharacteristic characteristic, boolean enabled) 538 throws BluetoothException { 539 boolean isIndication; 540 int properties = characteristic.getProperties(); 541 if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) { 542 isIndication = false; 543 } else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0) { 544 isIndication = true; 545 } else { 546 throw new BluetoothException(String.format( 547 "%s on device %s supports neither notifications nor indications.", 548 BluetoothGattUtils.toString(characteristic), 549 mGatt.getDevice())); 550 } 551 BluetoothGattDescriptor clientConfigDescriptor = 552 characteristic.getDescriptor( 553 ReservedUuids.Descriptors.CLIENT_CHARACTERISTIC_CONFIGURATION); 554 if (clientConfigDescriptor == null) { 555 throw new BluetoothException(String.format( 556 "%s on device %s is missing client config descriptor.", 557 BluetoothGattUtils.toString(characteristic), 558 mGatt.getDevice())); 559 } 560 long startTime = System.currentTimeMillis(); 561 Log.d(TAG, String.format("%s %s on characteristic %s.", enabled ? "Enabling" : "Disabling", 562 isIndication ? "indication" : "notification", characteristic.getUuid())); 563 564 if (enabled) { 565 mGatt.setCharacteristicNotification(characteristic, enabled); 566 } 567 writeDescriptor(clientConfigDescriptor, 568 enabled 569 ? (isIndication 570 ? BluetoothGattDescriptor.ENABLE_INDICATION_VALUE : 571 BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE) 572 : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE); 573 if (!enabled) { 574 mGatt.setCharacteristicNotification(characteristic, enabled); 575 } 576 577 Log.d(TAG, String.format("Done in %d ms.", System.currentTimeMillis() - startTime)); 578 } 579 580 /** 581 * Enables notification. 582 */ 583 public ChangeObserver enableNotification(UUID serviceUuid, UUID characteristicUuid) 584 throws BluetoothException { 585 return enableNotification(getCharacteristic(serviceUuid, characteristicUuid)); 586 } 587 588 /** 589 * Enables notification. 590 */ 591 public ChangeObserver enableNotification(final BluetoothGattCharacteristic characteristic) 592 throws BluetoothException { 593 return mBluetoothOperationExecutor.executeNonnull( 594 new SynchronousOperation<ChangeObserver>( 595 OperationType.NOTIFICATION_CHANGE, 596 characteristic) { 597 @Override 598 public ChangeObserver call() throws BluetoothException { 599 ChangeObserver changeObserver = new ChangeObserver(); 600 mChangeObservers.put(characteristic, changeObserver); 601 setNotificationEnabled(characteristic, true); 602 return changeObserver; 603 } 604 }); 605 } 606 607 /** 608 * Disables notification. 609 */ 610 public void disableNotification(UUID serviceUuid, UUID characteristicUuid) 611 throws BluetoothException { 612 disableNotification(getCharacteristic(serviceUuid, characteristicUuid)); 613 } 614 615 /** 616 * Disables notification. 617 */ 618 public void disableNotification(final BluetoothGattCharacteristic characteristic) 619 throws BluetoothException { 620 mBluetoothOperationExecutor.execute( 621 new SynchronousOperation<Void>( 622 OperationType.NOTIFICATION_CHANGE, 623 characteristic) { 624 @Nullable 625 @Override 626 public Void call() throws BluetoothException { 627 setNotificationEnabled(characteristic, false); 628 mChangeObservers.remove(characteristic); 629 return null; 630 } 631 }); 632 } 633 634 /** 635 * Adds a close listener. 636 */ 637 public void addCloseListener(ConnectionCloseListener listener) { 638 mCloseListeners.add(listener); 639 if (!mIsConnected) { 640 listener.onClose(); 641 return; 642 } 643 } 644 645 /** 646 * Removes a close listener. 647 */ 648 public void removeCloseListener(ConnectionCloseListener listener) { 649 mCloseListeners.remove(listener); 650 } 651 652 /** onCharacteristicChanged callback. */ 653 public void onCharacteristicChanged(BluetoothGattCharacteristic characteristic, byte[] value) { 654 ChangeObserver observer = mChangeObservers.get(characteristic); 655 if (observer == null) { 656 return; 657 } 658 observer.onValueChange(value); 659 } 660 661 @Override 662 public void close() throws BluetoothException { 663 Log.d(TAG, "close"); 664 try { 665 if (!mIsConnected) { 666 // Don't call disconnect on a closed connection, since Android framework won't 667 // provide any feedback. 668 return; 669 } 670 mBluetoothOperationExecutor.execute( 671 new Operation<Void>(OperationType.DISCONNECT, mGatt.getDevice()) { 672 @Override 673 public void run() throws BluetoothException { 674 mGatt.disconnect(); 675 } 676 }, mOperationTimeoutMillis); 677 } finally { 678 mGatt.close(); 679 } 680 } 681 682 /** onConnected callback. */ 683 public void onConnected() { 684 Log.d(TAG, "onConnected"); 685 mIsConnected = true; 686 } 687 688 /** onClosed callback. */ 689 public void onClosed() { 690 Log.d(TAG, "onClosed"); 691 if (!mIsConnected) { 692 return; 693 } 694 mIsConnected = false; 695 for (ConnectionCloseListener listener : mCloseListeners) { 696 listener.onClose(); 697 } 698 mGatt.close(); 699 } 700 701 /** 702 * Observer to wait or be called back when value change. 703 */ 704 public static class ChangeObserver { 705 706 private final BlockingDeque<byte[]> mValues = new LinkedBlockingDeque<byte[]>(); 707 708 @GuardedBy("mValues") 709 @Nullable 710 private volatile CharacteristicChangeListener mListener; 711 712 /** 713 * Set listener. 714 */ 715 public void setListener(@Nullable CharacteristicChangeListener listener) { 716 synchronized (mValues) { 717 mListener = listener; 718 if (listener != null) { 719 byte[] value; 720 while ((value = mValues.poll()) != null) { 721 listener.onValueChange(value); 722 } 723 } 724 } 725 } 726 727 /** 728 * onValueChange callback. 729 */ 730 public void onValueChange(byte[] newValue) { 731 synchronized (mValues) { 732 CharacteristicChangeListener listener = mListener; 733 if (listener == null) { 734 mValues.add(newValue); 735 } else { 736 listener.onValueChange(newValue); 737 } 738 } 739 } 740 741 /** 742 * Waits for update for a given time. 743 */ 744 public byte[] waitForUpdate(long timeoutMillis) throws BluetoothException { 745 byte[] result; 746 try { 747 result = mValues.poll(timeoutMillis, TimeUnit.MILLISECONDS); 748 } catch (InterruptedException e) { 749 Thread.currentThread().interrupt(); 750 throw new BluetoothException("Operation interrupted."); 751 } 752 if (result == null) { 753 throw new BluetoothTimeoutException( 754 String.format("Operation timed out after %dms", timeoutMillis)); 755 } 756 return result; 757 } 758 } 759 760 /** 761 * Listener for characteristic data changes over notifications or indications. 762 */ 763 public interface CharacteristicChangeListener { 764 765 /** 766 * onValueChange callback. 767 */ 768 void onValueChange(byte[] newValue); 769 } 770 771 /** 772 * Listener for connection close events. 773 */ 774 public interface ConnectionCloseListener { 775 776 /** 777 * onClose callback. 778 */ 779 void onClose(); 780 } 781 } 782