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