• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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 android.nearby.fastpair.provider.bluetooth;
18 
19 import android.annotation.TargetApi;
20 import android.bluetooth.BluetoothGatt;
21 import android.bluetooth.BluetoothGattCharacteristic;
22 import android.bluetooth.BluetoothGattDescriptor;
23 import android.util.Log;
24 
25 import androidx.annotation.Nullable;
26 
27 import com.android.server.nearby.common.bluetooth.BluetoothException;
28 import com.android.server.nearby.common.bluetooth.BluetoothGattException;
29 import com.android.server.nearby.common.bluetooth.ReservedUuids;
30 import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothDevice;
31 import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor;
32 import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.Operation;
33 
34 import com.google.common.annotations.VisibleForTesting;
35 import com.google.common.base.Objects;
36 import com.google.common.base.Preconditions;
37 import com.google.common.io.BaseEncoding;
38 
39 import java.io.ByteArrayOutputStream;
40 import java.io.Closeable;
41 import java.io.IOException;
42 import java.util.ArrayList;
43 import java.util.HashMap;
44 import java.util.List;
45 import java.util.Map;
46 import java.util.Map.Entry;
47 import java.util.SortedMap;
48 import java.util.TreeMap;
49 import java.util.concurrent.TimeUnit;
50 
51 /**
52  * Connection to a bluetooth LE device over Gatt.
53  */
54 @TargetApi(18)
55 public class BluetoothGattServerConnection implements Closeable {
56     @SuppressWarnings("unused")
57     private static final String TAG = BluetoothGattServerConnection.class.getSimpleName();
58 
59     /** See {@link BluetoothGattDescriptor#DISABLE_NOTIFICATION_VALUE}. */
60     private static final short DISABLE_NOTIFICATION_VALUE = 0x0000;
61 
62     /** See {@link BluetoothGattDescriptor#ENABLE_NOTIFICATION_VALUE}. */
63     private static final short ENABLE_NOTIFICATION_VALUE = 0x0001;
64 
65     /** See {@link BluetoothGattDescriptor#ENABLE_INDICATION_VALUE}. */
66     private static final short ENABLE_INDICATION_VALUE = 0x0002;
67 
68     /** Default MTU when value is unknown. */
69     public static final int DEFAULT_MTU = 23;
70 
71     @VisibleForTesting
72     static final long OPERATION_TIMEOUT = TimeUnit.SECONDS.toMillis(1);
73 
74     /** Notification types as defined by the BLE spec vol 4, sec G, part 3.3.3.3 */
75     public enum NotificationType {
76         NOTIFICATION,
77         INDICATION
78     }
79 
80     /** BT operation types that can be in flight. */
81     public enum OperationType {
82         SEND_NOTIFICATION
83     }
84 
85     private final Map<ScopedKey, Object> mContextValues = new HashMap<ScopedKey, Object>();
86     private final List<Listener> mCloseListeners = new ArrayList<Listener>();
87 
88     private final BluetoothGattServerHelper mBluetoothGattServerHelper;
89     private final BluetoothDevice mBluetoothDevice;
90 
91     @VisibleForTesting
92     BluetoothOperationExecutor mBluetoothOperationScheduler =
93             new BluetoothOperationExecutor(1);
94 
95     /** Stores pending writes. For each UUID, we store an offset and a byte[] of data. */
96     @VisibleForTesting
97     final Map<BluetoothGattServlet, SortedMap<Integer, byte[]>> mQueuedCharacteristicWrites =
98             new HashMap<BluetoothGattServlet, SortedMap<Integer, byte[]>>();
99 
100     @VisibleForTesting
101     final Map<BluetoothGattCharacteristic, Notifier> mRegisteredNotifications =
102             new HashMap<BluetoothGattCharacteristic, Notifier>();
103 
104     private final Map<BluetoothGattCharacteristic, BluetoothGattServlet> mServlets;
105 
BluetoothGattServerConnection( BluetoothGattServerHelper bluetoothGattServerHelper, BluetoothDevice device, BluetoothGattServerConfig serverConfig)106     public BluetoothGattServerConnection(
107             BluetoothGattServerHelper bluetoothGattServerHelper,
108             BluetoothDevice device,
109             BluetoothGattServerConfig serverConfig) {
110         mBluetoothGattServerHelper = bluetoothGattServerHelper;
111         mBluetoothDevice = device;
112         mServlets = serverConfig.getServlets();
113     }
114 
setContextValue(Object scope, String key, @Nullable Object value)115     public void setContextValue(Object scope, String key, @Nullable Object value) {
116         mContextValues.put(new ScopedKey(scope, key), value);
117     }
118 
119     @Nullable
getContextValue(Object scope, String key)120     public Object getContextValue(Object scope, String key) {
121         return mContextValues.get(new ScopedKey(scope, key));
122     }
123 
getDevice()124     public BluetoothDevice getDevice() {
125         return mBluetoothDevice;
126     }
127 
getMtu()128     public int getMtu() {
129         return DEFAULT_MTU;
130     }
131 
getMaxDataPacketSize()132     public int getMaxDataPacketSize() {
133         // Per BT specs (3.2.9), only MTU - 3 bytes can be used to transmit data
134         return getMtu() - 3;
135     }
136 
addCloseListener(Listener listener)137     public void addCloseListener(Listener listener) {
138         synchronized (mCloseListeners) {
139             mCloseListeners.add(listener);
140         }
141     }
142 
removeCloseListener(Listener listener)143     public void removeCloseListener(Listener listener) {
144         synchronized (mCloseListeners) {
145             mCloseListeners.remove(listener);
146         }
147     }
148 
getServlet(BluetoothGattCharacteristic characteristic)149     private BluetoothGattServlet getServlet(BluetoothGattCharacteristic characteristic)
150             throws BluetoothGattException {
151         BluetoothGattServlet servlet = mServlets.get(characteristic);
152         if (servlet == null) {
153             throw new BluetoothGattException(
154                     String.format("No handler registered for characteristic %s.",
155                             characteristic.getUuid()),
156                     BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
157         }
158         return servlet;
159     }
160 
readCharacteristic(int offset, BluetoothGattCharacteristic characteristic)161     public byte[] readCharacteristic(int offset, BluetoothGattCharacteristic characteristic)
162             throws BluetoothGattException {
163         return getServlet(characteristic).read(this, offset);
164     }
165 
writeCharacteristic(BluetoothGattCharacteristic characteristic, boolean preparedWrite, int offset, byte[] value)166     public void writeCharacteristic(BluetoothGattCharacteristic characteristic,
167             boolean preparedWrite,
168             int offset, byte[] value) throws BluetoothGattException {
169         Log.d(TAG, String.format(
170                 "Received %d bytes at offset %d on %s from device %s, prepareWrite=%s.",
171                 value.length,
172                 offset,
173                 BluetoothGattUtils.toString(characteristic),
174                 mBluetoothDevice,
175                 preparedWrite));
176         BluetoothGattServlet servlet = getServlet(characteristic);
177         if (preparedWrite) {
178             SortedMap<Integer, byte[]> bytePackets = mQueuedCharacteristicWrites.get(servlet);
179             if (bytePackets == null) {
180                 bytePackets = new TreeMap<Integer, byte[]>();
181                 mQueuedCharacteristicWrites.put(servlet, bytePackets);
182             }
183             bytePackets.put(offset, value);
184             return;
185         }
186 
187         Log.d(TAG, servlet.toString());
188         servlet.write(this, offset, value);
189     }
190 
readDescriptor(int offset, BluetoothGattDescriptor descriptor)191     public byte[] readDescriptor(int offset, BluetoothGattDescriptor descriptor)
192             throws BluetoothGattException {
193         BluetoothGattCharacteristic characteristic = descriptor.getCharacteristic();
194         if (characteristic == null) {
195             throw new BluetoothGattException(String.format(
196                     "Descriptor %s not associated with a characteristics!",
197                     BluetoothGattUtils.toString(descriptor)), BluetoothGatt.GATT_FAILURE);
198         }
199         return getServlet(characteristic).readDescriptor(this, descriptor, offset);
200     }
201 
writeDescriptor( BluetoothGattDescriptor descriptor, boolean preparedWrite, int offset, byte[] value)202     public void writeDescriptor(
203             BluetoothGattDescriptor descriptor,
204             boolean preparedWrite,
205             int offset,
206             byte[] value) throws BluetoothGattException {
207         Log.d(TAG, String.format(
208                 "Received %d bytes at offset %d on %s from device %s, prepareWrite=%s.",
209                 value.length,
210                 offset,
211                 BluetoothGattUtils.toString(descriptor),
212                 mBluetoothDevice,
213                 preparedWrite));
214         if (preparedWrite) {
215             throw new BluetoothGattException(
216                     String.format("Prepare write not supported for descriptor %s.", descriptor),
217                     BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
218         }
219 
220         BluetoothGattCharacteristic characteristic = descriptor.getCharacteristic();
221         if (characteristic == null) {
222             throw new BluetoothGattException(String.format(
223                     "Descriptor %s not associated with a characteristics!",
224                     BluetoothGattUtils.toString(descriptor)), BluetoothGatt.GATT_FAILURE);
225         }
226         BluetoothGattServlet servlet = getServlet(characteristic);
227         if (descriptor.getUuid().equals(
228                 ReservedUuids.Descriptors.CLIENT_CHARACTERISTIC_CONFIGURATION)) {
229             handleCharacteristicConfigurationChange(characteristic, servlet, offset, value);
230             return;
231         }
232         servlet.writeDescriptor(this, descriptor, offset, value);
233     }
234 
handleCharacteristicConfigurationChange( final BluetoothGattCharacteristic characteristic, BluetoothGattServlet servlet, int offset, byte[] value)235     private void handleCharacteristicConfigurationChange(
236             final BluetoothGattCharacteristic characteristic, BluetoothGattServlet servlet,
237             int offset,
238             byte[] value)
239             throws BluetoothGattException {
240         if (offset != 0) {
241             throw new BluetoothGattException(String.format(
242                     "Offset should be 0 when changing the client characteristic config: %d.",
243                     offset),
244                     BluetoothGatt.GATT_INVALID_OFFSET);
245         }
246         if (value.length != 2) {
247             throw new BluetoothGattException(String.format(
248                     "Value 0x%s is undefined for the client characteristic config",
249                     BaseEncoding.base16().encode(value)),
250                     BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH);
251         }
252 
253         boolean notificationRegistered = mRegisteredNotifications.containsKey(characteristic);
254         Notifier notifier;
255         switch (toShort(value)) {
256             case ENABLE_NOTIFICATION_VALUE:
257                 if (!notificationRegistered) {
258                     notifier = new Notifier() {
259                         @Override
260                         public void notify(byte[] data) throws BluetoothException {
261                             sendNotification(characteristic, NotificationType.NOTIFICATION, data);
262                         }
263                     };
264                     mRegisteredNotifications.put(characteristic, notifier);
265                     servlet.enableNotification(this, notifier);
266                 }
267                 break;
268             case ENABLE_INDICATION_VALUE:
269                 if (!notificationRegistered) {
270                     notifier = new Notifier() {
271                         @Override
272                         public void notify(byte[] data) throws BluetoothException {
273                             sendNotification(characteristic, NotificationType.INDICATION, data);
274                         }
275                     };
276                     mRegisteredNotifications.put(characteristic, notifier);
277                     servlet.enableNotification(this, notifier);
278                 }
279                 break;
280             case DISABLE_NOTIFICATION_VALUE:
281                 // Note: this disables notifications or indications.
282                 if (notificationRegistered) {
283                     notifier = mRegisteredNotifications.remove(characteristic);
284                     if (notifier == null) {
285                         return; // this is not supposed to happen
286                     }
287                     servlet.disableNotification(this, notifier);
288                 }
289                 break;
290             default:
291                 throw new BluetoothGattException(String.format(
292                         "Value 0x%s is undefined for the client characteristic config",
293                         BaseEncoding.base16().encode(value)),
294                         BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
295         }
296     }
297 
toShort(byte[] value)298     private static short toShort(byte[] value) {
299         Preconditions.checkNotNull(value);
300         Preconditions.checkArgument(value.length == 2, "Length should be 2 bytes.");
301 
302         return (short) ((value[0] & 0x00FF) | (value[1] << 8));
303     }
304 
executeWrite(boolean execute)305     public void executeWrite(boolean execute) throws BluetoothGattException {
306         if (!execute) {
307             mQueuedCharacteristicWrites.clear();
308             return;
309         }
310 
311         try {
312             for (Entry<BluetoothGattServlet, SortedMap<Integer, byte[]>> queuedWrite :
313                     mQueuedCharacteristicWrites.entrySet()) {
314                 BluetoothGattServlet servlet = queuedWrite.getKey();
315                 SortedMap<Integer, byte[]> chunks = queuedWrite.getValue();
316                 if (servlet == null || chunks == null) {
317                     // This is not supposed to happen
318                     throw new IllegalStateException();
319                 }
320                 assembleByteChunksAndHandle(servlet, chunks);
321             }
322         } finally {
323             mQueuedCharacteristicWrites.clear();
324         }
325     }
326 
327     /**
328      * Assembles the specified queued writes and calls the provided write handler on the assembled
329      * chunks. Tries to assemble all the chunks into one write request. For example, if the content
330      * of byteChunks is:
331      * <code>
332      * offset data_size
333      * 0       10
334      * 10        1
335      * 11        5
336      * </code>
337      *
338      * then this method would call <code>writeHandler.onWrite(0, byte[16])</code>
339      *
340      * However, if all the chunks cannot be assembled into a continuous byte[], then onWrite() will
341      * be called multiple times with the largest continuous chunks. For example, if the content of
342      * byteChunks is:
343      * <code>
344      * offset data_size
345      * 10       12
346      * 30        5
347      * 35        9
348      * </code>
349      *
350      * then this method would call <code>writeHandler.onWrite(10, byte[12)</code> and
351      * <code>writeHandler.onWrite(30, byte[14]).
352      */
assembleByteChunksAndHandle(BluetoothGattServlet servlet, SortedMap<Integer, byte[]> byteChunks)353     private void assembleByteChunksAndHandle(BluetoothGattServlet servlet,
354             SortedMap<Integer, byte[]> byteChunks) throws BluetoothGattException {
355         ByteArrayOutputStream assembledRequest = new ByteArrayOutputStream();
356         Integer startWritingAtOffset = 0;
357 
358         while (!byteChunks.isEmpty()) {
359             Integer offset = byteChunks.firstKey();
360 
361             if (offset.intValue() < startWritingAtOffset + assembledRequest.size()) {
362                 throw new BluetoothGattException(
363                         "Expected offset of at least " + assembledRequest.size()
364                                 + ", but got offset " + offset, BluetoothGatt.GATT_INVALID_OFFSET);
365             }
366 
367             // If we have a hole, then write what we've already assembled and start assembling a new
368             // long write
369             if (offset.intValue() > startWritingAtOffset + assembledRequest.size()) {
370                 servlet.write(this, startWritingAtOffset.intValue(),
371                         assembledRequest.toByteArray());
372                 startWritingAtOffset = offset;
373                 assembledRequest.reset();
374             }
375 
376             try {
377                 byte[] dataChunk = byteChunks.remove(offset);
378                 if (dataChunk == null) {
379                     // This is not supposed to happen
380                     throw new IllegalStateException();
381                 }
382                 assembledRequest.write(dataChunk);
383             } catch (IOException e) {
384                 throw new BluetoothGattException("Error assembling request",
385                         BluetoothGatt.GATT_FAILURE);
386             }
387         }
388 
389         // If there is anything to write, write it
390         if (assembledRequest.size() > 0) {
391             Preconditions.checkNotNull(startWritingAtOffset); // should never be null at this point
392             servlet.write(this, startWritingAtOffset.intValue(), assembledRequest.toByteArray());
393         }
394     }
395 
sendNotification(final BluetoothGattCharacteristic characteristic, final NotificationType notificationType, final byte[] data)396     private void sendNotification(final BluetoothGattCharacteristic characteristic,
397             final NotificationType notificationType, final byte[] data)
398             throws BluetoothException {
399         mBluetoothOperationScheduler.execute(
400                 new Operation<Void>(OperationType.SEND_NOTIFICATION) {
401                     @Override
402                     public void run() throws BluetoothException {
403                         mBluetoothGattServerHelper.sendNotification(mBluetoothDevice,
404                                 characteristic,
405                                 data,
406                                 notificationType == NotificationType.INDICATION ? true : false);
407                     }
408                 },
409                 OPERATION_TIMEOUT);
410     }
411 
412     @Override
close()413     public void close() throws IOException {
414         try {
415             mBluetoothGattServerHelper.closeConnection(mBluetoothDevice);
416         } catch (BluetoothException e) {
417             throw new IOException("Failed to close connection", e);
418         }
419     }
420 
notifyNotificationSent(int status)421     public void notifyNotificationSent(int status) {
422         mBluetoothOperationScheduler.notifyCompletion(
423                 new Operation<Void>(OperationType.SEND_NOTIFICATION), status);
424     }
425 
onClose()426     public void onClose() {
427         synchronized (mCloseListeners) {
428             for (Listener listener : mCloseListeners) {
429                 listener.onClose();
430             }
431         }
432     }
433 
434     /** Scope/key pair to use to reference contextual values. */
435     private static class ScopedKey {
436         private final Object mScope;
437         private final String mKey;
438 
ScopedKey(Object scope, String key)439         ScopedKey(Object scope, String key) {
440             mScope = scope;
441             mKey = key;
442         }
443 
444         @Override
equals(@ullable Object o)445         public boolean equals(@Nullable Object o) {
446             if (!(o instanceof ScopedKey)) {
447                 return false;
448             }
449             ScopedKey other = (ScopedKey) o;
450             return other.mScope.equals(mScope) && other.mKey.equals(mKey);
451         }
452 
453         @Override
hashCode()454         public int hashCode() {
455             return Objects.hashCode(mScope, mKey);
456         }
457     }
458 
459     /** Listener to be notified when the connection closes. */
460     public interface Listener {
onClose()461         void onClose();
462     }
463 
464     /** Notifier to notify data over notification or indication. */
465     public interface Notifier {
notify(byte[] data)466         void notify(byte[] data) throws BluetoothException;
467     }
468 }
469