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
17package com.android.companiondevicemanager;
18
19import static android.companion.BluetoothDeviceFilterUtils.getDeviceDisplayNameInternal;
20import static android.companion.BluetoothDeviceFilterUtils.getDeviceMacAddress;
21
22import static com.android.internal.util.ArrayUtils.isEmpty;
23import static com.android.internal.util.CollectionUtils.emptyIfNull;
24import static com.android.internal.util.CollectionUtils.size;
25
26import android.annotation.NonNull;
27import android.annotation.Nullable;
28import android.app.PendingIntent;
29import android.app.Service;
30import android.bluetooth.BluetoothAdapter;
31import android.bluetooth.BluetoothDevice;
32import android.bluetooth.BluetoothManager;
33import android.bluetooth.le.BluetoothLeScanner;
34import android.bluetooth.le.ScanCallback;
35import android.bluetooth.le.ScanFilter;
36import android.bluetooth.le.ScanResult;
37import android.bluetooth.le.ScanSettings;
38import android.companion.AssociationRequest;
39import android.companion.BluetoothDeviceFilter;
40import android.companion.BluetoothLeDeviceFilter;
41import android.companion.DeviceFilter;
42import android.companion.ICompanionDeviceDiscoveryService;
43import android.companion.ICompanionDeviceDiscoveryServiceCallback;
44import android.companion.IFindDeviceCallback;
45import android.companion.WifiDeviceFilter;
46import android.content.BroadcastReceiver;
47import android.content.Context;
48import android.content.Intent;
49import android.content.IntentFilter;
50import android.graphics.Color;
51import android.graphics.drawable.Drawable;
52import android.net.wifi.WifiManager;
53import android.os.IBinder;
54import android.os.Parcelable;
55import android.os.RemoteException;
56import android.text.TextUtils;
57import android.util.Log;
58import android.view.View;
59import android.view.ViewGroup;
60import android.widget.ArrayAdapter;
61import android.widget.TextView;
62
63import com.android.internal.util.ArrayUtils;
64import com.android.internal.util.CollectionUtils;
65import com.android.internal.util.Preconditions;
66
67import java.util.ArrayList;
68import java.util.List;
69import java.util.Objects;
70
71public 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
121    public IBinder onBind(Intent intent) {
122        if (DEBUG) Log.i(LOG_TAG, "onBind(" + intent + ")");
123        return mBinder.asBinder();
124    }
125
126    @Override
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
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
201    private boolean shouldScan(List<? extends DeviceFilter> mediumSpecificFilters) {
202        return !isEmpty(mediumSpecificFilters) || isEmpty(mFilters);
203    }
204
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
214    public boolean onUnbind(Intent intent) {
215        stopScan();
216        return super.onUnbind(intent);
217    }
218
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
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
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
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
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
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
293        private Drawable icon(int drawableRes) {
294            Drawable icon = getResources().getDrawable(drawableRes, null);
295            icon.setTint(Color.DKGRAY);
296            return icon;
297        }
298
299        public DevicesAdapter() {
300            super(DeviceDiscoveryService.this, 0, mDevicesFound);
301        }
302
303        @Override
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
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
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
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
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
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
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
403        public int hashCode() {
404            return Objects.hash(getDeviceMacAddress(device));
405        }
406
407        @Override
408        public String toString() {
409            return "DeviceFilterPair{" +
410                    "device=" + device +
411                    ", filter=" + filter +
412                    '}';
413        }
414    }
415
416    private class BLEScanCallback extends ScanCallback {
417
418        public BLEScanCallback() {
419            if (DEBUG) Log.i(LOG_TAG, "new BLEScanCallback() -> " + this);
420        }
421
422        @Override
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
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
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