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 com.android.companiondevicemanager.Utils.runOnMainThread; 20 import static com.android.internal.util.ArrayUtils.isEmpty; 21 import static com.android.internal.util.CollectionUtils.filter; 22 import static com.android.internal.util.CollectionUtils.find; 23 import static com.android.internal.util.CollectionUtils.map; 24 25 import static java.lang.Math.max; 26 import static java.lang.Math.min; 27 import static java.util.Objects.requireNonNull; 28 29 import android.annotation.MainThread; 30 import android.annotation.NonNull; 31 import android.annotation.Nullable; 32 import android.annotation.SuppressLint; 33 import android.app.Service; 34 import android.bluetooth.BluetoothAdapter; 35 import android.bluetooth.BluetoothDevice; 36 import android.bluetooth.BluetoothManager; 37 import android.bluetooth.BluetoothProfile; 38 import android.bluetooth.le.BluetoothLeScanner; 39 import android.bluetooth.le.ScanCallback; 40 import android.bluetooth.le.ScanFilter; 41 import android.bluetooth.le.ScanResult; 42 import android.bluetooth.le.ScanSettings; 43 import android.companion.AssociationRequest; 44 import android.companion.BluetoothDeviceFilter; 45 import android.companion.BluetoothLeDeviceFilter; 46 import android.companion.DeviceFilter; 47 import android.companion.WifiDeviceFilter; 48 import android.content.BroadcastReceiver; 49 import android.content.Context; 50 import android.content.Intent; 51 import android.content.IntentFilter; 52 import android.net.wifi.WifiManager; 53 import android.os.Handler; 54 import android.os.IBinder; 55 import android.os.Parcelable; 56 import android.os.SystemProperties; 57 import android.text.TextUtils; 58 import android.util.Log; 59 60 import androidx.lifecycle.LiveData; 61 import androidx.lifecycle.MutableLiveData; 62 63 import java.util.ArrayList; 64 import java.util.Collection; 65 import java.util.Collections; 66 import java.util.List; 67 import java.util.Objects; 68 69 /** 70 * A CompanionDevice service response for scanning nearby devices 71 */ 72 @SuppressLint("LongLogTag") 73 public class CompanionDeviceDiscoveryService extends Service { 74 private static final boolean DEBUG = false; 75 private static final String TAG = "CDM_CompanionDeviceDiscoveryService"; 76 77 private static final String SYS_PROP_DEBUG_TIMEOUT = "debug.cdm.discovery_timeout"; 78 private static final long TIMEOUT_DEFAULT = 20_000L; // 20 seconds 79 private static final long TIMEOUT_MIN = 1_000L; // 1 sec 80 private static final long TIMEOUT_MAX = 60_000L; // 1 min 81 82 private static final String ACTION_START_DISCOVERY = 83 "com.android.companiondevicemanager.action.START_DISCOVERY"; 84 private static final String ACTION_STOP_DISCOVERY = 85 "com.android.companiondevicemanager.action.ACTION_STOP_DISCOVERY"; 86 private static final String EXTRA_ASSOCIATION_REQUEST = "association_request"; 87 88 private static MutableLiveData<List<DeviceFilterPair<?>>> sScanResultsLiveData = 89 new MutableLiveData<>(Collections.emptyList()); 90 private static MutableLiveData<DiscoveryState> sStateLiveData = 91 new MutableLiveData<>(DiscoveryState.NOT_STARTED); 92 93 private BluetoothManager mBtManager; 94 private BluetoothAdapter mBtAdapter; 95 private WifiManager mWifiManager; 96 private BluetoothLeScanner mBleScanner; 97 98 private ScanCallback mBleScanCallback; 99 private BluetoothBroadcastReceiver mBtReceiver; 100 private WifiBroadcastReceiver mWifiReceiver; 101 102 private boolean mDiscoveryStarted = false; 103 private boolean mDiscoveryStopped = false; 104 private final List<DeviceFilterPair<?>> mDevicesFound = new ArrayList<>(); 105 106 private final Runnable mTimeoutRunnable = this::timeout; 107 108 private boolean mStopAfterFirstMatch; 109 110 /** 111 * A state enum for devices' discovery. 112 */ 113 enum DiscoveryState { 114 NOT_STARTED, 115 STARTING, 116 DISCOVERY_IN_PROGRESS, 117 FINISHED_STOPPED, 118 FINISHED_TIMEOUT 119 } 120 startForRequest( @onNull Context context, @NonNull AssociationRequest associationRequest)121 static void startForRequest( 122 @NonNull Context context, @NonNull AssociationRequest associationRequest) { 123 requireNonNull(associationRequest); 124 final Intent intent = new Intent(context, CompanionDeviceDiscoveryService.class); 125 intent.setAction(ACTION_START_DISCOVERY); 126 intent.putExtra(EXTRA_ASSOCIATION_REQUEST, associationRequest); 127 sStateLiveData.setValue(DiscoveryState.STARTING); 128 sScanResultsLiveData.setValue(Collections.emptyList()); 129 130 context.startService(intent); 131 } 132 stop(@onNull Context context)133 static void stop(@NonNull Context context) { 134 final Intent intent = new Intent(context, CompanionDeviceDiscoveryService.class); 135 intent.setAction(ACTION_STOP_DISCOVERY); 136 context.startService(intent); 137 } 138 getScanResult()139 static LiveData<List<DeviceFilterPair<?>>> getScanResult() { 140 return sScanResultsLiveData; 141 } 142 getDiscoveryState()143 static LiveData<DiscoveryState> getDiscoveryState() { 144 return sStateLiveData; 145 } 146 147 @Override onCreate()148 public void onCreate() { 149 super.onCreate(); 150 if (DEBUG) Log.d(TAG, "onCreate()"); 151 152 mBtManager = getSystemService(BluetoothManager.class); 153 mBtAdapter = mBtManager.getAdapter(); 154 mBleScanner = mBtAdapter.getBluetoothLeScanner(); 155 mWifiManager = getSystemService(WifiManager.class); 156 } 157 158 @Override onStartCommand(Intent intent, int flags, int startId)159 public int onStartCommand(Intent intent, int flags, int startId) { 160 final String action = intent.getAction(); 161 if (DEBUG) Log.d(TAG, "onStartCommand() action=" + action); 162 163 switch (action) { 164 case ACTION_START_DISCOVERY: 165 final AssociationRequest request = 166 intent.getParcelableExtra(EXTRA_ASSOCIATION_REQUEST); 167 startDiscovery(request); 168 break; 169 170 case ACTION_STOP_DISCOVERY: 171 stopDiscoveryAndFinish(/* timeout */ false); 172 break; 173 } 174 return START_NOT_STICKY; 175 } 176 177 @Override onDestroy()178 public void onDestroy() { 179 super.onDestroy(); 180 if (DEBUG) Log.d(TAG, "onDestroy()"); 181 } 182 183 @MainThread startDiscovery(@onNull AssociationRequest request)184 private void startDiscovery(@NonNull AssociationRequest request) { 185 if (DEBUG) Log.i(TAG, "startDiscovery() request=" + request); 186 requireNonNull(request); 187 188 if (mDiscoveryStarted) throw new RuntimeException("Discovery in progress."); 189 mStopAfterFirstMatch = request.isSingleDevice(); 190 mDiscoveryStarted = true; 191 sStateLiveData.setValue(DiscoveryState.DISCOVERY_IN_PROGRESS); 192 193 final List<DeviceFilter<?>> allFilters = request.getDeviceFilters(); 194 final List<BluetoothDeviceFilter> btFilters = 195 filter(allFilters, BluetoothDeviceFilter.class); 196 final List<BluetoothLeDeviceFilter> bleFilters = 197 filter(allFilters, BluetoothLeDeviceFilter.class); 198 final List<WifiDeviceFilter> wifiFilters = filter(allFilters, WifiDeviceFilter.class); 199 200 checkBoundDevicesIfNeeded(request, btFilters); 201 202 // If no filters are specified: look for everything. 203 final boolean forceStartScanningAll = isEmpty(allFilters); 204 // Start BT scanning (if needed) 205 mBtReceiver = startBtScanningIfNeeded(btFilters, forceStartScanningAll); 206 // Start Wi-Fi scanning (if needed) 207 mWifiReceiver = startWifiScanningIfNeeded(wifiFilters, forceStartScanningAll); 208 // Start BLE scanning (if needed) 209 mBleScanCallback = startBleScanningIfNeeded(bleFilters, forceStartScanningAll); 210 211 scheduleTimeout(); 212 } 213 214 @MainThread stopDiscoveryAndFinish(boolean timeout)215 private void stopDiscoveryAndFinish(boolean timeout) { 216 if (DEBUG) Log.i(TAG, "stopDiscovery()"); 217 218 if (!mDiscoveryStarted) { 219 stopSelf(); 220 return; 221 } 222 223 if (mDiscoveryStopped) return; 224 mDiscoveryStopped = true; 225 226 // Stop BT discovery. 227 if (mBtReceiver != null) { 228 // Cancel discovery. 229 mBtAdapter.cancelDiscovery(); 230 // Unregister receiver. 231 unregisterReceiver(mBtReceiver); 232 mBtReceiver = null; 233 } 234 235 // Stop Wi-Fi scanning. 236 if (mWifiReceiver != null) { 237 // TODO: need to stop scan? 238 // Unregister receiver. 239 unregisterReceiver(mWifiReceiver); 240 mWifiReceiver = null; 241 } 242 243 // Stop BLE scanning. 244 if (mBleScanCallback != null) { 245 mBleScanner.stopScan(mBleScanCallback); 246 } 247 248 Handler.getMain().removeCallbacks(mTimeoutRunnable); 249 250 if (timeout) { 251 sStateLiveData.setValue(DiscoveryState.FINISHED_TIMEOUT); 252 } else { 253 sStateLiveData.setValue(DiscoveryState.FINISHED_STOPPED); 254 } 255 256 // "Finish". 257 stopSelf(); 258 } 259 checkBoundDevicesIfNeeded(@onNull AssociationRequest request, @NonNull List<BluetoothDeviceFilter> btFilters)260 private void checkBoundDevicesIfNeeded(@NonNull AssociationRequest request, 261 @NonNull List<BluetoothDeviceFilter> btFilters) { 262 // If filtering to get single device by mac address, also search in the set of already 263 // bonded devices to allow linking those directly 264 if (btFilters.isEmpty() || !request.isSingleDevice()) return; 265 266 final BluetoothDeviceFilter singleMacAddressFilter = 267 find(btFilters, filter -> !TextUtils.isEmpty(filter.getAddress())); 268 269 if (singleMacAddressFilter == null) return; 270 271 findAndReportMatches(mBtAdapter.getBondedDevices(), btFilters); 272 findAndReportMatches(mBtManager.getConnectedDevices(BluetoothProfile.GATT), btFilters); 273 findAndReportMatches( 274 mBtManager.getConnectedDevices(BluetoothProfile.GATT_SERVER), btFilters); 275 } 276 findAndReportMatches(@ullable Collection<BluetoothDevice> devices, @NonNull List<BluetoothDeviceFilter> filters)277 private void findAndReportMatches(@Nullable Collection<BluetoothDevice> devices, 278 @NonNull List<BluetoothDeviceFilter> filters) { 279 if (devices == null) return; 280 281 for (BluetoothDevice device : devices) { 282 final DeviceFilterPair<BluetoothDevice> match = findMatch(device, filters); 283 if (match != null) { 284 onDeviceFound(match); 285 } 286 } 287 } 288 startBtScanningIfNeeded( List<BluetoothDeviceFilter> filters, boolean force)289 private BluetoothBroadcastReceiver startBtScanningIfNeeded( 290 List<BluetoothDeviceFilter> filters, boolean force) { 291 if (isEmpty(filters) && !force) return null; 292 if (DEBUG) Log.d(TAG, "registerReceiver(BluetoothDevice.ACTION_FOUND)"); 293 294 final BluetoothBroadcastReceiver receiver = new BluetoothBroadcastReceiver(filters); 295 296 final IntentFilter intentFilter = new IntentFilter(BluetoothDevice.ACTION_FOUND); 297 registerReceiver(receiver, intentFilter); 298 299 mBtAdapter.startDiscovery(); 300 301 return receiver; 302 } 303 startWifiScanningIfNeeded( List<WifiDeviceFilter> filters, boolean force)304 private WifiBroadcastReceiver startWifiScanningIfNeeded( 305 List<WifiDeviceFilter> filters, boolean force) { 306 if (isEmpty(filters) && !force) return null; 307 if (DEBUG) Log.d(TAG, "registerReceiver(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)"); 308 309 final WifiBroadcastReceiver receiver = new WifiBroadcastReceiver(filters); 310 311 final IntentFilter intentFilter = new IntentFilter( 312 WifiManager.SCAN_RESULTS_AVAILABLE_ACTION); 313 registerReceiver(receiver, intentFilter); 314 315 mWifiManager.startScan(); 316 317 return receiver; 318 } 319 startBleScanningIfNeeded( List<BluetoothLeDeviceFilter> filters, boolean force)320 private ScanCallback startBleScanningIfNeeded( 321 List<BluetoothLeDeviceFilter> filters, boolean force) { 322 if (isEmpty(filters) && !force) return null; 323 if (DEBUG) Log.d(TAG, "BLEScanner.startScan"); 324 325 if (mBleScanner == null) { 326 Log.w(TAG, "BLE Scanner is not available."); 327 return null; 328 } 329 330 final BLEScanCallback callback = new BLEScanCallback(filters); 331 332 final List<ScanFilter> scanFilters = map( 333 filters, BluetoothLeDeviceFilter::getScanFilter); 334 final ScanSettings scanSettings = new ScanSettings.Builder() 335 .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) 336 .build(); 337 mBleScanner.startScan(scanFilters, scanSettings, callback); 338 339 return callback; 340 } 341 onDeviceFound(@onNull DeviceFilterPair<?> device)342 private void onDeviceFound(@NonNull DeviceFilterPair<?> device) { 343 runOnMainThread(() -> { 344 if (DEBUG) Log.v(TAG, "onDeviceFound() " + device); 345 if (mDiscoveryStopped) return; 346 if (mDevicesFound.contains(device)) { 347 // TODO: update the device instead of ignoring (new found device may contain 348 // additional/updated info, eg. name of the device). 349 if (DEBUG) { 350 Log.d(TAG, "onDeviceFound() " + device.toShortString() 351 + " - Already seen: ignore."); 352 } 353 return; 354 } 355 Log.i(TAG, "onDeviceFound() " + device.toShortString() + " - New device."); 356 357 // First: make change. 358 mDevicesFound.add(device); 359 // Then: notify observers. 360 sScanResultsLiveData.setValue(mDevicesFound); 361 // Stop discovery when there's one device found for singleDevice. 362 if (mStopAfterFirstMatch) { 363 stopDiscoveryAndFinish(/* timeout */ false); 364 } 365 }); 366 } 367 onDeviceLost(@onNull DeviceFilterPair<?> device)368 private void onDeviceLost(@NonNull DeviceFilterPair<?> device) { 369 runOnMainThread(() -> { 370 Log.i(TAG, "onDeviceLost(), device=" + device.toShortString()); 371 372 // First: make change. 373 mDevicesFound.remove(device); 374 // Then: notify observers. 375 sScanResultsLiveData.setValue(mDevicesFound); 376 }); 377 } 378 scheduleTimeout()379 private void scheduleTimeout() { 380 long timeout = SystemProperties.getLong(SYS_PROP_DEBUG_TIMEOUT, -1); 381 if (timeout <= 0) { 382 // 0 or negative values indicate that the sysprop was never set or should be ignored. 383 timeout = TIMEOUT_DEFAULT; 384 } else { 385 timeout = min(timeout, TIMEOUT_MAX); // should be <= 1 min (TIMEOUT_MAX) 386 timeout = max(timeout, TIMEOUT_MIN); // should be >= 1 sec (TIMEOUT_MIN) 387 } 388 389 if (DEBUG) Log.d(TAG, "scheduleTimeout(), timeout=" + timeout); 390 391 Handler.getMain().postDelayed(mTimeoutRunnable, timeout); 392 } 393 timeout()394 private void timeout() { 395 if (DEBUG) Log.i(TAG, "timeout()"); 396 stopDiscoveryAndFinish(/* timeout */ true); 397 } 398 399 @Override onBind(Intent intent)400 public IBinder onBind(Intent intent) { 401 return null; 402 } 403 404 private class BLEScanCallback extends ScanCallback { 405 final List<BluetoothLeDeviceFilter> mFilters; 406 BLEScanCallback(List<BluetoothLeDeviceFilter> filters)407 BLEScanCallback(List<BluetoothLeDeviceFilter> filters) { 408 mFilters = filters; 409 } 410 411 @Override onScanResult(int callbackType, ScanResult result)412 public void onScanResult(int callbackType, ScanResult result) { 413 if (DEBUG) { 414 Log.v(TAG, "BLE.onScanResult() callback=" + callbackType + ", result=" + result); 415 } 416 417 final DeviceFilterPair<ScanResult> match = findMatch(result, mFilters); 418 if (match == null) return; 419 420 if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) { 421 onDeviceLost(match); 422 } else { 423 // TODO: check this logic. 424 onDeviceFound(match); 425 } 426 } 427 } 428 429 private class BluetoothBroadcastReceiver extends BroadcastReceiver { 430 final List<BluetoothDeviceFilter> mFilters; 431 BluetoothBroadcastReceiver(List<BluetoothDeviceFilter> filters)432 BluetoothBroadcastReceiver(List<BluetoothDeviceFilter> filters) { 433 this.mFilters = filters; 434 } 435 436 @Override onReceive(Context context, Intent intent)437 public void onReceive(Context context, Intent intent) { 438 final String action = intent.getAction(); 439 final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 440 441 if (DEBUG) Log.v(TAG, action + ", device=" + device); 442 443 if (action == null) return; 444 445 final DeviceFilterPair<BluetoothDevice> match = findMatch(device, mFilters); 446 if (match == null) return; 447 448 if (action.equals(BluetoothDevice.ACTION_FOUND)) { 449 onDeviceFound(match); 450 } else { 451 // TODO: check this logic. 452 onDeviceLost(match); 453 } 454 } 455 } 456 457 private class WifiBroadcastReceiver extends BroadcastReceiver { 458 final List<WifiDeviceFilter> mFilters; 459 WifiBroadcastReceiver(List<WifiDeviceFilter> filters)460 private WifiBroadcastReceiver(List<WifiDeviceFilter> filters) { 461 this.mFilters = filters; 462 } 463 464 @Override onReceive(Context context, Intent intent)465 public void onReceive(Context context, Intent intent) { 466 if (!Objects.equals(intent.getAction(), WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) { 467 return; 468 } 469 470 final List<android.net.wifi.ScanResult> scanResults = mWifiManager.getScanResults(); 471 if (DEBUG) { 472 Log.v(TAG, "WifiManager.SCAN_RESULTS_AVAILABLE_ACTION, results:\n " 473 + TextUtils.join("\n ", scanResults)); 474 } 475 476 for (int i = 0; i < scanResults.size(); i++) { 477 final android.net.wifi.ScanResult scanResult = scanResults.get(i); 478 final DeviceFilterPair<?> match = findMatch(scanResult, mFilters); 479 if (match != null) { 480 onDeviceFound(match); 481 } 482 } 483 } 484 } 485 486 /** 487 * {@code (device, null)} if the filters list is empty or null 488 * {@code null} if none of the provided filters match the device 489 * {@code (device, filter)} where filter is among the list of filters and matches the device 490 */ 491 @Nullable findMatch( T dev, @Nullable List<? extends DeviceFilter<T>> filters)492 public static <T extends Parcelable> DeviceFilterPair<T> findMatch( 493 T dev, @Nullable List<? extends DeviceFilter<T>> filters) { 494 if (isEmpty(filters)) return new DeviceFilterPair<>(dev, null); 495 final DeviceFilter<T> matchingFilter = find(filters, f -> f.matches(dev)); 496 497 DeviceFilterPair<T> result = matchingFilter != null 498 ? new DeviceFilterPair<>(dev, matchingFilter) : null; 499 if (DEBUG) { 500 Log.v(TAG, "findMatch(dev=" + dev + ", filters=" + filters + ") -> " + result); 501 } 502 return result; 503 } 504 } 505