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