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 17 package com.android.companiondevicemanager; 18 19 import static android.companion.BluetoothDeviceFilterUtils.getDeviceDisplayNameInternal; 20 import static android.companion.BluetoothDeviceFilterUtils.getDeviceMacAddress; 21 22 import static com.android.internal.util.ArrayUtils.isEmpty; 23 import static com.android.internal.util.CollectionUtils.emptyIfNull; 24 import static com.android.internal.util.CollectionUtils.size; 25 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; 26 27 import android.annotation.NonNull; 28 import android.annotation.Nullable; 29 import android.app.PendingIntent; 30 import android.app.Service; 31 import android.bluetooth.BluetoothAdapter; 32 import android.bluetooth.BluetoothDevice; 33 import android.bluetooth.BluetoothManager; 34 import android.bluetooth.le.BluetoothLeScanner; 35 import android.bluetooth.le.ScanCallback; 36 import android.bluetooth.le.ScanFilter; 37 import android.bluetooth.le.ScanResult; 38 import android.bluetooth.le.ScanSettings; 39 import android.companion.AssociationRequest; 40 import android.companion.BluetoothDeviceFilter; 41 import android.companion.BluetoothLeDeviceFilter; 42 import android.companion.DeviceFilter; 43 import android.companion.ICompanionDeviceDiscoveryService; 44 import android.companion.ICompanionDeviceDiscoveryServiceCallback; 45 import android.companion.IFindDeviceCallback; 46 import android.companion.WifiDeviceFilter; 47 import android.content.BroadcastReceiver; 48 import android.content.Context; 49 import android.content.Intent; 50 import android.content.IntentFilter; 51 import android.graphics.Color; 52 import android.graphics.drawable.Drawable; 53 import android.net.wifi.WifiManager; 54 import android.os.Handler; 55 import android.os.IBinder; 56 import android.os.Parcelable; 57 import android.os.RemoteException; 58 import android.text.TextUtils; 59 import android.util.Log; 60 import android.view.View; 61 import android.view.ViewGroup; 62 import android.widget.ArrayAdapter; 63 import android.widget.TextView; 64 65 import com.android.internal.util.ArrayUtils; 66 import com.android.internal.util.CollectionUtils; 67 import com.android.internal.util.Preconditions; 68 69 import java.util.ArrayList; 70 import java.util.List; 71 import java.util.Objects; 72 73 public class DeviceDiscoveryService extends Service { 74 75 private static final boolean DEBUG = false; 76 private static final String LOG_TAG = "DeviceDiscoveryService"; 77 78 private static final long SCAN_TIMEOUT = 20000; 79 80 static DeviceDiscoveryService sInstance; 81 82 private BluetoothAdapter mBluetoothAdapter; 83 private WifiManager mWifiManager; 84 @Nullable private BluetoothLeScanner mBLEScanner; 85 private ScanSettings mDefaultScanSettings = new ScanSettings.Builder().build(); 86 87 private List<DeviceFilter<?>> mFilters; 88 private List<BluetoothLeDeviceFilter> mBLEFilters; 89 private List<BluetoothDeviceFilter> mBluetoothFilters; 90 private List<WifiDeviceFilter> mWifiFilters; 91 private List<ScanFilter> mBLEScanFilters; 92 93 AssociationRequest mRequest; 94 List<DeviceFilterPair> mDevicesFound; 95 DeviceFilterPair mSelectedDevice; 96 DevicesAdapter mDevicesAdapter; 97 IFindDeviceCallback mFindCallback; 98 99 ICompanionDeviceDiscoveryServiceCallback mServiceCallback; 100 boolean mIsScanning = false; 101 @Nullable DeviceChooserActivity mActivity = null; 102 103 private final ICompanionDeviceDiscoveryService mBinder = 104 new ICompanionDeviceDiscoveryService.Stub() { 105 @Override 106 public void startDiscovery(AssociationRequest request, 107 String callingPackage, 108 IFindDeviceCallback findCallback, 109 ICompanionDeviceDiscoveryServiceCallback serviceCallback) { 110 if (DEBUG) { 111 Log.i(LOG_TAG, 112 "startDiscovery() called with: filter = [" + request 113 + "], findCallback = [" + findCallback + "]" 114 + "], serviceCallback = [" + serviceCallback + "]"); 115 } 116 mFindCallback = findCallback; 117 mServiceCallback = serviceCallback; 118 DeviceDiscoveryService.this.startDiscovery(request); 119 } 120 }; 121 122 private ScanCallback mBLEScanCallback; 123 private BluetoothBroadcastReceiver mBluetoothBroadcastReceiver; 124 private WifiBroadcastReceiver mWifiBroadcastReceiver; 125 126 @Override onBind(Intent intent)127 public IBinder onBind(Intent intent) { 128 if (DEBUG) Log.i(LOG_TAG, "onBind(" + intent + ")"); 129 return mBinder.asBinder(); 130 } 131 132 @Override onCreate()133 public void onCreate() { 134 super.onCreate(); 135 136 if (DEBUG) Log.i(LOG_TAG, "onCreate()"); 137 138 mBluetoothAdapter = getSystemService(BluetoothManager.class).getAdapter(); 139 mBLEScanner = mBluetoothAdapter.getBluetoothLeScanner(); 140 mWifiManager = getSystemService(WifiManager.class); 141 142 mDevicesFound = new ArrayList<>(); 143 mDevicesAdapter = new DevicesAdapter(); 144 145 sInstance = this; 146 } 147 startDiscovery(AssociationRequest request)148 private void startDiscovery(AssociationRequest request) { 149 if (!request.equals(mRequest)) { 150 mRequest = request; 151 152 mFilters = request.getDeviceFilters(); 153 mWifiFilters = CollectionUtils.filter(mFilters, WifiDeviceFilter.class); 154 mBluetoothFilters = CollectionUtils.filter(mFilters, BluetoothDeviceFilter.class); 155 mBLEFilters = CollectionUtils.filter(mFilters, BluetoothLeDeviceFilter.class); 156 mBLEScanFilters = CollectionUtils.map(mBLEFilters, BluetoothLeDeviceFilter::getScanFilter); 157 158 reset(); 159 } else if (DEBUG) Log.i(LOG_TAG, "startDiscovery: duplicate request: " + request); 160 161 if (!ArrayUtils.isEmpty(mDevicesFound)) { 162 onReadyToShowUI(); 163 } 164 165 // If filtering to get single device by mac address, also search in the set of already 166 // bonded devices to allow linking those directly 167 String singleMacAddressFilter = null; 168 if (mRequest.isSingleDevice()) { 169 int numFilters = size(mBluetoothFilters); 170 for (int i = 0; i < numFilters; i++) { 171 BluetoothDeviceFilter filter = mBluetoothFilters.get(i); 172 if (!TextUtils.isEmpty(filter.getAddress())) { 173 singleMacAddressFilter = filter.getAddress(); 174 break; 175 } 176 } 177 } 178 if (singleMacAddressFilter != null) { 179 for (BluetoothDevice dev : emptyIfNull(mBluetoothAdapter.getBondedDevices())) { 180 onDeviceFound(DeviceFilterPair.findMatch(dev, mBluetoothFilters)); 181 } 182 } 183 184 if (shouldScan(mBluetoothFilters)) { 185 final IntentFilter intentFilter = new IntentFilter(); 186 intentFilter.addAction(BluetoothDevice.ACTION_FOUND); 187 intentFilter.addAction(BluetoothDevice.ACTION_DISAPPEARED); 188 189 mBluetoothBroadcastReceiver = new BluetoothBroadcastReceiver(); 190 registerReceiver(mBluetoothBroadcastReceiver, intentFilter); 191 mBluetoothAdapter.startDiscovery(); 192 } 193 194 if (shouldScan(mBLEFilters) && mBLEScanner != null) { 195 mBLEScanCallback = new BLEScanCallback(); 196 mBLEScanner.startScan(mBLEScanFilters, mDefaultScanSettings, mBLEScanCallback); 197 } 198 199 if (shouldScan(mWifiFilters)) { 200 mWifiBroadcastReceiver = new WifiBroadcastReceiver(); 201 registerReceiver(mWifiBroadcastReceiver, 202 new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)); 203 mWifiManager.startScan(); 204 } 205 mIsScanning = true; 206 Handler.getMain().sendMessageDelayed( 207 obtainMessage(DeviceDiscoveryService::stopScan, this), 208 SCAN_TIMEOUT); 209 } 210 shouldScan(List<? extends DeviceFilter> mediumSpecificFilters)211 private boolean shouldScan(List<? extends DeviceFilter> mediumSpecificFilters) { 212 return !isEmpty(mediumSpecificFilters) || isEmpty(mFilters); 213 } 214 reset()215 private void reset() { 216 if (DEBUG) Log.i(LOG_TAG, "reset()"); 217 stopScan(); 218 mDevicesFound.clear(); 219 mSelectedDevice = null; 220 notifyDataSetChanged(); 221 } 222 223 @Override onUnbind(Intent intent)224 public boolean onUnbind(Intent intent) { 225 stopScan(); 226 return super.onUnbind(intent); 227 } 228 stopScan()229 private void stopScan() { 230 if (DEBUG) Log.i(LOG_TAG, "stopScan()"); 231 232 if (!mIsScanning) return; 233 mIsScanning = false; 234 235 DeviceChooserActivity activity = mActivity; 236 if (activity != null) { 237 if (activity.mDeviceListView != null) { 238 activity.mDeviceListView.removeFooterView(activity.mLoadingIndicator); 239 } 240 mActivity = null; 241 } 242 243 mBluetoothAdapter.cancelDiscovery(); 244 if (mBluetoothBroadcastReceiver != null) { 245 unregisterReceiver(mBluetoothBroadcastReceiver); 246 mBluetoothBroadcastReceiver = null; 247 } 248 if (mBLEScanner != null) mBLEScanner.stopScan(mBLEScanCallback); 249 if (mWifiBroadcastReceiver != null) { 250 unregisterReceiver(mWifiBroadcastReceiver); 251 mWifiBroadcastReceiver = null; 252 } 253 } 254 onDeviceFound(@ullable DeviceFilterPair device)255 private void onDeviceFound(@Nullable DeviceFilterPair device) { 256 if (device == null) return; 257 258 if (mDevicesFound.contains(device)) { 259 return; 260 } 261 262 if (DEBUG) Log.i(LOG_TAG, "Found device " + device); 263 264 if (mDevicesFound.isEmpty()) { 265 onReadyToShowUI(); 266 } 267 mDevicesFound.add(device); 268 notifyDataSetChanged(); 269 } 270 notifyDataSetChanged()271 private void notifyDataSetChanged() { 272 Handler.getMain().sendMessage(obtainMessage( 273 DevicesAdapter::notifyDataSetChanged, mDevicesAdapter)); 274 } 275 276 //TODO also, on timeout -> call onFailure onReadyToShowUI()277 private void onReadyToShowUI() { 278 try { 279 mFindCallback.onSuccess(PendingIntent.getActivity( 280 this, 0, 281 new Intent(this, DeviceChooserActivity.class), 282 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT 283 | PendingIntent.FLAG_IMMUTABLE)); 284 } catch (RemoteException e) { 285 throw new RuntimeException(e); 286 } 287 } 288 onDeviceLost(@ullable DeviceFilterPair device)289 private void onDeviceLost(@Nullable DeviceFilterPair device) { 290 mDevicesFound.remove(device); 291 notifyDataSetChanged(); 292 if (DEBUG) Log.i(LOG_TAG, "Lost device " + device.getDisplayName()); 293 } 294 onDeviceSelected(String callingPackage, String deviceAddress)295 void onDeviceSelected(String callingPackage, String deviceAddress) { 296 try { 297 mServiceCallback.onDeviceSelected( 298 //TODO is this the right userId? 299 callingPackage, getUserId(), deviceAddress); 300 } catch (RemoteException e) { 301 Log.e(LOG_TAG, "Failed to record association: " 302 + callingPackage + " <-> " + deviceAddress); 303 } 304 } 305 onCancel()306 void onCancel() { 307 if (DEBUG) Log.i(LOG_TAG, "onCancel()"); 308 try { 309 mServiceCallback.onDeviceSelectionCancel(); 310 } catch (RemoteException e) { 311 throw new RuntimeException(e); 312 } 313 } 314 315 class DevicesAdapter extends ArrayAdapter<DeviceFilterPair> { 316 private Drawable BLUETOOTH_ICON = icon(android.R.drawable.stat_sys_data_bluetooth); 317 private Drawable WIFI_ICON = icon(com.android.internal.R.drawable.ic_wifi_signal_3); 318 icon(int drawableRes)319 private Drawable icon(int drawableRes) { 320 Drawable icon = getResources().getDrawable(drawableRes, null); 321 icon.setTint(Color.DKGRAY); 322 return icon; 323 } 324 DevicesAdapter()325 public DevicesAdapter() { 326 super(DeviceDiscoveryService.this, 0, mDevicesFound); 327 } 328 329 @Override getView( int position, @Nullable View convertView, @NonNull ViewGroup parent)330 public View getView( 331 int position, 332 @Nullable View convertView, 333 @NonNull ViewGroup parent) { 334 TextView view = convertView instanceof TextView 335 ? (TextView) convertView 336 : newView(); 337 bind(view, getItem(position)); 338 return view; 339 } 340 bind(TextView textView, DeviceFilterPair device)341 private void bind(TextView textView, DeviceFilterPair device) { 342 textView.setText(device.getDisplayName()); 343 textView.setBackgroundColor( 344 device.equals(mSelectedDevice) 345 ? Color.GRAY 346 : Color.TRANSPARENT); 347 textView.setCompoundDrawablesWithIntrinsicBounds( 348 device.device instanceof android.net.wifi.ScanResult 349 ? WIFI_ICON 350 : BLUETOOTH_ICON, 351 null, null, null); 352 textView.setOnClickListener((view) -> { 353 mSelectedDevice = device; 354 notifyDataSetChanged(); 355 }); 356 } 357 358 //TODO move to a layout file newView()359 private TextView newView() { 360 final TextView textView = new TextView(DeviceDiscoveryService.this); 361 textView.setTextColor(Color.BLACK); 362 final int padding = DeviceChooserActivity.getPadding(getResources()); 363 textView.setPadding(padding, padding, padding, padding); 364 textView.setCompoundDrawablePadding(padding); 365 return textView; 366 } 367 } 368 369 /** 370 * A pair of device and a filter that matched this device if any. 371 * 372 * @param <T> device type 373 */ 374 static class DeviceFilterPair<T extends Parcelable> { 375 public final T device; 376 @Nullable 377 public final DeviceFilter<T> filter; 378 DeviceFilterPair(T device, @Nullable DeviceFilter<T> filter)379 private DeviceFilterPair(T device, @Nullable DeviceFilter<T> filter) { 380 this.device = device; 381 this.filter = filter; 382 } 383 384 /** 385 * {@code (device, null)} if the filters list is empty or null 386 * {@code null} if none of the provided filters match the device 387 * {@code (device, filter)} where filter is among the list of filters and matches the device 388 */ 389 @Nullable findMatch( T dev, @Nullable List<? extends DeviceFilter<T>> filters)390 public static <T extends Parcelable> DeviceFilterPair<T> findMatch( 391 T dev, @Nullable List<? extends DeviceFilter<T>> filters) { 392 if (isEmpty(filters)) return new DeviceFilterPair<>(dev, null); 393 final DeviceFilter<T> matchingFilter 394 = CollectionUtils.find(filters, f -> f.matches(dev)); 395 396 DeviceFilterPair<T> result = matchingFilter != null 397 ? new DeviceFilterPair<>(dev, matchingFilter) 398 : null; 399 if (DEBUG) Log.i(LOG_TAG, "findMatch(dev = " + dev + ", filters = " + filters + 400 ") -> " + result); 401 return result; 402 } 403 getDisplayName()404 public String getDisplayName() { 405 if (filter == null) { 406 Preconditions.checkNotNull(device); 407 if (device instanceof BluetoothDevice) { 408 return getDeviceDisplayNameInternal((BluetoothDevice) device); 409 } else if (device instanceof android.net.wifi.ScanResult) { 410 return getDeviceDisplayNameInternal((android.net.wifi.ScanResult) device); 411 } else if (device instanceof ScanResult) { 412 return getDeviceDisplayNameInternal(((ScanResult) device).getDevice()); 413 } else { 414 throw new IllegalArgumentException("Unknown device type: " + device.getClass()); 415 } 416 } 417 return filter.getDeviceDisplayName(device); 418 } 419 420 @Override equals(Object o)421 public boolean equals(Object o) { 422 if (this == o) return true; 423 if (o == null || getClass() != o.getClass()) return false; 424 DeviceFilterPair<?> that = (DeviceFilterPair<?>) o; 425 return Objects.equals(getDeviceMacAddress(device), getDeviceMacAddress(that.device)); 426 } 427 428 @Override hashCode()429 public int hashCode() { 430 return Objects.hash(getDeviceMacAddress(device)); 431 } 432 433 @Override toString()434 public String toString() { 435 return "DeviceFilterPair{" + 436 "device=" + device + 437 ", filter=" + filter + 438 '}'; 439 } 440 } 441 442 private class BLEScanCallback extends ScanCallback { 443 BLEScanCallback()444 public BLEScanCallback() { 445 if (DEBUG) Log.i(LOG_TAG, "new BLEScanCallback() -> " + this); 446 } 447 448 @Override onScanResult(int callbackType, ScanResult result)449 public void onScanResult(int callbackType, ScanResult result) { 450 if (DEBUG) { 451 Log.i(LOG_TAG, 452 "BLE.onScanResult(callbackType = " + callbackType + ", result = " + result 453 + ")"); 454 } 455 final DeviceFilterPair<ScanResult> deviceFilterPair 456 = DeviceFilterPair.findMatch(result, mBLEFilters); 457 if (deviceFilterPair == null) return; 458 if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) { 459 onDeviceLost(deviceFilterPair); 460 } else { 461 onDeviceFound(deviceFilterPair); 462 } 463 } 464 } 465 466 private class BluetoothBroadcastReceiver extends BroadcastReceiver { 467 @Override onReceive(Context context, Intent intent)468 public void onReceive(Context context, Intent intent) { 469 if (DEBUG) { 470 Log.i(LOG_TAG, 471 "BL.onReceive(context = " + context + ", intent = " + intent + ")"); 472 } 473 final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 474 final DeviceFilterPair<BluetoothDevice> deviceFilterPair 475 = DeviceFilterPair.findMatch(device, mBluetoothFilters); 476 if (deviceFilterPair == null) return; 477 if (intent.getAction().equals(BluetoothDevice.ACTION_FOUND)) { 478 onDeviceFound(deviceFilterPair); 479 } else { 480 onDeviceLost(deviceFilterPair); 481 } 482 } 483 } 484 485 private class WifiBroadcastReceiver extends BroadcastReceiver { 486 @Override onReceive(Context context, Intent intent)487 public void onReceive(Context context, Intent intent) { 488 if (intent.getAction().equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) { 489 List<android.net.wifi.ScanResult> scanResults = mWifiManager.getScanResults(); 490 491 if (DEBUG) { 492 Log.i(LOG_TAG, "Wifi scan results: " + TextUtils.join("\n", scanResults)); 493 } 494 495 for (int i = 0; i < scanResults.size(); i++) { 496 onDeviceFound(DeviceFilterPair.findMatch(scanResults.get(i), mWifiFilters)); 497 } 498 } 499 } 500 } 501 } 502