• 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.BluetoothGatt;
20 import android.bluetooth.BluetoothGattCharacteristic;
21 import android.bluetooth.BluetoothGattDescriptor;
22 import android.bluetooth.le.ScanFilter;
23 import android.bluetooth.le.ScanSettings;
24 import android.content.Context;
25 import android.os.ParcelUuid;
26 import android.util.Log;
27 
28 import androidx.annotation.IntDef;
29 import androidx.annotation.VisibleForTesting;
30 
31 import com.android.server.nearby.common.bluetooth.BluetoothException;
32 import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothAdapter;
33 import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothDevice;
34 import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothGattCallback;
35 import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothGattWrapper;
36 import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le.BluetoothLeScanner;
37 import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le.ScanCallback;
38 import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le.ScanResult;
39 import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor;
40 import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.BluetoothOperationTimeoutException;
41 import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.Operation;
42 
43 import com.google.common.base.Preconditions;
44 
45 import java.lang.annotation.Retention;
46 import java.lang.annotation.RetentionPolicy;
47 import java.util.Arrays;
48 import java.util.Locale;
49 import java.util.Objects;
50 import java.util.Optional;
51 import java.util.UUID;
52 import java.util.concurrent.ConcurrentHashMap;
53 import java.util.concurrent.ConcurrentMap;
54 import java.util.concurrent.TimeUnit;
55 
56 import javax.annotation.Nullable;
57 import javax.annotation.concurrent.GuardedBy;
58 
59 /**
60  * Wrapper of {@link BluetoothGattWrapper} that provides blocking methods, errors and timeout
61  * handling.
62  */
63 @SuppressWarnings("Guava") // java.util.Optional is not available until API 24
64 public class BluetoothGattHelper {
65 
66     private static final String TAG = BluetoothGattHelper.class.getSimpleName();
67 
68     @VisibleForTesting
69     static final long LOW_LATENCY_SCAN_MILLIS = TimeUnit.SECONDS.toMillis(5);
70     private static final long POLL_INTERVAL_MILLIS = 5L /* milliseconds */;
71 
72     /**
73      * BT operation types that can be in flight.
74      */
75     @Retention(RetentionPolicy.SOURCE)
76     @IntDef(
77             value = {
78                     OperationType.SCAN,
79                     OperationType.CONNECT,
80                     OperationType.DISCOVER_SERVICES,
81                     OperationType.DISCOVER_SERVICES_INTERNAL,
82                     OperationType.NOTIFICATION_CHANGE,
83                     OperationType.READ_CHARACTERISTIC,
84                     OperationType.WRITE_CHARACTERISTIC,
85                     OperationType.READ_DESCRIPTOR,
86                     OperationType.WRITE_DESCRIPTOR,
87                     OperationType.READ_RSSI,
88                     OperationType.WRITE_RELIABLE,
89                     OperationType.CHANGE_MTU,
90                     OperationType.DISCONNECT,
91             })
92     public @interface OperationType {
93         int SCAN = 0;
94         int CONNECT = 1;
95         int DISCOVER_SERVICES = 2;
96         int DISCOVER_SERVICES_INTERNAL = 3;
97         int NOTIFICATION_CHANGE = 4;
98         int READ_CHARACTERISTIC = 5;
99         int WRITE_CHARACTERISTIC = 6;
100         int READ_DESCRIPTOR = 7;
101         int WRITE_DESCRIPTOR = 8;
102         int READ_RSSI = 9;
103         int WRITE_RELIABLE = 10;
104         int CHANGE_MTU = 11;
105         int DISCONNECT = 12;
106     }
107 
108     @VisibleForTesting
109     final ScanCallback mScanCallback = new InternalScanCallback();
110     @VisibleForTesting
111     final BluetoothGattCallback mBluetoothGattCallback =
112             new InternalBluetoothGattCallback();
113     @VisibleForTesting
114     final ConcurrentMap<BluetoothGattWrapper, BluetoothGattConnection> mConnections =
115             new ConcurrentHashMap<>();
116 
117     private final Context mApplicationContext;
118     private final BluetoothAdapter mBluetoothAdapter;
119     private final BluetoothOperationExecutor mBluetoothOperationExecutor;
120 
121     @VisibleForTesting
BluetoothGattHelper( Context applicationContext, BluetoothAdapter bluetoothAdapter, BluetoothOperationExecutor bluetoothOperationExecutor)122     BluetoothGattHelper(
123             Context applicationContext,
124             BluetoothAdapter bluetoothAdapter,
125             BluetoothOperationExecutor bluetoothOperationExecutor) {
126         mApplicationContext = applicationContext;
127         mBluetoothAdapter = bluetoothAdapter;
128         mBluetoothOperationExecutor = bluetoothOperationExecutor;
129     }
130 
BluetoothGattHelper(Context applicationContext, BluetoothAdapter bluetoothAdapter)131     public BluetoothGattHelper(Context applicationContext, BluetoothAdapter bluetoothAdapter) {
132         this(
133                 Preconditions.checkNotNull(applicationContext),
134                 Preconditions.checkNotNull(bluetoothAdapter),
135                 new BluetoothOperationExecutor(5));
136     }
137 
138     /**
139      * Auto-connects a serice Uuid.
140      */
autoConnect(final UUID serviceUuid)141     public BluetoothGattConnection autoConnect(final UUID serviceUuid) throws BluetoothException {
142         Log.d(TAG, String.format("Starting autoconnection to a device advertising service %s.",
143                 serviceUuid));
144         BluetoothDevice device = null;
145         int retries = 3;
146         final BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner();
147         if (scanner == null) {
148             throw new BluetoothException("Bluetooth is disabled or LE is not supported.");
149         }
150         final ScanFilter serviceFilter = new ScanFilter.Builder()
151                 .setServiceUuid(new ParcelUuid(serviceUuid))
152                 .build();
153         ScanSettings.Builder scanSettingsBuilder = new ScanSettings.Builder()
154                 .setReportDelay(0);
155         final ScanSettings scanSettingsLowLatency = scanSettingsBuilder
156                 .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
157                 .build();
158         final ScanSettings scanSettingsLowPower = scanSettingsBuilder
159                 .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
160                 .build();
161         while (true) {
162             long startTimeMillis = System.currentTimeMillis();
163             try {
164                 Log.d(TAG, "Starting low latency scanning.");
165                 device =
166                         mBluetoothOperationExecutor.executeNonnull(
167                                 new Operation<BluetoothDevice>(OperationType.SCAN) {
168                                     @Override
169                                     public void run() throws BluetoothException {
170                                         scanner.startScan(Arrays.asList(serviceFilter),
171                                                 scanSettingsLowLatency, mScanCallback);
172                                     }
173                                 }, LOW_LATENCY_SCAN_MILLIS);
174             } catch (BluetoothOperationTimeoutException e) {
175                 Log.d(TAG, String.format(
176                         "Cannot find a nearby device in low latency scanning after %s ms.",
177                         LOW_LATENCY_SCAN_MILLIS));
178             } finally {
179                 scanner.stopScan(mScanCallback);
180             }
181             if (device == null) {
182                 Log.d(TAG, "Starting low power scanning.");
183                 try {
184                     device = mBluetoothOperationExecutor.executeNonnull(
185                             new Operation<BluetoothDevice>(OperationType.SCAN) {
186                                 @Override
187                                 public void run() throws BluetoothException {
188                                     scanner.startScan(Arrays.asList(serviceFilter),
189                                             scanSettingsLowPower, mScanCallback);
190                                 }
191                             });
192                 } finally {
193                     scanner.stopScan(mScanCallback);
194                 }
195             }
196             Log.d(TAG, String.format("Scanning done in %d ms. Found device %s.",
197                     System.currentTimeMillis() - startTimeMillis, device));
198 
199             try {
200                 return connect(device);
201             } catch (BluetoothException e) {
202                 retries--;
203                 if (retries == 0) {
204                     throw e;
205                 } else {
206                     Log.d(TAG, String.format(
207                             "Connection failed: %s. Retrying %d more times.", e, retries));
208                 }
209             }
210         }
211     }
212 
213     /**
214      * Connects to a device using default connection options.
215      */
connect(BluetoothDevice bluetoothDevice)216     public BluetoothGattConnection connect(BluetoothDevice bluetoothDevice)
217             throws BluetoothException {
218         return connect(bluetoothDevice, ConnectionOptions.builder().build());
219     }
220 
221     /**
222      * Connects to a device using specifies connection options.
223      */
connect( BluetoothDevice bluetoothDevice, ConnectionOptions options)224     public BluetoothGattConnection connect(
225             BluetoothDevice bluetoothDevice, ConnectionOptions options) throws BluetoothException {
226         Log.d(TAG, String.format("Connecting to device %s.", bluetoothDevice));
227         long startTimeMillis = System.currentTimeMillis();
228 
229         Operation<BluetoothGattConnection> connectOperation =
230                 new Operation<BluetoothGattConnection>(OperationType.CONNECT, bluetoothDevice) {
231                     private final Object mLock = new Object();
232 
233                     @GuardedBy("mLock")
234                     private boolean mIsCanceled = false;
235 
236                     @GuardedBy("mLock")
237                     @Nullable(/* null before operation is executed */)
238                     private BluetoothGattWrapper mBluetoothGatt;
239 
240                     @Override
241                     public void run() throws BluetoothException {
242                         synchronized (mLock) {
243                             if (mIsCanceled) {
244                                 return;
245                             }
246                             BluetoothGattWrapper bluetoothGattWrapper;
247                             Log.d(TAG, "Use LE transport");
248                             bluetoothGattWrapper =
249                                     bluetoothDevice.connectGatt(
250                                             mApplicationContext,
251                                             options.autoConnect(),
252                                             mBluetoothGattCallback,
253                                             android.bluetooth.BluetoothDevice.TRANSPORT_LE);
254                             if (bluetoothGattWrapper == null) {
255                                 throw new BluetoothException("connectGatt() returned null.");
256                             }
257 
258                             try {
259                                 // Set connection priority without waiting for connection callback.
260                                 // Per code, btif_gatt_client.c, when priority is set before
261                                 // connection, this sets preferred connection parameters that will
262                                 // be used during the connection establishment.
263                                 Optional<Integer> connectionPriorityOption =
264                                         options.connectionPriority();
265                                 if (connectionPriorityOption.isPresent()) {
266                                     // requestConnectionPriority can only be called when
267                                     // BluetoothGatt is connected to the system BluetoothGatt
268                                     // service (see android/bluetooth/BluetoothGatt.java code).
269                                     // However, there is no callback to the app to inform when this
270                                     // is done. requestConnectionPriority will returns false with no
271                                     // side-effect before the service is connected, so we just poll
272                                     // here until true is returned.
273                                     int connectionPriority = connectionPriorityOption.get();
274                                     long startTimeMillis = System.currentTimeMillis();
275                                     while (!bluetoothGattWrapper.requestConnectionPriority(
276                                             connectionPriority)) {
277                                         if (System.currentTimeMillis() - startTimeMillis
278                                                 > options.connectionTimeoutMillis()) {
279                                             throw new BluetoothException(
280                                                     String.format(
281                                                             Locale.US,
282                                                             "Failed to set connectionPriority "
283                                                                     + "after %dms.",
284                                                             options.connectionTimeoutMillis()));
285                                         }
286                                         try {
287                                             Thread.sleep(POLL_INTERVAL_MILLIS);
288                                         } catch (InterruptedException e) {
289                                             Thread.currentThread().interrupt();
290                                             throw new BluetoothException(
291                                                     "connect() operation interrupted.");
292                                         }
293                                     }
294                                 }
295                             } catch (Exception e) {
296                                 // Make sure to clean connection.
297                                 bluetoothGattWrapper.disconnect();
298                                 bluetoothGattWrapper.close();
299                                 throw e;
300                             }
301 
302                             BluetoothGattConnection connection = new BluetoothGattConnection(
303                                     bluetoothGattWrapper, mBluetoothOperationExecutor, options);
304                             mConnections.put(bluetoothGattWrapper, connection);
305                             mBluetoothGatt = bluetoothGattWrapper;
306                         }
307                     }
308 
309                     @Override
310                     public void cancel() {
311                         // Clean connection if connection times out.
312                         synchronized (mLock) {
313                             if (mIsCanceled) {
314                                 return;
315                             }
316                             mIsCanceled = true;
317                             BluetoothGattWrapper bluetoothGattWrapper = mBluetoothGatt;
318                             if (bluetoothGattWrapper == null) {
319                                 return;
320                             }
321                             mConnections.remove(bluetoothGattWrapper);
322                             bluetoothGattWrapper.disconnect();
323                             bluetoothGattWrapper.close();
324                         }
325                     }
326                 };
327         BluetoothGattConnection result;
328         if (options.autoConnect()) {
329             result = mBluetoothOperationExecutor.executeNonnull(connectOperation);
330         } else {
331             result =
332                     mBluetoothOperationExecutor.executeNonnull(
333                             connectOperation, options.connectionTimeoutMillis());
334         }
335         Log.d(TAG, String.format("Connection success in %d ms.",
336                 System.currentTimeMillis() - startTimeMillis));
337         return result;
338     }
339 
getConnectionByGatt(BluetoothGattWrapper gatt)340     private BluetoothGattConnection getConnectionByGatt(BluetoothGattWrapper gatt)
341             throws BluetoothException {
342         BluetoothGattConnection connection = mConnections.get(gatt);
343         if (connection == null) {
344             throw new BluetoothException("Receive callback on unexpected device: " + gatt);
345         }
346         return connection;
347     }
348 
349     private class InternalBluetoothGattCallback extends BluetoothGattCallback {
350 
351         @Override
onConnectionStateChange(BluetoothGattWrapper gatt, int status, int newState)352         public void onConnectionStateChange(BluetoothGattWrapper gatt, int status, int newState) {
353             BluetoothGattConnection connection;
354             BluetoothDevice device = gatt.getDevice();
355             switch (newState) {
356                 case BluetoothGatt.STATE_CONNECTED: {
357                     connection = mConnections.get(gatt);
358                     if (connection == null) {
359                         Log.w(TAG, String.format(
360                                 "Received unexpected successful connection for dev %s! Ignoring.",
361                                 device));
362                         break;
363                     }
364 
365                     Operation<BluetoothGattConnection> operation =
366                             new Operation<>(OperationType.CONNECT, device);
367                     if (status != BluetoothGatt.GATT_SUCCESS) {
368                         mConnections.remove(gatt);
369                         gatt.disconnect();
370                         gatt.close();
371                         mBluetoothOperationExecutor.notifyCompletion(operation, status, null);
372                         break;
373                     }
374 
375                     // Process connection options
376                     ConnectionOptions options = connection.getConnectionOptions();
377                     Optional<Integer> mtuOption = options.mtu();
378                     if (mtuOption.isPresent()) {
379                         // Requesting MTU and waiting for MTU callback.
380                         boolean success = gatt.requestMtu(mtuOption.get());
381                         if (!success) {
382                             mBluetoothOperationExecutor.notifyFailure(operation,
383                                     new BluetoothException(String.format(Locale.US,
384                                             "Failed to request MTU of %d for dev %s: "
385                                                     + "returned false.",
386                                             mtuOption.get(), device)));
387                             // Make sure to clean connection.
388                             mConnections.remove(gatt);
389                             gatt.disconnect();
390                             gatt.close();
391                         }
392                         break;
393                     }
394 
395                     // Connection successful
396                     connection.onConnected();
397                     mBluetoothOperationExecutor.notifyCompletion(operation, status, connection);
398                     break;
399                 }
400                 case BluetoothGatt.STATE_DISCONNECTED: {
401                     connection = mConnections.remove(gatt);
402                     if (connection == null) {
403                         Log.w(TAG, String.format("Received unexpected disconnection"
404                                 + " for device %s! Ignoring.", device));
405                         break;
406                     }
407                     if (!connection.isConnected()) {
408                         // This is a failed connection attempt
409                         if (status == BluetoothGatt.GATT_SUCCESS) {
410                             // This is weird... considering this as a failure
411                             Log.w(TAG, String.format(
412                                     "Received a success for a failed connection "
413                                             + "attempt for device %s! Ignoring.", device));
414                             status = BluetoothGatt.GATT_FAILURE;
415                         }
416                         mBluetoothOperationExecutor
417                                 .notifyCompletion(new Operation<BluetoothGattConnection>(
418                                         OperationType.CONNECT, device), status, null);
419                         // Clean Gatt object in every case.
420                         gatt.disconnect();
421                         gatt.close();
422                         break;
423                     }
424                     connection.onClosed();
425                     mBluetoothOperationExecutor.notifyCompletion(
426                             new Operation<>(OperationType.DISCONNECT, device), status);
427                     break;
428                 }
429                 default:
430                     Log.e(TAG, "Unexpected connection state: " + newState);
431             }
432         }
433 
434         @Override
onMtuChanged(BluetoothGattWrapper gatt, int mtu, int status)435         public void onMtuChanged(BluetoothGattWrapper gatt, int mtu, int status) {
436             BluetoothGattConnection connection = mConnections.get(gatt);
437             BluetoothDevice device = gatt.getDevice();
438             if (connection == null) {
439                 Log.w(TAG, String.format(
440                         "Received unexpected MTU change for device %s! Ignoring.", device));
441                 return;
442             }
443             if (connection.isConnected()) {
444                 // This is the callback for the deprecated BluetoothGattConnection.requestMtu.
445                 mBluetoothOperationExecutor.notifyCompletion(
446                         new Operation<>(OperationType.CHANGE_MTU, gatt), status, mtu);
447             } else {
448                 // This is the callback when requesting MTU right after connecting.
449                 connection.onConnected();
450                 mBluetoothOperationExecutor.notifyCompletion(
451                         new Operation<>(OperationType.CONNECT, device), status, connection);
452                 if (status != BluetoothGatt.GATT_SUCCESS) {
453                     Log.w(TAG, String.format(
454                             "%s responds MTU change failed, status %s.", device, status));
455                     // Clean connection if it's failed.
456                     mConnections.remove(gatt);
457                     gatt.disconnect();
458                     gatt.close();
459                     return;
460                 }
461             }
462         }
463 
464         @Override
onServicesDiscovered(BluetoothGattWrapper gatt, int status)465         public void onServicesDiscovered(BluetoothGattWrapper gatt, int status) {
466             mBluetoothOperationExecutor.notifyCompletion(
467                     new Operation<Void>(OperationType.DISCOVER_SERVICES_INTERNAL, gatt), status);
468         }
469 
470         @Override
onCharacteristicRead(BluetoothGattWrapper gatt, BluetoothGattCharacteristic characteristic, int status)471         public void onCharacteristicRead(BluetoothGattWrapper gatt,
472                 BluetoothGattCharacteristic characteristic, int status) {
473             mBluetoothOperationExecutor.notifyCompletion(
474                     new Operation<byte[]>(OperationType.READ_CHARACTERISTIC, gatt, characteristic),
475                     status, characteristic.getValue());
476         }
477 
478         @Override
onCharacteristicWrite(BluetoothGattWrapper gatt, BluetoothGattCharacteristic characteristic, int status)479         public void onCharacteristicWrite(BluetoothGattWrapper gatt,
480                 BluetoothGattCharacteristic characteristic, int status) {
481             mBluetoothOperationExecutor.notifyCompletion(new Operation<Void>(
482                     OperationType.WRITE_CHARACTERISTIC, gatt, characteristic), status);
483         }
484 
485         @Override
onDescriptorRead(BluetoothGattWrapper gatt, BluetoothGattDescriptor descriptor, int status)486         public void onDescriptorRead(BluetoothGattWrapper gatt, BluetoothGattDescriptor descriptor,
487                 int status) {
488             mBluetoothOperationExecutor.notifyCompletion(
489                     new Operation<byte[]>(OperationType.READ_DESCRIPTOR, gatt, descriptor), status,
490                     descriptor.getValue());
491         }
492 
493         @Override
onDescriptorWrite(BluetoothGattWrapper gatt, BluetoothGattDescriptor descriptor, int status)494         public void onDescriptorWrite(BluetoothGattWrapper gatt, BluetoothGattDescriptor descriptor,
495                 int status) {
496             Log.d(TAG, String.format("onDescriptorWrite %s, %s, %d",
497                     gatt.getDevice(), descriptor.getUuid(), status));
498             mBluetoothOperationExecutor.notifyCompletion(
499                     new Operation<Void>(OperationType.WRITE_DESCRIPTOR, gatt, descriptor), status);
500         }
501 
502         @Override
onReadRemoteRssi(BluetoothGattWrapper gatt, int rssi, int status)503         public void onReadRemoteRssi(BluetoothGattWrapper gatt, int rssi, int status) {
504             mBluetoothOperationExecutor.notifyCompletion(
505                     new Operation<Integer>(OperationType.READ_RSSI, gatt), status, rssi);
506         }
507 
508         @Override
onReliableWriteCompleted(BluetoothGattWrapper gatt, int status)509         public void onReliableWriteCompleted(BluetoothGattWrapper gatt, int status) {
510             mBluetoothOperationExecutor.notifyCompletion(
511                     new Operation<Void>(OperationType.WRITE_RELIABLE, gatt), status);
512         }
513 
514         @Override
onCharacteristicChanged(BluetoothGattWrapper gatt, BluetoothGattCharacteristic characteristic)515         public void onCharacteristicChanged(BluetoothGattWrapper gatt,
516                 BluetoothGattCharacteristic characteristic) {
517             byte[] value = characteristic.getValue();
518             if (value == null) {
519                 // Value is not supposed to be null, but just to be safe...
520                 value = new byte[0];
521             }
522             Log.d(TAG, String.format("Characteristic %s changed, Gatt device: %s",
523                     characteristic.getUuid(), gatt.getDevice()));
524             try {
525                 getConnectionByGatt(gatt).onCharacteristicChanged(characteristic, value);
526             } catch (BluetoothException e) {
527                 Log.e(TAG, "Error in onCharacteristicChanged", e);
528             }
529         }
530     }
531 
532     private class InternalScanCallback extends ScanCallback {
533 
534         @Override
onScanFailed(int errorCode)535         public void onScanFailed(int errorCode) {
536             String errorMessage;
537             switch (errorCode) {
538                 case ScanCallback.SCAN_FAILED_ALREADY_STARTED:
539                     errorMessage = "SCAN_FAILED_ALREADY_STARTED";
540                     break;
541                 case ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED:
542                     errorMessage = "SCAN_FAILED_APPLICATION_REGISTRATION_FAILED";
543                     break;
544                 case ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED:
545                     errorMessage = "SCAN_FAILED_FEATURE_UNSUPPORTED";
546                     break;
547                 case ScanCallback.SCAN_FAILED_INTERNAL_ERROR:
548                     errorMessage = "SCAN_FAILED_INTERNAL_ERROR";
549                     break;
550                 default:
551                     errorMessage = "Unknown error code - " + errorCode;
552             }
553             mBluetoothOperationExecutor.notifyFailure(
554                     new Operation<BluetoothDevice>(OperationType.SCAN),
555                     new BluetoothException("Scan failed: " + errorMessage));
556         }
557 
558         @Override
onScanResult(int callbackType, ScanResult result)559         public void onScanResult(int callbackType, ScanResult result) {
560             mBluetoothOperationExecutor.notifySuccess(
561                     new Operation<BluetoothDevice>(OperationType.SCAN), result.getDevice());
562         }
563     }
564 
565     /**
566      * Options for {@link #connect}.
567      */
568     public static class ConnectionOptions {
569 
570         private boolean mAutoConnect;
571         private long mConnectionTimeoutMillis;
572         private Optional<Integer> mConnectionPriority;
573         private Optional<Integer> mMtu;
574 
ConnectionOptions(boolean autoConnect, long connectionTimeoutMillis, Optional<Integer> connectionPriority, Optional<Integer> mtu)575         private ConnectionOptions(boolean autoConnect, long connectionTimeoutMillis,
576                 Optional<Integer> connectionPriority,
577                 Optional<Integer> mtu) {
578             this.mAutoConnect = autoConnect;
579             this.mConnectionTimeoutMillis = connectionTimeoutMillis;
580             this.mConnectionPriority = connectionPriority;
581             this.mMtu = mtu;
582         }
583 
autoConnect()584         boolean autoConnect() {
585             return mAutoConnect;
586         }
587 
connectionTimeoutMillis()588         long connectionTimeoutMillis() {
589             return mConnectionTimeoutMillis;
590         }
591 
connectionPriority()592         Optional<Integer> connectionPriority() {
593             return mConnectionPriority;
594         }
595 
mtu()596         Optional<Integer> mtu() {
597             return mMtu;
598         }
599 
600         @Override
toString()601         public String toString() {
602             return "ConnectionOptions{"
603                     + "autoConnect=" + mAutoConnect + ", "
604                     + "connectionTimeoutMillis=" + mConnectionTimeoutMillis + ", "
605                     + "connectionPriority=" + mConnectionPriority + ", "
606                     + "mtu=" + mMtu
607                     + "}";
608         }
609 
610         @Override
equals(Object o)611         public boolean equals(Object o) {
612             if (this == o) {
613                 return true;
614             }
615             if (o instanceof ConnectionOptions) {
616                 ConnectionOptions that = (ConnectionOptions) o;
617                 return this.mAutoConnect == that.autoConnect()
618                         && this.mConnectionTimeoutMillis == that.connectionTimeoutMillis()
619                         && this.mConnectionPriority.equals(that.connectionPriority())
620                         && this.mMtu.equals(that.mtu());
621             }
622             return false;
623         }
624 
625         @Override
hashCode()626         public int hashCode() {
627             return Objects.hash(mAutoConnect, mConnectionTimeoutMillis, mConnectionPriority, mMtu);
628         }
629 
630         /**
631          * Creates a builder of ConnectionOptions.
632          */
builder()633         public static Builder builder() {
634             return new ConnectionOptions.Builder()
635                     .setAutoConnect(false)
636                     .setConnectionTimeoutMillis(TimeUnit.SECONDS.toMillis(5));
637         }
638 
639         /**
640          * Builder for {@link ConnectionOptions}.
641          */
642         public static class Builder {
643 
644             private boolean mAutoConnect;
645             private long mConnectionTimeoutMillis;
646             private Optional<Integer> mConnectionPriority = Optional.empty();
647             private Optional<Integer> mMtu = Optional.empty();
648 
649             /**
650              * See {@link android.bluetooth.BluetoothDevice#connectGatt}.
651              */
setAutoConnect(boolean autoConnect)652             public Builder setAutoConnect(boolean autoConnect) {
653                 this.mAutoConnect = autoConnect;
654                 return this;
655             }
656 
657             /**
658              * See {@link android.bluetooth.BluetoothGatt#requestConnectionPriority(int)}.
659              */
setConnectionPriority(int connectionPriority)660             public Builder setConnectionPriority(int connectionPriority) {
661                 this.mConnectionPriority = Optional.of(connectionPriority);
662                 return this;
663             }
664 
665             /**
666              * See {@link android.bluetooth.BluetoothGatt#requestMtu(int)}.
667              */
setMtu(int mtu)668             public Builder setMtu(int mtu) {
669                 this.mMtu = Optional.of(mtu);
670                 return this;
671             }
672 
673             /**
674              * Sets the timeout for the GATT connection.
675              */
setConnectionTimeoutMillis(long connectionTimeoutMillis)676             public Builder setConnectionTimeoutMillis(long connectionTimeoutMillis) {
677                 this.mConnectionTimeoutMillis = connectionTimeoutMillis;
678                 return this;
679             }
680 
681             /**
682              * Builds ConnectionOptions.
683              */
build()684             public ConnectionOptions build() {
685                 return new ConnectionOptions(mAutoConnect, mConnectionTimeoutMillis,
686                         mConnectionPriority, mMtu);
687             }
688         }
689     }
690 }
691