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