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