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.google.common.truth.Truth.assertThat; 20 21 import static org.mockito.ArgumentMatchers.any; 22 import static org.mockito.ArgumentMatchers.eq; 23 import static org.mockito.Mockito.mockitoSession; 24 import static org.mockito.Mockito.spy; 25 import static org.mockito.Mockito.timeout; 26 import static org.mockito.Mockito.verify; 27 28 import android.annotation.NonNull; 29 import android.bluetooth.BluetoothAdapter; 30 import android.bluetooth.BluetoothDevice; 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.car.encryptionrunner.Key; 36 import android.os.ParcelUuid; 37 38 import androidx.test.ext.junit.runners.AndroidJUnit4; 39 40 import com.android.car.connecteddevice.AssociationCallback; 41 import com.android.car.connecteddevice.model.AssociatedDevice; 42 import com.android.car.connecteddevice.storage.ConnectedDeviceStorage; 43 import com.android.car.connecteddevice.util.ByteUtils; 44 45 import org.junit.After; 46 import org.junit.AfterClass; 47 import org.junit.Before; 48 import org.junit.BeforeClass; 49 import org.junit.Test; 50 import org.junit.runner.RunWith; 51 import org.mockito.ArgumentCaptor; 52 import org.mockito.Mock; 53 import org.mockito.MockitoSession; 54 import org.mockito.quality.Strictness; 55 56 import java.util.UUID; 57 import java.util.concurrent.Semaphore; 58 import java.util.concurrent.TimeUnit; 59 60 @RunWith(AndroidJUnit4.class) 61 public class CarBlePeripheralManagerTest { 62 private static final UUID ASSOCIATION_SERVICE_UUID = UUID.randomUUID(); 63 private static final UUID WRITE_UUID = UUID.randomUUID(); 64 private static final UUID READ_UUID = UUID.randomUUID(); 65 private static final int DEVICE_NAME_LENGTH_LIMIT = 8; 66 private static final String TEST_REMOTE_DEVICE_ADDRESS = "00:11:22:33:AA:BB"; 67 private static final UUID TEST_REMOTE_DEVICE_ID = UUID.randomUUID(); 68 private static final String TEST_VERIFICATION_CODE = "000000"; 69 private static final byte[] TEST_KEY = "Key".getBytes(); 70 private static String sAdapterName; 71 72 @Mock private BlePeripheralManager mMockPeripheralManager; 73 @Mock private ConnectedDeviceStorage mMockStorage; 74 75 private CarBlePeripheralManager mCarBlePeripheralManager; 76 77 private MockitoSession mMockitoSession; 78 79 @BeforeClass beforeSetUp()80 public static void beforeSetUp() { 81 sAdapterName = BluetoothAdapter.getDefaultAdapter().getName(); 82 } 83 @Before setUp()84 public void setUp() { 85 mMockitoSession = mockitoSession() 86 .initMocks(this) 87 .strictness(Strictness.LENIENT) 88 .startMocking(); 89 mCarBlePeripheralManager = new CarBlePeripheralManager(mMockPeripheralManager, mMockStorage, 90 ASSOCIATION_SERVICE_UUID, WRITE_UUID, READ_UUID); 91 } 92 93 @After tearDown()94 public void tearDown() { 95 if (mCarBlePeripheralManager != null) { 96 mCarBlePeripheralManager.stop(); 97 } 98 if (mMockitoSession != null) { 99 mMockitoSession.finishMocking(); 100 } 101 } 102 103 @AfterClass afterTearDown()104 public static void afterTearDown() { 105 BluetoothAdapter.getDefaultAdapter().setName(sAdapterName); 106 } 107 108 @Test testStartAssociationAdvertisingSuccess()109 public void testStartAssociationAdvertisingSuccess() { 110 Semaphore semaphore = new Semaphore(0); 111 AssociationCallback callback = createAssociationCallback(semaphore); 112 String testDeviceName = getNameForAssociation(); 113 startAssociation(callback, testDeviceName); 114 ArgumentCaptor<AdvertiseData> dataCaptor = ArgumentCaptor.forClass(AdvertiseData.class); 115 verify(mMockPeripheralManager, timeout(3000)).startAdvertising(any(), 116 dataCaptor.capture(), any()); 117 AdvertiseData data = dataCaptor.getValue(); 118 assertThat(data.getIncludeDeviceName()).isTrue(); 119 ParcelUuid expected = new ParcelUuid(ASSOCIATION_SERVICE_UUID); 120 assertThat(data.getServiceUuids().get(0)).isEqualTo(expected); 121 assertThat(BluetoothAdapter.getDefaultAdapter().getName()).isEqualTo(testDeviceName); 122 } 123 124 @Test testStartAssociationAdvertisingFailure()125 public void testStartAssociationAdvertisingFailure() throws InterruptedException { 126 Semaphore semaphore = new Semaphore(0); 127 AssociationCallback callback = createAssociationCallback(semaphore); 128 startAssociation(callback, getNameForAssociation()); 129 ArgumentCaptor<AdvertiseCallback> callbackCaptor = 130 ArgumentCaptor.forClass(AdvertiseCallback.class); 131 verify(mMockPeripheralManager, timeout(3000)) 132 .startAdvertising(any(), any(), callbackCaptor.capture()); 133 AdvertiseCallback advertiseCallback = callbackCaptor.getValue(); 134 int testErrorCode = 2; 135 advertiseCallback.onStartFailure(testErrorCode); 136 assertThat(tryAcquire(semaphore)).isTrue(); 137 verify(callback).onAssociationStartFailure(); 138 } 139 140 @Test testNotifyAssociationSuccess()141 public void testNotifyAssociationSuccess() throws InterruptedException { 142 Semaphore semaphore = new Semaphore(0); 143 AssociationCallback callback = createAssociationCallback(semaphore); 144 String testDeviceName = getNameForAssociation(); 145 startAssociation(callback, testDeviceName); 146 ArgumentCaptor<AdvertiseCallback> callbackCaptor = 147 ArgumentCaptor.forClass(AdvertiseCallback.class); 148 verify(mMockPeripheralManager, timeout(3000)) 149 .startAdvertising(any(), any(), callbackCaptor.capture()); 150 AdvertiseCallback advertiseCallback = callbackCaptor.getValue(); 151 AdvertiseSettings settings = new AdvertiseSettings.Builder().build(); 152 advertiseCallback.onStartSuccess(settings); 153 assertThat(tryAcquire(semaphore)).isTrue(); 154 verify(callback).onAssociationStartSuccess(eq(testDeviceName)); 155 } 156 157 @Test testShowVerificationCode()158 public void testShowVerificationCode() throws InterruptedException { 159 Semaphore semaphore = new Semaphore(0); 160 AssociationCallback callback = createAssociationCallback(semaphore); 161 SecureBleChannel channel = getChannelForAssociation(callback); 162 channel.getShowVerificationCodeListener().showVerificationCode(TEST_VERIFICATION_CODE); 163 assertThat(tryAcquire(semaphore)).isTrue(); 164 verify(callback).onVerificationCodeAvailable(eq(TEST_VERIFICATION_CODE)); 165 } 166 167 @Test testAssociationSuccess()168 public void testAssociationSuccess() throws InterruptedException { 169 Semaphore semaphore = new Semaphore(0); 170 AssociationCallback callback = createAssociationCallback(semaphore); 171 SecureBleChannel channel = getChannelForAssociation(callback); 172 SecureBleChannel.Callback channelCallback = channel.getCallback(); 173 assertThat(channelCallback).isNotNull(); 174 channelCallback.onDeviceIdReceived(TEST_REMOTE_DEVICE_ID.toString()); 175 Key key = EncryptionRunnerFactory.newDummyRunner().keyOf(TEST_KEY); 176 channelCallback.onSecureChannelEstablished(); 177 ArgumentCaptor<AssociatedDevice> deviceCaptor = 178 ArgumentCaptor.forClass(AssociatedDevice.class); 179 verify(mMockStorage).addAssociatedDeviceForActiveUser(deviceCaptor.capture()); 180 AssociatedDevice device = deviceCaptor.getValue(); 181 assertThat(device.getDeviceId()).isEqualTo(TEST_REMOTE_DEVICE_ID.toString()); 182 assertThat(tryAcquire(semaphore)).isTrue(); 183 verify(callback).onAssociationCompleted(eq(TEST_REMOTE_DEVICE_ID.toString())); 184 } 185 186 @Test testAssociationFailure_channelError()187 public void testAssociationFailure_channelError() throws InterruptedException { 188 Semaphore semaphore = new Semaphore(0); 189 AssociationCallback callback = createAssociationCallback(semaphore); 190 SecureBleChannel channel = getChannelForAssociation(callback); 191 SecureBleChannel.Callback channelCallback = channel.getCallback(); 192 int testErrorCode = 1; 193 assertThat(channelCallback).isNotNull(); 194 channelCallback.onDeviceIdReceived(TEST_REMOTE_DEVICE_ID.toString()); 195 channelCallback.onEstablishSecureChannelFailure(testErrorCode); 196 assertThat(tryAcquire(semaphore)).isTrue(); 197 verify(callback).onAssociationError(eq(testErrorCode)); 198 } 199 200 @Test connectToDevice_stopsAdvertisingAfterTimeout()201 public void connectToDevice_stopsAdvertisingAfterTimeout() { 202 int timeoutSeconds = 2; 203 mCarBlePeripheralManager.connectToDevice(UUID.randomUUID(), timeoutSeconds); 204 ArgumentCaptor<AdvertiseCallback> callbackCaptor = 205 ArgumentCaptor.forClass(AdvertiseCallback.class); 206 verify(mMockPeripheralManager).startAdvertising(any(), any(), callbackCaptor.capture()); 207 callbackCaptor.getValue().onStartSuccess(null); 208 verify(mMockPeripheralManager, timeout(TimeUnit.SECONDS.toMillis(timeoutSeconds + 1))) 209 .stopAdvertising(any(AdvertiseCallback.class)); 210 } 211 startAssociation(AssociationCallback callback, String deviceName)212 private BlePeripheralManager.Callback startAssociation(AssociationCallback callback, 213 String deviceName) { 214 ArgumentCaptor<BlePeripheralManager.Callback> callbackCaptor = 215 ArgumentCaptor.forClass(BlePeripheralManager.Callback.class); 216 mCarBlePeripheralManager.startAssociation(deviceName, callback); 217 verify(mMockPeripheralManager, timeout(3000)).registerCallback(callbackCaptor.capture()); 218 return callbackCaptor.getValue(); 219 } 220 getChannelForAssociation(AssociationCallback callback)221 private SecureBleChannel getChannelForAssociation(AssociationCallback callback) { 222 BlePeripheralManager.Callback bleManagerCallback = startAssociation(callback, 223 getNameForAssociation()); 224 BluetoothDevice bluetoothDevice = BluetoothAdapter.getDefaultAdapter() 225 .getRemoteDevice(TEST_REMOTE_DEVICE_ADDRESS); 226 bleManagerCallback.onRemoteDeviceConnected(bluetoothDevice); 227 return mCarBlePeripheralManager.getConnectedDeviceChannel(); 228 } 229 tryAcquire(Semaphore semaphore)230 private boolean tryAcquire(Semaphore semaphore) throws InterruptedException { 231 return semaphore.tryAcquire(100, TimeUnit.MILLISECONDS); 232 } 233 getNameForAssociation()234 private String getNameForAssociation() { 235 return ByteUtils.generateRandomNumberString(DEVICE_NAME_LENGTH_LIMIT); 236 237 } 238 239 @NonNull createAssociationCallback(@onNull final Semaphore semaphore)240 private AssociationCallback createAssociationCallback(@NonNull final Semaphore semaphore) { 241 return spy(new AssociationCallback() { 242 @Override 243 public void onAssociationStartSuccess(String deviceName) { 244 semaphore.release(); 245 } 246 @Override 247 public void onAssociationStartFailure() { 248 semaphore.release(); 249 } 250 251 @Override 252 public void onAssociationError(int error) { 253 semaphore.release(); 254 } 255 256 @Override 257 public void onVerificationCodeAvailable(String code) { 258 semaphore.release(); 259 } 260 261 @Override 262 public void onAssociationCompleted(String deviceId) { 263 semaphore.release(); 264 } 265 }); 266 } 267 } 268