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