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 android.companion.BluetoothDeviceFilterUtils.getDeviceDisplayNameInternal;
20 import static android.companion.BluetoothDeviceFilterUtils.getDeviceMacAddress;
21 
22 import static com.android.internal.util.ArrayUtils.isEmpty;
23 import static com.android.internal.util.CollectionUtils.emptyIfNull;
24 import static com.android.internal.util.CollectionUtils.size;
25 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
26 
27 import android.annotation.MainThread;
28 import android.annotation.NonNull;
29 import android.annotation.Nullable;
30 import android.app.PendingIntent;
31 import android.app.Service;
32 import android.bluetooth.BluetoothAdapter;
33 import android.bluetooth.BluetoothDevice;
34 import android.bluetooth.BluetoothManager;
35 import android.bluetooth.BluetoothProfile;
36 import android.bluetooth.le.BluetoothLeScanner;
37 import android.bluetooth.le.ScanCallback;
38 import android.bluetooth.le.ScanFilter;
39 import android.bluetooth.le.ScanResult;
40 import android.bluetooth.le.ScanSettings;
41 import android.companion.Association;
42 import android.companion.AssociationRequest;
43 import android.companion.BluetoothDeviceFilter;
44 import android.companion.BluetoothLeDeviceFilter;
45 import android.companion.DeviceFilter;
46 import android.companion.ICompanionDeviceDiscoveryService;
47 import android.companion.IFindDeviceCallback;
48 import android.companion.WifiDeviceFilter;
49 import android.content.BroadcastReceiver;
50 import android.content.Context;
51 import android.content.Intent;
52 import android.content.IntentFilter;
53 import android.net.wifi.WifiManager;
54 import android.os.Handler;
55 import android.os.IBinder;
56 import android.os.Parcelable;
57 import android.os.RemoteException;
58 import android.text.TextUtils;
59 import android.util.Log;
60 
61 import com.android.internal.infra.AndroidFuture;
62 import com.android.internal.util.ArrayUtils;
63 import com.android.internal.util.CollectionUtils;
64 import com.android.internal.util.Preconditions;
65 
66 import java.util.ArrayList;
67 import java.util.List;
68 import java.util.Objects;
69 
70 public class CompanionDeviceDiscoveryService extends Service {
71 
72     private static final boolean DEBUG = false;
73     private static final String LOG_TAG = CompanionDeviceDiscoveryService.class.getSimpleName();
74 
75     private static final long SCAN_TIMEOUT = 20000;
76 
77     static CompanionDeviceDiscoveryService sInstance;
78 
79     private BluetoothManager mBluetoothManager;
80     private BluetoothAdapter mBluetoothAdapter;
81     private WifiManager mWifiManager;
82     @Nullable private BluetoothLeScanner mBLEScanner;
83     private ScanSettings mDefaultScanSettings = new ScanSettings.Builder()
84             .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
85             .build();
86 
87     private List<DeviceFilter<?>> mFilters;
88     private List<BluetoothLeDeviceFilter> mBLEFilters;
89     private List<BluetoothDeviceFilter> mBluetoothFilters;
90     private List<WifiDeviceFilter> mWifiFilters;
91     private List<ScanFilter> mBLEScanFilters;
92 
93     AssociationRequest mRequest;
94     List<DeviceFilterPair> mDevicesFound;
95     DeviceFilterPair mSelectedDevice;
96     IFindDeviceCallback mFindCallback;
97 
98     AndroidFuture<Association> mServiceCallback;
99     boolean mIsScanning = false;
100     @Nullable
101     CompanionDeviceActivity mActivity = null;
102 
103     private final ICompanionDeviceDiscoveryService mBinder =
104             new ICompanionDeviceDiscoveryService.Stub() {
105         @Override
106         public void startDiscovery(AssociationRequest request,
107                 String callingPackage,
108                 IFindDeviceCallback findCallback,
109                 AndroidFuture serviceCallback) {
110             Log.i(LOG_TAG,
111                     "startDiscovery() called with: filter = [" + request
112                             + "], findCallback = [" + findCallback + "]"
113                             + "], serviceCallback = [" + serviceCallback + "]");
114             mFindCallback = findCallback;
115             mServiceCallback = serviceCallback;
116             Handler.getMain().sendMessage(obtainMessage(
117                     CompanionDeviceDiscoveryService::startDiscovery,
118                     CompanionDeviceDiscoveryService.this, request));
119         }
120 
121         @Override
122         public void onAssociationCreated() {
123             Handler.getMain().post(CompanionDeviceDiscoveryService.this::onAssociationCreated);
124         }
125     };
126 
127     private ScanCallback mBLEScanCallback;
128     private BluetoothBroadcastReceiver mBluetoothBroadcastReceiver;
129     private WifiBroadcastReceiver mWifiBroadcastReceiver;
130 
131     @Override
onBind(Intent intent)132     public IBinder onBind(Intent intent) {
133         Log.i(LOG_TAG, "onBind(" + intent + ")");
134         return mBinder.asBinder();
135     }
136 
137     @Override
onCreate()138     public void onCreate() {
139         super.onCreate();
140 
141         Log.i(LOG_TAG, "onCreate()");
142 
143         mBluetoothManager = getSystemService(BluetoothManager.class);
144         mBluetoothAdapter = mBluetoothManager.getAdapter();
145         mBLEScanner = mBluetoothAdapter.getBluetoothLeScanner();
146         mWifiManager = getSystemService(WifiManager.class);
147 
148         mDevicesFound = new ArrayList<>();
149 
150         sInstance = this;
151     }
152 
153     @MainThread
startDiscovery(AssociationRequest request)154     private void startDiscovery(AssociationRequest request) {
155         if (!request.equals(mRequest)) {
156             mRequest = request;
157 
158             mFilters = request.getDeviceFilters();
159             mWifiFilters = CollectionUtils.filter(mFilters, WifiDeviceFilter.class);
160             mBluetoothFilters = CollectionUtils.filter(mFilters, BluetoothDeviceFilter.class);
161             mBLEFilters = CollectionUtils.filter(mFilters, BluetoothLeDeviceFilter.class);
162             mBLEScanFilters
163                     = CollectionUtils.map(mBLEFilters, BluetoothLeDeviceFilter::getScanFilter);
164 
165             reset();
166         } else {
167             Log.i(LOG_TAG, "startDiscovery: duplicate request: " + request);
168         }
169 
170         if (!ArrayUtils.isEmpty(mDevicesFound)) {
171             onReadyToShowUI();
172         }
173 
174         // If filtering to get single device by mac address, also search in the set of already
175         // bonded devices to allow linking those directly
176         String singleMacAddressFilter = null;
177         if (mRequest.isSingleDevice()) {
178             int numFilters = size(mBluetoothFilters);
179             for (int i = 0; i < numFilters; i++) {
180                 BluetoothDeviceFilter filter = mBluetoothFilters.get(i);
181                 if (!TextUtils.isEmpty(filter.getAddress())) {
182                     singleMacAddressFilter = filter.getAddress();
183                     break;
184                 }
185             }
186         }
187         if (singleMacAddressFilter != null) {
188             for (BluetoothDevice dev : emptyIfNull(mBluetoothAdapter.getBondedDevices())) {
189                 onDeviceFound(DeviceFilterPair.findMatch(dev, mBluetoothFilters));
190             }
191             for (BluetoothDevice dev : emptyIfNull(
192                     mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT))) {
193                 onDeviceFound(DeviceFilterPair.findMatch(dev, mBluetoothFilters));
194             }
195             for (BluetoothDevice dev : emptyIfNull(
196                     mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT_SERVER))) {
197                 onDeviceFound(DeviceFilterPair.findMatch(dev, mBluetoothFilters));
198             }
199         }
200 
201         if (shouldScan(mBluetoothFilters)) {
202             final IntentFilter intentFilter = new IntentFilter();
203             intentFilter.addAction(BluetoothDevice.ACTION_FOUND);
204 
205             Log.i(LOG_TAG, "registerReceiver(BluetoothDevice.ACTION_FOUND)");
206             mBluetoothBroadcastReceiver = new BluetoothBroadcastReceiver();
207             registerReceiver(mBluetoothBroadcastReceiver, intentFilter);
208             mBluetoothAdapter.startDiscovery();
209         }
210 
211         if (shouldScan(mBLEFilters) && mBLEScanner != null) {
212             Log.i(LOG_TAG, "BLEScanner.startScan");
213             mBLEScanCallback = new BLEScanCallback();
214             mBLEScanner.startScan(mBLEScanFilters, mDefaultScanSettings, mBLEScanCallback);
215         }
216 
217         if (shouldScan(mWifiFilters)) {
218             Log.i(LOG_TAG, "registerReceiver(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)");
219             mWifiBroadcastReceiver = new WifiBroadcastReceiver();
220             registerReceiver(mWifiBroadcastReceiver,
221                     new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION));
222             mWifiManager.startScan();
223         }
224         mIsScanning = true;
225         Handler.getMain().sendMessageDelayed(
226                 obtainMessage(CompanionDeviceDiscoveryService::stopScan, this),
227                 SCAN_TIMEOUT);
228     }
229 
230     @MainThread
onAssociationCreated()231     private void onAssociationCreated() {
232         mActivity.setResultAndFinish();
233     }
234 
shouldScan(List<? extends DeviceFilter> mediumSpecificFilters)235     private boolean shouldScan(List<? extends DeviceFilter> mediumSpecificFilters) {
236         return !isEmpty(mediumSpecificFilters) || isEmpty(mFilters);
237     }
238 
239     @MainThread
reset()240     private void reset() {
241         Log.i(LOG_TAG, "reset()");
242         stopScan();
243         mDevicesFound.clear();
244         mSelectedDevice = null;
245         CompanionDeviceActivity.notifyDevicesChanged();
246     }
247 
248     @Override
onUnbind(Intent intent)249     public boolean onUnbind(Intent intent) {
250         Log.i(LOG_TAG, "onUnbind(intent = " + intent + ")");
251         stopScan();
252         return super.onUnbind(intent);
253     }
254 
stopScan()255     private void stopScan() {
256         Log.i(LOG_TAG, "stopScan()");
257 
258         if (!mIsScanning) return;
259         mIsScanning = false;
260 
261         if (mActivity != null && mActivity.mDeviceListView != null) {
262             mActivity.mDeviceListView.removeFooterView(mActivity.mLoadingIndicator);
263         }
264 
265         mBluetoothAdapter.cancelDiscovery();
266         if (mBluetoothBroadcastReceiver != null) {
267             unregisterReceiver(mBluetoothBroadcastReceiver);
268             mBluetoothBroadcastReceiver = null;
269         }
270         if (mBLEScanner != null) mBLEScanner.stopScan(mBLEScanCallback);
271         if (mWifiBroadcastReceiver != null) {
272             unregisterReceiver(mWifiBroadcastReceiver);
273             mWifiBroadcastReceiver = null;
274         }
275     }
276 
onDeviceFound(@ullable DeviceFilterPair device)277     private void onDeviceFound(@Nullable DeviceFilterPair device) {
278         if (device == null) return;
279 
280         Handler.getMain().sendMessage(obtainMessage(
281                 CompanionDeviceDiscoveryService::onDeviceFoundMainThread, this, device));
282     }
283 
284     @MainThread
onDeviceFoundMainThread(@onNull DeviceFilterPair device)285     void onDeviceFoundMainThread(@NonNull DeviceFilterPair device) {
286         if (mDevicesFound.contains(device)) {
287             Log.i(LOG_TAG, "Skipping device " + device + " - already among found devices");
288             return;
289         }
290 
291         Log.i(LOG_TAG, "Found device " + device);
292 
293         if (mDevicesFound.isEmpty()) {
294             onReadyToShowUI();
295         }
296         mDevicesFound.add(device);
297         CompanionDeviceActivity.notifyDevicesChanged();
298     }
299 
300     //TODO also, on timeout -> call onFailure
onReadyToShowUI()301     private void onReadyToShowUI() {
302         try {
303             mFindCallback.onSuccess(PendingIntent.getActivity(
304                     this, 0,
305                     new Intent(this, CompanionDeviceActivity.class),
306                     PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT
307                             | PendingIntent.FLAG_IMMUTABLE));
308         } catch (RemoteException e) {
309             throw new RuntimeException(e);
310         }
311     }
312 
onDeviceLost(@ullable DeviceFilterPair device)313     private void onDeviceLost(@Nullable DeviceFilterPair device) {
314         Log.i(LOG_TAG, "Lost device " + device.getDisplayName());
315         Handler.getMain().sendMessage(obtainMessage(
316                 CompanionDeviceDiscoveryService::onDeviceLostMainThread, this, device));
317     }
318 
319     @MainThread
onDeviceLostMainThread(@ullable DeviceFilterPair device)320     void onDeviceLostMainThread(@Nullable DeviceFilterPair device) {
321         mDevicesFound.remove(device);
322         CompanionDeviceActivity.notifyDevicesChanged();
323     }
324 
onDeviceSelected(String callingPackage, String deviceAddress)325     void onDeviceSelected(String callingPackage, String deviceAddress) {
326         if (callingPackage == null || deviceAddress == null) {
327             return;
328         }
329         mServiceCallback.complete(new Association(
330                 getUserId(), deviceAddress, callingPackage, mRequest.getDeviceProfile(), false,
331                 System.currentTimeMillis()));
332     }
333 
onCancel()334     void onCancel() {
335         if (DEBUG) Log.i(LOG_TAG, "onCancel()");
336         mActivity = null;
337         mServiceCallback.cancel(true);
338     }
339 
340     /**
341      * A pair of device and a filter that matched this device if any.
342      *
343      * @param <T> device type
344      */
345     static class DeviceFilterPair<T extends Parcelable> {
346         public final T device;
347         @Nullable
348         public final DeviceFilter<T> filter;
349 
DeviceFilterPair(T device, @Nullable DeviceFilter<T> filter)350         private DeviceFilterPair(T device, @Nullable DeviceFilter<T> filter) {
351             this.device = device;
352             this.filter = filter;
353         }
354 
355         /**
356          * {@code (device, null)} if the filters list is empty or null
357          * {@code null} if none of the provided filters match the device
358          * {@code (device, filter)} where filter is among the list of filters and matches the device
359          */
360         @Nullable
findMatch( T dev, @Nullable List<? extends DeviceFilter<T>> filters)361         public static <T extends Parcelable> DeviceFilterPair<T> findMatch(
362                 T dev, @Nullable List<? extends DeviceFilter<T>> filters) {
363             if (isEmpty(filters)) return new DeviceFilterPair<>(dev, null);
364             final DeviceFilter<T> matchingFilter
365                     = CollectionUtils.find(filters, f -> f.matches(dev));
366 
367             DeviceFilterPair<T> result = matchingFilter != null
368                     ? new DeviceFilterPair<>(dev, matchingFilter)
369                     : null;
370             if (DEBUG) Log.i(LOG_TAG, "findMatch(dev = " + dev + ", filters = " + filters +
371                     ") -> " + result);
372             return result;
373         }
374 
getDisplayName()375         public String getDisplayName() {
376             if (filter == null) {
377                 Preconditions.checkNotNull(device);
378                 if (device instanceof BluetoothDevice) {
379                     return getDeviceDisplayNameInternal((BluetoothDevice) device);
380                 } else if (device instanceof android.net.wifi.ScanResult) {
381                     return getDeviceDisplayNameInternal((android.net.wifi.ScanResult) device);
382                 } else if (device instanceof ScanResult) {
383                     return getDeviceDisplayNameInternal(((ScanResult) device).getDevice());
384                 } else {
385                     throw new IllegalArgumentException("Unknown device type: " + device.getClass());
386                 }
387             }
388             return filter.getDeviceDisplayName(device);
389         }
390 
391         @Override
equals(Object o)392         public boolean equals(Object o) {
393             if (this == o) return true;
394             if (o == null || getClass() != o.getClass()) return false;
395             DeviceFilterPair<?> that = (DeviceFilterPair<?>) o;
396             return Objects.equals(getDeviceMacAddress(device), getDeviceMacAddress(that.device));
397         }
398 
399         @Override
hashCode()400         public int hashCode() {
401             return Objects.hash(getDeviceMacAddress(device));
402         }
403 
404         @Override
toString()405         public String toString() {
406             return "DeviceFilterPair{"
407                     + "device=" + device + " " + getDisplayName()
408                     + ", filter=" + filter
409                     + '}';
410         }
411     }
412 
413     private class BLEScanCallback extends ScanCallback {
414 
BLEScanCallback()415         public BLEScanCallback() {
416             if (DEBUG) Log.i(LOG_TAG, "new BLEScanCallback() -> " + this);
417         }
418 
419         @Override
onScanResult(int callbackType, ScanResult result)420         public void onScanResult(int callbackType, ScanResult result) {
421             if (DEBUG) {
422                 Log.i(LOG_TAG,
423                         "BLE.onScanResult(callbackType = " + callbackType + ", result = " + result
424                                 + ")");
425             }
426             final DeviceFilterPair<ScanResult> deviceFilterPair
427                     = DeviceFilterPair.findMatch(result, mBLEFilters);
428             if (deviceFilterPair == null) return;
429             if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) {
430                 onDeviceLost(deviceFilterPair);
431             } else {
432                 onDeviceFound(deviceFilterPair);
433             }
434         }
435     }
436 
437     private class BluetoothBroadcastReceiver extends BroadcastReceiver {
438         @Override
onReceive(Context context, Intent intent)439         public void onReceive(Context context, Intent intent) {
440             if (DEBUG) {
441                 Log.i(LOG_TAG,
442                         "BL.onReceive(context = " + context + ", intent = " + intent + ")");
443             }
444             final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
445             final DeviceFilterPair<BluetoothDevice> deviceFilterPair
446                     = DeviceFilterPair.findMatch(device, mBluetoothFilters);
447             if (deviceFilterPair == null) return;
448             if (intent.getAction().equals(BluetoothDevice.ACTION_FOUND)) {
449                 onDeviceFound(deviceFilterPair);
450             } else {
451                 onDeviceLost(deviceFilterPair);
452             }
453         }
454     }
455 
456     private class WifiBroadcastReceiver extends BroadcastReceiver {
457         @Override
onReceive(Context context, Intent intent)458         public void onReceive(Context context, Intent intent) {
459             if (intent.getAction().equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) {
460                 List<android.net.wifi.ScanResult> scanResults = mWifiManager.getScanResults();
461 
462                 if (DEBUG) {
463                     Log.i(LOG_TAG, "Wifi scan results: " + TextUtils.join("\n", scanResults));
464                 }
465 
466                 for (int i = 0; i < scanResults.size(); i++) {
467                     onDeviceFound(DeviceFilterPair.findMatch(scanResults.get(i), mWifiFilters));
468                 }
469             }
470         }
471     }
472 }
473