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