blob: 4add85e3c7d1f8b21ee19294e2db4f8cef33e23e [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.connecteddevice;
import static com.android.car.connecteddevice.util.SafeLog.logd;
import static com.android.car.connecteddevice.util.SafeLog.loge;
import static com.android.car.connecteddevice.util.SafeLog.logw;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import com.android.car.connecteddevice.ble.BleCentralManager;
import com.android.car.connecteddevice.ble.BlePeripheralManager;
import com.android.car.connecteddevice.ble.CarBleCentralManager;
import com.android.car.connecteddevice.ble.CarBleManager;
import com.android.car.connecteddevice.ble.CarBlePeripheralManager;
import com.android.car.connecteddevice.ble.DeviceMessage;
import com.android.car.connecteddevice.model.AssociatedDevice;
import com.android.car.connecteddevice.model.ConnectedDevice;
import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
import com.android.car.connecteddevice.storage.ConnectedDeviceStorage.AssociatedDeviceCallback;
import com.android.car.connecteddevice.util.ByteUtils;
import com.android.car.connecteddevice.util.EventLog;
import com.android.car.connecteddevice.util.ThreadSafeCallbacks;
import com.android.internal.annotations.VisibleForTesting;
import java.lang.annotation.Retention;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
/** Manager of devices connected to the car. */
public class ConnectedDeviceManager {
private static final String TAG = "ConnectedDeviceManager";
// Device name length is limited by available bytes in BLE advertisement data packet.
//
// BLE advertisement limits data packet length to 31
// Currently we send:
// - 18 bytes for 16 chars UUID: 16 bytes + 2 bytes for header;
// - 3 bytes for advertisement being connectable;
// which leaves 10 bytes.
// Subtracting 2 bytes used by header, we have 8 bytes for device name.
private static final int DEVICE_NAME_LENGTH_LIMIT = 8;
// The mac address randomly rotates every 7-15 minutes. To be safe, we will rotate our
// reconnect advertisement every 6 minutes to avoid crossing a rotation.
private static final Duration MAX_ADVERTISEMENT_DURATION = Duration.ofMinutes(6);
private final ConnectedDeviceStorage mStorage;
private final CarBleCentralManager mCentralManager;
private final CarBlePeripheralManager mPeripheralManager;
private final ThreadSafeCallbacks<DeviceAssociationCallback> mDeviceAssociationCallbacks =
new ThreadSafeCallbacks<>();
private final ThreadSafeCallbacks<ConnectionCallback> mActiveUserConnectionCallbacks =
new ThreadSafeCallbacks<>();
private final ThreadSafeCallbacks<ConnectionCallback> mAllUserConnectionCallbacks =
new ThreadSafeCallbacks<>();
// deviceId -> (recipientId -> callbacks)
private final Map<String, Map<UUID, ThreadSafeCallbacks<DeviceCallback>>> mDeviceCallbacks =
new ConcurrentHashMap<>();
// deviceId -> device
private final Map<String, InternalConnectedDevice> mConnectedDevices =
new ConcurrentHashMap<>();
// recipientId -> (deviceId -> message bytes)
private final Map<UUID, Map<String, List<byte[]>>> mRecipientMissedMessages =
new ConcurrentHashMap<>();
// Recipient ids that received multiple callback registrations indicate that the recipient id
// has been compromised. Another party now has access the messages intended for that recipient.
// As a safeguard, that recipient id will be added to this list and blocked from further
// callback notifications.
private final Set<UUID> mBlacklistedRecipients = new CopyOnWriteArraySet<>();
private final AtomicBoolean mIsConnectingToUserDevice = new AtomicBoolean(false);
private final AtomicBoolean mHasStarted = new AtomicBoolean(false);
private String mNameForAssociation;
private AssociationCallback mAssociationCallback;
private MessageDeliveryDelegate mMessageDeliveryDelegate;
@Retention(SOURCE)
@IntDef(prefix = { "DEVICE_ERROR_" },
value = {
DEVICE_ERROR_INVALID_HANDSHAKE,
DEVICE_ERROR_INVALID_MSG,
DEVICE_ERROR_INVALID_DEVICE_ID,
DEVICE_ERROR_INVALID_VERIFICATION,
DEVICE_ERROR_INVALID_CHANNEL_STATE,
DEVICE_ERROR_INVALID_ENCRYPTION_KEY,
DEVICE_ERROR_STORAGE_FAILURE,
DEVICE_ERROR_INVALID_SECURITY_KEY,
DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED,
DEVICE_ERROR_UNEXPECTED_DISCONNECTION
}
)
public @interface DeviceError {}
public static final int DEVICE_ERROR_INVALID_HANDSHAKE = 0;
public static final int DEVICE_ERROR_INVALID_MSG = 1;
public static final int DEVICE_ERROR_INVALID_DEVICE_ID = 2;
public static final int DEVICE_ERROR_INVALID_VERIFICATION = 3;
public static final int DEVICE_ERROR_INVALID_CHANNEL_STATE = 4;
public static final int DEVICE_ERROR_INVALID_ENCRYPTION_KEY = 5;
public static final int DEVICE_ERROR_STORAGE_FAILURE = 6;
public static final int DEVICE_ERROR_INVALID_SECURITY_KEY = 7;
public static final int DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED = 8;
public static final int DEVICE_ERROR_UNEXPECTED_DISCONNECTION = 9;
public ConnectedDeviceManager(@NonNull Context context) {
this(context, new ConnectedDeviceStorage(context), new BleCentralManager(context),
new BlePeripheralManager(context),
UUID.fromString(context.getString(R.string.car_service_uuid)),
UUID.fromString(context.getString(R.string.car_association_service_uuid)),
UUID.fromString(context.getString(R.string.car_reconnect_service_uuid)),
UUID.fromString(context.getString(R.string.car_reconnect_data_uuid)),
context.getString(R.string.car_bg_mask),
UUID.fromString(context.getString(R.string.car_secure_write_uuid)),
UUID.fromString(context.getString(R.string.car_secure_read_uuid)),
context.getResources().getInteger(R.integer.car_default_mtu_size));
}
private ConnectedDeviceManager(
@NonNull Context context,
@NonNull ConnectedDeviceStorage storage,
@NonNull BleCentralManager bleCentralManager,
@NonNull BlePeripheralManager blePeripheralManager,
@NonNull UUID serviceUuid,
@NonNull UUID associationServiceUuid,
@NonNull UUID reconnectServiceUuid,
@NonNull UUID reconnectDataUuid,
@NonNull String bgMask,
@NonNull UUID writeCharacteristicUuid,
@NonNull UUID readCharacteristicUuid,
int defaultMtuSize) {
this(storage,
new CarBleCentralManager(context, bleCentralManager, storage, serviceUuid, bgMask,
writeCharacteristicUuid, readCharacteristicUuid),
new CarBlePeripheralManager(blePeripheralManager, storage, associationServiceUuid,
reconnectServiceUuid, reconnectDataUuid, writeCharacteristicUuid,
readCharacteristicUuid, MAX_ADVERTISEMENT_DURATION, defaultMtuSize));
}
@VisibleForTesting
ConnectedDeviceManager(
@NonNull ConnectedDeviceStorage storage,
@NonNull CarBleCentralManager centralManager,
@NonNull CarBlePeripheralManager peripheralManager) {
Executor callbackExecutor = Executors.newSingleThreadExecutor();
mStorage = storage;
mCentralManager = centralManager;
mPeripheralManager = peripheralManager;
mCentralManager.registerCallback(generateCarBleCallback(centralManager), callbackExecutor);
mPeripheralManager.registerCallback(generateCarBleCallback(peripheralManager),
callbackExecutor);
mStorage.setAssociatedDeviceCallback(mAssociatedDeviceCallback);
}
/**
* Start internal processes and begin discovering devices. Must be called before any
* connections can be made using {@link #connectToActiveUserDevice()}.
*/
public void start() {
if (mHasStarted.getAndSet(true)) {
reset();
} else {
logd(TAG, "Starting ConnectedDeviceManager.");
EventLog.onConnectedDeviceManagerStarted();
}
// TODO (b/141312136) Start central manager
mPeripheralManager.start();
connectToActiveUserDevice();
}
/** Reset internal processes and disconnect any active connections. */
public void reset() {
logd(TAG, "Resetting ConnectedDeviceManager.");
for (InternalConnectedDevice device : mConnectedDevices.values()) {
removeConnectedDevice(device.mConnectedDevice.getDeviceId(), device.mCarBleManager);
}
mPeripheralManager.stop();
// TODO (b/141312136) Stop central manager
mIsConnectingToUserDevice.set(false);
}
/** Returns {@link List<ConnectedDevice>} of devices currently connected. */
@NonNull
public List<ConnectedDevice> getActiveUserConnectedDevices() {
List<ConnectedDevice> activeUserConnectedDevices = new ArrayList<>();
for (InternalConnectedDevice device : mConnectedDevices.values()) {
if (device.mConnectedDevice.isAssociatedWithActiveUser()) {
activeUserConnectedDevices.add(device.mConnectedDevice);
}
}
logd(TAG, "Returned " + activeUserConnectedDevices.size() + " active user devices.");
return activeUserConnectedDevices;
}
/**
* Register a callback for triggered associated device related events.
*
* @param callback {@link DeviceAssociationCallback} to register.
* @param executor {@link Executor} to execute triggers on.
*/
public void registerDeviceAssociationCallback(@NonNull DeviceAssociationCallback callback,
@NonNull @CallbackExecutor Executor executor) {
mDeviceAssociationCallbacks.add(callback, executor);
}
/**
* Unregister a device association callback.
*
* @param callback {@link DeviceAssociationCallback} to unregister.
*/
public void unregisterDeviceAssociationCallback(@NonNull DeviceAssociationCallback callback) {
mDeviceAssociationCallbacks.remove(callback);
}
/**
* Register a callback for manager triggered connection events for only the currently active
* user's devices.
*
* @param callback {@link ConnectionCallback} to register.
* @param executor {@link Executor} to execute triggers on.
*/
public void registerActiveUserConnectionCallback(@NonNull ConnectionCallback callback,
@NonNull @CallbackExecutor Executor executor) {
mActiveUserConnectionCallbacks.add(callback, executor);
}
/**
* Unregister a connection callback from manager.
*
* @param callback {@link ConnectionCallback} to unregister.
*/
public void unregisterConnectionCallback(ConnectionCallback callback) {
mActiveUserConnectionCallbacks.remove(callback);
mAllUserConnectionCallbacks.remove(callback);
}
/** Connect to a device for the active user if available. */
@VisibleForTesting
void connectToActiveUserDevice() {
Executors.defaultThreadFactory().newThread(() -> {
logd(TAG, "Received request to connect to active user's device.");
connectToActiveUserDeviceInternal();
}).start();
}
private void connectToActiveUserDeviceInternal() {
try {
if (mIsConnectingToUserDevice.get()) {
logd(TAG, "A request has already been made to connect to this user's device. "
+ "Ignoring redundant request.");
return;
}
List<AssociatedDevice> userDevices = mStorage.getActiveUserAssociatedDevices();
if (userDevices.isEmpty()) {
logw(TAG, "No devices associated with active user. Ignoring.");
return;
}
// Only currently support one device per user for fast association, so take the
// first one.
AssociatedDevice userDevice = userDevices.get(0);
if (!userDevice.isConnectionEnabled()) {
logd(TAG, "Connection is disabled on device " + userDevice + ".");
return;
}
if (mConnectedDevices.containsKey(userDevice.getDeviceId())) {
logd(TAG, "Device has already been connected. No need to attempt connection "
+ "again.");
return;
}
EventLog.onStartDeviceSearchStarted();
mIsConnectingToUserDevice.set(true);
mPeripheralManager.connectToDevice(UUID.fromString(userDevice.getDeviceId()));
} catch (Exception e) {
loge(TAG, "Exception while attempting connection with active user's device.", e);
}
}
/**
* Start the association with a new device.
*
* @param callback Callback for association events.
*/
public void startAssociation(@NonNull AssociationCallback callback) {
mAssociationCallback = callback;
Executors.defaultThreadFactory().newThread(() -> {
logd(TAG, "Received request to start association.");
mPeripheralManager.startAssociation(getNameForAssociation(),
mInternalAssociationCallback);
}).start();
}
/** Stop the association with any device. */
public void stopAssociation(@NonNull AssociationCallback callback) {
if (mAssociationCallback != callback) {
logd(TAG, "Stop association called with unrecognized callback. Ignoring.");
return;
}
mAssociationCallback = null;
mPeripheralManager.stopAssociation(mInternalAssociationCallback);
}
/**
* Get a list of associated devices for the given user.
*
* @return Associated device list.
*/
@NonNull
public List<AssociatedDevice> getActiveUserAssociatedDevices() {
return mStorage.getActiveUserAssociatedDevices();
}
/** Notify that the user has accepted a pairing code or any out-of-band confirmation. */
public void notifyOutOfBandAccepted() {
mPeripheralManager.notifyOutOfBandAccepted();
}
/**
* Remove the associated device with the given device identifier for the current user.
*
* @param deviceId Device identifier.
*/
public void removeActiveUserAssociatedDevice(@NonNull String deviceId) {
mStorage.removeAssociatedDeviceForActiveUser(deviceId);
disconnectDevice(deviceId);
}
/**
* Enable connection on an associated device.
*
* @param deviceId Device identifier.
*/
public void enableAssociatedDeviceConnection(@NonNull String deviceId) {
logd(TAG, "enableAssociatedDeviceConnection() called on " + deviceId);
mStorage.updateAssociatedDeviceConnectionEnabled(deviceId,
/* isConnectionEnabled= */ true);
connectToActiveUserDevice();
}
/**
* Disable connection on an associated device.
*
* @param deviceId Device identifier.
*/
public void disableAssociatedDeviceConnection(@NonNull String deviceId) {
logd(TAG, "disableAssociatedDeviceConnection() called on " + deviceId);
mStorage.updateAssociatedDeviceConnectionEnabled(deviceId,
/* isConnectionEnabled= */ false);
disconnectDevice(deviceId);
}
private void disconnectDevice(String deviceId) {
InternalConnectedDevice device = mConnectedDevices.get(deviceId);
if (device != null) {
device.mCarBleManager.disconnectDevice(deviceId);
removeConnectedDevice(deviceId, device.mCarBleManager);
}
}
/**
* Register a callback for a specific device and recipient.
*
* @param device {@link ConnectedDevice} to register triggers on.
* @param recipientId {@link UUID} to register as recipient of.
* @param callback {@link DeviceCallback} to register.
* @param executor {@link Executor} on which to execute callback.
*/
public void registerDeviceCallback(@NonNull ConnectedDevice device, @NonNull UUID recipientId,
@NonNull DeviceCallback callback, @NonNull @CallbackExecutor Executor executor) {
if (isRecipientBlacklisted(recipientId)) {
notifyOfBlacklisting(device, recipientId, callback, executor);
return;
}
logd(TAG, "New callback registered on device " + device.getDeviceId() + " for recipient "
+ recipientId);
String deviceId = device.getDeviceId();
Map<UUID, ThreadSafeCallbacks<DeviceCallback>> recipientCallbacks =
mDeviceCallbacks.computeIfAbsent(deviceId, key -> new HashMap<>());
// Device already has a callback registered with this recipient UUID. For the
// protection of the user, this UUID is now blacklisted from future subscriptions
// and the original subscription is notified and removed.
if (recipientCallbacks.containsKey(recipientId)) {
blacklistRecipient(deviceId, recipientId);
notifyOfBlacklisting(device, recipientId, callback, executor);
return;
}
ThreadSafeCallbacks<DeviceCallback> newCallbacks = new ThreadSafeCallbacks<>();
newCallbacks.add(callback, executor);
recipientCallbacks.put(recipientId, newCallbacks);
List<byte[]> messages = popMissedMessages(recipientId, device.getDeviceId());
if (messages != null) {
for (byte[] message : messages) {
newCallbacks.invoke(deviceCallback ->
deviceCallback.onMessageReceived(device, message));
}
}
}
/**
* Set the delegate for message delivery operations.
*
* @param delegate The {@link MessageDeliveryDelegate} to set. {@code null} to unset.
*/
public void setMessageDeliveryDelegate(@Nullable MessageDeliveryDelegate delegate) {
mMessageDeliveryDelegate = delegate;
}
private void notifyOfBlacklisting(@NonNull ConnectedDevice device, @NonNull UUID recipientId,
@NonNull DeviceCallback callback, @NonNull Executor executor) {
loge(TAG, "Multiple callbacks registered for recipient " + recipientId + "! Your "
+ "recipient id is no longer secure and has been blocked from future use.");
executor.execute(() ->
callback.onDeviceError(device, DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED));
}
private void saveMissedMessage(@NonNull String deviceId, @NonNull UUID recipientId,
@NonNull byte[] message) {
// Store last message in case recipient registers callbacks in the future.
logd(TAG, "No recipient registered for device " + deviceId + " and recipient "
+ recipientId + " combination. Saving message.");
mRecipientMissedMessages.computeIfAbsent(recipientId, __ -> new HashMap<>())
.computeIfAbsent(deviceId, __ -> new ArrayList<>()).add(message);
}
/**
* Remove the last message sent for this device prior to a {@link DeviceCallback} being
* registered.
*
* @param recipientId Recipient's id
* @param deviceId Device id
* @return The missed {@code byte[]} messages, or {@code null} if no messages were
* missed.
*/
@Nullable
private List<byte[]> popMissedMessages(@NonNull UUID recipientId, @NonNull String deviceId) {
Map<String, List<byte[]>> missedMessages = mRecipientMissedMessages.get(recipientId);
if (missedMessages == null) {
return null;
}
return missedMessages.remove(deviceId);
}
/**
* Unregister callback from device events.
*
* @param device {@link ConnectedDevice} callback was registered on.
* @param recipientId {@link UUID} callback was registered under.
* @param callback {@link DeviceCallback} to unregister.
*/
public void unregisterDeviceCallback(@NonNull ConnectedDevice device,
@NonNull UUID recipientId, @NonNull DeviceCallback callback) {
logd(TAG, "Device callback unregistered on device " + device.getDeviceId() + " for "
+ "recipient " + recipientId + ".");
Map<UUID, ThreadSafeCallbacks<DeviceCallback>> recipientCallbacks =
mDeviceCallbacks.get(device.getDeviceId());
if (recipientCallbacks == null) {
return;
}
ThreadSafeCallbacks<DeviceCallback> callbacks = recipientCallbacks.get(recipientId);
if (callbacks == null) {
return;
}
callbacks.remove(callback);
if (callbacks.size() == 0) {
recipientCallbacks.remove(recipientId);
}
}
/**
* Securely send message to a device.
*
* @param device {@link ConnectedDevice} to send the message to.
* @param recipientId Recipient {@link UUID}.
* @param message Message to send.
* @throws IllegalStateException Secure channel has not been established.
*/
public void sendMessageSecurely(@NonNull ConnectedDevice device, @NonNull UUID recipientId,
@NonNull byte[] message) throws IllegalStateException {
sendMessage(device, recipientId, message, /* isEncrypted= */ true);
}
/**
* Send an unencrypted message to a device.
*
* @param device {@link ConnectedDevice} to send the message to.
* @param recipientId Recipient {@link UUID}.
* @param message Message to send.
*/
public void sendMessageUnsecurely(@NonNull ConnectedDevice device, @NonNull UUID recipientId,
@NonNull byte[] message) {
sendMessage(device, recipientId, message, /* isEncrypted= */ false);
}
private void sendMessage(@NonNull ConnectedDevice device, @NonNull UUID recipientId,
@NonNull byte[] message, boolean isEncrypted) throws IllegalStateException {
String deviceId = device.getDeviceId();
logd(TAG, "Sending new message to device " + deviceId + " for " + recipientId
+ " containing " + message.length + ". Message will be sent securely: "
+ isEncrypted + ".");
InternalConnectedDevice connectedDevice = mConnectedDevices.get(deviceId);
if (connectedDevice == null) {
loge(TAG, "Attempted to send message to unknown device " + deviceId + ". Ignoring.");
return;
}
if (isEncrypted && !connectedDevice.mConnectedDevice.hasSecureChannel()) {
throw new IllegalStateException("Cannot send a message securely to device that has not "
+ "established a secure channel.");
}
connectedDevice.mCarBleManager.sendMessage(deviceId,
new DeviceMessage(recipientId, isEncrypted, message));
}
private boolean isRecipientBlacklisted(UUID recipientId) {
return mBlacklistedRecipients.contains(recipientId);
}
private void blacklistRecipient(@NonNull String deviceId, @NonNull UUID recipientId) {
Map<UUID, ThreadSafeCallbacks<DeviceCallback>> recipientCallbacks =
mDeviceCallbacks.get(deviceId);
if (recipientCallbacks == null) {
// Should never happen, but null-safety check.
return;
}
ThreadSafeCallbacks<DeviceCallback> existingCallback = recipientCallbacks.get(recipientId);
if (existingCallback == null) {
// Should never happen, but null-safety check.
return;
}
InternalConnectedDevice connectedDevice = mConnectedDevices.get(deviceId);
if (connectedDevice != null) {
recipientCallbacks.get(recipientId).invoke(
callback ->
callback.onDeviceError(connectedDevice.mConnectedDevice,
DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED)
);
}
recipientCallbacks.remove(recipientId);
mBlacklistedRecipients.add(recipientId);
}
@VisibleForTesting
void addConnectedDevice(@NonNull String deviceId, @NonNull CarBleManager bleManager) {
if (mConnectedDevices.containsKey(deviceId)) {
// Device already connected. No-op until secure channel established.
return;
}
logd(TAG, "New device with id " + deviceId + " connected.");
ConnectedDevice connectedDevice = new ConnectedDevice(
deviceId,
/* deviceName= */ null,
mStorage.getActiveUserAssociatedDeviceIds().contains(deviceId),
/* hasSecureChannel= */ false
);
mConnectedDevices.put(deviceId, new InternalConnectedDevice(connectedDevice, bleManager));
invokeConnectionCallbacks(connectedDevice.isAssociatedWithActiveUser(),
callback -> callback.onDeviceConnected(connectedDevice));
}
@VisibleForTesting
void removeConnectedDevice(@NonNull String deviceId, @NonNull CarBleManager bleManager) {
logd(TAG, "Device " + deviceId + " disconnected from manager " + bleManager);
InternalConnectedDevice connectedDevice = getConnectedDeviceForManager(deviceId,
bleManager);
// If disconnect happened on peripheral, open for future requests to connect.
if (bleManager == mPeripheralManager) {
mIsConnectingToUserDevice.set(false);
}
if (connectedDevice == null) {
return;
}
mConnectedDevices.remove(deviceId);
boolean isAssociated = connectedDevice.mConnectedDevice.isAssociatedWithActiveUser();
invokeConnectionCallbacks(isAssociated,
callback -> callback.onDeviceDisconnected(connectedDevice.mConnectedDevice));
if (isAssociated || mConnectedDevices.isEmpty()) {
// Try to regain connection to active user's device.
connectToActiveUserDevice();
}
}
@VisibleForTesting
void onSecureChannelEstablished(@NonNull String deviceId,
@NonNull CarBleManager bleManager) {
if (mConnectedDevices.get(deviceId) == null) {
loge(TAG, "Secure channel established on unknown device " + deviceId + ".");
return;
}
ConnectedDevice connectedDevice = mConnectedDevices.get(deviceId).mConnectedDevice;
ConnectedDevice updatedConnectedDevice = new ConnectedDevice(connectedDevice.getDeviceId(),
connectedDevice.getDeviceName(), connectedDevice.isAssociatedWithActiveUser(),
/* hasSecureChannel= */ true);
boolean notifyCallbacks = getConnectedDeviceForManager(deviceId, bleManager) != null;
// TODO (b/143088482) Implement interrupt
// Ignore if central already holds the active device connection and interrupt the
// connection.
mConnectedDevices.put(deviceId,
new InternalConnectedDevice(updatedConnectedDevice, bleManager));
logd(TAG, "Secure channel established to " + deviceId + " . Notifying callbacks: "
+ notifyCallbacks + ".");
if (notifyCallbacks) {
notifyAllDeviceCallbacks(deviceId,
callback -> callback.onSecureChannelEstablished(updatedConnectedDevice));
}
}
@VisibleForTesting
void onMessageReceived(@NonNull String deviceId, @NonNull DeviceMessage message) {
logd(TAG, "New message received from device " + deviceId + " intended for "
+ message.getRecipient() + " containing " + message.getMessage().length
+ " bytes.");
InternalConnectedDevice connectedDevice = mConnectedDevices.get(deviceId);
if (connectedDevice == null) {
logw(TAG, "Received message from unknown device " + deviceId + "or to unknown "
+ "recipient " + message.getRecipient() + ".");
return;
}
if (mMessageDeliveryDelegate != null
&& !mMessageDeliveryDelegate.shouldDeliverMessageForDevice(
connectedDevice.mConnectedDevice)) {
logw(TAG, "The message delegate has rejected this message. It will not be "
+ "delivered to the intended recipient.");
return;
}
UUID recipientId = message.getRecipient();
Map<UUID, ThreadSafeCallbacks<DeviceCallback>> deviceCallbacks =
mDeviceCallbacks.get(deviceId);
if (deviceCallbacks == null) {
saveMissedMessage(deviceId, recipientId, message.getMessage());
return;
}
ThreadSafeCallbacks<DeviceCallback> recipientCallbacks =
deviceCallbacks.get(recipientId);
if (recipientCallbacks == null) {
saveMissedMessage(deviceId, recipientId, message.getMessage());
return;
}
recipientCallbacks.invoke(
callback -> callback.onMessageReceived(connectedDevice.mConnectedDevice,
message.getMessage()));
}
@VisibleForTesting
void deviceErrorOccurred(@NonNull String deviceId) {
InternalConnectedDevice connectedDevice = mConnectedDevices.get(deviceId);
if (connectedDevice == null) {
logw(TAG, "Failed to establish secure channel on unknown device " + deviceId + ".");
return;
}
notifyAllDeviceCallbacks(deviceId,
callback -> callback.onDeviceError(connectedDevice.mConnectedDevice,
DEVICE_ERROR_INVALID_SECURITY_KEY));
}
@VisibleForTesting
void onAssociationCompleted(@NonNull String deviceId) {
InternalConnectedDevice connectedDevice =
getConnectedDeviceForManager(deviceId, mPeripheralManager);
if (connectedDevice == null) {
return;
}
// The previous device is now obsolete and should be replaced with a new one properly
// reflecting the state of belonging to the active user and notify features.
if (connectedDevice.mConnectedDevice.isAssociatedWithActiveUser()) {
// Device was already marked as belonging to active user. No need to reissue callbacks.
return;
}
removeConnectedDevice(deviceId, mPeripheralManager);
addConnectedDevice(deviceId, mPeripheralManager);
}
@NonNull
private List<String> getActiveUserDeviceIds() {
return mStorage.getActiveUserAssociatedDeviceIds();
}
@Nullable
private InternalConnectedDevice getConnectedDeviceForManager(@NonNull String deviceId,
@NonNull CarBleManager bleManager) {
InternalConnectedDevice connectedDevice = mConnectedDevices.get(deviceId);
if (connectedDevice != null && connectedDevice.mCarBleManager == bleManager) {
return connectedDevice;
}
return null;
}
private void invokeConnectionCallbacks(boolean belongsToActiveUser,
@NonNull Consumer<ConnectionCallback> notification) {
logd(TAG, "Notifying connection callbacks for device belonging to active user "
+ belongsToActiveUser + ".");
if (belongsToActiveUser) {
mActiveUserConnectionCallbacks.invoke(notification);
}
mAllUserConnectionCallbacks.invoke(notification);
}
private void notifyAllDeviceCallbacks(@NonNull String deviceId,
@NonNull Consumer<DeviceCallback> notification) {
logd(TAG, "Notifying all device callbacks for device " + deviceId + ".");
Map<UUID, ThreadSafeCallbacks<DeviceCallback>> deviceCallbacks =
mDeviceCallbacks.get(deviceId);
if (deviceCallbacks == null) {
return;
}
for (ThreadSafeCallbacks<DeviceCallback> callbacks : deviceCallbacks.values()) {
callbacks.invoke(notification);
}
}
/**
* Returns the name that should be used for the device during enrollment of a trusted device.
*
* <p>The returned name will be a combination of a prefix sysprop and randomized digits.
*/
@NonNull
private String getNameForAssociation() {
if (mNameForAssociation == null) {
mNameForAssociation = ByteUtils.generateRandomNumberString(DEVICE_NAME_LENGTH_LIMIT);
}
return mNameForAssociation;
}
@NonNull
private CarBleManager.Callback generateCarBleCallback(@NonNull CarBleManager carBleManager) {
return new CarBleManager.Callback() {
@Override
public void onDeviceConnected(String deviceId) {
EventLog.onDeviceIdReceived();
addConnectedDevice(deviceId, carBleManager);
}
@Override
public void onDeviceDisconnected(String deviceId) {
removeConnectedDevice(deviceId, carBleManager);
}
@Override
public void onSecureChannelEstablished(String deviceId) {
EventLog.onSecureChannelEstablished();
ConnectedDeviceManager.this.onSecureChannelEstablished(deviceId, carBleManager);
}
@Override
public void onMessageReceived(String deviceId, DeviceMessage message) {
ConnectedDeviceManager.this.onMessageReceived(deviceId, message);
}
@Override
public void onSecureChannelError(String deviceId) {
deviceErrorOccurred(deviceId);
}
};
}
private final AssociationCallback mInternalAssociationCallback = new AssociationCallback() {
@Override
public void onAssociationStartSuccess(String deviceName) {
if (mAssociationCallback != null) {
mAssociationCallback.onAssociationStartSuccess(deviceName);
}
}
@Override
public void onAssociationStartFailure() {
if (mAssociationCallback != null) {
mAssociationCallback.onAssociationStartFailure();
}
}
@Override
public void onAssociationError(int error) {
if (mAssociationCallback != null) {
mAssociationCallback.onAssociationError(error);
}
}
@Override
public void onVerificationCodeAvailable(String code) {
if (mAssociationCallback != null) {
mAssociationCallback.onVerificationCodeAvailable(code);
}
}
@Override
public void onAssociationCompleted(String deviceId) {
if (mAssociationCallback != null) {
mAssociationCallback.onAssociationCompleted(deviceId);
}
ConnectedDeviceManager.this.onAssociationCompleted(deviceId);
}
};
private final AssociatedDeviceCallback mAssociatedDeviceCallback =
new AssociatedDeviceCallback() {
@Override
public void onAssociatedDeviceAdded(
AssociatedDevice device) {
mDeviceAssociationCallbacks.invoke(callback ->
callback.onAssociatedDeviceAdded(device));
}
@Override
public void onAssociatedDeviceRemoved(AssociatedDevice device) {
mDeviceAssociationCallbacks.invoke(callback ->
callback.onAssociatedDeviceRemoved(device));
logd(TAG, "Successfully removed associated device " + device + ".");
}
@Override
public void onAssociatedDeviceUpdated(AssociatedDevice device) {
mDeviceAssociationCallbacks.invoke(callback ->
callback.onAssociatedDeviceUpdated(device));
}
};
/** Callback for triggered connection events from {@link ConnectedDeviceManager}. */
public interface ConnectionCallback {
/** Triggered when a new device has connected. */
void onDeviceConnected(@NonNull ConnectedDevice device);
/** Triggered when a device has disconnected. */
void onDeviceDisconnected(@NonNull ConnectedDevice device);
}
/** Triggered device events for a connected device from {@link ConnectedDeviceManager}. */
public interface DeviceCallback {
/**
* Triggered when secure channel has been established on a device. Encrypted messaging now
* available.
*/
void onSecureChannelEstablished(@NonNull ConnectedDevice device);
/** Triggered when a new message is received from a device. */
void onMessageReceived(@NonNull ConnectedDevice device, @NonNull byte[] message);
/** Triggered when an error has occurred for a device. */
void onDeviceError(@NonNull ConnectedDevice device, @DeviceError int error);
}
/** Callback for association device related events. */
public interface DeviceAssociationCallback {
/** Triggered when an associated device has been added. */
void onAssociatedDeviceAdded(@NonNull AssociatedDevice device);
/** Triggered when an associated device has been removed. */
void onAssociatedDeviceRemoved(@NonNull AssociatedDevice device);
/** Triggered when the name of an associated device has been updated. */
void onAssociatedDeviceUpdated(@NonNull AssociatedDevice device);
}
/** Delegate for message delivery operations. */
public interface MessageDeliveryDelegate {
/** Indicate whether a message should be delivered for the specified device. */
boolean shouldDeliverMessageForDevice(@NonNull ConnectedDevice device);
}
private static class InternalConnectedDevice {
private final ConnectedDevice mConnectedDevice;
private final CarBleManager mCarBleManager;
InternalConnectedDevice(@NonNull ConnectedDevice connectedDevice,
@NonNull CarBleManager carBleManager) {
mConnectedDevice = connectedDevice;
mCarBleManager = carBleManager;
}
}
}