blob: 00b9ba50993abae61b62e8a686d1dd2f6dffb61e [file] [log] [blame]
/*
* Copyright (C) 2020 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.google.android.connecteddevice.connection.ble;
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.le.AdvertiseCallback;
import android.bluetooth.le.AdvertiseData;
import android.bluetooth.le.AdvertiseSettings;
import android.os.Handler;
import android.os.Looper;
import android.os.ParcelUuid;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.companionprotos.VersionExchangeProto.VersionExchange;
import com.google.android.connecteddevice.connection.AssociationCallback;
import com.google.android.connecteddevice.connection.AssociationSecureChannel;
import com.google.android.connecteddevice.connection.ConnectionResolver;
import com.google.android.connecteddevice.connection.SecureChannel;
import com.google.android.connecteddevice.model.AssociatedDevice;
import com.google.android.connecteddevice.model.OobData;
import com.google.android.connecteddevice.model.StartAssociationResponse;
import com.google.android.connecteddevice.oob.OobConnectionManager;
import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
import com.google.android.connecteddevice.transport.ble.BlePeripheralManager;
import com.google.android.connecteddevice.util.ByteUtils;
import java.time.Duration;
import java.util.Arrays;
import java.util.UUID;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.shadows.ShadowLooper;
@RunWith(AndroidJUnit4.class)
public class CarBlePeripheralManagerTest {
private static final UUID ASSOCIATION_SERVICE_UUID = UUID.randomUUID();
private static final UUID RECONNECT_SERVICE_UUID = UUID.randomUUID();
private static final UUID RECONNECT_DATA_UUID = UUID.randomUUID();
private static final UUID ADVERTISE_DATA_CHARACTERISTIC_UUID = UUID.randomUUID();
private static final UUID WRITE_UUID = UUID.randomUUID();
private static final UUID READ_UUID = UUID.randomUUID();
private static final int DEVICE_NAME_LENGTH_LIMIT = 2;
private static final String TEST_REMOTE_DEVICE_ADDRESS = "00:11:22:33:AA:BB";
private static final UUID TEST_REMOTE_DEVICE_ID = UUID.randomUUID();
private static final String TEST_VERIFICATION_CODE = "000000";
private static final String TEST_ENCRYPTED_VERIFICATION_CODE = "12345";
private static final Duration RECONNECT_ADVERTISEMENT_DURATION = Duration.ofMillis(2);
private static final int DEFAULT_MTU_SIZE = 23;
private static final boolean COMPRESSION_ENABLED = true;
private static final boolean EXCHANGE_CAPABILITIES = false;
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
@Mock private BlePeripheralManager mockBlePeripheralManager;
@Mock private ConnectedDeviceStorage mockConnectedDeviceStorage;
@Mock private OobConnectionManager mockOobConnectionManager;
@Mock private AssociationCallback mockAssociationCallback;
private CarBlePeripheralManager carBlePeripheralManager;
@Before
public void setUp() throws Exception {
carBlePeripheralManager =
new CarBlePeripheralManager(
mockBlePeripheralManager,
mockConnectedDeviceStorage,
ASSOCIATION_SERVICE_UUID,
RECONNECT_SERVICE_UUID,
RECONNECT_DATA_UUID,
ADVERTISE_DATA_CHARACTERISTIC_UUID,
WRITE_UUID,
READ_UUID,
RECONNECT_ADVERTISEMENT_DURATION,
DEFAULT_MTU_SIZE,
COMPRESSION_ENABLED,
EXCHANGE_CAPABILITIES);
when(mockOobConnectionManager.encryptVerificationCode(TEST_VERIFICATION_CODE.getBytes(UTF_8)))
.thenReturn(TEST_ENCRYPTED_VERIFICATION_CODE.getBytes(UTF_8));
when(mockOobConnectionManager.decryptVerificationCode(
TEST_ENCRYPTED_VERIFICATION_CODE.getBytes(UTF_8)))
.thenReturn(TEST_VERIFICATION_CODE.getBytes(UTF_8));
carBlePeripheralManager.start();
}
@After
public void tearDown() {
if (carBlePeripheralManager != null) {
carBlePeripheralManager.stop();
}
}
@Test
public void testStartAssociationAdvertisingSuccess() {
byte[] testDeviceName = getNameForAssociation();
startAssociation(mockAssociationCallback, testDeviceName);
ArgumentCaptor<AdvertiseData> advertisementDataCaptor =
ArgumentCaptor.forClass(AdvertiseData.class);
ArgumentCaptor<AdvertiseData> scanResponseDataCaptor =
ArgumentCaptor.forClass(AdvertiseData.class);
verify(mockBlePeripheralManager)
.startAdvertising(
any(), advertisementDataCaptor.capture(), scanResponseDataCaptor.capture(), any());
AdvertiseData advertisementData = advertisementDataCaptor.getValue();
ParcelUuid serviceUuid = new ParcelUuid(ASSOCIATION_SERVICE_UUID);
assertThat(advertisementData.getServiceUuids()).contains(serviceUuid);
AdvertiseData scanResponseData = scanResponseDataCaptor.getValue();
assertThat(scanResponseData.getIncludeDeviceName()).isFalse();
ParcelUuid dataUuid = new ParcelUuid(RECONNECT_DATA_UUID);
assertThat(scanResponseData.getServiceData().get(dataUuid)).isEqualTo(testDeviceName);
}
@Test
public void testStartAssociationAdvertisingFailure() {
startAssociation(mockAssociationCallback, getNameForAssociation());
ArgumentCaptor<AdvertiseCallback> callbackCaptor =
ArgumentCaptor.forClass(AdvertiseCallback.class);
verify(mockBlePeripheralManager)
.startAdvertising(any(), any(), any(), callbackCaptor.capture());
AdvertiseCallback advertiseCallback = callbackCaptor.getValue();
int testErrorCode = 2;
advertiseCallback.onStartFailure(testErrorCode);
verify(mockAssociationCallback).onAssociationStartFailure();
}
@Test
public void testNotifyAssociationSuccess() {
byte[] testDeviceName = getNameForAssociation();
startAssociation(mockAssociationCallback, testDeviceName);
ArgumentCaptor<AdvertiseCallback> callbackCaptor =
ArgumentCaptor.forClass(AdvertiseCallback.class);
verify(mockBlePeripheralManager)
.startAdvertising(any(), any(), any(), callbackCaptor.capture());
AdvertiseCallback advertiseCallback = callbackCaptor.getValue();
AdvertiseSettings settings = new AdvertiseSettings.Builder().build();
advertiseCallback.onStartSuccess(settings);
OobData emptyOobData = new OobData(new byte[0], new byte[0], new byte[0]);
verify(mockAssociationCallback)
.onAssociationStartSuccess(
new StartAssociationResponse(
emptyOobData, testDeviceName, ByteUtils.byteArrayToHexString(testDeviceName)));
}
@Test
public void testShowVerificationCode() {
AssociationSecureChannel channel = getChannelForAssociation(mockAssociationCallback);
channel.getShowVerificationCodeListener().showVerificationCode(TEST_VERIFICATION_CODE);
verify(mockAssociationCallback).onVerificationCodeAvailable(eq(TEST_VERIFICATION_CODE));
}
@Test
public void testAssociationSuccess() {
SecureChannel channel = getChannelForAssociation(mockAssociationCallback);
SecureChannel.Callback channelCallback = channel.getCallback();
assertThat(channelCallback).isNotNull();
channelCallback.onDeviceIdReceived(TEST_REMOTE_DEVICE_ID.toString());
channelCallback.onSecureChannelEstablished();
ArgumentCaptor<AssociatedDevice> deviceCaptor = ArgumentCaptor.forClass(AssociatedDevice.class);
verify(mockConnectedDeviceStorage).addAssociatedDeviceForDriver(deviceCaptor.capture());
AssociatedDevice device = deviceCaptor.getValue();
assertThat(device.getDeviceId()).isEqualTo(TEST_REMOTE_DEVICE_ID.toString());
verify(mockAssociationCallback).onAssociationCompleted(eq(TEST_REMOTE_DEVICE_ID.toString()));
}
@Test
public void testAssociationFailure_channelError() {
SecureChannel channel = getChannelForAssociation(mockAssociationCallback);
SecureChannel.Callback channelCallback = channel.getCallback();
int testErrorCode = 1;
assertThat(channelCallback).isNotNull();
channelCallback.onDeviceIdReceived(TEST_REMOTE_DEVICE_ID.toString());
channelCallback.onEstablishSecureChannelFailure(testErrorCode);
verify(mockAssociationCallback).onAssociationError(eq(testErrorCode));
}
@Test
public void connectToDevice_embedAdvertiseDataInCharacteristic() {
carBlePeripheralManager.setTimeoutHandler(new Handler(Looper.getMainLooper()));
when(mockConnectedDeviceStorage.hashWithChallengeSecret(any(), any()))
.thenReturn(ByteUtils.randomBytes(32));
carBlePeripheralManager.connectToDevice(UUID.randomUUID());
ArgumentCaptor<AdvertiseData> advertiseDataCaptor =
ArgumentCaptor.forClass(AdvertiseData.class);
ArgumentCaptor<BluetoothGattService> gattServiceCaptor =
ArgumentCaptor.forClass(BluetoothGattService.class);
verify(mockBlePeripheralManager)
.startAdvertising(gattServiceCaptor.capture(), advertiseDataCaptor.capture(), any(), any());
assertThat(
Arrays.equals(
gattServiceCaptor
.getValue()
.getCharacteristic(ADVERTISE_DATA_CHARACTERISTIC_UUID)
.getValue(),
advertiseDataCaptor.getValue().getServiceData().values().iterator().next()))
.isTrue();
}
@Test
public void connectToDevice_stopsAdvertisingAfterTimeout() {
carBlePeripheralManager.setTimeoutHandler(new Handler(Looper.getMainLooper()));
when(mockConnectedDeviceStorage.hashWithChallengeSecret(any(), any()))
.thenReturn(ByteUtils.randomBytes(32));
carBlePeripheralManager.connectToDevice(UUID.randomUUID());
ArgumentCaptor<AdvertiseCallback> callbackCaptor =
ArgumentCaptor.forClass(AdvertiseCallback.class);
verify(mockBlePeripheralManager)
.startAdvertising(any(), any(), any(), callbackCaptor.capture());
callbackCaptor.getValue().onStartSuccess(null);
// Simulate the timeout.
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
verify(mockBlePeripheralManager).stopAdvertising(any(AdvertiseCallback.class));
}
@Test
public void disconnectDevice_stopsAdvertisingForPendingReconnect() {
when(mockConnectedDeviceStorage.hashWithChallengeSecret(any(), any()))
.thenReturn(ByteUtils.randomBytes(32));
UUID deviceId = UUID.randomUUID();
carBlePeripheralManager.connectToDevice(deviceId);
reset(mockBlePeripheralManager);
carBlePeripheralManager.disconnectDevice(deviceId.toString());
verify(mockBlePeripheralManager).cleanup();
}
private AssociationSecureChannel getChannelForAssociation(AssociationCallback callback) {
BlePeripheralManager.Callback bleManagerCallback =
startAssociation(callback, getNameForAssociation());
BluetoothDevice bluetoothDevice =
ApplicationProvider.getApplicationContext()
.getSystemService(BluetoothManager.class)
.getAdapter()
.getRemoteDevice(TEST_REMOTE_DEVICE_ADDRESS);
bleManagerCallback.onRemoteDeviceConnected(bluetoothDevice);
ArgumentCaptor<BlePeripheralManager.OnCharacteristicWriteListener> listenerCaptor =
ArgumentCaptor.forClass(BlePeripheralManager.OnCharacteristicWriteListener.class);
verify(mockBlePeripheralManager).addOnCharacteristicWriteListener(listenerCaptor.capture());
// Use the MIN_SECURITY_VERSION for both to avoid further complexity introduced with
// the capabilities exchange that has nothing to do with the tests here. In the future this
// class will be deprecated in favor of a protocol agnostic version.
VersionExchange versionExchangeMessage =
VersionExchange.newBuilder()
.setMinSupportedMessagingVersion(ConnectionResolver.MESSAGING_VERSION)
.setMaxSupportedMessagingVersion(ConnectionResolver.MESSAGING_VERSION)
.setMinSupportedSecurityVersion(ConnectionResolver.MIN_SECURITY_VERSION)
.setMaxSupportedSecurityVersion(ConnectionResolver.MIN_SECURITY_VERSION)
.build();
listenerCaptor
.getValue()
.onCharacteristicWrite(
bluetoothDevice,
carBlePeripheralManager.readCharacteristic,
versionExchangeMessage.toByteArray());
return (AssociationSecureChannel) carBlePeripheralManager.getConnectedDeviceChannel();
}
private BlePeripheralManager.Callback startAssociation(
AssociationCallback callback, byte[] deviceName) {
ArgumentCaptor<BlePeripheralManager.Callback> callbackCaptor =
ArgumentCaptor.forClass(BlePeripheralManager.Callback.class);
carBlePeripheralManager.startAssociation(deviceName, callback);
verify(mockBlePeripheralManager).registerCallback(callbackCaptor.capture());
return callbackCaptor.getValue();
}
private static byte[] getNameForAssociation() {
return ByteUtils.randomBytes(DEVICE_NAME_LENGTH_LIMIT);
}
}