Loading framework/tests/bumble/AndroidManifest.xml +6 −0 Original line number Diff line number Diff line Loading @@ -9,6 +9,12 @@ <application> <uses-library android:name="android.test.runner" /> <receiver android:name="android.bluetooth.PendingIntentScanReceiver" > <intent-filter> <action android:name="android.bluetooth.test.ACTION_SCAN_RESULT" /> </intent-filter> </receiver> </application> <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" Loading framework/tests/bumble/src/android/bluetooth/LeScanningTest.java +120 −17 Original line number Diff line number Diff line Loading @@ -19,11 +19,21 @@ package android.bluetooth; import static com.google.common.io.BaseEncoding.base16; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import android.app.PendingIntent; import android.bluetooth.le.BluetoothLeScanner; import android.bluetooth.le.ScanCallback; import android.bluetooth.le.ScanFilter; import android.bluetooth.le.ScanResult; import android.bluetooth.le.ScanSettings; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.ParcelUuid; import android.util.Log; Loading @@ -35,6 +45,7 @@ import com.android.compatibility.common.util.AdoptShellPermissionsRule; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import java.util.ArrayList; import java.util.List; Loading @@ -51,16 +62,27 @@ public class LeScanningTest { private static final String TAG = "LeScanningTest"; private static final int TIMEOUT_SCANNING_MS = 2000; @Rule public final AdoptShellPermissionsRule mPermissionRule = new AdoptShellPermissionsRule(); @Rule public final AdoptShellPermissionsRule mPermissionRule = new AdoptShellPermissionsRule(); @Rule public final PandoraDevice mBumble = new PandoraDevice(); @Rule public final PandoraDevice mBumble = new PandoraDevice(); private static final String TEST_ADDRESS_RANDOM_STATIC = "F0:43:A8:23:10:11"; // IRK must match what's defined in bumble_config.json private static final byte[] TEST_IRK = base16().decode("1F66F4B5F0C742F807DD0DDBF64E9213"); private final String TEST_UUID_STRING = "00001805-0000-1000-8000-00805f9b34fb"; private final Context mContext = ApplicationProvider.getApplicationContext(); private final BluetoothManager mBluetoothManager = mContext.getSystemService(BluetoothManager.class); private final BluetoothAdapter mBluetoothAdapter = mBluetoothManager.getAdapter(); private final BluetoothLeScanner mLeScanner = mBluetoothAdapter.getBluetoothLeScanner(); private static final String TEST_UUID_STRING = "00001805-0000-1000-8000-00805f9b34fb"; private static final String ACTION_DYNAMIC_RECEIVER_SCAN_RESULT = "android.bluetooth.test.ACTION_DYNAMIC_RECEIVER_SCAN_RESULT"; @Test public void startBleScan_withCallbackTypeAllMatches() { Loading @@ -72,8 +94,9 @@ public class LeScanningTest { .build(); List<ScanResult> results = startScanning(scanFilter, ScanSettings.CALLBACK_TYPE_ALL_MATCHES).join(); startScanning(scanFilter, ScanSettings.CALLBACK_TYPE_ALL_MATCHES); assertThat(results).isNotNull(); assertThat(results.get(0).getScanRecord().getServiceUuids().get(0)) .isEqualTo(ParcelUuid.fromString(TEST_UUID_STRING)); assertThat(results.get(1).getScanRecord().getServiceUuids().get(0)) Loading @@ -93,23 +116,99 @@ public class LeScanningTest { .build(); List<ScanResult> results = startScanning(scanFilter, ScanSettings.CALLBACK_TYPE_ALL_MATCHES).join(); startScanning(scanFilter, ScanSettings.CALLBACK_TYPE_ALL_MATCHES); assertThat(results).isNotEmpty(); assertThat(results.get(0).getDevice().getAddress()).isEqualTo(TEST_ADDRESS_RANDOM_STATIC); } private CompletableFuture<List<ScanResult>> startScanning( ScanFilter scanFilter, int callbackType) { CompletableFuture<List<ScanResult>> future = new CompletableFuture<>(); List<ScanResult> scanResults = new ArrayList<>(); @Test public void startBleScan_withPendingIntentAndDynamicReceiverAndCallbackTypeAllMatches() { BroadcastReceiver mockReceiver = mock(BroadcastReceiver.class); IntentFilter intentFilter = new IntentFilter(ACTION_DYNAMIC_RECEIVER_SCAN_RESULT); mContext.registerReceiver(mockReceiver, intentFilter); advertiseWithBumble(TEST_UUID_STRING, OwnAddressType.PUBLIC); ScanSettings scanSettings = new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) .build(); android.content.Context context = ApplicationProvider.getApplicationContext(); BluetoothManager bluetoothManager = context.getSystemService(BluetoothManager.class); BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter(); ScanFilter scanFilter = new ScanFilter.Builder() .setServiceUuid(ParcelUuid.fromString(TEST_UUID_STRING)) .build(); // NOTE: Intent.setClass() must not be called, or else scan results won't be received. Intent scanIntent = new Intent(ACTION_DYNAMIC_RECEIVER_SCAN_RESULT); PendingIntent pendingIntent = PendingIntent.getBroadcast( mContext, 0, scanIntent, PendingIntent.FLAG_CANCEL_CURRENT); mLeScanner.startScan(List.of(scanFilter), scanSettings, pendingIntent); ArgumentCaptor<Intent> intent = ArgumentCaptor.forClass(Intent.class); verify(mockReceiver, timeout(TIMEOUT_SCANNING_MS)).onReceive(any(), intent.capture()); // Start scanning BluetoothLeScanner leScanner = bluetoothAdapter.getBluetoothLeScanner(); mLeScanner.stopScan(pendingIntent); mContext.unregisterReceiver(mockReceiver); assertThat(intent.getValue().getAction()).isEqualTo(ACTION_DYNAMIC_RECEIVER_SCAN_RESULT); assertThat(intent.getValue().getIntExtra(BluetoothLeScanner.EXTRA_CALLBACK_TYPE, -1)) .isEqualTo(ScanSettings.CALLBACK_TYPE_ALL_MATCHES); List<ScanResult> results = intent.getValue() .getParcelableArrayListExtra( BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT, ScanResult.class); assertThat(results).isNotEmpty(); assertThat(results.get(0).getScanRecord().getServiceUuids()).isNotEmpty(); assertThat(results.get(0).getScanRecord().getServiceUuids().get(0)) .isEqualTo(ParcelUuid.fromString(TEST_UUID_STRING)); assertThat(results.get(0).getScanRecord().getServiceUuids()) .containsExactly(ParcelUuid.fromString(TEST_UUID_STRING)); } @Test public void startBleScan_withPendingIntentAndStaticReceiverAndCallbackTypeAllMatches() { advertiseWithBumble(TEST_UUID_STRING, OwnAddressType.PUBLIC); ScanSettings scanSettings = new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) .build(); ArrayList<ScanFilter> scanFilters = new ArrayList<>(); ScanFilter scanFilter = new ScanFilter.Builder() .setServiceUuid(ParcelUuid.fromString(TEST_UUID_STRING)) .build(); scanFilters.add(scanFilter); PendingIntent pendingIntent = PendingIntentScanReceiver.newBroadcastPendingIntent(mContext, 0); mLeScanner.startScan(scanFilters, scanSettings, pendingIntent); List<ScanResult> results = PendingIntentScanReceiver.nextScanResult() .completeOnTimeout(null, TIMEOUT_SCANNING_MS, TimeUnit.MILLISECONDS) .join(); mLeScanner.stopScan(pendingIntent); PendingIntentScanReceiver.resetNextScanResultFuture(); assertThat(results).isNotEmpty(); assertThat(results.get(0).getScanRecord().getServiceUuids()).isNotEmpty(); assertThat(results.get(0).getScanRecord().getServiceUuids()) .containsExactly(ParcelUuid.fromString(TEST_UUID_STRING)); } private List<ScanResult> startScanning(ScanFilter scanFilter, int callbackType) { CompletableFuture<List<ScanResult>> future = new CompletableFuture<>(); List<ScanResult> scanResults = new ArrayList<>(); ScanSettings scanSettings = new ScanSettings.Builder() Loading Loading @@ -148,10 +247,14 @@ public class LeScanningTest { } }; leScanner.startScan(List.of(scanFilter), scanSettings, scanCallback); mLeScanner.startScan(List.of(scanFilter), scanSettings, scanCallback); List<ScanResult> result = future.completeOnTimeout(null, TIMEOUT_SCANNING_MS, TimeUnit.MILLISECONDS).join(); mLeScanner.stopScan(scanCallback); // Make sure completableFuture object completes with null after some timeout return future.completeOnTimeout(null, TIMEOUT_SCANNING_MS, TimeUnit.MILLISECONDS); return result; } private void advertiseWithBumble(String serviceUuid, OwnAddressType addressType) { Loading framework/tests/bumble/src/android/bluetooth/PendingIntentScanReceiver.java 0 → 100644 +115 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 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 android.bluetooth; import android.app.PendingIntent; import android.bluetooth.le.BluetoothLeScanner; import android.bluetooth.le.ScanResult; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.util.Log; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; /** * PendingIntentScanReceiver is registered statically in the manifest file as a BroadcastReceiver * for the android.bluetooth.ACTION_SCAN_RESULT action. Tests can use nextScanResult() to get a * future that completes when scan results are next delivered. */ public class PendingIntentScanReceiver extends BroadcastReceiver { private static final String TAG = "PendingIntentScanReceiver"; public static final String ACTION_SCAN_RESULT = "android.bluetooth.test.ACTION_SCAN_RESULT"; private static Optional<CompletableFuture<List<ScanResult>>> sNextScanResultFuture = Optional.empty(); /** * Constructs a new Intent associated with this class. * * @param context The context the to associate with the Intent. * @return The new Intent. */ private static Intent newIntent(Context context) { Intent intent = new Intent(); intent.setAction(PendingIntentScanReceiver.ACTION_SCAN_RESULT); intent.setClass(context, PendingIntentScanReceiver.class); return intent; } /** * Constructs a new PendingIntent associated with this class. * * @param context The context to associate the PendingIntent with. * @param requestCode The request code to uniquely identify this PendingIntent with. * @return */ public static PendingIntent newBroadcastPendingIntent(Context context, int requestCode) { return PendingIntent.getBroadcast( context, requestCode, newIntent(context), PendingIntent.FLAG_CANCEL_CURRENT); } /** * Use this method for statically registered receivers. * * @return A future that will complete when the next scan result is received. */ public static CompletableFuture<List<ScanResult>> nextScanResult() throws IllegalStateException { if (sNextScanResultFuture.isPresent()) { throw new IllegalStateException("scan result future already set"); } sNextScanResultFuture = Optional.of(new CompletableFuture<List<ScanResult>>()); return sNextScanResultFuture.get(); } /** Clears the future waiting for the next static receiver scan result, if any. */ public static void resetNextScanResultFuture() { sNextScanResultFuture = Optional.empty(); } @Override public void onReceive(Context context, Intent intent) { Log.d(TAG, "onReceive() intent: " + intent); if (intent.getAction() != ACTION_SCAN_RESULT) { throw new RuntimeException(); } int errorCode = intent.getIntExtra(BluetoothLeScanner.EXTRA_ERROR_CODE, 0); if (errorCode != 0) { Log.e(TAG, "onReceive() error: " + errorCode); throw new RuntimeException("onReceive() unexpected error: " + errorCode); } List<ScanResult> scanResults = intent.getParcelableExtra( BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT, new ArrayList<ScanResult>().getClass()); if (sNextScanResultFuture.isPresent()) { sNextScanResultFuture.get().complete(scanResults); sNextScanResultFuture = Optional.empty(); } else { throw new IllegalStateException("scan result received but no future set"); } } } Loading
framework/tests/bumble/AndroidManifest.xml +6 −0 Original line number Diff line number Diff line Loading @@ -9,6 +9,12 @@ <application> <uses-library android:name="android.test.runner" /> <receiver android:name="android.bluetooth.PendingIntentScanReceiver" > <intent-filter> <action android:name="android.bluetooth.test.ACTION_SCAN_RESULT" /> </intent-filter> </receiver> </application> <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" Loading
framework/tests/bumble/src/android/bluetooth/LeScanningTest.java +120 −17 Original line number Diff line number Diff line Loading @@ -19,11 +19,21 @@ package android.bluetooth; import static com.google.common.io.BaseEncoding.base16; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import android.app.PendingIntent; import android.bluetooth.le.BluetoothLeScanner; import android.bluetooth.le.ScanCallback; import android.bluetooth.le.ScanFilter; import android.bluetooth.le.ScanResult; import android.bluetooth.le.ScanSettings; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.ParcelUuid; import android.util.Log; Loading @@ -35,6 +45,7 @@ import com.android.compatibility.common.util.AdoptShellPermissionsRule; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import java.util.ArrayList; import java.util.List; Loading @@ -51,16 +62,27 @@ public class LeScanningTest { private static final String TAG = "LeScanningTest"; private static final int TIMEOUT_SCANNING_MS = 2000; @Rule public final AdoptShellPermissionsRule mPermissionRule = new AdoptShellPermissionsRule(); @Rule public final AdoptShellPermissionsRule mPermissionRule = new AdoptShellPermissionsRule(); @Rule public final PandoraDevice mBumble = new PandoraDevice(); @Rule public final PandoraDevice mBumble = new PandoraDevice(); private static final String TEST_ADDRESS_RANDOM_STATIC = "F0:43:A8:23:10:11"; // IRK must match what's defined in bumble_config.json private static final byte[] TEST_IRK = base16().decode("1F66F4B5F0C742F807DD0DDBF64E9213"); private final String TEST_UUID_STRING = "00001805-0000-1000-8000-00805f9b34fb"; private final Context mContext = ApplicationProvider.getApplicationContext(); private final BluetoothManager mBluetoothManager = mContext.getSystemService(BluetoothManager.class); private final BluetoothAdapter mBluetoothAdapter = mBluetoothManager.getAdapter(); private final BluetoothLeScanner mLeScanner = mBluetoothAdapter.getBluetoothLeScanner(); private static final String TEST_UUID_STRING = "00001805-0000-1000-8000-00805f9b34fb"; private static final String ACTION_DYNAMIC_RECEIVER_SCAN_RESULT = "android.bluetooth.test.ACTION_DYNAMIC_RECEIVER_SCAN_RESULT"; @Test public void startBleScan_withCallbackTypeAllMatches() { Loading @@ -72,8 +94,9 @@ public class LeScanningTest { .build(); List<ScanResult> results = startScanning(scanFilter, ScanSettings.CALLBACK_TYPE_ALL_MATCHES).join(); startScanning(scanFilter, ScanSettings.CALLBACK_TYPE_ALL_MATCHES); assertThat(results).isNotNull(); assertThat(results.get(0).getScanRecord().getServiceUuids().get(0)) .isEqualTo(ParcelUuid.fromString(TEST_UUID_STRING)); assertThat(results.get(1).getScanRecord().getServiceUuids().get(0)) Loading @@ -93,23 +116,99 @@ public class LeScanningTest { .build(); List<ScanResult> results = startScanning(scanFilter, ScanSettings.CALLBACK_TYPE_ALL_MATCHES).join(); startScanning(scanFilter, ScanSettings.CALLBACK_TYPE_ALL_MATCHES); assertThat(results).isNotEmpty(); assertThat(results.get(0).getDevice().getAddress()).isEqualTo(TEST_ADDRESS_RANDOM_STATIC); } private CompletableFuture<List<ScanResult>> startScanning( ScanFilter scanFilter, int callbackType) { CompletableFuture<List<ScanResult>> future = new CompletableFuture<>(); List<ScanResult> scanResults = new ArrayList<>(); @Test public void startBleScan_withPendingIntentAndDynamicReceiverAndCallbackTypeAllMatches() { BroadcastReceiver mockReceiver = mock(BroadcastReceiver.class); IntentFilter intentFilter = new IntentFilter(ACTION_DYNAMIC_RECEIVER_SCAN_RESULT); mContext.registerReceiver(mockReceiver, intentFilter); advertiseWithBumble(TEST_UUID_STRING, OwnAddressType.PUBLIC); ScanSettings scanSettings = new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) .build(); android.content.Context context = ApplicationProvider.getApplicationContext(); BluetoothManager bluetoothManager = context.getSystemService(BluetoothManager.class); BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter(); ScanFilter scanFilter = new ScanFilter.Builder() .setServiceUuid(ParcelUuid.fromString(TEST_UUID_STRING)) .build(); // NOTE: Intent.setClass() must not be called, or else scan results won't be received. Intent scanIntent = new Intent(ACTION_DYNAMIC_RECEIVER_SCAN_RESULT); PendingIntent pendingIntent = PendingIntent.getBroadcast( mContext, 0, scanIntent, PendingIntent.FLAG_CANCEL_CURRENT); mLeScanner.startScan(List.of(scanFilter), scanSettings, pendingIntent); ArgumentCaptor<Intent> intent = ArgumentCaptor.forClass(Intent.class); verify(mockReceiver, timeout(TIMEOUT_SCANNING_MS)).onReceive(any(), intent.capture()); // Start scanning BluetoothLeScanner leScanner = bluetoothAdapter.getBluetoothLeScanner(); mLeScanner.stopScan(pendingIntent); mContext.unregisterReceiver(mockReceiver); assertThat(intent.getValue().getAction()).isEqualTo(ACTION_DYNAMIC_RECEIVER_SCAN_RESULT); assertThat(intent.getValue().getIntExtra(BluetoothLeScanner.EXTRA_CALLBACK_TYPE, -1)) .isEqualTo(ScanSettings.CALLBACK_TYPE_ALL_MATCHES); List<ScanResult> results = intent.getValue() .getParcelableArrayListExtra( BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT, ScanResult.class); assertThat(results).isNotEmpty(); assertThat(results.get(0).getScanRecord().getServiceUuids()).isNotEmpty(); assertThat(results.get(0).getScanRecord().getServiceUuids().get(0)) .isEqualTo(ParcelUuid.fromString(TEST_UUID_STRING)); assertThat(results.get(0).getScanRecord().getServiceUuids()) .containsExactly(ParcelUuid.fromString(TEST_UUID_STRING)); } @Test public void startBleScan_withPendingIntentAndStaticReceiverAndCallbackTypeAllMatches() { advertiseWithBumble(TEST_UUID_STRING, OwnAddressType.PUBLIC); ScanSettings scanSettings = new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) .build(); ArrayList<ScanFilter> scanFilters = new ArrayList<>(); ScanFilter scanFilter = new ScanFilter.Builder() .setServiceUuid(ParcelUuid.fromString(TEST_UUID_STRING)) .build(); scanFilters.add(scanFilter); PendingIntent pendingIntent = PendingIntentScanReceiver.newBroadcastPendingIntent(mContext, 0); mLeScanner.startScan(scanFilters, scanSettings, pendingIntent); List<ScanResult> results = PendingIntentScanReceiver.nextScanResult() .completeOnTimeout(null, TIMEOUT_SCANNING_MS, TimeUnit.MILLISECONDS) .join(); mLeScanner.stopScan(pendingIntent); PendingIntentScanReceiver.resetNextScanResultFuture(); assertThat(results).isNotEmpty(); assertThat(results.get(0).getScanRecord().getServiceUuids()).isNotEmpty(); assertThat(results.get(0).getScanRecord().getServiceUuids()) .containsExactly(ParcelUuid.fromString(TEST_UUID_STRING)); } private List<ScanResult> startScanning(ScanFilter scanFilter, int callbackType) { CompletableFuture<List<ScanResult>> future = new CompletableFuture<>(); List<ScanResult> scanResults = new ArrayList<>(); ScanSettings scanSettings = new ScanSettings.Builder() Loading Loading @@ -148,10 +247,14 @@ public class LeScanningTest { } }; leScanner.startScan(List.of(scanFilter), scanSettings, scanCallback); mLeScanner.startScan(List.of(scanFilter), scanSettings, scanCallback); List<ScanResult> result = future.completeOnTimeout(null, TIMEOUT_SCANNING_MS, TimeUnit.MILLISECONDS).join(); mLeScanner.stopScan(scanCallback); // Make sure completableFuture object completes with null after some timeout return future.completeOnTimeout(null, TIMEOUT_SCANNING_MS, TimeUnit.MILLISECONDS); return result; } private void advertiseWithBumble(String serviceUuid, OwnAddressType addressType) { Loading
framework/tests/bumble/src/android/bluetooth/PendingIntentScanReceiver.java 0 → 100644 +115 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 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 android.bluetooth; import android.app.PendingIntent; import android.bluetooth.le.BluetoothLeScanner; import android.bluetooth.le.ScanResult; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.util.Log; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; /** * PendingIntentScanReceiver is registered statically in the manifest file as a BroadcastReceiver * for the android.bluetooth.ACTION_SCAN_RESULT action. Tests can use nextScanResult() to get a * future that completes when scan results are next delivered. */ public class PendingIntentScanReceiver extends BroadcastReceiver { private static final String TAG = "PendingIntentScanReceiver"; public static final String ACTION_SCAN_RESULT = "android.bluetooth.test.ACTION_SCAN_RESULT"; private static Optional<CompletableFuture<List<ScanResult>>> sNextScanResultFuture = Optional.empty(); /** * Constructs a new Intent associated with this class. * * @param context The context the to associate with the Intent. * @return The new Intent. */ private static Intent newIntent(Context context) { Intent intent = new Intent(); intent.setAction(PendingIntentScanReceiver.ACTION_SCAN_RESULT); intent.setClass(context, PendingIntentScanReceiver.class); return intent; } /** * Constructs a new PendingIntent associated with this class. * * @param context The context to associate the PendingIntent with. * @param requestCode The request code to uniquely identify this PendingIntent with. * @return */ public static PendingIntent newBroadcastPendingIntent(Context context, int requestCode) { return PendingIntent.getBroadcast( context, requestCode, newIntent(context), PendingIntent.FLAG_CANCEL_CURRENT); } /** * Use this method for statically registered receivers. * * @return A future that will complete when the next scan result is received. */ public static CompletableFuture<List<ScanResult>> nextScanResult() throws IllegalStateException { if (sNextScanResultFuture.isPresent()) { throw new IllegalStateException("scan result future already set"); } sNextScanResultFuture = Optional.of(new CompletableFuture<List<ScanResult>>()); return sNextScanResultFuture.get(); } /** Clears the future waiting for the next static receiver scan result, if any. */ public static void resetNextScanResultFuture() { sNextScanResultFuture = Optional.empty(); } @Override public void onReceive(Context context, Intent intent) { Log.d(TAG, "onReceive() intent: " + intent); if (intent.getAction() != ACTION_SCAN_RESULT) { throw new RuntimeException(); } int errorCode = intent.getIntExtra(BluetoothLeScanner.EXTRA_ERROR_CODE, 0); if (errorCode != 0) { Log.e(TAG, "onReceive() error: " + errorCode); throw new RuntimeException("onReceive() unexpected error: " + errorCode); } List<ScanResult> scanResults = intent.getParcelableExtra( BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT, new ArrayList<ScanResult>().getClass()); if (sNextScanResultFuture.isPresent()) { sNextScanResultFuture.get().complete(scanResults); sNextScanResultFuture = Optional.empty(); } else { throw new IllegalStateException("scan result received but no future set"); } } }