1 /*
2  * Copyright (C) 2022 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 
18 package com.android.server.companion.devicepresence;
19 
20 import static android.bluetooth.BluetoothAdapter.ACTION_BLE_STATE_CHANGED;
21 import static android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED;
22 import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_ALL_MATCHES;
23 import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_FIRST_MATCH;
24 import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_MATCH_LOST;
25 import static android.bluetooth.le.ScanSettings.SCAN_MODE_LOW_POWER;
26 
27 import static java.util.Objects.requireNonNull;
28 
29 import android.annotation.MainThread;
30 import android.annotation.NonNull;
31 import android.annotation.Nullable;
32 import android.annotation.SuppressLint;
33 import android.bluetooth.BluetoothAdapter;
34 import android.bluetooth.BluetoothDevice;
35 import android.bluetooth.le.BluetoothLeScanner;
36 import android.bluetooth.le.ScanCallback;
37 import android.bluetooth.le.ScanFilter;
38 import android.bluetooth.le.ScanResult;
39 import android.bluetooth.le.ScanSettings;
40 import android.companion.AssociationInfo;
41 import android.content.BroadcastReceiver;
42 import android.content.Context;
43 import android.content.Intent;
44 import android.content.IntentFilter;
45 import android.os.Handler;
46 import android.os.Looper;
47 import android.util.Slog;
48 
49 import com.android.server.companion.association.AssociationStore;
50 import com.android.server.companion.association.AssociationStore.ChangeType;
51 
52 import java.util.ArrayList;
53 import java.util.HashSet;
54 import java.util.List;
55 import java.util.Set;
56 
57 @SuppressLint("LongLogTag")
58 class BleDeviceProcessor implements AssociationStore.OnChangeListener {
59     private static final String TAG = "CDM_BleDeviceProcessor";
60 
61     interface Callback {
onBleCompanionDeviceFound(int associationId, int userId)62         void onBleCompanionDeviceFound(int associationId, int userId);
63 
onBleCompanionDeviceLost(int associationId, int userId)64         void onBleCompanionDeviceLost(int associationId, int userId);
65     }
66 
67     @NonNull
68     private final AssociationStore mAssociationStore;
69     @NonNull
70     private final Callback mCallback;
71 
72     // Non-null after init().
73     @Nullable
74     private BluetoothAdapter mBtAdapter;
75     // Non-null after init() and when BLE is available. Otherwise - null.
76     @Nullable
77     private BluetoothLeScanner mBleScanner;
78     // Only accessed from the Main thread.
79     private boolean mScanning = false;
80 
BleDeviceProcessor(@onNull AssociationStore associationStore, @NonNull Callback callback)81     BleDeviceProcessor(@NonNull AssociationStore associationStore, @NonNull Callback callback) {
82         mAssociationStore = associationStore;
83         mCallback = callback;
84     }
85 
86     @MainThread
init(@onNull Context context, @NonNull BluetoothAdapter btAdapter)87     void init(@NonNull Context context, @NonNull BluetoothAdapter btAdapter) {
88         if (mBtAdapter != null) {
89             throw new IllegalStateException(getClass().getSimpleName() + " is already initialized");
90         }
91         mBtAdapter = requireNonNull(btAdapter);
92 
93         checkBleState();
94         registerBluetoothStateBroadcastReceiver(context);
95 
96         mAssociationStore.registerLocalListener(this);
97     }
98 
99     @MainThread
restartScan()100     final void restartScan() {
101         enforceInitialized();
102 
103         if (mBleScanner == null) {
104             return;
105         }
106 
107         stopScanIfNeeded();
108         startScan();
109     }
110 
111     @Override
onAssociationChanged(@hangeType int changeType, AssociationInfo association)112     public void onAssociationChanged(@ChangeType int changeType, AssociationInfo association) {
113         // Simply restart scanning.
114         if (Looper.getMainLooper().isCurrentThread()) {
115             restartScan();
116         } else {
117             new Handler(Looper.getMainLooper()).post(this::restartScan);
118         }
119     }
120 
121     @MainThread
checkBleState()122     private void checkBleState() {
123         enforceInitialized();
124 
125         final boolean bleAvailable = mBtAdapter.isLeEnabled();
126         if ((bleAvailable && mBleScanner != null) || (!bleAvailable && mBleScanner == null)) {
127             // Nothing changed.
128             return;
129         }
130 
131         if (bleAvailable) {
132             mBleScanner = mBtAdapter.getBluetoothLeScanner();
133             if (mBleScanner == null) {
134                 // Oops, that's a race condition. Can return.
135                 return;
136             }
137 
138             startScan();
139         } else {
140             stopScanIfNeeded();
141             mBleScanner = null;
142         }
143     }
144 
145     @MainThread
startScan()146     void startScan() {
147         enforceInitialized();
148 
149         Slog.i(TAG, "startBleScan()");
150         // This method should not be called if scan is already in progress.
151         if (mScanning) {
152             Slog.w(TAG, "Scan is already in progress.");
153             return;
154         }
155 
156         // Neither should this method be called if the adapter is not available.
157         if (mBleScanner == null) {
158             Slog.w(TAG, "BLE is not available.");
159             return;
160         }
161 
162         // Collect MAC addresses from all associations.
163         final Set<String> macAddresses = new HashSet<>();
164         for (AssociationInfo association : mAssociationStore.getActiveAssociations()) {
165             if (!association.isNotifyOnDeviceNearby()) continue;
166 
167             // Beware that BT stack does not consider low-case MAC addresses valid, while
168             // MacAddress.toString() return a low-case String.
169             final String macAddress = association.getDeviceMacAddressAsString();
170             if (macAddress != null) {
171                 macAddresses.add(macAddress);
172             }
173         }
174         if (macAddresses.isEmpty()) {
175             return;
176         }
177 
178         final List<ScanFilter> filters = new ArrayList<>(macAddresses.size());
179         for (String macAddress : macAddresses) {
180             final ScanFilter filter = new ScanFilter.Builder()
181                     .setDeviceAddress(macAddress)
182                     .build();
183             filters.add(filter);
184         }
185 
186         // BluetoothLeScanner will throw an IllegalStateException if startScan() is called while LE
187         // is not enabled.
188         if (mBtAdapter.isLeEnabled()) {
189             try {
190                 mBleScanner.startScan(filters, SCAN_SETTINGS, mScanCallback);
191                 mScanning = true;
192             } catch (IllegalStateException e) {
193                 Slog.w(TAG, "Exception while starting BLE scanning", e);
194             }
195         } else {
196             Slog.w(TAG, "BLE scanning is not turned on");
197         }
198     }
199 
stopScanIfNeeded()200     void stopScanIfNeeded() {
201         enforceInitialized();
202 
203         Slog.i(TAG, "stopBleScan()");
204         if (!mScanning) {
205             return;
206         }
207         // mScanCallback is non-null here - it cannot be null when mScanning is true.
208 
209         // BluetoothLeScanner will throw an IllegalStateException if stopScan() is called while LE
210         // is not enabled.
211         if (mBtAdapter.isLeEnabled()) {
212             try {
213                 mBleScanner.stopScan(mScanCallback);
214             } catch (IllegalStateException e) {
215                 Slog.w(TAG, "Exception while stopping BLE scanning", e);
216             }
217         } else {
218             Slog.w(TAG, "BLE scanning is not turned on");
219         }
220 
221         mScanning = false;
222     }
223 
224     @MainThread
notifyDeviceFound(@onNull BluetoothDevice device)225     private void notifyDeviceFound(@NonNull BluetoothDevice device) {
226         for (AssociationInfo association : mAssociationStore.getActiveAssociationsByAddress(
227                 device.getAddress())) {
228             mCallback.onBleCompanionDeviceFound(association.getId(), association.getUserId());
229         }
230     }
231 
232     @MainThread
notifyDeviceLost(@onNull BluetoothDevice device)233     private void notifyDeviceLost(@NonNull BluetoothDevice device) {
234         for (AssociationInfo association : mAssociationStore.getActiveAssociationsByAddress(
235                 device.getAddress())) {
236             mCallback.onBleCompanionDeviceLost(association.getId(), association.getUserId());
237         }
238     }
239 
registerBluetoothStateBroadcastReceiver(Context context)240     private void registerBluetoothStateBroadcastReceiver(Context context) {
241         final BroadcastReceiver receiver = new BroadcastReceiver() {
242             @Override
243             public void onReceive(Context context, Intent intent) {
244                 // Post to the main thread to make sure it is a Non-Blocking call.
245                 new Handler(Looper.getMainLooper()).post(() -> checkBleState());
246             }
247         };
248 
249         final IntentFilter filter = new IntentFilter();
250         filter.addAction(ACTION_STATE_CHANGED);
251         filter.addAction(ACTION_BLE_STATE_CHANGED);
252 
253         context.registerReceiver(receiver, filter);
254     }
255 
enforceInitialized()256     private void enforceInitialized() {
257         if (mBtAdapter != null) return;
258         throw new IllegalStateException(getClass().getSimpleName() + " is not initialized");
259     }
260 
261     private final ScanCallback mScanCallback = new ScanCallback() {
262         @MainThread
263         @Override
264         public void onScanResult(int callbackType, ScanResult result) {
265             final BluetoothDevice device = result.getDevice();
266 
267             switch (callbackType) {
268                 case CALLBACK_TYPE_FIRST_MATCH:
269                     notifyDeviceFound(device);
270                     break;
271 
272                 case CALLBACK_TYPE_MATCH_LOST:
273                     notifyDeviceLost(device);
274                     break;
275 
276                 default:
277                     Slog.wtf(TAG, "Unexpected callback "
278                             + nameForBleScanCallbackType(callbackType));
279                     break;
280             }
281         }
282 
283         @MainThread
284         @Override
285         public void onScanFailed(int errorCode) {
286             mScanning = false;
287         }
288     };
289 
nameForBleScanCallbackType(int callbackType)290     private static String nameForBleScanCallbackType(int callbackType) {
291         final String name = switch (callbackType) {
292             case CALLBACK_TYPE_ALL_MATCHES -> "ALL_MATCHES";
293             case CALLBACK_TYPE_FIRST_MATCH -> "FIRST_MATCH";
294             case CALLBACK_TYPE_MATCH_LOST -> "MATCH_LOST";
295             default -> "Unknown";
296         };
297         return name + "(" + callbackType + ")";
298     }
299 
300     private static final ScanSettings SCAN_SETTINGS = new ScanSettings.Builder()
301             .setCallbackType(CALLBACK_TYPE_FIRST_MATCH | CALLBACK_TYPE_MATCH_LOST)
302             .setScanMode(SCAN_MODE_LOW_POWER)
303             .build();
304 }
305