| /* |
| * 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.ble; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| |
| import static org.mockito.ArgumentMatchers.any; |
| import static org.mockito.ArgumentMatchers.eq; |
| import static org.mockito.Mockito.mockitoSession; |
| import static org.mockito.Mockito.reset; |
| import static org.mockito.Mockito.timeout; |
| import static org.mockito.Mockito.verify; |
| import static org.mockito.Mockito.when; |
| |
| import android.bluetooth.BluetoothAdapter; |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.le.AdvertiseCallback; |
| import android.bluetooth.le.AdvertiseData; |
| import android.bluetooth.le.AdvertiseSettings; |
| import android.os.ParcelUuid; |
| |
| import androidx.test.ext.junit.runners.AndroidJUnit4; |
| |
| import com.android.car.connecteddevice.AssociationCallback; |
| import com.android.car.connecteddevice.model.AssociatedDevice; |
| import com.android.car.connecteddevice.storage.ConnectedDeviceStorage; |
| 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.time.Duration; |
| import java.util.UUID; |
| |
| @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 WRITE_UUID = UUID.randomUUID(); |
| private static final UUID READ_UUID = UUID.randomUUID(); |
| private static final int DEVICE_NAME_LENGTH_LIMIT = 8; |
| 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 Duration RECONNECT_ADVERTISEMENT_DURATION = Duration.ofSeconds(2); |
| private static final int DEFAULT_MTU_SIZE = 23; |
| |
| @Mock |
| private BlePeripheralManager mMockPeripheralManager; |
| @Mock |
| private ConnectedDeviceStorage mMockStorage; |
| @Mock |
| private AssociationCallback mAssociationCallback; |
| |
| private CarBlePeripheralManager mCarBlePeripheralManager; |
| |
| private MockitoSession mMockitoSession; |
| |
| @Before |
| public void setUp() { |
| mMockitoSession = mockitoSession() |
| .initMocks(this) |
| .strictness(Strictness.WARN) |
| .startMocking(); |
| mCarBlePeripheralManager = new CarBlePeripheralManager(mMockPeripheralManager, mMockStorage, |
| ASSOCIATION_SERVICE_UUID, RECONNECT_SERVICE_UUID, RECONNECT_DATA_UUID, |
| WRITE_UUID, READ_UUID, RECONNECT_ADVERTISEMENT_DURATION, DEFAULT_MTU_SIZE); |
| mCarBlePeripheralManager.start(); |
| } |
| |
| @After |
| public void tearDown() { |
| if (mCarBlePeripheralManager != null) { |
| mCarBlePeripheralManager.stop(); |
| } |
| if (mMockitoSession != null) { |
| mMockitoSession.finishMocking(); |
| } |
| } |
| |
| @Test |
| public void testStartAssociationAdvertisingSuccess() { |
| String testDeviceName = getNameForAssociation(); |
| startAssociation(mAssociationCallback, testDeviceName); |
| ArgumentCaptor<AdvertiseData> advertiseDataCaptor = |
| ArgumentCaptor.forClass(AdvertiseData.class); |
| ArgumentCaptor<AdvertiseData> scanResponseDataCaptor = |
| ArgumentCaptor.forClass(AdvertiseData.class); |
| verify(mMockPeripheralManager).startAdvertising(any(), advertiseDataCaptor.capture(), |
| scanResponseDataCaptor.capture(), any()); |
| AdvertiseData advertisementData = advertiseDataCaptor.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.getBytes()); |
| } |
| |
| @Test |
| public void testStartAssociationAdvertisingFailure() { |
| startAssociation(mAssociationCallback, getNameForAssociation()); |
| ArgumentCaptor<AdvertiseCallback> callbackCaptor = |
| ArgumentCaptor.forClass(AdvertiseCallback.class); |
| verify(mMockPeripheralManager).startAdvertising(any(), any(), any(), |
| callbackCaptor.capture()); |
| AdvertiseCallback advertiseCallback = callbackCaptor.getValue(); |
| int testErrorCode = 2; |
| advertiseCallback.onStartFailure(testErrorCode); |
| verify(mAssociationCallback).onAssociationStartFailure(); |
| } |
| |
| @Test |
| public void testNotifyAssociationSuccess() { |
| String testDeviceName = getNameForAssociation(); |
| startAssociation(mAssociationCallback, testDeviceName); |
| ArgumentCaptor<AdvertiseCallback> callbackCaptor = |
| ArgumentCaptor.forClass(AdvertiseCallback.class); |
| verify(mMockPeripheralManager).startAdvertising(any(), any(), any(), |
| callbackCaptor.capture()); |
| AdvertiseCallback advertiseCallback = callbackCaptor.getValue(); |
| AdvertiseSettings settings = new AdvertiseSettings.Builder().build(); |
| advertiseCallback.onStartSuccess(settings); |
| verify(mAssociationCallback).onAssociationStartSuccess(eq(testDeviceName)); |
| } |
| |
| @Test |
| public void testShowVerificationCode() { |
| AssociationSecureChannel channel = getChannelForAssociation(mAssociationCallback); |
| channel.getShowVerificationCodeListener().showVerificationCode(TEST_VERIFICATION_CODE); |
| verify(mAssociationCallback).onVerificationCodeAvailable(eq(TEST_VERIFICATION_CODE)); |
| } |
| |
| @Test |
| public void testAssociationSuccess() { |
| SecureBleChannel channel = getChannelForAssociation(mAssociationCallback); |
| SecureBleChannel.Callback channelCallback = channel.getCallback(); |
| assertThat(channelCallback).isNotNull(); |
| channelCallback.onDeviceIdReceived(TEST_REMOTE_DEVICE_ID.toString()); |
| channelCallback.onSecureChannelEstablished(); |
| ArgumentCaptor<AssociatedDevice> deviceCaptor = |
| ArgumentCaptor.forClass(AssociatedDevice.class); |
| verify(mMockStorage).addAssociatedDeviceForActiveUser(deviceCaptor.capture()); |
| AssociatedDevice device = deviceCaptor.getValue(); |
| assertThat(device.getDeviceId()).isEqualTo(TEST_REMOTE_DEVICE_ID.toString()); |
| verify(mAssociationCallback).onAssociationCompleted(eq(TEST_REMOTE_DEVICE_ID.toString())); |
| } |
| |
| @Test |
| public void testAssociationFailure_channelError() { |
| SecureBleChannel channel = getChannelForAssociation(mAssociationCallback); |
| SecureBleChannel.Callback channelCallback = channel.getCallback(); |
| int testErrorCode = 1; |
| assertThat(channelCallback).isNotNull(); |
| channelCallback.onDeviceIdReceived(TEST_REMOTE_DEVICE_ID.toString()); |
| channelCallback.onEstablishSecureChannelFailure(testErrorCode); |
| verify(mAssociationCallback).onAssociationError(eq(testErrorCode)); |
| } |
| |
| @Test |
| public void connectToDevice_stopsAdvertisingAfterTimeout() { |
| when(mMockStorage.hashWithChallengeSecret(any(), any())) |
| .thenReturn(ByteUtils.randomBytes(32)); |
| mCarBlePeripheralManager.connectToDevice(UUID.randomUUID()); |
| ArgumentCaptor<AdvertiseCallback> callbackCaptor = |
| ArgumentCaptor.forClass(AdvertiseCallback.class); |
| verify(mMockPeripheralManager).startAdvertising(any(), any(), any(), |
| callbackCaptor.capture()); |
| callbackCaptor.getValue().onStartSuccess(null); |
| verify(mMockPeripheralManager, |
| timeout(RECONNECT_ADVERTISEMENT_DURATION.plusSeconds(1).toMillis())) |
| .stopAdvertising(any(AdvertiseCallback.class)); |
| } |
| |
| @Test |
| public void disconnectDevice_stopsAdvertisingForPendingReconnect() { |
| when(mMockStorage.hashWithChallengeSecret(any(), any())) |
| .thenReturn(ByteUtils.randomBytes(32)); |
| UUID deviceId = UUID.randomUUID(); |
| mCarBlePeripheralManager.connectToDevice(deviceId); |
| reset(mMockPeripheralManager); |
| mCarBlePeripheralManager.disconnectDevice(deviceId.toString()); |
| verify(mMockPeripheralManager).cleanup(); |
| } |
| |
| private BlePeripheralManager.Callback startAssociation(AssociationCallback callback, |
| String deviceName) { |
| ArgumentCaptor<BlePeripheralManager.Callback> callbackCaptor = |
| ArgumentCaptor.forClass(BlePeripheralManager.Callback.class); |
| mCarBlePeripheralManager.startAssociation(deviceName, callback); |
| verify(mMockPeripheralManager, timeout(3000)).registerCallback(callbackCaptor.capture()); |
| return callbackCaptor.getValue(); |
| } |
| |
| private AssociationSecureChannel getChannelForAssociation(AssociationCallback callback) { |
| BlePeripheralManager.Callback bleManagerCallback = startAssociation(callback, |
| getNameForAssociation()); |
| BluetoothDevice bluetoothDevice = BluetoothAdapter.getDefaultAdapter() |
| .getRemoteDevice(TEST_REMOTE_DEVICE_ADDRESS); |
| bleManagerCallback.onRemoteDeviceConnected(bluetoothDevice); |
| return (AssociationSecureChannel) mCarBlePeripheralManager.getConnectedDeviceChannel(); |
| } |
| |
| private String getNameForAssociation() { |
| return ByteUtils.generateRandomNumberString(DEVICE_NAME_LENGTH_LIMIT); |
| |
| } |
| } |