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