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