• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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