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