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