1 /* 2 * Copyright (C) 2019 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.car.connecteddevice.ble; 18 19 import static com.android.car.connecteddevice.ConnectedDeviceManager.DEVICE_ERROR_UNEXPECTED_DISCONNECTION; 20 import static com.android.car.connecteddevice.util.SafeLog.logd; 21 import static com.android.car.connecteddevice.util.SafeLog.loge; 22 import static com.android.car.connecteddevice.util.SafeLog.logw; 23 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.bluetooth.BluetoothAdapter; 27 import android.bluetooth.BluetoothDevice; 28 import android.bluetooth.BluetoothGattCharacteristic; 29 import android.bluetooth.BluetoothGattDescriptor; 30 import android.bluetooth.BluetoothGattService; 31 import android.bluetooth.le.AdvertiseCallback; 32 import android.bluetooth.le.AdvertiseData; 33 import android.bluetooth.le.AdvertiseSettings; 34 import android.car.encryptionrunner.EncryptionRunnerFactory; 35 import android.os.Handler; 36 import android.os.Looper; 37 import android.os.ParcelUuid; 38 39 import com.android.car.connecteddevice.AssociationCallback; 40 import com.android.car.connecteddevice.model.AssociatedDevice; 41 import com.android.car.connecteddevice.storage.ConnectedDeviceStorage; 42 import com.android.car.connecteddevice.util.EventLog; 43 import com.android.internal.annotations.VisibleForTesting; 44 45 import java.util.UUID; 46 import java.util.concurrent.Executors; 47 import java.util.concurrent.ScheduledExecutorService; 48 import java.util.concurrent.ScheduledFuture; 49 import java.util.concurrent.TimeUnit; 50 51 /** 52 * Communication manager that allows for targeted connections to a specific device in the car. 53 */ 54 public class CarBlePeripheralManager extends CarBleManager { 55 56 private static final String TAG = "CarBlePeripheralManager"; 57 58 // Attribute protocol bytes attached to message. Available write size is MTU size minus att 59 // bytes. 60 private static final int ATT_PROTOCOL_BYTES = 3; 61 62 // Arbitrary delay time for a retry of association advertising if bluetooth adapter name change 63 // fails. 64 private static final long ASSOCIATE_ADVERTISING_DELAY_MS = 10L; 65 66 private static final UUID CLIENT_CHARACTERISTIC_CONFIG = 67 UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); 68 69 private final BluetoothGattDescriptor mDescriptor = 70 new BluetoothGattDescriptor(CLIENT_CHARACTERISTIC_CONFIG, 71 BluetoothGattDescriptor.PERMISSION_READ 72 | BluetoothGattDescriptor.PERMISSION_WRITE); 73 74 private final ScheduledExecutorService mScheduler = 75 Executors.newSingleThreadScheduledExecutor(); 76 77 private final BlePeripheralManager mBlePeripheralManager; 78 79 private final UUID mAssociationServiceUuid; 80 81 private final BluetoothGattCharacteristic mWriteCharacteristic; 82 83 private final BluetoothGattCharacteristic mReadCharacteristic; 84 85 private final Handler mTimeoutHandler; 86 87 // BLE default is 23, minus 3 bytes for ATT_PROTOCOL. 88 private int mWriteSize = 20; 89 90 private String mOriginalBluetoothName; 91 92 private String mClientDeviceName; 93 94 private String mClientDeviceAddress; 95 96 private AssociationCallback mAssociationCallback; 97 98 private AdvertiseCallback mAdvertiseCallback; 99 100 /** 101 * Initialize a new instance of manager. 102 * 103 * @param blePeripheralManager {@link BlePeripheralManager} for establishing connection. 104 * @param connectedDeviceStorage Shared {@link ConnectedDeviceStorage} for companion features. 105 * @param associationServiceUuid {@link UUID} of association service. 106 * @param writeCharacteristicUuid {@link UUID} of characteristic the car will write to. 107 * @param readCharacteristicUuid {@link UUID} of characteristic the device will write to. 108 */ CarBlePeripheralManager(@onNull BlePeripheralManager blePeripheralManager, @NonNull ConnectedDeviceStorage connectedDeviceStorage, @NonNull UUID associationServiceUuid, @NonNull UUID writeCharacteristicUuid, @NonNull UUID readCharacteristicUuid)109 public CarBlePeripheralManager(@NonNull BlePeripheralManager blePeripheralManager, 110 @NonNull ConnectedDeviceStorage connectedDeviceStorage, 111 @NonNull UUID associationServiceUuid, @NonNull UUID writeCharacteristicUuid, 112 @NonNull UUID readCharacteristicUuid) { 113 super(connectedDeviceStorage); 114 mBlePeripheralManager = blePeripheralManager; 115 mAssociationServiceUuid = associationServiceUuid; 116 mDescriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE); 117 mWriteCharacteristic = new BluetoothGattCharacteristic(writeCharacteristicUuid, 118 BluetoothGattCharacteristic.PROPERTY_NOTIFY, 119 BluetoothGattCharacteristic.PROPERTY_READ); 120 mReadCharacteristic = new BluetoothGattCharacteristic(readCharacteristicUuid, 121 BluetoothGattCharacteristic.PROPERTY_WRITE 122 | BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE, 123 BluetoothGattCharacteristic.PERMISSION_WRITE); 124 mReadCharacteristic.addDescriptor(mDescriptor); 125 mTimeoutHandler = new Handler(Looper.getMainLooper()); 126 } 127 128 @Override start()129 public void start() { 130 super.start(); 131 BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 132 if (adapter == null) { 133 return; 134 } 135 String originalBluetoothName = mStorage.getStoredBluetoothName(); 136 if (originalBluetoothName == null) { 137 return; 138 } 139 if (originalBluetoothName.equals(adapter.getName())) { 140 mStorage.removeStoredBluetoothName(); 141 return; 142 } 143 144 logw(TAG, "Discovered mismatch in bluetooth adapter name. Resetting back to " 145 + originalBluetoothName + "."); 146 adapter.setName(originalBluetoothName); 147 mScheduler.schedule( 148 () -> verifyBluetoothNameRestored(originalBluetoothName), 149 ASSOCIATE_ADVERTISING_DELAY_MS, TimeUnit.MILLISECONDS); 150 } 151 152 @Override stop()153 public void stop() { 154 super.stop(); 155 reset(); 156 } 157 158 @Override disconnectDevice(@onNull String deviceId)159 public void disconnectDevice(@NonNull String deviceId) { 160 BleDevice connectedDevice = getConnectedDevice(); 161 if (connectedDevice == null || !deviceId.equals(connectedDevice.mDeviceId)) { 162 return; 163 } 164 reset(); 165 } 166 reset()167 private void reset() { 168 resetBluetoothAdapterName(); 169 mClientDeviceAddress = null; 170 mClientDeviceName = null; 171 mAssociationCallback = null; 172 mBlePeripheralManager.cleanup(); 173 mConnectedDevices.clear(); 174 } 175 176 /** Attempt to connect to device with provided id within set timeout period. */ connectToDevice(@onNull UUID deviceId, int timeoutSeconds)177 public void connectToDevice(@NonNull UUID deviceId, int timeoutSeconds) { 178 for (BleDevice device : mConnectedDevices) { 179 if (UUID.fromString(device.mDeviceId).equals(deviceId)) { 180 logd(TAG, "Already connected to device " + deviceId + "."); 181 // Already connected to this device. Ignore requests to connect again. 182 return; 183 } 184 } 185 186 // Clear any previous session before starting a new one. 187 reset(); 188 189 mAdvertiseCallback = new AdvertiseCallback() { 190 @Override 191 public void onStartSuccess(AdvertiseSettings settingsInEffect) { 192 super.onStartSuccess(settingsInEffect); 193 mTimeoutHandler.postDelayed(mTimeoutRunnable, 194 TimeUnit.SECONDS.toMillis(timeoutSeconds)); 195 logd(TAG, "Successfully started advertising for device " + deviceId 196 + " for " + timeoutSeconds + " seconds."); 197 } 198 }; 199 mBlePeripheralManager.unregisterCallback(mAssociationPeripheralCallback); 200 mBlePeripheralManager.registerCallback(mReconnectPeripheralCallback); 201 mTimeoutHandler.removeCallbacks(mTimeoutRunnable); 202 startAdvertising(deviceId, mAdvertiseCallback, /* includeDeviceName = */ false); 203 } 204 205 @Nullable getConnectedDevice()206 private BleDevice getConnectedDevice() { 207 if (mConnectedDevices.isEmpty()) { 208 return null; 209 } 210 return mConnectedDevices.iterator().next(); 211 } 212 213 /** Start the association with a new device */ startAssociation(@onNull String nameForAssociation, @NonNull AssociationCallback callback)214 public void startAssociation(@NonNull String nameForAssociation, 215 @NonNull AssociationCallback callback) { 216 BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 217 if (adapter == null) { 218 loge(TAG, "Bluetooth is unavailable on this device. Unable to start associating."); 219 return; 220 } 221 222 reset(); 223 mAssociationCallback = callback; 224 if (mOriginalBluetoothName == null) { 225 mOriginalBluetoothName = adapter.getName(); 226 mStorage.storeBluetoothName(mOriginalBluetoothName); 227 } 228 adapter.setName(nameForAssociation); 229 logd(TAG, "Changing bluetooth adapter name from " + mOriginalBluetoothName + " to " 230 + nameForAssociation + "."); 231 mBlePeripheralManager.unregisterCallback(mReconnectPeripheralCallback); 232 mBlePeripheralManager.registerCallback(mAssociationPeripheralCallback); 233 mAdvertiseCallback = new AdvertiseCallback() { 234 @Override 235 public void onStartSuccess(AdvertiseSettings settingsInEffect) { 236 super.onStartSuccess(settingsInEffect); 237 callback.onAssociationStartSuccess(nameForAssociation); 238 logd(TAG, "Successfully started advertising for association."); 239 } 240 241 @Override 242 public void onStartFailure(int errorCode) { 243 super.onStartFailure(errorCode); 244 callback.onAssociationStartFailure(); 245 logd(TAG, "Failed to start advertising for association. Error code: " + errorCode); 246 } 247 }; 248 attemptAssociationAdvertising(nameForAssociation, callback); 249 } 250 251 /** Stop the association with any device. */ stopAssociation(@onNull AssociationCallback callback)252 public void stopAssociation(@NonNull AssociationCallback callback) { 253 if (!isAssociating() || callback != mAssociationCallback) { 254 return; 255 } 256 reset(); 257 } 258 attemptAssociationAdvertising(@onNull String adapterName, @NonNull AssociationCallback callback)259 private void attemptAssociationAdvertising(@NonNull String adapterName, 260 @NonNull AssociationCallback callback) { 261 if (mOriginalBluetoothName != null 262 && adapterName.equals(BluetoothAdapter.getDefaultAdapter().getName())) { 263 startAdvertising(mAssociationServiceUuid, mAdvertiseCallback, 264 /* includeDeviceName = */ true); 265 return; 266 } 267 268 ScheduledFuture future = mScheduler.schedule( 269 () -> attemptAssociationAdvertising(adapterName, callback), 270 ASSOCIATE_ADVERTISING_DELAY_MS, TimeUnit.MILLISECONDS); 271 if (future.isCancelled()) { 272 // Association failed to start. 273 callback.onAssociationStartFailure(); 274 return; 275 } 276 logd(TAG, "Adapter name change has not taken affect prior to advertising attempt. Trying " 277 + "again in " + ASSOCIATE_ADVERTISING_DELAY_MS + " milliseconds."); 278 } 279 startAdvertising(@onNull UUID serviceUuid, @NonNull AdvertiseCallback callback, boolean includeDeviceName)280 private void startAdvertising(@NonNull UUID serviceUuid, @NonNull AdvertiseCallback callback, 281 boolean includeDeviceName) { 282 BluetoothGattService gattService = new BluetoothGattService(serviceUuid, 283 BluetoothGattService.SERVICE_TYPE_PRIMARY); 284 gattService.addCharacteristic(mWriteCharacteristic); 285 gattService.addCharacteristic(mReadCharacteristic); 286 287 AdvertiseData advertiseData = new AdvertiseData.Builder() 288 .setIncludeDeviceName(includeDeviceName) 289 .addServiceUuid(new ParcelUuid(serviceUuid)) 290 .build(); 291 mBlePeripheralManager.startAdvertising(gattService, advertiseData, callback); 292 } 293 294 /** Notify that the user has accepted a pairing code or other out-of-band confirmation. */ notifyOutOfBandAccepted()295 public void notifyOutOfBandAccepted() { 296 if (getConnectedDevice() == null) { 297 disconnectWithError("Null connected device found when out-of-band confirmation " 298 + "received."); 299 return; 300 } 301 302 SecureBleChannel secureChannel = getConnectedDevice().mSecureChannel; 303 if (secureChannel == null) { 304 disconnectWithError("Null SecureBleChannel found for the current connected device " 305 + "when out-of-band confirmation received."); 306 return; 307 } 308 309 secureChannel.notifyOutOfBandAccepted(); 310 } 311 312 @VisibleForTesting 313 @Nullable getConnectedDeviceChannel()314 SecureBleChannel getConnectedDeviceChannel() { 315 BleDevice connectedDevice = getConnectedDevice(); 316 if (connectedDevice == null) { 317 return null; 318 } 319 320 return connectedDevice.mSecureChannel; 321 } 322 setDeviceId(@onNull String deviceId)323 private void setDeviceId(@NonNull String deviceId) { 324 logd(TAG, "Setting device id: " + deviceId); 325 BleDevice connectedDevice = getConnectedDevice(); 326 if (connectedDevice == null) { 327 disconnectWithError("Null connected device found when device id received."); 328 return; 329 } 330 331 connectedDevice.mDeviceId = deviceId; 332 mCallbacks.invoke(callback -> callback.onDeviceConnected(deviceId)); 333 } 334 disconnectWithError(@onNull String errorMessage)335 private void disconnectWithError(@NonNull String errorMessage) { 336 loge(TAG, errorMessage); 337 reset(); 338 } 339 resetBluetoothAdapterName()340 private void resetBluetoothAdapterName() { 341 if (mOriginalBluetoothName == null) { 342 return; 343 } 344 logd(TAG, "Changing bluetooth adapter name back to " + mOriginalBluetoothName + "."); 345 BluetoothAdapter.getDefaultAdapter().setName(mOriginalBluetoothName); 346 mOriginalBluetoothName = null; 347 } 348 verifyBluetoothNameRestored(@onNull String expectedName)349 private void verifyBluetoothNameRestored(@NonNull String expectedName) { 350 String currentName = BluetoothAdapter.getDefaultAdapter().getName(); 351 if (expectedName.equals(currentName)) { 352 logd(TAG, "Bluetooth adapter name restoration completed successfully. Removing stored " 353 + "adapter name."); 354 mStorage.removeStoredBluetoothName(); 355 return; 356 } 357 logd(TAG, "Bluetooth adapter name restoration has not taken affect yet. Checking again in " 358 + ASSOCIATE_ADVERTISING_DELAY_MS + " milliseconds."); 359 mScheduler.schedule( 360 () -> verifyBluetoothNameRestored(expectedName), 361 ASSOCIATE_ADVERTISING_DELAY_MS, TimeUnit.MILLISECONDS); 362 } 363 addConnectedDevice(BluetoothDevice device, boolean isReconnect)364 private void addConnectedDevice(BluetoothDevice device, boolean isReconnect) { 365 EventLog.onDeviceConnected(); 366 mBlePeripheralManager.stopAdvertising(mAdvertiseCallback); 367 mTimeoutHandler.removeCallbacks(mTimeoutRunnable); 368 mClientDeviceAddress = device.getAddress(); 369 mClientDeviceName = device.getName(); 370 if (mClientDeviceName == null) { 371 logd(TAG, "Device connected, but name is null; issuing request to retrieve device " 372 + "name."); 373 mBlePeripheralManager.retrieveDeviceName(device); 374 } 375 376 BleDeviceMessageStream secureStream = new BleDeviceMessageStream(mBlePeripheralManager, 377 device, mWriteCharacteristic, mReadCharacteristic); 378 secureStream.setMaxWriteSize(mWriteSize); 379 SecureBleChannel secureChannel = new SecureBleChannel(secureStream, mStorage, isReconnect, 380 EncryptionRunnerFactory.newRunner()); 381 secureChannel.registerCallback(mSecureChannelCallback); 382 BleDevice bleDevice = new BleDevice(device, /* gatt = */ null); 383 bleDevice.mSecureChannel = secureChannel; 384 addConnectedDevice(bleDevice); 385 } 386 setMtuSize(int mtuSize)387 private void setMtuSize(int mtuSize) { 388 mWriteSize = mtuSize - ATT_PROTOCOL_BYTES; 389 BleDevice connectedDevice = getConnectedDevice(); 390 if (connectedDevice != null 391 && connectedDevice.mSecureChannel != null 392 && connectedDevice.mSecureChannel.getStream() != null) { 393 connectedDevice.mSecureChannel.getStream().setMaxWriteSize(mWriteSize); 394 } 395 } 396 isAssociating()397 private boolean isAssociating() { 398 return mAssociationCallback != null; 399 } 400 401 private final BlePeripheralManager.Callback mReconnectPeripheralCallback = 402 new BlePeripheralManager.Callback() { 403 404 @Override 405 public void onDeviceNameRetrieved(String deviceName) { 406 // Ignored. 407 } 408 409 @Override 410 public void onMtuSizeChanged(int size) { 411 setMtuSize(size); 412 } 413 414 @Override 415 public void onRemoteDeviceConnected(BluetoothDevice device) { 416 addConnectedDevice(device, /* isReconnect= */ true); 417 } 418 419 @Override 420 public void onRemoteDeviceDisconnected(BluetoothDevice device) { 421 String deviceId = null; 422 BleDevice connectedDevice = getConnectedDevice(device); 423 // Reset before invoking callbacks to avoid a race condition with reconnect 424 // logic. 425 reset(); 426 if (connectedDevice != null) { 427 deviceId = connectedDevice.mDeviceId; 428 } 429 final String finalDeviceId = deviceId; 430 if (finalDeviceId != null) { 431 logd(TAG, "Connected device " + finalDeviceId + " disconnected."); 432 mCallbacks.invoke(callback -> callback.onDeviceDisconnected(finalDeviceId)); 433 } 434 } 435 }; 436 437 private final BlePeripheralManager.Callback mAssociationPeripheralCallback = 438 new BlePeripheralManager.Callback() { 439 @Override 440 public void onDeviceNameRetrieved(String deviceName) { 441 if (deviceName == null) { 442 return; 443 } 444 mClientDeviceName = deviceName; 445 BleDevice connectedDevice = getConnectedDevice(); 446 if (connectedDevice == null || connectedDevice.mDeviceId == null) { 447 return; 448 } 449 mStorage.updateAssociatedDeviceName(connectedDevice.mDeviceId, deviceName); 450 } 451 452 @Override 453 public void onMtuSizeChanged(int size) { 454 setMtuSize(size); 455 } 456 457 @Override 458 public void onRemoteDeviceConnected(BluetoothDevice device) { 459 resetBluetoothAdapterName(); 460 addConnectedDevice(device, /* isReconnect = */ false); 461 BleDevice connectedDevice = getConnectedDevice(); 462 if (connectedDevice == null || connectedDevice.mSecureChannel == null) { 463 return; 464 } 465 connectedDevice.mSecureChannel.setShowVerificationCodeListener( 466 code -> { 467 if (!isAssociating()) { 468 loge(TAG, "No valid callback for association."); 469 return; 470 } 471 mAssociationCallback.onVerificationCodeAvailable(code); 472 }); 473 } 474 475 @Override 476 public void onRemoteDeviceDisconnected(BluetoothDevice device) { 477 BleDevice connectedDevice = getConnectedDevice(device); 478 if (isAssociating()) { 479 mAssociationCallback.onAssociationError( 480 DEVICE_ERROR_UNEXPECTED_DISCONNECTION); 481 } 482 // Reset before invoking callbacks to avoid a race condition with reconnect 483 // logic. 484 reset(); 485 if (connectedDevice != null && connectedDevice.mDeviceId != null) { 486 mCallbacks.invoke(callback -> callback.onDeviceDisconnected( 487 connectedDevice.mDeviceId)); 488 } 489 } 490 }; 491 492 private final SecureBleChannel.Callback mSecureChannelCallback = 493 new SecureBleChannel.Callback() { 494 @Override 495 public void onSecureChannelEstablished() { 496 BleDevice connectedDevice = getConnectedDevice(); 497 if (connectedDevice == null || connectedDevice.mDeviceId == null) { 498 disconnectWithError("Null device id found when secure channel " 499 + "established."); 500 return; 501 } 502 String deviceId = connectedDevice.mDeviceId; 503 if (mClientDeviceAddress == null) { 504 disconnectWithError("Null device address found when secure channel " 505 + "established."); 506 return; 507 } 508 if (isAssociating()) { 509 logd(TAG, "Secure channel established for un-associated device. Saving " 510 + "association of that device for current user."); 511 mStorage.addAssociatedDeviceForActiveUser( 512 new AssociatedDevice(deviceId, mClientDeviceAddress, 513 mClientDeviceName, /* isConnectionEnabled = */ true)); 514 if (mAssociationCallback != null) { 515 mAssociationCallback.onAssociationCompleted(deviceId); 516 mAssociationCallback = null; 517 } 518 } 519 mCallbacks.invoke(callback -> callback.onSecureChannelEstablished(deviceId)); 520 } 521 522 @Override 523 public void onEstablishSecureChannelFailure(int error) { 524 BleDevice connectedDevice = getConnectedDevice(); 525 if (connectedDevice == null || connectedDevice.mDeviceId == null) { 526 disconnectWithError("Null device id found when secure channel failed to " 527 + "establish."); 528 return; 529 } 530 String deviceId = connectedDevice.mDeviceId; 531 mCallbacks.invoke(callback -> callback.onSecureChannelError(deviceId)); 532 533 if (isAssociating()) { 534 mAssociationCallback.onAssociationError(error); 535 disconnectWithError("Error while establishing secure connection."); 536 } 537 } 538 539 @Override 540 public void onMessageReceived(DeviceMessage deviceMessage) { 541 BleDevice connectedDevice = getConnectedDevice(); 542 if (connectedDevice == null || connectedDevice.mDeviceId == null) { 543 disconnectWithError("Null device id found when message received."); 544 return; 545 } 546 547 logd(TAG, "Received new message from " + connectedDevice.mDeviceId 548 + " with " + deviceMessage.getMessage().length + " bytes in its " 549 + "payload. Notifying " + mCallbacks.size() + " callbacks."); 550 mCallbacks.invoke( 551 callback ->callback.onMessageReceived(connectedDevice.mDeviceId, 552 deviceMessage)); 553 } 554 555 @Override 556 public void onMessageReceivedError(Exception exception) { 557 // TODO(b/143879960) Extend the message error from here to continue up the 558 // chain. 559 } 560 561 @Override 562 public void onDeviceIdReceived(String deviceId) { 563 setDeviceId(deviceId); 564 } 565 }; 566 567 private final Runnable mTimeoutRunnable = new Runnable() { 568 @Override 569 public void run() { 570 logd(TAG, "Timeout period expired without a connection. Stopping advertisement."); 571 mBlePeripheralManager.stopAdvertising(mAdvertiseCallback); 572 } 573 }; 574 } 575