1 /*
2  * Copyright (C) 2023 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 android.bluetooth;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 
21 import static org.mockito.ArgumentMatchers.anyInt;
22 import static org.mockito.Mockito.after;
23 import static org.mockito.Mockito.any;
24 import static org.mockito.Mockito.eq;
25 import static org.mockito.Mockito.mock;
26 import static org.mockito.Mockito.never;
27 import static org.mockito.Mockito.timeout;
28 import static org.mockito.Mockito.verify;
29 
30 import android.app.PendingIntent;
31 import android.bluetooth.le.BluetoothLeScanner;
32 import android.bluetooth.le.ScanCallback;
33 import android.bluetooth.le.ScanFilter;
34 import android.bluetooth.le.ScanResult;
35 import android.bluetooth.le.ScanSettings;
36 import android.content.BroadcastReceiver;
37 import android.content.Context;
38 import android.content.Intent;
39 import android.content.IntentFilter;
40 import android.os.ParcelUuid;
41 import android.util.Log;
42 
43 import androidx.test.core.app.ApplicationProvider;
44 import androidx.test.ext.junit.runners.AndroidJUnit4;
45 
46 import com.android.compatibility.common.util.AdoptShellPermissionsRule;
47 
48 import com.google.protobuf.ByteString;
49 
50 import org.junit.Rule;
51 import org.junit.Test;
52 import org.junit.runner.RunWith;
53 import org.mockito.ArgumentCaptor;
54 
55 import pandora.HostProto;
56 import pandora.HostProto.AdvertiseRequest;
57 import pandora.HostProto.AdvertiseResponse;
58 import pandora.HostProto.OwnAddressType;
59 
60 import java.util.ArrayList;
61 import java.util.List;
62 import java.util.concurrent.CompletableFuture;
63 import java.util.concurrent.TimeUnit;
64 import java.util.stream.Stream;
65 
66 @RunWith(AndroidJUnit4.class)
67 public class LeScanningTest {
68     private static final String TAG = "LeScanningTest";
69     private static final int TIMEOUT_SCANNING_MS = 2000;
70     private static final String TEST_UUID_STRING = "00001805-0000-1000-8000-00805f9b34fb";
71     private static final String TEST_ADDRESS_RANDOM_STATIC = "F0:43:A8:23:10:11";
72     private static final String ACTION_DYNAMIC_RECEIVER_SCAN_RESULT =
73             "android.bluetooth.test.ACTION_DYNAMIC_RECEIVER_SCAN_RESULT";
74 
75     @Rule public final AdoptShellPermissionsRule mPermissionRule = new AdoptShellPermissionsRule();
76 
77     @Rule public final PandoraDevice mBumble = new PandoraDevice();
78 
79     private final Context mContext = ApplicationProvider.getApplicationContext();
80     private final BluetoothManager mBluetoothManager =
81             mContext.getSystemService(BluetoothManager.class);
82     private final BluetoothAdapter mBluetoothAdapter = mBluetoothManager.getAdapter();
83     private final BluetoothLeScanner mLeScanner = mBluetoothAdapter.getBluetoothLeScanner();
84 
85     @Test
startBleScan_withCallbackTypeAllMatches()86     public void startBleScan_withCallbackTypeAllMatches() {
87         advertiseWithBumble(TEST_UUID_STRING, OwnAddressType.PUBLIC);
88 
89         ScanFilter scanFilter =
90                 new ScanFilter.Builder()
91                         .setServiceUuid(ParcelUuid.fromString(TEST_UUID_STRING))
92                         .build();
93 
94         List<ScanResult> results =
95                 startScanning(scanFilter, ScanSettings.CALLBACK_TYPE_ALL_MATCHES);
96 
97         assertThat(results).isNotNull();
98         assertThat(results.get(0).getScanRecord().getServiceUuids().get(0))
99                 .isEqualTo(ParcelUuid.fromString(TEST_UUID_STRING));
100         assertThat(results.get(1).getScanRecord().getServiceUuids().get(0))
101                 .isEqualTo(ParcelUuid.fromString(TEST_UUID_STRING));
102     }
103 
104     @Test
scanForIrkIdentityAddress_withCallbackTypeAllMatches()105     public void scanForIrkIdentityAddress_withCallbackTypeAllMatches() {
106         advertiseWithBumble(null, OwnAddressType.RANDOM);
107 
108         ScanFilter scanFilter =
109                 new ScanFilter.Builder()
110                         .setDeviceAddress(
111                                 TEST_ADDRESS_RANDOM_STATIC,
112                                 BluetoothDevice.ADDRESS_TYPE_RANDOM,
113                                 Utils.BUMBLE_IRK)
114                         .build();
115 
116         List<ScanResult> results =
117                 startScanning(scanFilter, ScanSettings.CALLBACK_TYPE_ALL_MATCHES);
118 
119         assertThat(results).isNotEmpty();
120         assertThat(results.get(0).getDevice().getAddress()).isEqualTo(TEST_ADDRESS_RANDOM_STATIC);
121     }
122 
123     @Test
startBleScan_withCallbackTypeFirstMatchSilentlyFails()124     public void startBleScan_withCallbackTypeFirstMatchSilentlyFails() {
125         advertiseWithBumble(TEST_UUID_STRING, OwnAddressType.PUBLIC);
126 
127         ScanSettings scanSettings =
128                 new ScanSettings.Builder()
129                         .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
130                         .setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH)
131                         .build();
132 
133         ScanFilter scanFilter =
134                 new ScanFilter.Builder()
135                         .setServiceUuid(ParcelUuid.fromString(TEST_UUID_STRING))
136                         .build();
137 
138         ScanCallback mockScanCallback = mock(ScanCallback.class);
139 
140         mLeScanner.startScan(List.of(scanFilter), scanSettings, mockScanCallback);
141         verify(mockScanCallback, after(TIMEOUT_SCANNING_MS).never()).onScanFailed(anyInt());
142         mLeScanner.stopScan(mockScanCallback);
143     }
144 
145     @Test
startBleScan_withCallbackTypeMatchLostSilentlyFails()146     public void startBleScan_withCallbackTypeMatchLostSilentlyFails() {
147         advertiseWithBumble(TEST_UUID_STRING, OwnAddressType.PUBLIC);
148 
149         ScanSettings scanSettings =
150                 new ScanSettings.Builder()
151                         .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
152                         .setCallbackType(ScanSettings.CALLBACK_TYPE_MATCH_LOST)
153                         .build();
154 
155         ScanFilter scanFilter =
156                 new ScanFilter.Builder()
157                         .setServiceUuid(ParcelUuid.fromString(TEST_UUID_STRING))
158                         .build();
159 
160         ScanCallback mockScanCallback = mock(ScanCallback.class);
161 
162         mLeScanner.startScan(List.of(scanFilter), scanSettings, mockScanCallback);
163         verify(mockScanCallback, after(TIMEOUT_SCANNING_MS).never()).onScanFailed(anyInt());
164         mLeScanner.stopScan(mockScanCallback);
165     }
166 
167     @Test
startBleScan_withPendingIntentAndDynamicReceiverAndCallbackTypeAllMatches()168     public void startBleScan_withPendingIntentAndDynamicReceiverAndCallbackTypeAllMatches() {
169         BroadcastReceiver mockReceiver = mock(BroadcastReceiver.class);
170         IntentFilter intentFilter = new IntentFilter(ACTION_DYNAMIC_RECEIVER_SCAN_RESULT);
171         mContext.registerReceiver(mockReceiver, intentFilter, Context.RECEIVER_EXPORTED);
172 
173         advertiseWithBumble(TEST_UUID_STRING, OwnAddressType.PUBLIC);
174 
175         ScanSettings scanSettings =
176                 new ScanSettings.Builder()
177                         .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
178                         .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
179                         .build();
180 
181         ScanFilter scanFilter =
182                 new ScanFilter.Builder()
183                         .setServiceUuid(ParcelUuid.fromString(TEST_UUID_STRING))
184                         .build();
185 
186         // NOTE: Intent.setClass() must not be called, or else scan results won't be received.
187         Intent scanIntent = new Intent(ACTION_DYNAMIC_RECEIVER_SCAN_RESULT);
188         PendingIntent pendingIntent =
189                 PendingIntent.getBroadcast(
190                         mContext,
191                         0,
192                         scanIntent,
193                         PendingIntent.FLAG_MUTABLE
194                                 | PendingIntent.FLAG_CANCEL_CURRENT
195                                 | PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT);
196 
197         mLeScanner.startScan(List.of(scanFilter), scanSettings, pendingIntent);
198 
199         ArgumentCaptor<Intent> intent = ArgumentCaptor.forClass(Intent.class);
200         verify(mockReceiver, timeout(TIMEOUT_SCANNING_MS)).onReceive(any(), intent.capture());
201 
202         mLeScanner.stopScan(pendingIntent);
203         mContext.unregisterReceiver(mockReceiver);
204 
205         assertThat(intent.getValue().getAction()).isEqualTo(ACTION_DYNAMIC_RECEIVER_SCAN_RESULT);
206         assertThat(intent.getValue().getIntExtra(BluetoothLeScanner.EXTRA_CALLBACK_TYPE, -1))
207                 .isEqualTo(ScanSettings.CALLBACK_TYPE_ALL_MATCHES);
208 
209         List<ScanResult> results =
210                 intent.getValue()
211                         .getParcelableArrayListExtra(
212                                 BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT, ScanResult.class);
213         assertThat(results).isNotEmpty();
214         assertThat(results.get(0).getScanRecord().getServiceUuids()).isNotEmpty();
215         assertThat(results.get(0).getScanRecord().getServiceUuids().get(0))
216                 .isEqualTo(ParcelUuid.fromString(TEST_UUID_STRING));
217         assertThat(results.get(0).getScanRecord().getServiceUuids())
218                 .containsExactly(ParcelUuid.fromString(TEST_UUID_STRING));
219     }
220 
221     @Test
startBleScan_withPendingIntentAndStaticReceiverAndCallbackTypeAllMatches()222     public void startBleScan_withPendingIntentAndStaticReceiverAndCallbackTypeAllMatches() {
223         advertiseWithBumble(TEST_UUID_STRING, OwnAddressType.PUBLIC);
224 
225         ScanSettings scanSettings =
226                 new ScanSettings.Builder()
227                         .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
228                         .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
229                         .build();
230 
231         ArrayList<ScanFilter> scanFilters = new ArrayList<>();
232         ScanFilter scanFilter =
233                 new ScanFilter.Builder()
234                         .setServiceUuid(ParcelUuid.fromString(TEST_UUID_STRING))
235                         .build();
236         scanFilters.add(scanFilter);
237 
238         PendingIntent pendingIntent =
239                 PendingIntentScanReceiver.newBroadcastPendingIntent(mContext, 0);
240 
241         mLeScanner.startScan(scanFilters, scanSettings, pendingIntent);
242         List<ScanResult> results =
243                 PendingIntentScanReceiver.nextScanResult()
244                         .completeOnTimeout(null, TIMEOUT_SCANNING_MS, TimeUnit.MILLISECONDS)
245                         .join();
246         mLeScanner.stopScan(pendingIntent);
247         PendingIntentScanReceiver.resetNextScanResultFuture();
248 
249         assertThat(results).isNotEmpty();
250         assertThat(results.get(0).getScanRecord().getServiceUuids()).isNotEmpty();
251         assertThat(results.get(0).getScanRecord().getServiceUuids())
252                 .containsExactly(ParcelUuid.fromString(TEST_UUID_STRING));
253     }
254 
255     @Test
startBleScan_oneTooManyScansFails()256     public void startBleScan_oneTooManyScansFails() {
257         final int maxNumScans = 32;
258         advertiseWithBumble(TEST_UUID_STRING, OwnAddressType.PUBLIC);
259 
260         ScanSettings scanSettings =
261                 new ScanSettings.Builder()
262                         .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
263                         .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
264                         .build();
265 
266         ScanFilter scanFilter =
267                 new ScanFilter.Builder()
268                         .setServiceUuid(ParcelUuid.fromString(TEST_UUID_STRING))
269                         .build();
270 
271         List<ScanCallback> scanCallbacks =
272                 Stream.generate(() -> mock(ScanCallback.class)).limit(maxNumScans).toList();
273         for (ScanCallback mockScanCallback : scanCallbacks) {
274             mLeScanner.startScan(List.of(scanFilter), scanSettings, mockScanCallback);
275         }
276         // This last scan should fail
277         ScanCallback lastMockScanCallback = mock(ScanCallback.class);
278         mLeScanner.startScan(List.of(scanFilter), scanSettings, lastMockScanCallback);
279 
280         // We expect an error only for the last scan, which was over the maximum active scans limit.
281         for (ScanCallback mockScanCallback : scanCallbacks) {
282             verify(mockScanCallback, timeout(TIMEOUT_SCANNING_MS).atLeast(1))
283                     .onScanResult(eq(ScanSettings.CALLBACK_TYPE_ALL_MATCHES), any());
284             verify(mockScanCallback, never()).onScanFailed(anyInt());
285             mLeScanner.stopScan(mockScanCallback);
286         }
287         verify(lastMockScanCallback, timeout(TIMEOUT_SCANNING_MS))
288                 .onScanFailed(eq(ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED));
289         mLeScanner.stopScan(lastMockScanCallback);
290     }
291 
292     @Test
startBleScan_withNonConnectablePublicAdvertisement()293     public void startBleScan_withNonConnectablePublicAdvertisement() {
294         AdvertiseRequest.Builder requestBuilder =
295                 AdvertiseRequest.newBuilder()
296                         .setConnectable(false)
297                         .setOwnAddressType(OwnAddressType.PUBLIC);
298         advertiseWithBumble(requestBuilder);
299 
300         ScanFilter scanFilter =
301                 new ScanFilter.Builder()
302                         .setDeviceAddress(mBumble.getRemoteDevice().getAddress())
303                         .build();
304 
305         List<ScanResult> results =
306                 startScanning(scanFilter, ScanSettings.CALLBACK_TYPE_ALL_MATCHES);
307 
308         assertThat(results).isNotNull();
309         assertThat(results.get(0).isConnectable()).isFalse();
310         assertThat(results.get(1).isConnectable()).isFalse();
311     }
312 
313     @Test
startBleScan_withNonConnectableScannablePublicAdvertisement()314     public void startBleScan_withNonConnectableScannablePublicAdvertisement() {
315         byte[] payload = {0x02, 0x03};
316         // first 2 bytes are the manufacturer ID 0x00E0 (Google) in little endian
317         byte[] manufacturerData = {(byte) 0xE0, 0x00, payload[0], payload[1]};
318         HostProto.DataTypes.Builder scanResponse =
319                 HostProto.DataTypes.newBuilder()
320                         .setManufacturerSpecificData(ByteString.copyFrom(manufacturerData));
321 
322         AdvertiseRequest.Builder requestBuilder =
323                 AdvertiseRequest.newBuilder()
324                         .setConnectable(false)
325                         .setOwnAddressType(OwnAddressType.PUBLIC)
326                         .setScanResponseData(scanResponse);
327         advertiseWithBumble(requestBuilder);
328 
329         ScanFilter scanFilter =
330                 new ScanFilter.Builder()
331                         .setDeviceAddress(mBumble.getRemoteDevice().getAddress())
332                         .build();
333 
334         List<ScanResult> results =
335                 startScanning(scanFilter, ScanSettings.CALLBACK_TYPE_ALL_MATCHES);
336 
337         assertThat(results).isNotNull();
338         assertThat(results.get(0).isConnectable()).isFalse();
339         assertThat(results.get(0).getScanRecord().getManufacturerSpecificData(0x00E0))
340                 .isEqualTo(payload);
341     }
342 
startScanning(ScanFilter scanFilter, int callbackType)343     private List<ScanResult> startScanning(ScanFilter scanFilter, int callbackType) {
344         CompletableFuture<List<ScanResult>> future = new CompletableFuture<>();
345         List<ScanResult> scanResults = new ArrayList<>();
346 
347         ScanSettings scanSettings =
348                 new ScanSettings.Builder()
349                         .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
350                         .setCallbackType(callbackType)
351                         .build();
352 
353         ScanCallback scanCallback =
354                 new ScanCallback() {
355                     @Override
356                     public void onScanResult(int callbackType, ScanResult result) {
357                         Log.i(
358                                 TAG,
359                                 "onScanResult "
360                                         + "address: "
361                                         + result.getDevice().getAddress()
362                                         + ", connectable: "
363                                         + result.isConnectable()
364                                         + ", callbackType: "
365                                         + callbackType
366                                         + ", service uuids: "
367                                         + result.getScanRecord().getServiceUuids());
368 
369                         if (callbackType == ScanSettings.CALLBACK_TYPE_ALL_MATCHES) {
370                             if (scanResults.size() < 2) {
371                                 scanResults.add(result);
372                             } else {
373                                 future.complete(scanResults);
374                             }
375                         } else {
376                             scanResults.add(result);
377                             future.complete(scanResults);
378                         }
379                     }
380 
381                     @Override
382                     public void onScanFailed(int errorCode) {
383                         Log.i(TAG, "onScanFailed " + "errorCode: " + errorCode);
384                         future.complete(null);
385                     }
386                 };
387 
388         mLeScanner.startScan(List.of(scanFilter), scanSettings, scanCallback);
389 
390         List<ScanResult> result =
391                 future.completeOnTimeout(null, TIMEOUT_SCANNING_MS, TimeUnit.MILLISECONDS).join();
392 
393         mLeScanner.stopScan(scanCallback);
394 
395         return result;
396     }
397 
advertiseWithBumble(String serviceUuid, OwnAddressType addressType)398     private void advertiseWithBumble(String serviceUuid, OwnAddressType addressType) {
399         AdvertiseRequest.Builder requestBuilder =
400                 AdvertiseRequest.newBuilder().setOwnAddressType(addressType);
401 
402         if (serviceUuid != null) {
403             HostProto.DataTypes.Builder dataTypeBuilder = HostProto.DataTypes.newBuilder();
404             dataTypeBuilder.addCompleteServiceClassUuids128(serviceUuid);
405             requestBuilder.setData(dataTypeBuilder.build());
406         }
407 
408         advertiseWithBumble(requestBuilder);
409     }
410 
advertiseWithBumble(AdvertiseRequest.Builder requestBuilder)411     private void advertiseWithBumble(AdvertiseRequest.Builder requestBuilder) {
412         // Bumble currently only supports legacy advertising.
413         requestBuilder.setLegacy(true);
414         // Collect and ignore responses.
415         StreamObserverSpliterator<AdvertiseResponse> responseObserver =
416                 new StreamObserverSpliterator<>();
417         mBumble.host().advertise(requestBuilder.build(), responseObserver);
418     }
419 }
420