1 /*
2  * Copyright (C) 2013 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 com.android.companiondevicemanager;
18 
19 import static com.android.companiondevicemanager.Utils.runOnMainThread;
20 import static com.android.internal.util.ArrayUtils.isEmpty;
21 import static com.android.internal.util.CollectionUtils.filter;
22 import static com.android.internal.util.CollectionUtils.find;
23 import static com.android.internal.util.CollectionUtils.map;
24 
25 import static java.lang.Math.max;
26 import static java.lang.Math.min;
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.app.Service;
34 import android.bluetooth.BluetoothAdapter;
35 import android.bluetooth.BluetoothDevice;
36 import android.bluetooth.BluetoothManager;
37 import android.bluetooth.BluetoothProfile;
38 import android.bluetooth.le.BluetoothLeScanner;
39 import android.bluetooth.le.ScanCallback;
40 import android.bluetooth.le.ScanFilter;
41 import android.bluetooth.le.ScanResult;
42 import android.bluetooth.le.ScanSettings;
43 import android.companion.AssociationRequest;
44 import android.companion.BluetoothDeviceFilter;
45 import android.companion.BluetoothLeDeviceFilter;
46 import android.companion.DeviceFilter;
47 import android.companion.WifiDeviceFilter;
48 import android.content.BroadcastReceiver;
49 import android.content.Context;
50 import android.content.Intent;
51 import android.content.IntentFilter;
52 import android.net.wifi.WifiManager;
53 import android.os.Handler;
54 import android.os.IBinder;
55 import android.os.Parcelable;
56 import android.os.SystemProperties;
57 import android.text.TextUtils;
58 import android.util.Log;
59 
60 import androidx.lifecycle.LiveData;
61 import androidx.lifecycle.MutableLiveData;
62 
63 import java.util.ArrayList;
64 import java.util.Collection;
65 import java.util.Collections;
66 import java.util.List;
67 import java.util.Objects;
68 
69 /**
70  *  A CompanionDevice service response for scanning nearby devices
71  */
72 @SuppressLint("LongLogTag")
73 public class CompanionDeviceDiscoveryService extends Service {
74     private static final boolean DEBUG = false;
75     private static final String TAG = "CDM_CompanionDeviceDiscoveryService";
76 
77     private static final String SYS_PROP_DEBUG_TIMEOUT = "debug.cdm.discovery_timeout";
78     private static final long TIMEOUT_DEFAULT = 20_000L; // 20 seconds
79     private static final long TIMEOUT_MIN = 1_000L; // 1 sec
80     private static final long TIMEOUT_MAX = 60_000L; // 1 min
81 
82     private static final String ACTION_START_DISCOVERY =
83             "com.android.companiondevicemanager.action.START_DISCOVERY";
84     private static final String ACTION_STOP_DISCOVERY =
85             "com.android.companiondevicemanager.action.ACTION_STOP_DISCOVERY";
86     private static final String EXTRA_ASSOCIATION_REQUEST = "association_request";
87 
88     private static MutableLiveData<List<DeviceFilterPair<?>>> sScanResultsLiveData =
89             new MutableLiveData<>(Collections.emptyList());
90     private static MutableLiveData<DiscoveryState> sStateLiveData =
91             new MutableLiveData<>(DiscoveryState.NOT_STARTED);
92 
93     private BluetoothManager mBtManager;
94     private BluetoothAdapter mBtAdapter;
95     private WifiManager mWifiManager;
96     private BluetoothLeScanner mBleScanner;
97 
98     private ScanCallback mBleScanCallback;
99     private BluetoothBroadcastReceiver mBtReceiver;
100     private WifiBroadcastReceiver mWifiReceiver;
101 
102     private boolean mDiscoveryStarted = false;
103     private boolean mDiscoveryStopped = false;
104     private final List<DeviceFilterPair<?>> mDevicesFound = new ArrayList<>();
105 
106     private final Runnable mTimeoutRunnable = this::timeout;
107 
108     private boolean mStopAfterFirstMatch;
109 
110     /**
111      * A state enum for devices' discovery.
112      */
113     enum DiscoveryState {
114         NOT_STARTED,
115         STARTING,
116         DISCOVERY_IN_PROGRESS,
117         FINISHED_STOPPED,
118         FINISHED_TIMEOUT
119     }
120 
startForRequest( @onNull Context context, @NonNull AssociationRequest associationRequest)121     static void startForRequest(
122             @NonNull Context context, @NonNull AssociationRequest associationRequest) {
123         requireNonNull(associationRequest);
124         final Intent intent = new Intent(context, CompanionDeviceDiscoveryService.class);
125         intent.setAction(ACTION_START_DISCOVERY);
126         intent.putExtra(EXTRA_ASSOCIATION_REQUEST, associationRequest);
127         sStateLiveData.setValue(DiscoveryState.STARTING);
128         sScanResultsLiveData.setValue(Collections.emptyList());
129 
130         context.startService(intent);
131     }
132 
stop(@onNull Context context)133     static void stop(@NonNull Context context) {
134         final Intent intent = new Intent(context, CompanionDeviceDiscoveryService.class);
135         intent.setAction(ACTION_STOP_DISCOVERY);
136         context.startService(intent);
137     }
138 
getScanResult()139     static LiveData<List<DeviceFilterPair<?>>> getScanResult() {
140         return sScanResultsLiveData;
141     }
142 
getDiscoveryState()143     static LiveData<DiscoveryState> getDiscoveryState() {
144         return sStateLiveData;
145     }
146 
147     @Override
onCreate()148     public void onCreate() {
149         super.onCreate();
150         if (DEBUG) Log.d(TAG, "onCreate()");
151 
152         mBtManager = getSystemService(BluetoothManager.class);
153         mBtAdapter = mBtManager.getAdapter();
154         mBleScanner = mBtAdapter.getBluetoothLeScanner();
155         mWifiManager = getSystemService(WifiManager.class);
156     }
157 
158     @Override
onStartCommand(Intent intent, int flags, int startId)159     public int onStartCommand(Intent intent, int flags, int startId) {
160         final String action = intent.getAction();
161         if (DEBUG) Log.d(TAG, "onStartCommand() action=" + action);
162 
163         switch (action) {
164             case ACTION_START_DISCOVERY:
165                 final AssociationRequest request =
166                         intent.getParcelableExtra(EXTRA_ASSOCIATION_REQUEST);
167                 startDiscovery(request);
168                 break;
169 
170             case ACTION_STOP_DISCOVERY:
171                 stopDiscoveryAndFinish(/* timeout */ false);
172                 break;
173         }
174         return START_NOT_STICKY;
175     }
176 
177     @Override
onDestroy()178     public void onDestroy() {
179         super.onDestroy();
180         if (DEBUG) Log.d(TAG, "onDestroy()");
181     }
182 
183     @MainThread
startDiscovery(@onNull AssociationRequest request)184     private void startDiscovery(@NonNull AssociationRequest request) {
185         if (DEBUG) Log.i(TAG, "startDiscovery() request=" + request);
186         requireNonNull(request);
187 
188         if (mDiscoveryStarted) throw new RuntimeException("Discovery in progress.");
189         mStopAfterFirstMatch = request.isSingleDevice();
190         mDiscoveryStarted = true;
191         sStateLiveData.setValue(DiscoveryState.DISCOVERY_IN_PROGRESS);
192 
193         final List<DeviceFilter<?>> allFilters = request.getDeviceFilters();
194         final List<BluetoothDeviceFilter> btFilters =
195                 filter(allFilters, BluetoothDeviceFilter.class);
196         final List<BluetoothLeDeviceFilter> bleFilters =
197                 filter(allFilters, BluetoothLeDeviceFilter.class);
198         final List<WifiDeviceFilter> wifiFilters = filter(allFilters, WifiDeviceFilter.class);
199 
200         checkBoundDevicesIfNeeded(request, btFilters);
201 
202         // If no filters are specified: look for everything.
203         final boolean forceStartScanningAll = isEmpty(allFilters);
204         // Start BT scanning (if needed)
205         mBtReceiver = startBtScanningIfNeeded(btFilters, forceStartScanningAll);
206         // Start Wi-Fi scanning (if needed)
207         mWifiReceiver = startWifiScanningIfNeeded(wifiFilters, forceStartScanningAll);
208         // Start BLE scanning (if needed)
209         mBleScanCallback = startBleScanningIfNeeded(bleFilters, forceStartScanningAll);
210 
211         scheduleTimeout();
212     }
213 
214     @MainThread
stopDiscoveryAndFinish(boolean timeout)215     private void stopDiscoveryAndFinish(boolean timeout) {
216         if (DEBUG) Log.i(TAG, "stopDiscovery()");
217 
218         if (!mDiscoveryStarted) {
219             stopSelf();
220             return;
221         }
222 
223         if (mDiscoveryStopped) return;
224         mDiscoveryStopped = true;
225 
226         // Stop BT discovery.
227         if (mBtReceiver != null) {
228             // Cancel discovery.
229             mBtAdapter.cancelDiscovery();
230             // Unregister receiver.
231             unregisterReceiver(mBtReceiver);
232             mBtReceiver = null;
233         }
234 
235         // Stop Wi-Fi scanning.
236         if (mWifiReceiver != null) {
237             // TODO: need to stop scan?
238             // Unregister receiver.
239             unregisterReceiver(mWifiReceiver);
240             mWifiReceiver = null;
241         }
242 
243         // Stop BLE scanning.
244         if (mBleScanCallback != null) {
245             mBleScanner.stopScan(mBleScanCallback);
246         }
247 
248         Handler.getMain().removeCallbacks(mTimeoutRunnable);
249 
250         if (timeout) {
251             sStateLiveData.setValue(DiscoveryState.FINISHED_TIMEOUT);
252         } else {
253             sStateLiveData.setValue(DiscoveryState.FINISHED_STOPPED);
254         }
255 
256         // "Finish".
257         stopSelf();
258     }
259 
checkBoundDevicesIfNeeded(@onNull AssociationRequest request, @NonNull List<BluetoothDeviceFilter> btFilters)260     private void checkBoundDevicesIfNeeded(@NonNull AssociationRequest request,
261             @NonNull List<BluetoothDeviceFilter> btFilters) {
262         // If filtering to get single device by mac address, also search in the set of already
263         // bonded devices to allow linking those directly
264         if (btFilters.isEmpty() || !request.isSingleDevice()) return;
265 
266         final BluetoothDeviceFilter singleMacAddressFilter =
267                 find(btFilters, filter -> !TextUtils.isEmpty(filter.getAddress()));
268 
269         if (singleMacAddressFilter == null) return;
270 
271         findAndReportMatches(mBtAdapter.getBondedDevices(), btFilters);
272         findAndReportMatches(mBtManager.getConnectedDevices(BluetoothProfile.GATT), btFilters);
273         findAndReportMatches(
274                 mBtManager.getConnectedDevices(BluetoothProfile.GATT_SERVER), btFilters);
275     }
276 
findAndReportMatches(@ullable Collection<BluetoothDevice> devices, @NonNull List<BluetoothDeviceFilter> filters)277     private void findAndReportMatches(@Nullable Collection<BluetoothDevice> devices,
278             @NonNull List<BluetoothDeviceFilter> filters) {
279         if (devices == null) return;
280 
281         for (BluetoothDevice device : devices) {
282             final DeviceFilterPair<BluetoothDevice> match = findMatch(device, filters);
283             if (match != null) {
284                 onDeviceFound(match);
285             }
286         }
287     }
288 
startBtScanningIfNeeded( List<BluetoothDeviceFilter> filters, boolean force)289     private BluetoothBroadcastReceiver startBtScanningIfNeeded(
290             List<BluetoothDeviceFilter> filters, boolean force) {
291         if (isEmpty(filters) && !force) return null;
292         if (DEBUG) Log.d(TAG, "registerReceiver(BluetoothDevice.ACTION_FOUND)");
293 
294         final BluetoothBroadcastReceiver receiver = new BluetoothBroadcastReceiver(filters);
295 
296         final IntentFilter intentFilter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
297         registerReceiver(receiver, intentFilter);
298 
299         mBtAdapter.startDiscovery();
300 
301         return receiver;
302     }
303 
startWifiScanningIfNeeded( List<WifiDeviceFilter> filters, boolean force)304     private WifiBroadcastReceiver startWifiScanningIfNeeded(
305             List<WifiDeviceFilter> filters, boolean force) {
306         if (isEmpty(filters) && !force) return null;
307         if (DEBUG) Log.d(TAG, "registerReceiver(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)");
308 
309         final WifiBroadcastReceiver receiver = new WifiBroadcastReceiver(filters);
310 
311         final IntentFilter intentFilter = new IntentFilter(
312                 WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
313         registerReceiver(receiver, intentFilter);
314 
315         mWifiManager.startScan();
316 
317         return receiver;
318     }
319 
startBleScanningIfNeeded( List<BluetoothLeDeviceFilter> filters, boolean force)320     private ScanCallback startBleScanningIfNeeded(
321             List<BluetoothLeDeviceFilter> filters, boolean force) {
322         if (isEmpty(filters) && !force) return null;
323         if (DEBUG) Log.d(TAG, "BLEScanner.startScan");
324 
325         if (mBleScanner == null) {
326             Log.w(TAG, "BLE Scanner is not available.");
327             return null;
328         }
329 
330         final BLEScanCallback callback = new BLEScanCallback(filters);
331 
332         final List<ScanFilter> scanFilters = map(
333                 filters, BluetoothLeDeviceFilter::getScanFilter);
334         final ScanSettings scanSettings = new ScanSettings.Builder()
335                 .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
336                 .build();
337         mBleScanner.startScan(scanFilters, scanSettings, callback);
338 
339         return callback;
340     }
341 
onDeviceFound(@onNull DeviceFilterPair<?> device)342     private void onDeviceFound(@NonNull DeviceFilterPair<?> device) {
343         runOnMainThread(() -> {
344             if (DEBUG) Log.v(TAG, "onDeviceFound() " + device);
345             if (mDiscoveryStopped) return;
346             if (mDevicesFound.contains(device)) {
347                 // TODO: update the device instead of ignoring (new found device may contain
348                 //  additional/updated info, eg. name of the device).
349                 if (DEBUG) {
350                     Log.d(TAG, "onDeviceFound() " + device.toShortString()
351                             + " - Already seen: ignore.");
352                 }
353                 return;
354             }
355             Log.i(TAG, "onDeviceFound() " + device.toShortString() + " - New device.");
356 
357             // First: make change.
358             mDevicesFound.add(device);
359             // Then: notify observers.
360             sScanResultsLiveData.setValue(mDevicesFound);
361             // Stop discovery when there's one device found for singleDevice.
362             if (mStopAfterFirstMatch) {
363                 stopDiscoveryAndFinish(/* timeout */ false);
364             }
365         });
366     }
367 
onDeviceLost(@onNull DeviceFilterPair<?> device)368     private void onDeviceLost(@NonNull DeviceFilterPair<?> device) {
369         runOnMainThread(() -> {
370             Log.i(TAG, "onDeviceLost(), device=" + device.toShortString());
371 
372             // First: make change.
373             mDevicesFound.remove(device);
374             // Then: notify observers.
375             sScanResultsLiveData.setValue(mDevicesFound);
376         });
377     }
378 
scheduleTimeout()379     private void scheduleTimeout() {
380         long timeout = SystemProperties.getLong(SYS_PROP_DEBUG_TIMEOUT, -1);
381         if (timeout <= 0) {
382             // 0 or negative values indicate that the sysprop was never set or should be ignored.
383             timeout = TIMEOUT_DEFAULT;
384         } else {
385             timeout = min(timeout, TIMEOUT_MAX); // should be <= 1 min (TIMEOUT_MAX)
386             timeout = max(timeout, TIMEOUT_MIN); // should be >= 1 sec (TIMEOUT_MIN)
387         }
388 
389         if (DEBUG) Log.d(TAG, "scheduleTimeout(), timeout=" + timeout);
390 
391         Handler.getMain().postDelayed(mTimeoutRunnable, timeout);
392     }
393 
timeout()394     private void timeout() {
395         if (DEBUG) Log.i(TAG, "timeout()");
396         stopDiscoveryAndFinish(/* timeout */ true);
397     }
398 
399     @Override
onBind(Intent intent)400     public IBinder onBind(Intent intent) {
401         return null;
402     }
403 
404     private class BLEScanCallback extends ScanCallback {
405         final List<BluetoothLeDeviceFilter> mFilters;
406 
BLEScanCallback(List<BluetoothLeDeviceFilter> filters)407         BLEScanCallback(List<BluetoothLeDeviceFilter> filters) {
408             mFilters = filters;
409         }
410 
411         @Override
onScanResult(int callbackType, ScanResult result)412         public void onScanResult(int callbackType, ScanResult result) {
413             if (DEBUG) {
414                 Log.v(TAG, "BLE.onScanResult() callback=" + callbackType + ", result=" + result);
415             }
416 
417             final DeviceFilterPair<ScanResult> match = findMatch(result, mFilters);
418             if (match == null) return;
419 
420             if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) {
421                 onDeviceLost(match);
422             } else {
423                 // TODO: check this logic.
424                 onDeviceFound(match);
425             }
426         }
427     }
428 
429     private class BluetoothBroadcastReceiver extends BroadcastReceiver {
430         final List<BluetoothDeviceFilter> mFilters;
431 
BluetoothBroadcastReceiver(List<BluetoothDeviceFilter> filters)432         BluetoothBroadcastReceiver(List<BluetoothDeviceFilter> filters) {
433             this.mFilters = filters;
434         }
435 
436         @Override
onReceive(Context context, Intent intent)437         public void onReceive(Context context, Intent intent) {
438             final String action = intent.getAction();
439             final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
440 
441             if (DEBUG) Log.v(TAG, action + ", device=" + device);
442 
443             if (action == null) return;
444 
445             final DeviceFilterPair<BluetoothDevice> match = findMatch(device, mFilters);
446             if (match == null) return;
447 
448             if (action.equals(BluetoothDevice.ACTION_FOUND)) {
449                 onDeviceFound(match);
450             } else {
451                 // TODO: check this logic.
452                 onDeviceLost(match);
453             }
454         }
455     }
456 
457     private class WifiBroadcastReceiver extends BroadcastReceiver {
458         final List<WifiDeviceFilter> mFilters;
459 
WifiBroadcastReceiver(List<WifiDeviceFilter> filters)460         private WifiBroadcastReceiver(List<WifiDeviceFilter> filters) {
461             this.mFilters = filters;
462         }
463 
464         @Override
onReceive(Context context, Intent intent)465         public void onReceive(Context context, Intent intent) {
466             if (!Objects.equals(intent.getAction(), WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) {
467                 return;
468             }
469 
470             final List<android.net.wifi.ScanResult> scanResults = mWifiManager.getScanResults();
471             if (DEBUG) {
472                 Log.v(TAG, "WifiManager.SCAN_RESULTS_AVAILABLE_ACTION, results:\n  "
473                         + TextUtils.join("\n  ", scanResults));
474             }
475 
476             for (int i = 0; i < scanResults.size(); i++) {
477                 final android.net.wifi.ScanResult scanResult = scanResults.get(i);
478                 final DeviceFilterPair<?> match = findMatch(scanResult, mFilters);
479                 if (match != null) {
480                     onDeviceFound(match);
481                 }
482             }
483         }
484     }
485 
486     /**
487      * {@code (device, null)} if the filters list is empty or null
488      * {@code null} if none of the provided filters match the device
489      * {@code (device, filter)} where filter is among the list of filters and matches the device
490      */
491     @Nullable
findMatch( T dev, @Nullable List<? extends DeviceFilter<T>> filters)492     public static <T extends Parcelable> DeviceFilterPair<T> findMatch(
493             T dev, @Nullable List<? extends DeviceFilter<T>> filters) {
494         if (isEmpty(filters)) return new DeviceFilterPair<>(dev, null);
495         final DeviceFilter<T> matchingFilter = find(filters, f -> f.matches(dev));
496 
497         DeviceFilterPair<T> result = matchingFilter != null
498                 ? new DeviceFilterPair<>(dev, matchingFilter) : null;
499         if (DEBUG) {
500             Log.v(TAG, "findMatch(dev=" + dev + ", filters=" + filters + ") -> " + result);
501         }
502         return result;
503     }
504 }
505