DeviceDiscoveryService.java revision e70e6aa62c6f3a9a79624a4f9d97df95edda0364
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;
23
24import android.annotation.NonNull;
25import android.annotation.Nullable;
26import android.app.PendingIntent;
27import android.app.Service;
28import android.bluetooth.BluetoothAdapter;
29import android.bluetooth.BluetoothDevice;
30import android.bluetooth.BluetoothManager;
31import android.bluetooth.le.BluetoothLeScanner;
32import android.bluetooth.le.ScanCallback;
33import android.bluetooth.le.ScanFilter;
34import android.bluetooth.le.ScanResult;
35import android.bluetooth.le.ScanSettings;
36import android.companion.AssociationRequest;
37import android.companion.BluetoothDeviceFilter;
38import android.companion.BluetoothDeviceFilterUtils;
39import android.companion.BluetoothLEDeviceFilter;
40import android.companion.CompanionDeviceManager;
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.Preconditions;
65
66import java.util.ArrayList;
67import java.util.List;
68import java.util.Objects;
69
70public class DeviceDiscoveryService extends Service {
71
72    private static final boolean DEBUG = false;
73    private static final String LOG_TAG = "DeviceDiscoveryService";
74
75    static DeviceDiscoveryService sInstance;
76
77    private BluetoothAdapter mBluetoothAdapter;
78    private WifiManager mWifiManager;
79    private ScanSettings mDefaultScanSettings = new ScanSettings.Builder().build();
80    private List<DeviceFilter<?>> mFilters;
81    private List<BluetoothLEDeviceFilter> mBLEFilters;
82    private List<BluetoothDeviceFilter> mBluetoothFilters;
83    private List<WifiDeviceFilter> mWifiFilters;
84    private List<ScanFilter> mBLEScanFilters;
85    AssociationRequest mRequest;
86    List<DeviceFilterPair> mDevicesFound;
87    DeviceFilterPair mSelectedDevice;
88    DevicesAdapter mDevicesAdapter;
89    IFindDeviceCallback mFindCallback;
90    ICompanionDeviceDiscoveryServiceCallback mServiceCallback;
91
92    private final ICompanionDeviceDiscoveryService mBinder =
93            new ICompanionDeviceDiscoveryService.Stub() {
94        @Override
95        public void startDiscovery(AssociationRequest request,
96                String callingPackage,
97                IFindDeviceCallback findCallback,
98                ICompanionDeviceDiscoveryServiceCallback serviceCallback) {
99            if (DEBUG) {
100                Log.i(LOG_TAG,
101                        "startDiscovery() called with: filter = [" + request
102                                + "], findCallback = [" + findCallback + "]"
103                                + "], serviceCallback = [" + serviceCallback + "]");
104            }
105            mFindCallback = findCallback;
106            mServiceCallback = serviceCallback;
107            DeviceDiscoveryService.this.startDiscovery(request);
108        }
109    };
110
111    private final ScanCallback mBLEScanCallback = new ScanCallback() {
112        @Override
113        public void onScanResult(int callbackType, ScanResult result) {
114            final DeviceFilterPair<ScanResult> deviceFilterPair
115                    = DeviceFilterPair.findMatch(result, mBLEFilters);
116            if (deviceFilterPair == null) return;
117            if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) {
118                onDeviceLost(deviceFilterPair);
119            } else {
120                onDeviceFound(deviceFilterPair);
121            }
122        }
123    };
124
125    private BluetoothLeScanner mBLEScanner;
126
127    private BroadcastReceiver mBluetoothDeviceFoundBroadcastReceiver = new BroadcastReceiver() {
128        @Override
129        public void onReceive(Context context, Intent intent) {
130            final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
131            final DeviceFilterPair<BluetoothDevice> deviceFilterPair
132                    = DeviceFilterPair.findMatch(device, mBluetoothFilters);
133            if (deviceFilterPair == null) return;
134            if (intent.getAction().equals(BluetoothDevice.ACTION_FOUND)) {
135                onDeviceFound(deviceFilterPair);
136            } else {
137                onDeviceLost(deviceFilterPair);
138            }
139        }
140    };
141
142    private BroadcastReceiver mWifiDeviceFoundBroadcastReceiver = new BroadcastReceiver() {
143        @Override
144        public void onReceive(Context context, Intent intent) {
145            if (intent.getAction().equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) {
146                List<android.net.wifi.ScanResult> scanResults = mWifiManager.getScanResults();
147
148                if (DEBUG) {
149                    Log.i(LOG_TAG, "Wifi scan results: " + TextUtils.join("\n", scanResults));
150                }
151
152                for (int i = 0; i < scanResults.size(); i++) {
153                    DeviceFilterPair<android.net.wifi.ScanResult> deviceFilterPair =
154                            DeviceFilterPair.findMatch(scanResults.get(i), mWifiFilters);
155                    if (deviceFilterPair != null) onDeviceFound(deviceFilterPair);
156                }
157            }
158
159        }
160    };
161
162    @Override
163    public IBinder onBind(Intent intent) {
164        if (DEBUG) Log.i(LOG_TAG, "onBind(" + intent + ")");
165        return mBinder.asBinder();
166    }
167
168    @Override
169    public void onCreate() {
170        super.onCreate();
171
172        if (DEBUG) Log.i(LOG_TAG, "onCreate()");
173
174        mBluetoothAdapter = getSystemService(BluetoothManager.class).getAdapter();
175        mBLEScanner = mBluetoothAdapter.getBluetoothLeScanner();
176        mWifiManager = getSystemService(WifiManager.class);
177
178        mDevicesFound = new ArrayList<>();
179        mDevicesAdapter = new DevicesAdapter();
180
181        sInstance = this;
182    }
183
184    private void startDiscovery(AssociationRequest request) {
185        mRequest = request;
186
187        mFilters = request.getDeviceFilters();
188        mWifiFilters = ArrayUtils.filter(mFilters, WifiDeviceFilter.class);
189        mBluetoothFilters = ArrayUtils.filter(mFilters, BluetoothDeviceFilter.class);
190        mBLEFilters = ArrayUtils.filter(mFilters, BluetoothLEDeviceFilter.class);
191        mBLEScanFilters = ArrayUtils.map(mBLEFilters, BluetoothLEDeviceFilter::getScanFilter);
192
193        reset();
194
195        if (shouldScan(mBluetoothFilters)) {
196            final IntentFilter intentFilter = new IntentFilter();
197            intentFilter.addAction(BluetoothDevice.ACTION_FOUND);
198            intentFilter.addAction(BluetoothDevice.ACTION_DISAPPEARED);
199
200            registerReceiver(mBluetoothDeviceFoundBroadcastReceiver, intentFilter);
201            mBluetoothAdapter.startDiscovery();
202        }
203
204        if (shouldScan(mBLEFilters)) {
205            mBLEScanner.startScan(mBLEScanFilters, mDefaultScanSettings, mBLEScanCallback);
206        }
207
208        if (shouldScan(mWifiFilters)) {
209            registerReceiver(mWifiDeviceFoundBroadcastReceiver,
210                    new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION));
211            mWifiManager.startScan();
212        }
213    }
214
215    private boolean shouldScan(List<? extends DeviceFilter> mediumSpecificFilters) {
216        return !isEmpty(mediumSpecificFilters) || isEmpty(mFilters);
217    }
218
219    private void reset() {
220        mDevicesFound.clear();
221        mSelectedDevice = null;
222        mDevicesAdapter.notifyDataSetChanged();
223    }
224
225    @Override
226    public boolean onUnbind(Intent intent) {
227        stopScan();
228        return super.onUnbind(intent);
229    }
230
231    private void stopScan() {
232        if (DEBUG) Log.i(LOG_TAG, "stopScan() called");
233        mBluetoothAdapter.cancelDiscovery();
234        mBLEScanner.stopScan(mBLEScanCallback);
235        unregisterReceiver(mBluetoothDeviceFoundBroadcastReceiver);
236        unregisterReceiver(mWifiDeviceFoundBroadcastReceiver);
237        stopSelf();
238    }
239
240    private void onDeviceFound(@Nullable DeviceFilterPair device) {
241        if (mDevicesFound.contains(device)) {
242            return;
243        }
244
245        if (DEBUG) Log.i(LOG_TAG, "Found device " + device.getDisplayName() + " "
246                + getDeviceMacAddress(device.device));
247
248        if (mDevicesFound.isEmpty()) {
249            onReadyToShowUI();
250        }
251        mDevicesFound.add(device);
252        mDevicesAdapter.notifyDataSetChanged();
253    }
254
255    //TODO also, on timeout -> call onFailure
256    private void onReadyToShowUI() {
257        try {
258            mFindCallback.onSuccess(PendingIntent.getActivity(
259                    this, 0,
260                    new Intent(this, DeviceChooserActivity.class),
261                    PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT
262                            | PendingIntent.FLAG_IMMUTABLE));
263        } catch (RemoteException e) {
264            throw new RuntimeException(e);
265        }
266    }
267
268    private void onDeviceLost(@Nullable DeviceFilterPair device) {
269        mDevicesFound.remove(device);
270        mDevicesAdapter.notifyDataSetChanged();
271        if (DEBUG) Log.i(LOG_TAG, "Lost device " + device.getDisplayName());
272    }
273
274    void onDeviceSelected(String callingPackage, String deviceAddress) {
275        try {
276            mServiceCallback.onDeviceSelected(
277                    //TODO is this the right userId?
278                    callingPackage, getUserId(), deviceAddress);
279        } catch (RemoteException e) {
280            Log.e(LOG_TAG, "Failed to record association: "
281                    + callingPackage + " <-> " + deviceAddress);
282        }
283    }
284
285    class DevicesAdapter extends ArrayAdapter<DeviceFilterPair> {
286        //TODO wifi icon
287        private Drawable BLUETOOTH_ICON = icon(android.R.drawable.stat_sys_data_bluetooth);
288
289        private Drawable icon(int drawableRes) {
290            Drawable icon = getResources().getDrawable(drawableRes, null);
291            icon.setTint(Color.DKGRAY);
292            return icon;
293        }
294
295        public DevicesAdapter() {
296            super(DeviceDiscoveryService.this, 0, mDevicesFound);
297        }
298
299        @Override
300        public View getView(
301                int position,
302                @Nullable View convertView,
303                @NonNull ViewGroup parent) {
304            TextView view = convertView instanceof TextView
305                    ? (TextView) convertView
306                    : newView();
307            bind(view, getItem(position));
308            return view;
309        }
310
311        private void bind(TextView textView, DeviceFilterPair device) {
312            textView.setText(device.getDisplayName());
313            textView.setBackgroundColor(
314                    device.equals(mSelectedDevice)
315                            ? Color.GRAY
316                            : Color.TRANSPARENT);
317            textView.setOnClickListener((view) -> {
318                mSelectedDevice = device;
319                notifyDataSetChanged();
320            });
321        }
322
323        //TODO move to a layout file
324        private TextView newView() {
325            final TextView textView = new TextView(DeviceDiscoveryService.this);
326            textView.setTextColor(Color.BLACK);
327            final int padding = DeviceChooserActivity.getPadding(getResources());
328            textView.setPadding(padding, padding, padding, padding);
329            textView.setCompoundDrawablesWithIntrinsicBounds(
330                    BLUETOOTH_ICON, null, null, null);
331            textView.setCompoundDrawablePadding(padding);
332            return textView;
333        }
334    }
335
336    /**
337     * A pair of device and a filter that matched this device if any.
338     *
339     * @param <T> device type
340     */
341    static class DeviceFilterPair<T extends Parcelable> {
342        public final T device;
343        @Nullable
344        public final DeviceFilter<T> filter;
345
346        private DeviceFilterPair(T device, @Nullable DeviceFilter<T> filter) {
347            this.device = device;
348            this.filter = filter;
349        }
350
351        /**
352         * {@code (device, null)} if the filters list is empty or null
353         * {@code null} if none of the provided filters match the device
354         * {@code (device, filter)} where filter is among the list of filters and matches the device
355         */
356        @Nullable
357        public static <T extends Parcelable> DeviceFilterPair<T> findMatch(
358                T dev, @Nullable List<? extends DeviceFilter<T>> filters) {
359            if (isEmpty(filters)) return new DeviceFilterPair<>(dev, null);
360            final DeviceFilter<T> matchingFilter = ArrayUtils.find(filters, (f) -> f.matches(dev));
361            return matchingFilter != null ? new DeviceFilterPair<>(dev, matchingFilter) : null;
362        }
363
364        public String getDisplayName() {
365            if (filter == null) {
366                Preconditions.checkNotNull(device);
367                if (device instanceof BluetoothDevice) {
368                    return getDeviceDisplayNameInternal((BluetoothDevice) device);
369                } else if (device instanceof android.net.wifi.ScanResult) {
370                    return getDeviceDisplayNameInternal((android.net.wifi.ScanResult) device);
371                } else if (device instanceof ScanResult) {
372                    return getDeviceDisplayNameInternal(((ScanResult) device).getDevice());
373                } else {
374                    throw new IllegalArgumentException("Unknown device type: " + device.getClass());
375                }
376            }
377            return filter.getDeviceDisplayName(device);
378        }
379
380        @Override
381        public boolean equals(Object o) {
382            if (this == o) return true;
383            if (o == null || getClass() != o.getClass()) return false;
384            DeviceFilterPair<?> that = (DeviceFilterPair<?>) o;
385            return Objects.equals(getDeviceMacAddress(device), getDeviceMacAddress(that.device));
386        }
387
388        @Override
389        public int hashCode() {
390            return Objects.hash(getDeviceMacAddress(device));
391        }
392    }
393}
394