DeviceDiscoveryService.java revision da0acdf938f1d6e7a978e143d5d80d8dd5af52ad
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.getDeviceDisplayName;
20import static android.companion.BluetoothLEDeviceFilter.nullsafe;
21
22import android.annotation.NonNull;
23import android.annotation.Nullable;
24import android.app.PendingIntent;
25import android.app.Service;
26import android.bluetooth.BluetoothAdapter;
27import android.bluetooth.BluetoothDevice;
28import android.bluetooth.BluetoothManager;
29import android.bluetooth.le.BluetoothLeScanner;
30import android.bluetooth.le.ScanCallback;
31import android.bluetooth.le.ScanFilter;
32import android.bluetooth.le.ScanResult;
33import android.bluetooth.le.ScanSettings;
34import android.companion.AssociationRequest;
35import android.companion.BluetoothLEDeviceFilter;
36import android.companion.ICompanionDeviceDiscoveryService;
37import android.companion.ICompanionDeviceDiscoveryServiceCallback;
38import android.companion.IFindDeviceCallback;
39import android.content.BroadcastReceiver;
40import android.content.Context;
41import android.content.Intent;
42import android.content.IntentFilter;
43import android.graphics.Color;
44import android.graphics.drawable.Drawable;
45import android.os.IBinder;
46import android.os.RemoteException;
47import android.util.Log;
48import android.view.View;
49import android.view.ViewGroup;
50import android.widget.ArrayAdapter;
51import android.widget.TextView;
52
53import java.util.ArrayList;
54import java.util.Collections;
55import java.util.List;
56
57public class DeviceDiscoveryService extends Service {
58
59    private static final boolean DEBUG = false;
60    private static final String LOG_TAG = "DeviceDiscoveryService";
61
62    static DeviceDiscoveryService sInstance;
63
64    private BluetoothAdapter mBluetoothAdapter;
65    private BluetoothLEDeviceFilter mFilter;
66    private ScanFilter mScanFilter;
67    private ScanSettings mDefaultScanSettings = new ScanSettings.Builder().build();
68    AssociationRequest<?> mRequest;
69    List<BluetoothDevice> mDevicesFound;
70    BluetoothDevice mSelectedDevice;
71    DevicesAdapter mDevicesAdapter;
72    IFindDeviceCallback mFindCallback;
73    ICompanionDeviceDiscoveryServiceCallback mServiceCallback;
74    String mCallingPackage;
75
76    private final ICompanionDeviceDiscoveryService mBinder =
77            new ICompanionDeviceDiscoveryService.Stub() {
78        @Override
79        public void startDiscovery(AssociationRequest request,
80                String callingPackage,
81                IFindDeviceCallback findCallback,
82                ICompanionDeviceDiscoveryServiceCallback serviceCallback) {
83            if (DEBUG) {
84                Log.i(LOG_TAG,
85                        "startDiscovery() called with: filter = [" + request
86                                + "], findCallback = [" + findCallback + "]"
87                                + "], serviceCallback = [" + serviceCallback + "]");
88            }
89            mFindCallback = findCallback;
90            mServiceCallback = serviceCallback;
91            mCallingPackage = callingPackage;
92            DeviceDiscoveryService.this.startDiscovery(request);
93        }
94    };
95
96    private final ScanCallback mBLEScanCallback = new ScanCallback() {
97        @Override
98        public void onScanResult(int callbackType, ScanResult result) {
99            final BluetoothDevice device = result.getDevice();
100            if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) {
101                onDeviceLost(device);
102            } else {
103                onDeviceFound(device);
104            }
105        }
106    };
107
108    private BluetoothLeScanner mBLEScanner;
109
110    private BroadcastReceiver mBluetoothDeviceFoundBroadcastReceiver = new BroadcastReceiver() {
111        @Override
112        public void onReceive(Context context, Intent intent) {
113            final BluetoothDevice device = intent.getParcelableExtra(
114                    BluetoothDevice.EXTRA_DEVICE);
115            if (!mFilter.matches(device)) return; // ignore device
116
117            if (intent.getAction().equals(BluetoothDevice.ACTION_FOUND)) {
118                onDeviceFound(device);
119            } else {
120                onDeviceLost(device);
121            }
122        }
123    };
124
125    @Override
126    public IBinder onBind(Intent intent) {
127        if (DEBUG) Log.i(LOG_TAG, "onBind(" + intent + ")");
128        return mBinder.asBinder();
129    }
130
131    @Override
132    public void onCreate() {
133        super.onCreate();
134
135        if (DEBUG) Log.i(LOG_TAG, "onCreate()");
136
137        mBluetoothAdapter = getSystemService(BluetoothManager.class).getAdapter();
138        mBLEScanner = mBluetoothAdapter.getBluetoothLeScanner();
139
140        mDevicesFound = new ArrayList<>();
141        mDevicesAdapter = new DevicesAdapter();
142
143        sInstance = this;
144    }
145
146    private void startDiscovery(AssociationRequest<?> request) {
147        //TODO support other protocols as well
148        mRequest = request;
149        mFilter = nullsafe((BluetoothLEDeviceFilter) request.getDeviceFilter());
150        mScanFilter = mFilter.getScanFilter();
151
152        reset();
153
154        final IntentFilter intentFilter = new IntentFilter();
155        intentFilter.addAction(BluetoothDevice.ACTION_FOUND);
156        intentFilter.addAction(BluetoothDevice.ACTION_DISAPPEARED);
157
158        registerReceiver(mBluetoothDeviceFoundBroadcastReceiver, intentFilter);
159        mBluetoothAdapter.startDiscovery();
160
161        mBLEScanner.startScan(
162                Collections.singletonList(mScanFilter), mDefaultScanSettings, mBLEScanCallback);
163    }
164
165    private void reset() {
166        mDevicesFound.clear();
167        mSelectedDevice = null;
168        mDevicesAdapter.notifyDataSetChanged();
169    }
170
171    @Override
172    public boolean onUnbind(Intent intent) {
173        stopScan();
174        return super.onUnbind(intent);
175    }
176
177    public void onDeviceSelected() {
178        try {
179            mServiceCallback.onDeviceSelected(mCallingPackage, getUserId());
180        } catch (RemoteException e) {
181            Log.e(LOG_TAG, "Error reporting selected device");
182        }
183    }
184
185    private void stopScan() {
186        if (DEBUG) Log.i(LOG_TAG, "stopScan() called");
187        mBluetoothAdapter.cancelDiscovery();
188        mBLEScanner.stopScan(mBLEScanCallback);
189        unregisterReceiver(mBluetoothDeviceFoundBroadcastReceiver);
190        stopSelf();
191    }
192
193    private void onDeviceFound(BluetoothDevice device) {
194        if (mDevicesFound.contains(device)) {
195            return;
196        }
197
198        if (DEBUG) {
199            Log.i(LOG_TAG, "Considering device " + getDeviceDisplayName(device));
200        }
201
202        if (!mFilter.matches(device)) {
203            return;
204        }
205
206        if (DEBUG) {
207            Log.i(LOG_TAG, "Found device " + getDeviceDisplayName(device));
208        }
209        if (mDevicesFound.isEmpty()) {
210            onReadyToShowUI();
211        }
212        mDevicesFound.add(device);
213        mDevicesAdapter.notifyDataSetChanged();
214    }
215
216    //TODO also, on timeout -> call onFailure
217    private void onReadyToShowUI() {
218        try {
219            mFindCallback.onSuccess(PendingIntent.getActivity(
220                    this, 0,
221                    new Intent(this, DeviceChooserActivity.class),
222                    PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT
223                            | PendingIntent.FLAG_IMMUTABLE));
224        } catch (RemoteException e) {
225            throw new RuntimeException(e);
226        }
227    }
228
229    private void onDeviceLost(BluetoothDevice device) {
230        mDevicesFound.remove(device);
231        mDevicesAdapter.notifyDataSetChanged();
232        if (DEBUG) {
233            Log.i(LOG_TAG, "Lost device " + getDeviceDisplayName(device));
234        }
235    }
236
237    class DevicesAdapter extends ArrayAdapter<BluetoothDevice> {
238        private Drawable BLUETOOTH_ICON = icon(android.R.drawable.stat_sys_data_bluetooth);
239
240        private Drawable icon(int drawableRes) {
241            Drawable icon = getResources().getDrawable(drawableRes, null);
242            icon.setTint(Color.DKGRAY);
243            return icon;
244        }
245
246        public DevicesAdapter() {
247            super(DeviceDiscoveryService.this, 0, mDevicesFound);
248        }
249
250        @Override
251        public View getView(
252                int position,
253                @Nullable View convertView,
254                @NonNull ViewGroup parent) {
255            TextView view = convertView instanceof TextView
256                    ? (TextView) convertView
257                    : newView();
258            bind(view, getItem(position));
259            return view;
260        }
261
262        private void bind(TextView textView, BluetoothDevice device) {
263            textView.setText(getDeviceDisplayName(device));
264            textView.setBackgroundColor(
265                    device.equals(mSelectedDevice)
266                            ? Color.GRAY
267                            : Color.TRANSPARENT);
268            textView.setOnClickListener((view) -> {
269                mSelectedDevice = device;
270                notifyDataSetChanged();
271            });
272        }
273
274        //TODO move to a layout file
275        private TextView newView() {
276            final TextView textView = new TextView(DeviceDiscoveryService.this);
277            textView.setTextColor(Color.BLACK);
278            final int padding = DeviceChooserActivity.getPadding(getResources());
279            textView.setPadding(padding, padding, padding, padding);
280            textView.setCompoundDrawablesWithIntrinsicBounds(
281                    BLUETOOTH_ICON, null, null, null);
282            textView.setCompoundDrawablePadding(padding);
283            return textView;
284        }
285    }
286}
287