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 android.app.PendingIntent; 20 import android.bluetooth.le.BluetoothLeScanner; 21 import android.bluetooth.le.ScanResult; 22 import android.content.BroadcastReceiver; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.util.Log; 26 27 import java.util.ArrayList; 28 import java.util.List; 29 import java.util.Optional; 30 import java.util.concurrent.CompletableFuture; 31 32 /** 33 * PendingIntentScanReceiver is registered statically in the manifest file as a BroadcastReceiver 34 * for the android.bluetooth.ACTION_SCAN_RESULT action. Tests can use nextScanResult() to get a 35 * future that completes when scan results are next delivered. 36 */ 37 public class PendingIntentScanReceiver extends BroadcastReceiver { 38 private static final String TAG = "PendingIntentScanReceiver"; 39 40 public static final String ACTION_SCAN_RESULT = "android.bluetooth.test.ACTION_SCAN_RESULT"; 41 42 private static Optional<CompletableFuture<List<ScanResult>>> sNextScanResultFuture = 43 Optional.empty(); 44 45 /** 46 * Constructs a new Intent associated with this class. 47 * 48 * @param context The context the to associate with the Intent. 49 * @return The new Intent. 50 */ newIntent(Context context)51 private static Intent newIntent(Context context) { 52 Intent intent = new Intent(); 53 intent.setAction(PendingIntentScanReceiver.ACTION_SCAN_RESULT); 54 intent.setClass(context, PendingIntentScanReceiver.class); 55 return intent; 56 } 57 58 /** 59 * Constructs a new PendingIntent associated with this class. 60 * 61 * @param context The context to associate the PendingIntent with. 62 * @param requestCode The request code to uniquely identify this PendingIntent with. 63 * @return 64 */ newBroadcastPendingIntent(Context context, int requestCode)65 public static PendingIntent newBroadcastPendingIntent(Context context, int requestCode) { 66 return PendingIntent.getBroadcast( 67 context, 68 requestCode, 69 newIntent(context), 70 PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT); 71 } 72 73 /** 74 * Use this method for statically registered receivers. 75 * 76 * @return A future that will complete when the next scan result is received. 77 */ nextScanResult()78 public static CompletableFuture<List<ScanResult>> nextScanResult() 79 throws IllegalStateException { 80 if (sNextScanResultFuture.isPresent()) { 81 throw new IllegalStateException("scan result future already set"); 82 } 83 sNextScanResultFuture = Optional.of(new CompletableFuture<List<ScanResult>>()); 84 return sNextScanResultFuture.get(); 85 } 86 87 /** Clears the future waiting for the next static receiver scan result, if any. */ resetNextScanResultFuture()88 public static void resetNextScanResultFuture() { 89 sNextScanResultFuture = Optional.empty(); 90 } 91 92 @Override onReceive(Context context, Intent intent)93 public void onReceive(Context context, Intent intent) { 94 Log.d(TAG, "onReceive() intent: " + intent); 95 96 if (intent.getAction() != ACTION_SCAN_RESULT) { 97 throw new RuntimeException(); 98 } 99 100 int errorCode = intent.getIntExtra(BluetoothLeScanner.EXTRA_ERROR_CODE, 0); 101 if (errorCode != 0) { 102 Log.e(TAG, "onReceive() error: " + errorCode); 103 throw new RuntimeException("onReceive() unexpected error: " + errorCode); 104 } 105 106 List<ScanResult> scanResults = 107 intent.getParcelableExtra( 108 BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT, 109 new ArrayList<ScanResult>().getClass()); 110 111 if (sNextScanResultFuture.isPresent()) { 112 sNextScanResultFuture.get().complete(scanResults); 113 sNextScanResultFuture = Optional.empty(); 114 } else { 115 throw new IllegalStateException("scan result received but no future set"); 116 } 117 } 118 } 119