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 
188             mBluetoothBroadcastReceiver = new BluetoothBroadcastReceiver();
189             registerReceiver(mBluetoothBroadcastReceiver, intentFilter);
190             mBluetoothAdapter.startDiscovery();
191         }
192 
193         if (shouldScan(mBLEFilters) && mBLEScanner != null) {
194             mBLEScanCallback = new BLEScanCallback();
195             mBLEScanner.startScan(mBLEScanFilters, mDefaultScanSettings, mBLEScanCallback);
196         }
197 
198         if (shouldScan(mWifiFilters)) {
199             mWifiBroadcastReceiver = new WifiBroadcastReceiver();
200             registerReceiver(mWifiBroadcastReceiver,
201                     new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION));
202             mWifiManager.startScan();
203         }
204         mIsScanning = true;
205         Handler.getMain().sendMessageDelayed(
206                 obtainMessage(DeviceDiscoveryService::stopScan, this),
207                 SCAN_TIMEOUT);
208     }
209 
shouldScan(List<? extends DeviceFilter> mediumSpecificFilters)210     private boolean shouldScan(List<? extends DeviceFilter> mediumSpecificFilters) {
211         return !isEmpty(mediumSpecificFilters) || isEmpty(mFilters);
212     }
213 
reset()214     private void reset() {
215         if (DEBUG) Log.i(LOG_TAG, "reset()");
216         stopScan();
217         mDevicesFound.clear();
218         mSelectedDevice = null;
219         notifyDataSetChanged();
220     }
221 
222     @Override
onUnbind(Intent intent)223     public boolean onUnbind(Intent intent) {
224         stopScan();
225         return super.onUnbind(intent);
226     }
227 
stopScan()228     private void stopScan() {
229         if (DEBUG) Log.i(LOG_TAG, "stopScan()");
230 
231         if (!mIsScanning) return;
232         mIsScanning = false;
233 
234         DeviceChooserActivity activity = mActivity;
235         if (activity != null) {
236             if (activity.mDeviceListView != null) {
237                 activity.mDeviceListView.removeFooterView(activity.mLoadingIndicator);
238             }
239             mActivity = null;
240         }
241 
242         mBluetoothAdapter.cancelDiscovery();
243         if (mBluetoothBroadcastReceiver != null) {
244             unregisterReceiver(mBluetoothBroadcastReceiver);
245             mBluetoothBroadcastReceiver = null;
246         }
247         if (mBLEScanner != null) mBLEScanner.stopScan(mBLEScanCallback);
248         if (mWifiBroadcastReceiver != null) {
249             unregisterReceiver(mWifiBroadcastReceiver);
250             mWifiBroadcastReceiver = null;
251         }
252     }
253 
onDeviceFound(@ullable DeviceFilterPair device)254     private void onDeviceFound(@Nullable DeviceFilterPair device) {
255         if (device == null) return;
256 
257         if (mDevicesFound.contains(device)) {
258             return;
259         }
260 
261         if (DEBUG) Log.i(LOG_TAG, "Found device " + device);
262 
263         if (mDevicesFound.isEmpty()) {
264             onReadyToShowUI();
265         }
266         mDevicesFound.add(device);
267         notifyDataSetChanged();
268     }
269 
notifyDataSetChanged()270     private void notifyDataSetChanged() {
271         Handler.getMain().sendMessage(obtainMessage(
272                 DevicesAdapter::notifyDataSetChanged, mDevicesAdapter));
273     }
274 
275     //TODO also, on timeout -> call onFailure
onReadyToShowUI()276     private void onReadyToShowUI() {
277         try {
278             mFindCallback.onSuccess(PendingIntent.getActivity(
279                     this, 0,
280                     new Intent(this, DeviceChooserActivity.class),
281                     PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT
282                             | PendingIntent.FLAG_IMMUTABLE));
283         } catch (RemoteException e) {
284             throw new RuntimeException(e);
285         }
286     }
287 
onDeviceLost(@ullable DeviceFilterPair device)288     private void onDeviceLost(@Nullable DeviceFilterPair device) {
289         mDevicesFound.remove(device);
290         notifyDataSetChanged();
291         if (DEBUG) Log.i(LOG_TAG, "Lost device " + device.getDisplayName());
292     }
293 
onDeviceSelected(String callingPackage, String deviceAddress)294     void onDeviceSelected(String callingPackage, String deviceAddress) {
295         try {
296             mServiceCallback.onDeviceSelected(
297                     //TODO is this the right userId?
298                     callingPackage, getUserId(), deviceAddress);
299         } catch (RemoteException e) {
300             Log.e(LOG_TAG, "Failed to record association: "
301                     + callingPackage + " <-> " + deviceAddress);
302         }
303     }
304 
onCancel()305     void onCancel() {
306         if (DEBUG) Log.i(LOG_TAG, "onCancel()");
307         try {
308             mServiceCallback.onDeviceSelectionCancel();
309         } catch (RemoteException e) {
310             throw new RuntimeException(e);
311         }
312     }
313 
314     class DevicesAdapter extends ArrayAdapter<DeviceFilterPair> {
315         private Drawable BLUETOOTH_ICON = icon(android.R.drawable.stat_sys_data_bluetooth);
316         private Drawable WIFI_ICON = icon(com.android.internal.R.drawable.ic_wifi_signal_3);
317 
icon(int drawableRes)318         private Drawable icon(int drawableRes) {
319             Drawable icon = getResources().getDrawable(drawableRes, null);
320             icon.setTint(Color.DKGRAY);
321             return icon;
322         }
323 
DevicesAdapter()324         public DevicesAdapter() {
325             super(DeviceDiscoveryService.this, 0, mDevicesFound);
326         }
327 
328         @Override
getView( int position, @Nullable View convertView, @NonNull ViewGroup parent)329         public View getView(
330                 int position,
331                 @Nullable View convertView,
332                 @NonNull ViewGroup parent) {
333             TextView view = convertView instanceof TextView
334                     ? (TextView) convertView
335                     : newView();
336             bind(view, getItem(position));
337             return view;
338         }
339 
bind(TextView textView, DeviceFilterPair device)340         private void bind(TextView textView, DeviceFilterPair device) {
341             textView.setText(device.getDisplayName());
342             textView.setBackgroundColor(
343                     device.equals(mSelectedDevice)
344                             ? Color.GRAY
345                             : Color.TRANSPARENT);
346             textView.setCompoundDrawablesWithIntrinsicBounds(
347                     device.device instanceof android.net.wifi.ScanResult
348                         ? WIFI_ICON
349                         : BLUETOOTH_ICON,
350                     null, null, null);
351             textView.setOnClickListener((view) -> {
352                 mSelectedDevice = device;
353                 notifyDataSetChanged();
354             });
355         }
356 
357         //TODO move to a layout file
newView()358         private TextView newView() {
359             final TextView textView = new TextView(DeviceDiscoveryService.this);
360             textView.setTextColor(Color.BLACK);
361             final int padding = DeviceChooserActivity.getPadding(getResources());
362             textView.setPadding(padding, padding, padding, padding);
363             textView.setCompoundDrawablePadding(padding);
364             return textView;
365         }
366     }
367 
368     /**
369      * A pair of device and a filter that matched this device if any.
370      *
371      * @param <T> device type
372      */
373     static class DeviceFilterPair<T extends Parcelable> {
374         public final T device;
375         @Nullable
376         public final DeviceFilter<T> filter;
377 
DeviceFilterPair(T device, @Nullable DeviceFilter<T> filter)378         private DeviceFilterPair(T device, @Nullable DeviceFilter<T> filter) {
379             this.device = device;
380             this.filter = filter;
381         }
382 
383         /**
384          * {@code (device, null)} if the filters list is empty or null
385          * {@code null} if none of the provided filters match the device
386          * {@code (device, filter)} where filter is among the list of filters and matches the device
387          */
388         @Nullable
findMatch( T dev, @Nullable List<? extends DeviceFilter<T>> filters)389         public static <T extends Parcelable> DeviceFilterPair<T> findMatch(
390                 T dev, @Nullable List<? extends DeviceFilter<T>> filters) {
391             if (isEmpty(filters)) return new DeviceFilterPair<>(dev, null);
392             final DeviceFilter<T> matchingFilter
393                     = CollectionUtils.find(filters, f -> f.matches(dev));
394 
395             DeviceFilterPair<T> result = matchingFilter != null
396                     ? new DeviceFilterPair<>(dev, matchingFilter)
397                     : null;
398             if (DEBUG) Log.i(LOG_TAG, "findMatch(dev = " + dev + ", filters = " + filters +
399                     ") -> " + result);
400             return result;
401         }
402 
getDisplayName()403         public String getDisplayName() {
404             if (filter == null) {
405                 Preconditions.checkNotNull(device);
406                 if (device instanceof BluetoothDevice) {
407                     return getDeviceDisplayNameInternal((BluetoothDevice) device);
408                 } else if (device instanceof android.net.wifi.ScanResult) {
409                     return getDeviceDisplayNameInternal((android.net.wifi.ScanResult) device);
410                 } else if (device instanceof ScanResult) {
411                     return getDeviceDisplayNameInternal(((ScanResult) device).getDevice());
412                 } else {
413                     throw new IllegalArgumentException("Unknown device type: " + device.getClass());
414                 }
415             }
416             return filter.getDeviceDisplayName(device);
417         }
418 
419         @Override
equals(Object o)420         public boolean equals(Object o) {
421             if (this == o) return true;
422             if (o == null || getClass() != o.getClass()) return false;
423             DeviceFilterPair<?> that = (DeviceFilterPair<?>) o;
424             return Objects.equals(getDeviceMacAddress(device), getDeviceMacAddress(that.device));
425         }
426 
427         @Override
hashCode()428         public int hashCode() {
429             return Objects.hash(getDeviceMacAddress(device));
430         }
431 
432         @Override
toString()433         public String toString() {
434             return "DeviceFilterPair{" +
435                     "device=" + device +
436                     ", filter=" + filter +
437                     '}';
438         }
439     }
440 
441     private class BLEScanCallback extends ScanCallback {
442 
BLEScanCallback()443         public BLEScanCallback() {
444             if (DEBUG) Log.i(LOG_TAG, "new BLEScanCallback() -> " + this);
445         }
446 
447         @Override
onScanResult(int callbackType, ScanResult result)448         public void onScanResult(int callbackType, ScanResult result) {
449             if (DEBUG) {
450                 Log.i(LOG_TAG,
451                         "BLE.onScanResult(callbackType = " + callbackType + ", result = " + result
452                                 + ")");
453             }
454             final DeviceFilterPair<ScanResult> deviceFilterPair
455                     = DeviceFilterPair.findMatch(result, mBLEFilters);
456             if (deviceFilterPair == null) return;
457             if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) {
458                 onDeviceLost(deviceFilterPair);
459             } else {
460                 onDeviceFound(deviceFilterPair);
461             }
462         }
463     }
464 
465     private class BluetoothBroadcastReceiver extends BroadcastReceiver {
466         @Override
onReceive(Context context, Intent intent)467         public void onReceive(Context context, Intent intent) {
468             if (DEBUG) {
469                 Log.i(LOG_TAG,
470                         "BL.onReceive(context = " + context + ", intent = " + intent + ")");
471             }
472             final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
473             final DeviceFilterPair<BluetoothDevice> deviceFilterPair
474                     = DeviceFilterPair.findMatch(device, mBluetoothFilters);
475             if (deviceFilterPair == null) return;
476             if (intent.getAction().equals(BluetoothDevice.ACTION_FOUND)) {
477                 onDeviceFound(deviceFilterPair);
478             } else {
479                 onDeviceLost(deviceFilterPair);
480             }
481         }
482     }
483 
484     private class WifiBroadcastReceiver extends BroadcastReceiver {
485         @Override
onReceive(Context context, Intent intent)486         public void onReceive(Context context, Intent intent) {
487             if (intent.getAction().equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) {
488                 List<android.net.wifi.ScanResult> scanResults = mWifiManager.getScanResults();
489 
490                 if (DEBUG) {
491                     Log.i(LOG_TAG, "Wifi scan results: " + TextUtils.join("\n", scanResults));
492                 }
493 
494                 for (int i = 0; i < scanResults.size(); i++) {
495                     onDeviceFound(DeviceFilterPair.findMatch(scanResults.get(i), mWifiFilters));
496                 }
497             }
498         }
499     }
500 }
501