blob: 579fe7defc1012eecff7e63511a5cda8281261fd [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.ConnectedDeviceManager.DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED;
import static com.android.car.connecteddevice.ConnectedDeviceManager.DEVICE_ERROR_INVALID_SECURITY_KEY;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mockitoSession;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.android.car.connecteddevice.ConnectedDeviceManager.ConnectionCallback;
import com.android.car.connecteddevice.ConnectedDeviceManager.DeviceAssociationCallback;
import com.android.car.connecteddevice.ConnectedDeviceManager.DeviceCallback;
import com.android.car.connecteddevice.ConnectedDeviceManager.MessageDeliveryDelegate;
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 org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoSession;
import org.mockito.quality.Strictness;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
@RunWith(AndroidJUnit4.class)
public class ConnectedDeviceManagerTest {
private static final String TEST_DEVICE_ADDRESS = "00:11:22:33:44:55";
private static final String TEST_DEVICE_NAME = "TEST_DEVICE_NAME";
private static final int DEFAULT_RECONNECT_TIMEOUT = 5;
private final Executor mCallbackExecutor = Executors.newSingleThreadExecutor();
private final UUID mRecipientId = UUID.randomUUID();
@Mock
private ConnectedDeviceStorage mMockStorage;
@Mock
private CarBlePeripheralManager mMockPeripheralManager;
@Mock
private CarBleCentralManager mMockCentralManager;
private ConnectedDeviceManager mConnectedDeviceManager;
private MockitoSession mMockingSession;
private AssociatedDeviceCallback mAssociatedDeviceCallback;
@Before
public void setUp() {
mMockingSession = mockitoSession()
.initMocks(this)
.strictness(Strictness.LENIENT)
.startMocking();
ArgumentCaptor<AssociatedDeviceCallback> callbackCaptor = ArgumentCaptor
.forClass(AssociatedDeviceCallback.class);
mConnectedDeviceManager = new ConnectedDeviceManager(mMockStorage, mMockCentralManager,
mMockPeripheralManager, DEFAULT_RECONNECT_TIMEOUT);
verify(mMockStorage).setAssociatedDeviceCallback(callbackCaptor.capture());
mAssociatedDeviceCallback = callbackCaptor.getValue();
mConnectedDeviceManager.start();
}
@After
public void tearDown() {
if (mMockingSession != null) {
mMockingSession.finishMocking();
}
}
@Test
public void getActiveUserConnectedDevices_initiallyShouldReturnEmptyList() {
assertThat(mConnectedDeviceManager.getActiveUserConnectedDevices()).isEmpty();
}
@Test
public void getActiveUserConnectedDevices_includesNewlyConnectedDevice() {
String deviceId = connectNewDevice(mMockCentralManager);
List<ConnectedDevice> activeUserDevices =
mConnectedDeviceManager.getActiveUserConnectedDevices();
ConnectedDevice expectedDevice = new ConnectedDevice(deviceId, /* deviceName = */ null,
/* belongsToActiveUser = */ true, /* hasSecureChannel = */ false);
assertThat(activeUserDevices).containsExactly(expectedDevice);
}
@Test
public void getActiveUserConnectedDevices_excludesDevicesNotBelongingToActiveUser() {
String deviceId = UUID.randomUUID().toString();
String otherUserDeviceId = UUID.randomUUID().toString();
when(mMockStorage.getActiveUserAssociatedDeviceIds()).thenReturn(
Collections.singletonList(otherUserDeviceId));
mConnectedDeviceManager.addConnectedDevice(deviceId, mMockCentralManager);
assertThat(mConnectedDeviceManager.getActiveUserConnectedDevices()).isEmpty();
}
@Test
public void getActiveUserConnectedDevices_reflectsSecureChannelEstablished() {
String deviceId = connectNewDevice(mMockCentralManager);
mConnectedDeviceManager.onSecureChannelEstablished(deviceId, mMockCentralManager);
ConnectedDevice connectedDevice =
mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
assertThat(connectedDevice.hasSecureChannel()).isTrue();
}
@Test
public void getActiveUserConnectedDevices_excludesDisconnectedDevice() {
String deviceId = connectNewDevice(mMockCentralManager);
mConnectedDeviceManager.removeConnectedDevice(deviceId, mMockCentralManager);
assertThat(mConnectedDeviceManager.getActiveUserConnectedDevices()).isEmpty();
}
@Test
public void getActiveUserConnectedDevices_unaffectedByOtherManagerDisconnect() {
String deviceId = connectNewDevice(mMockCentralManager);
mConnectedDeviceManager.removeConnectedDevice(deviceId, mMockPeripheralManager);
assertThat(mConnectedDeviceManager.getActiveUserConnectedDevices()).hasSize(1);
}
@Test(expected = IllegalStateException.class)
public void sendMessageSecurely_throwsIllegalStateExceptionIfNoSecureChannel() {
connectNewDevice(mMockCentralManager);
ConnectedDevice device = mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
UUID recipientId = UUID.randomUUID();
byte[] message = ByteUtils.randomBytes(10);
mConnectedDeviceManager.sendMessageSecurely(device, recipientId, message);
}
@Test
public void sendMessageSecurely_sendsEncryptedMessage() {
String deviceId = connectNewDevice(mMockCentralManager);
mConnectedDeviceManager.onSecureChannelEstablished(deviceId, mMockCentralManager);
ConnectedDevice device = mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
UUID recipientId = UUID.randomUUID();
byte[] message = ByteUtils.randomBytes(10);
mConnectedDeviceManager.sendMessageSecurely(device, recipientId, message);
ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(DeviceMessage.class);
verify(mMockCentralManager).sendMessage(eq(deviceId), messageCaptor.capture());
assertThat(messageCaptor.getValue().isMessageEncrypted()).isTrue();
}
@Test
public void sendMessageSecurely_doesNotSendIfDeviceDisconnected() {
String deviceId = connectNewDevice(mMockCentralManager);
ConnectedDevice device = mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
mConnectedDeviceManager.removeConnectedDevice(deviceId, mMockCentralManager);
UUID recipientId = UUID.randomUUID();
byte[] message = ByteUtils.randomBytes(10);
mConnectedDeviceManager.sendMessageSecurely(device, recipientId, message);
verify(mMockCentralManager, times(0)).sendMessage(eq(deviceId), any(DeviceMessage.class));
}
@Test
public void sendMessageUnsecurely_sendsMessageWithoutEncryption() {
String deviceId = connectNewDevice(mMockCentralManager);
ConnectedDevice device = mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
UUID recipientId = UUID.randomUUID();
byte[] message = ByteUtils.randomBytes(10);
mConnectedDeviceManager.sendMessageUnsecurely(device, recipientId, message);
ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(DeviceMessage.class);
verify(mMockCentralManager).sendMessage(eq(deviceId), messageCaptor.capture());
assertThat(messageCaptor.getValue().isMessageEncrypted()).isFalse();
}
@Test
public void connectionCallback_onDeviceConnectedInvokedForNewlyConnectedDevice()
throws InterruptedException {
Semaphore semaphore = new Semaphore(0);
ConnectionCallback connectionCallback = createConnectionCallback(semaphore);
mConnectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback,
mCallbackExecutor);
String deviceId = connectNewDevice(mMockCentralManager);
assertThat(tryAcquire(semaphore)).isTrue();
ArgumentCaptor<ConnectedDevice> deviceCaptor =
ArgumentCaptor.forClass(ConnectedDevice.class);
verify(connectionCallback).onDeviceConnected(deviceCaptor.capture());
ConnectedDevice connectedDevice = deviceCaptor.getValue();
assertThat(connectedDevice.getDeviceId()).isEqualTo(deviceId);
assertThat(connectedDevice.hasSecureChannel()).isFalse();
}
@Test
public void connectionCallback_onDeviceConnectedNotInvokedDeviceConnectedForDifferentUser()
throws InterruptedException {
Semaphore semaphore = new Semaphore(0);
ConnectionCallback connectionCallback = createConnectionCallback(semaphore);
mConnectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback,
mCallbackExecutor);
String deviceId = UUID.randomUUID().toString();
String otherUserDeviceId = UUID.randomUUID().toString();
when(mMockStorage.getActiveUserAssociatedDeviceIds()).thenReturn(
Collections.singletonList(otherUserDeviceId));
mConnectedDeviceManager.addConnectedDevice(deviceId, mMockCentralManager);
assertThat(tryAcquire(semaphore)).isFalse();
}
@Test
public void connectionCallback_onDeviceConnectedNotInvokedForDifferentBleManager()
throws InterruptedException {
Semaphore semaphore = new Semaphore(0);
String deviceId = connectNewDevice(mMockPeripheralManager);
ConnectionCallback connectionCallback = createConnectionCallback(semaphore);
mConnectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback,
mCallbackExecutor);
mConnectedDeviceManager.addConnectedDevice(deviceId, mMockCentralManager);
assertThat(tryAcquire(semaphore)).isFalse();
}
@Test
public void connectionCallback_onDeviceDisconnectedInvokedForActiveUserDevice()
throws InterruptedException {
Semaphore semaphore = new Semaphore(0);
String deviceId = connectNewDevice(mMockCentralManager);
ConnectionCallback connectionCallback = createConnectionCallback(semaphore);
mConnectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback,
mCallbackExecutor);
mConnectedDeviceManager.removeConnectedDevice(deviceId, mMockCentralManager);
assertThat(tryAcquire(semaphore)).isTrue();
ArgumentCaptor<ConnectedDevice> deviceCaptor =
ArgumentCaptor.forClass(ConnectedDevice.class);
verify(connectionCallback).onDeviceDisconnected(deviceCaptor.capture());
assertThat(deviceCaptor.getValue().getDeviceId()).isEqualTo(deviceId);
}
@Test
public void connectionCallback_onDeviceDisconnectedNotInvokedDeviceForDifferentUser()
throws InterruptedException {
Semaphore semaphore = new Semaphore(0);
String deviceId = UUID.randomUUID().toString();
mConnectedDeviceManager.addConnectedDevice(deviceId, mMockCentralManager);
ConnectionCallback connectionCallback = createConnectionCallback(semaphore);
mConnectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback,
mCallbackExecutor);
mConnectedDeviceManager.removeConnectedDevice(deviceId, mMockCentralManager);
assertThat(tryAcquire(semaphore)).isFalse();
}
@Test
public void unregisterConnectionCallback_removesCallbackAndNotInvoked()
throws InterruptedException {
Semaphore semaphore = new Semaphore(0);
ConnectionCallback connectionCallback = createConnectionCallback(semaphore);
mConnectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback,
mCallbackExecutor);
mConnectedDeviceManager.unregisterConnectionCallback(connectionCallback);
connectNewDevice(mMockCentralManager);
assertThat(tryAcquire(semaphore)).isFalse();
}
@Test
public void registerDeviceCallback_blacklistsDuplicateRecipientId()
throws InterruptedException {
connectNewDevice(mMockCentralManager);
ConnectedDevice connectedDevice =
mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
Semaphore firstSemaphore = new Semaphore(0);
Semaphore secondSemaphore = new Semaphore(0);
Semaphore thirdSemaphore = new Semaphore(0);
DeviceCallback firstDeviceCallback = createDeviceCallback(firstSemaphore);
DeviceCallback secondDeviceCallback = createDeviceCallback(secondSemaphore);
DeviceCallback thirdDeviceCallback = createDeviceCallback(thirdSemaphore);
// Register three times for following chain of events:
// 1. First callback registered without issue.
// 2. Second callback with same recipientId triggers blacklisting both callbacks and issues
// error callbacks on both. Both callbacks should be unregistered at this point.
// 3. Third callback gets rejected at registration and issues error callback.
mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
firstDeviceCallback, mCallbackExecutor);
mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
secondDeviceCallback, mCallbackExecutor);
DeviceMessage message = new DeviceMessage(mRecipientId, false, new byte[10]);
mConnectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
assertThat(tryAcquire(firstSemaphore)).isTrue();
assertThat(tryAcquire(secondSemaphore)).isTrue();
verify(firstDeviceCallback)
.onDeviceError(connectedDevice, DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED);
verify(secondDeviceCallback)
.onDeviceError(connectedDevice, DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED);
verify(firstDeviceCallback, times(0)).onMessageReceived(any(), any());
verify(secondDeviceCallback, times(0)).onMessageReceived(any(), any());
mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
thirdDeviceCallback, mCallbackExecutor);
assertThat(tryAcquire(thirdSemaphore)).isTrue();
verify(thirdDeviceCallback)
.onDeviceError(connectedDevice, DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED);
}
@Test
public void deviceCallback_onSecureChannelEstablishedInvoked() throws InterruptedException {
Semaphore semaphore = new Semaphore(0);
connectNewDevice(mMockCentralManager);
ConnectedDevice connectedDevice =
mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
DeviceCallback deviceCallback = createDeviceCallback(semaphore);
mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
deviceCallback, mCallbackExecutor);
mConnectedDeviceManager.onSecureChannelEstablished(connectedDevice.getDeviceId(),
mMockCentralManager);
connectedDevice =
mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
assertThat(tryAcquire(semaphore)).isTrue();
verify(deviceCallback).onSecureChannelEstablished(connectedDevice);
}
@Test
public void deviceCallback_onSecureChannelEstablishedNotInvokedWithSecondBleManager()
throws InterruptedException {
Semaphore semaphore = new Semaphore(0);
connectNewDevice(mMockCentralManager);
ConnectedDevice connectedDevice =
mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
mConnectedDeviceManager.onSecureChannelEstablished(connectedDevice.getDeviceId(),
mMockCentralManager);
DeviceCallback deviceCallback = createDeviceCallback(semaphore);
mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
deviceCallback, mCallbackExecutor);
mConnectedDeviceManager.onSecureChannelEstablished(connectedDevice.getDeviceId(),
mMockPeripheralManager);
assertThat(tryAcquire(semaphore)).isFalse();
}
@Test
public void deviceCallback_onMessageReceivedInvokedForSameRecipientId()
throws InterruptedException {
Semaphore semaphore = new Semaphore(0);
connectNewDevice(mMockCentralManager);
ConnectedDevice connectedDevice =
mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
DeviceCallback deviceCallback = createDeviceCallback(semaphore);
mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
deviceCallback, mCallbackExecutor);
byte[] payload = ByteUtils.randomBytes(10);
DeviceMessage message = new DeviceMessage(mRecipientId, false, payload);
mConnectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
assertThat(tryAcquire(semaphore)).isTrue();
verify(deviceCallback).onMessageReceived(connectedDevice, payload);
}
@Test
public void deviceCallback_onMessageReceivedNotInvokedForDifferentRecipientId()
throws InterruptedException {
Semaphore semaphore = new Semaphore(0);
connectNewDevice(mMockCentralManager);
ConnectedDevice connectedDevice =
mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
DeviceCallback deviceCallback = createDeviceCallback(semaphore);
mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
deviceCallback, mCallbackExecutor);
byte[] payload = ByteUtils.randomBytes(10);
DeviceMessage message = new DeviceMessage(UUID.randomUUID(), false, payload);
mConnectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
assertThat(tryAcquire(semaphore)).isFalse();
}
@Test
public void deviceCallback_onDeviceErrorInvokedOnChannelError() throws InterruptedException {
Semaphore semaphore = new Semaphore(0);
connectNewDevice(mMockCentralManager);
ConnectedDevice connectedDevice =
mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
DeviceCallback deviceCallback = createDeviceCallback(semaphore);
mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
deviceCallback, mCallbackExecutor);
mConnectedDeviceManager.deviceErrorOccurred(connectedDevice.getDeviceId());
assertThat(tryAcquire(semaphore)).isTrue();
verify(deviceCallback).onDeviceError(connectedDevice, DEVICE_ERROR_INVALID_SECURITY_KEY);
}
@Test
public void unregisterDeviceCallback_removesCallbackAndNotInvoked()
throws InterruptedException {
Semaphore semaphore = new Semaphore(0);
connectNewDevice(mMockCentralManager);
ConnectedDevice connectedDevice =
mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
DeviceCallback deviceCallback = createDeviceCallback(semaphore);
mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
deviceCallback, mCallbackExecutor);
mConnectedDeviceManager.unregisterDeviceCallback(connectedDevice, mRecipientId,
deviceCallback);
mConnectedDeviceManager.onSecureChannelEstablished(connectedDevice.getDeviceId(),
mMockPeripheralManager);
assertThat(tryAcquire(semaphore)).isFalse();
}
@Test
public void registerDeviceCallback_sendsMissedMessageAfterRegistration()
throws InterruptedException {
Semaphore semaphore = new Semaphore(0);
connectNewDevice(mMockCentralManager);
ConnectedDevice connectedDevice =
mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
byte[] payload = ByteUtils.randomBytes(10);
DeviceMessage message = new DeviceMessage(mRecipientId, false, payload);
mConnectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
DeviceCallback deviceCallback = createDeviceCallback(semaphore);
mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
deviceCallback, mCallbackExecutor);
assertThat(tryAcquire(semaphore)).isTrue();
verify(deviceCallback).onMessageReceived(connectedDevice, payload);
}
@Test
public void registerDeviceCallback_doesNotSendMissedMessageForDifferentRecipient()
throws InterruptedException {
Semaphore semaphore = new Semaphore(0);
connectNewDevice(mMockCentralManager);
ConnectedDevice connectedDevice =
mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
byte[] payload = ByteUtils.randomBytes(10);
DeviceMessage message = new DeviceMessage(UUID.randomUUID(), false, payload);
mConnectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
DeviceCallback deviceCallback = createDeviceCallback(semaphore);
mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
deviceCallback, mCallbackExecutor);
assertThat(tryAcquire(semaphore)).isFalse();
}
@Test
public void registerDeviceCallback_doesNotSendMissedMessageForDifferentDevice()
throws InterruptedException {
Semaphore semaphore = new Semaphore(0);
connectNewDevice(mMockCentralManager);
connectNewDevice(mMockCentralManager);
List<ConnectedDevice> connectedDevices =
mConnectedDeviceManager.getActiveUserConnectedDevices();
ConnectedDevice connectedDevice = connectedDevices.get(0);
ConnectedDevice otherDevice = connectedDevices.get(1);
byte[] payload = ByteUtils.randomBytes(10);
DeviceMessage message = new DeviceMessage(mRecipientId, false, payload);
mConnectedDeviceManager.onMessageReceived(otherDevice.getDeviceId(), message);
DeviceCallback deviceCallback = createDeviceCallback(semaphore);
mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
deviceCallback, mCallbackExecutor);
assertThat(tryAcquire(semaphore)).isFalse();
}
@Test
public void onAssociationCompleted_disconnectsOriginalDeviceAndReconnectsAsActiveUser()
throws InterruptedException {
String deviceId = UUID.randomUUID().toString();
mConnectedDeviceManager.addConnectedDevice(deviceId, mMockPeripheralManager);
Semaphore semaphore = new Semaphore(0);
ConnectionCallback connectionCallback = createConnectionCallback(semaphore);
mConnectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback,
mCallbackExecutor);
when(mMockStorage.getActiveUserAssociatedDeviceIds()).thenReturn(
Collections.singletonList(deviceId));
mConnectedDeviceManager.onAssociationCompleted(deviceId);
assertThat(tryAcquire(semaphore)).isTrue();
}
private boolean tryAcquire(Semaphore semaphore) throws InterruptedException {
return semaphore.tryAcquire(100, TimeUnit.MILLISECONDS);
}
@Test
public void deviceAssociationCallback_onAssociatedDeviceAdded() throws InterruptedException {
Semaphore semaphore = new Semaphore(0);
DeviceAssociationCallback callback = createDeviceAssociationCallback(semaphore);
mConnectedDeviceManager.registerDeviceAssociationCallback(callback, mCallbackExecutor);
String deviceId = UUID.randomUUID().toString();
AssociatedDevice testDevice = new AssociatedDevice(deviceId, TEST_DEVICE_ADDRESS,
TEST_DEVICE_NAME, /* isConnectionEnabled = */ true);
mAssociatedDeviceCallback.onAssociatedDeviceAdded(testDevice);
assertThat(tryAcquire(semaphore)).isTrue();
verify(callback).onAssociatedDeviceAdded(eq(testDevice));
}
@Test
public void deviceAssociationCallback_onAssociationDeviceRemoved() throws InterruptedException {
Semaphore semaphore = new Semaphore(0);
DeviceAssociationCallback callback = createDeviceAssociationCallback(semaphore);
mConnectedDeviceManager.registerDeviceAssociationCallback(callback, mCallbackExecutor);
String deviceId = UUID.randomUUID().toString();
AssociatedDevice testDevice = new AssociatedDevice(deviceId, TEST_DEVICE_ADDRESS,
TEST_DEVICE_NAME, /* isConnectionEnabled = */ true);
mAssociatedDeviceCallback.onAssociatedDeviceRemoved(testDevice);
assertThat(tryAcquire(semaphore)).isTrue();
verify(callback).onAssociatedDeviceRemoved(eq(testDevice));
}
@Test
public void deviceAssociationCallback_onAssociatedDeviceUpdated() throws InterruptedException {
Semaphore semaphore = new Semaphore(0);
DeviceAssociationCallback callback = createDeviceAssociationCallback(semaphore);
mConnectedDeviceManager.registerDeviceAssociationCallback(callback, mCallbackExecutor);
String deviceId = UUID.randomUUID().toString();
AssociatedDevice testDevice = new AssociatedDevice(deviceId, TEST_DEVICE_ADDRESS,
TEST_DEVICE_NAME, /* isConnectionEnabled = */ true);
mAssociatedDeviceCallback.onAssociatedDeviceUpdated(testDevice);
assertThat(tryAcquire(semaphore)).isTrue();
verify(callback).onAssociatedDeviceUpdated(eq(testDevice));
}
@Test
public void removeConnectedDevice_startsAdvertisingForActiveUserDeviceOnActiveUserDisconnect() {
String deviceId = UUID.randomUUID().toString();
when(mMockStorage.getActiveUserAssociatedDeviceIds()).thenReturn(
Collections.singletonList(deviceId));
AssociatedDevice device = new AssociatedDevice(deviceId, TEST_DEVICE_ADDRESS,
TEST_DEVICE_NAME, /* isConnectionEnabled = */ true);
when(mMockStorage.getActiveUserAssociatedDevices()).thenReturn(
Collections.singletonList(device));
mConnectedDeviceManager.addConnectedDevice(deviceId, mMockPeripheralManager);
mConnectedDeviceManager.removeConnectedDevice(deviceId, mMockPeripheralManager);
verify(mMockPeripheralManager, timeout(1000))
.connectToDevice(eq(UUID.fromString(deviceId)), anyInt());
}
@Test
public void removeConnectedDevice_startsAdvertisingForActiveUserDeviceOnLastDeviceDisconnect() {
String deviceId = UUID.randomUUID().toString();
String userDeviceId = UUID.randomUUID().toString();
when(mMockStorage.getActiveUserAssociatedDeviceIds()).thenReturn(
Collections.singletonList(userDeviceId));
AssociatedDevice userDevice = new AssociatedDevice(userDeviceId, TEST_DEVICE_ADDRESS,
TEST_DEVICE_NAME, /* isConnectionEnabled = */ true);
when(mMockStorage.getActiveUserAssociatedDevices()).thenReturn(
Collections.singletonList(userDevice));
mConnectedDeviceManager.addConnectedDevice(deviceId, mMockPeripheralManager);
mConnectedDeviceManager.removeConnectedDevice(deviceId, mMockPeripheralManager);
verify(mMockPeripheralManager, timeout(1000))
.connectToDevice(eq(UUID.fromString(userDeviceId)), anyInt());
}
@Test
public void removeConnectedDevice__doesNotAdvertiseForNonActiveUserDeviceNotLastDevice() {
String deviceId = UUID.randomUUID().toString();
String userDeviceId = UUID.randomUUID().toString();
when(mMockStorage.getActiveUserAssociatedDeviceIds()).thenReturn(
Collections.singletonList(userDeviceId));
AssociatedDevice userDevice = new AssociatedDevice(userDeviceId, TEST_DEVICE_ADDRESS,
TEST_DEVICE_NAME, /* isConnectionEnabled = */ true);
when(mMockStorage.getActiveUserAssociatedDevices()).thenReturn(
Collections.singletonList(userDevice));
mConnectedDeviceManager.addConnectedDevice(deviceId, mMockPeripheralManager);
mConnectedDeviceManager.addConnectedDevice(userDeviceId, mMockCentralManager);
mConnectedDeviceManager.removeConnectedDevice(deviceId, mMockPeripheralManager);
verify(mMockPeripheralManager, timeout(1000).times(0))
.connectToDevice(eq(UUID.fromString(userDeviceId)), anyInt());
}
@Test
public void removeActiveUserAssociatedDevice_deletesAssociatedDeviceFromStorage() {
String deviceId = UUID.randomUUID().toString();
mConnectedDeviceManager.removeActiveUserAssociatedDevice(deviceId);
verify(mMockStorage).removeAssociatedDeviceForActiveUser(deviceId);
}
@Test
public void removeActiveUserAssociatedDevice_disconnectsIfConnected() {
String deviceId = connectNewDevice(mMockPeripheralManager);
mConnectedDeviceManager.removeActiveUserAssociatedDevice(deviceId);
verify(mMockPeripheralManager).disconnectDevice(deviceId);
}
@Test
public void enableAssociatedDeviceConnection_enableDeviceConnectionInStorage() {
String deviceId = UUID.randomUUID().toString();
mConnectedDeviceManager.enableAssociatedDeviceConnection(deviceId);
verify(mMockStorage).updateAssociatedDeviceConnectionEnabled(deviceId, true);
}
@Test
public void disableAssociatedDeviceConnection_disableDeviceConnectionInStorage() {
String deviceId = UUID.randomUUID().toString();
mConnectedDeviceManager.disableAssociatedDeviceConnection(deviceId);
verify(mMockStorage).updateAssociatedDeviceConnectionEnabled(deviceId, false);
}
@Test
public void disableAssociatedDeviceConnection_disconnectsIfConnected() {
String deviceId = connectNewDevice(mMockPeripheralManager);
mConnectedDeviceManager.disableAssociatedDeviceConnection(deviceId);
verify(mMockPeripheralManager).disconnectDevice(deviceId);
}
@Test
public void onMessageReceived_deliversMessageIfDelegateIsNull() throws InterruptedException {
connectNewDevice(mMockCentralManager);
ConnectedDevice connectedDevice =
mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
Semaphore semaphore = new Semaphore(0);
DeviceCallback deviceCallback = createDeviceCallback(semaphore);
mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
deviceCallback, mCallbackExecutor);
DeviceMessage message = new DeviceMessage(mRecipientId, false, new byte[10]);
mConnectedDeviceManager.setMessageDeliveryDelegate(null);
mConnectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
assertThat(tryAcquire(semaphore)).isTrue();
}
@Test
public void onMessageReceived_deliversMessageIfDelegateAccepts() throws InterruptedException {
connectNewDevice(mMockCentralManager);
ConnectedDevice connectedDevice =
mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
Semaphore semaphore = new Semaphore(0);
DeviceCallback deviceCallback = createDeviceCallback(semaphore);
mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
deviceCallback, mCallbackExecutor);
DeviceMessage message = new DeviceMessage(mRecipientId, false, new byte[10]);
MessageDeliveryDelegate delegate = device -> true;
mConnectedDeviceManager.setMessageDeliveryDelegate(delegate);
mConnectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
assertThat(tryAcquire(semaphore)).isTrue();
}
@Test
public void onMessageReceived_doesNotDeliverMessageIfDelegateRejects()
throws InterruptedException {
connectNewDevice(mMockCentralManager);
ConnectedDevice connectedDevice =
mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
Semaphore semaphore = new Semaphore(0);
DeviceCallback deviceCallback = createDeviceCallback(semaphore);
mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
deviceCallback, mCallbackExecutor);
DeviceMessage message = new DeviceMessage(mRecipientId, false, new byte[10]);
MessageDeliveryDelegate delegate = device -> false;
mConnectedDeviceManager.setMessageDeliveryDelegate(delegate);
mConnectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
assertThat(tryAcquire(semaphore)).isFalse();
}
@NonNull
private String connectNewDevice(@NonNull CarBleManager carBleManager) {
String deviceId = UUID.randomUUID().toString();
AssociatedDevice device = new AssociatedDevice(deviceId, TEST_DEVICE_ADDRESS,
TEST_DEVICE_NAME, /* isConnectionEnabled = */ true);
when(mMockStorage.getActiveUserAssociatedDevices()).thenReturn(
Collections.singletonList(device));
when(mMockStorage.getActiveUserAssociatedDeviceIds()).thenReturn(
Collections.singletonList(deviceId));
mConnectedDeviceManager.addConnectedDevice(deviceId, carBleManager);
return deviceId;
}
@NonNull
private ConnectionCallback createConnectionCallback(@NonNull final Semaphore semaphore) {
return spy(new ConnectionCallback() {
@Override
public void onDeviceConnected(ConnectedDevice device) {
semaphore.release();
}
@Override
public void onDeviceDisconnected(ConnectedDevice device) {
semaphore.release();
}
});
}
@NonNull
private DeviceCallback createDeviceCallback(@NonNull final Semaphore semaphore) {
return spy(new DeviceCallback() {
@Override
public void onSecureChannelEstablished(ConnectedDevice device) {
semaphore.release();
}
@Override
public void onMessageReceived(ConnectedDevice device, byte[] message) {
semaphore.release();
}
@Override
public void onDeviceError(ConnectedDevice device, int error) {
semaphore.release();
}
});
}
@NonNull
private DeviceAssociationCallback createDeviceAssociationCallback(
@NonNull final Semaphore semaphore) {
return spy(new DeviceAssociationCallback() {
@Override
public void onAssociatedDeviceAdded(AssociatedDevice device) {
semaphore.release();
}
@Override
public void onAssociatedDeviceRemoved(
AssociatedDevice device) {
semaphore.release();
}
@Override
public void onAssociatedDeviceUpdated(AssociatedDevice device) {
semaphore.release();
}
});
}
}