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