1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17 package android.bluetooth.le; 18 19 import android.Manifest; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.annotation.RequiresPermission; 23 import android.annotation.SystemApi; 24 import android.app.ActivityThread; 25 import android.app.PendingIntent; 26 import android.bluetooth.BluetoothAdapter; 27 import android.bluetooth.BluetoothGatt; 28 import android.bluetooth.IBluetoothGatt; 29 import android.bluetooth.IBluetoothManager; 30 import android.os.Handler; 31 import android.os.Looper; 32 import android.os.RemoteException; 33 import android.os.WorkSource; 34 import android.util.Log; 35 36 import java.util.ArrayList; 37 import java.util.HashMap; 38 import java.util.List; 39 import java.util.Map; 40 41 /** 42 * This class provides methods to perform scan related operations for Bluetooth LE devices. An 43 * application can scan for a particular type of Bluetooth LE devices using {@link ScanFilter}. It 44 * can also request different types of callbacks for delivering the result. 45 * <p> 46 * Use {@link BluetoothAdapter#getBluetoothLeScanner()} to get an instance of 47 * {@link BluetoothLeScanner}. 48 * <p> 49 * <b>Note:</b> Most of the scan methods here require 50 * {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission. 51 * 52 * @see ScanFilter 53 */ 54 public final class BluetoothLeScanner { 55 56 private static final String TAG = "BluetoothLeScanner"; 57 private static final boolean DBG = true; 58 private static final boolean VDBG = false; 59 60 /** 61 * Extra containing a list of ScanResults. It can have one or more results if there was no 62 * error. In case of error, {@link #EXTRA_ERROR_CODE} will contain the error code and this 63 * extra will not be available. 64 */ 65 public static final String EXTRA_LIST_SCAN_RESULT = 66 "android.bluetooth.le.extra.LIST_SCAN_RESULT"; 67 68 /** 69 * Optional extra indicating the error code, if any. The error code will be one of the 70 * SCAN_FAILED_* codes in {@link ScanCallback}. 71 */ 72 public static final String EXTRA_ERROR_CODE = "android.bluetooth.le.extra.ERROR_CODE"; 73 74 /** 75 * Optional extra indicating the callback type, which will be one of 76 * CALLBACK_TYPE_* constants in {@link ScanSettings}. 77 * 78 * @see ScanCallback#onScanResult(int, ScanResult) 79 */ 80 public static final String EXTRA_CALLBACK_TYPE = "android.bluetooth.le.extra.CALLBACK_TYPE"; 81 82 private final IBluetoothManager mBluetoothManager; 83 private final Handler mHandler; 84 private BluetoothAdapter mBluetoothAdapter; 85 private final Map<ScanCallback, BleScanCallbackWrapper> mLeScanClients; 86 87 /** 88 * Use {@link BluetoothAdapter#getBluetoothLeScanner()} instead. 89 * 90 * @param bluetoothManager BluetoothManager that conducts overall Bluetooth Management. 91 * @hide 92 */ 93 public BluetoothLeScanner(IBluetoothManager bluetoothManager) { 94 mBluetoothManager = bluetoothManager; 95 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 96 mHandler = new Handler(Looper.getMainLooper()); 97 mLeScanClients = new HashMap<ScanCallback, BleScanCallbackWrapper>(); 98 } 99 100 /** 101 * Start Bluetooth LE scan with default parameters and no filters. The scan results will be 102 * delivered through {@code callback}. For unfiltered scans, scanning is stopped on screen 103 * off to save power. Scanning is resumed when screen is turned on again. To avoid this, use 104 * {@link #startScan(List, ScanSettings, ScanCallback)} with desired {@link ScanFilter}. 105 * <p> 106 * An app must hold 107 * {@link android.Manifest.permission#ACCESS_COARSE_LOCATION ACCESS_COARSE_LOCATION} or 108 * {@link android.Manifest.permission#ACCESS_FINE_LOCATION ACCESS_FINE_LOCATION} permission 109 * in order to get results. 110 * 111 * @param callback Callback used to deliver scan results. 112 * @throws IllegalArgumentException If {@code callback} is null. 113 */ 114 @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN) 115 public void startScan(final ScanCallback callback) { 116 startScan(null, new ScanSettings.Builder().build(), callback); 117 } 118 119 /** 120 * Start Bluetooth LE scan. The scan results will be delivered through {@code callback}. 121 * For unfiltered scans, scanning is stopped on screen off to save power. Scanning is 122 * resumed when screen is turned on again. To avoid this, do filetered scanning by 123 * using proper {@link ScanFilter}. 124 * <p> 125 * An app must hold 126 * {@link android.Manifest.permission#ACCESS_COARSE_LOCATION ACCESS_COARSE_LOCATION} or 127 * {@link android.Manifest.permission#ACCESS_FINE_LOCATION ACCESS_FINE_LOCATION} permission 128 * in order to get results. 129 * 130 * @param filters {@link ScanFilter}s for finding exact BLE devices. 131 * @param settings Settings for the scan. 132 * @param callback Callback used to deliver scan results. 133 * @throws IllegalArgumentException If {@code settings} or {@code callback} is null. 134 */ 135 @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN) 136 public void startScan(List<ScanFilter> filters, ScanSettings settings, 137 final ScanCallback callback) { 138 startScan(filters, settings, null, callback, /*callbackIntent=*/ null, null); 139 } 140 141 /** 142 * Start Bluetooth LE scan using a {@link PendingIntent}. The scan results will be delivered via 143 * the PendingIntent. Use this method of scanning if your process is not always running and it 144 * should be started when scan results are available. 145 * <p> 146 * An app must hold 147 * {@link android.Manifest.permission#ACCESS_COARSE_LOCATION ACCESS_COARSE_LOCATION} or 148 * {@link android.Manifest.permission#ACCESS_FINE_LOCATION ACCESS_FINE_LOCATION} permission 149 * in order to get results. 150 * <p> 151 * When the PendingIntent is delivered, the Intent passed to the receiver or activity 152 * will contain one or more of the extras {@link #EXTRA_CALLBACK_TYPE}, 153 * {@link #EXTRA_ERROR_CODE} and {@link #EXTRA_LIST_SCAN_RESULT} to indicate the result of 154 * the scan. 155 * 156 * @param filters Optional list of ScanFilters for finding exact BLE devices. 157 * @param settings Optional settings for the scan. 158 * @param callbackIntent The PendingIntent to deliver the result to. 159 * @return Returns 0 for success or an error code from {@link ScanCallback} if the scan request 160 * could not be sent. 161 * @see #stopScan(PendingIntent) 162 */ 163 @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN) 164 public int startScan(@Nullable List<ScanFilter> filters, @Nullable ScanSettings settings, 165 @NonNull PendingIntent callbackIntent) { 166 return startScan(filters, 167 settings != null ? settings : new ScanSettings.Builder().build(), 168 null, null, callbackIntent, null); 169 } 170 171 /** 172 * Start Bluetooth LE scan. Same as {@link #startScan(ScanCallback)} but allows the caller to 173 * specify on behalf of which application(s) the work is being done. 174 * 175 * @param workSource {@link WorkSource} identifying the application(s) for which to blame for 176 * the scan. 177 * @param callback Callback used to deliver scan results. 178 * @hide 179 */ 180 @SystemApi 181 @RequiresPermission(allOf = { 182 Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.UPDATE_DEVICE_STATS}) 183 public void startScanFromSource(final WorkSource workSource, final ScanCallback callback) { 184 startScanFromSource(null, new ScanSettings.Builder().build(), workSource, callback); 185 } 186 187 /** 188 * Start Bluetooth LE scan. Same as {@link #startScan(List, ScanSettings, ScanCallback)} but 189 * allows the caller to specify on behalf of which application(s) the work is being done. 190 * 191 * @param filters {@link ScanFilter}s for finding exact BLE devices. 192 * @param settings Settings for the scan. 193 * @param workSource {@link WorkSource} identifying the application(s) for which to blame for 194 * the scan. 195 * @param callback Callback used to deliver scan results. 196 * @hide 197 */ 198 @SystemApi 199 @RequiresPermission(allOf = { 200 Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.UPDATE_DEVICE_STATS}) 201 public void startScanFromSource(List<ScanFilter> filters, ScanSettings settings, 202 final WorkSource workSource, final ScanCallback callback) { 203 startScan(filters, settings, workSource, callback, null, null); 204 } 205 206 private int startScan(List<ScanFilter> filters, ScanSettings settings, 207 final WorkSource workSource, final ScanCallback callback, 208 final PendingIntent callbackIntent, 209 List<List<ResultStorageDescriptor>> resultStorages) { 210 BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter); 211 if (callback == null && callbackIntent == null) { 212 throw new IllegalArgumentException("callback is null"); 213 } 214 if (settings == null) { 215 throw new IllegalArgumentException("settings is null"); 216 } 217 synchronized (mLeScanClients) { 218 if (callback != null && mLeScanClients.containsKey(callback)) { 219 return postCallbackErrorOrReturn(callback, 220 ScanCallback.SCAN_FAILED_ALREADY_STARTED); 221 } 222 IBluetoothGatt gatt; 223 try { 224 gatt = mBluetoothManager.getBluetoothGatt(); 225 } catch (RemoteException e) { 226 gatt = null; 227 } 228 if (gatt == null) { 229 return postCallbackErrorOrReturn(callback, ScanCallback.SCAN_FAILED_INTERNAL_ERROR); 230 } 231 if (!isSettingsConfigAllowedForScan(settings)) { 232 return postCallbackErrorOrReturn(callback, 233 ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED); 234 } 235 if (!isHardwareResourcesAvailableForScan(settings)) { 236 return postCallbackErrorOrReturn(callback, 237 ScanCallback.SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES); 238 } 239 if (!isSettingsAndFilterComboAllowed(settings, filters)) { 240 return postCallbackErrorOrReturn(callback, 241 ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED); 242 } 243 if (callback != null) { 244 BleScanCallbackWrapper wrapper = new BleScanCallbackWrapper(gatt, filters, 245 settings, workSource, callback, resultStorages); 246 wrapper.startRegistration(); 247 } else { 248 try { 249 gatt.startScanForIntent(callbackIntent, settings, filters, 250 ActivityThread.currentOpPackageName()); 251 } catch (RemoteException e) { 252 return ScanCallback.SCAN_FAILED_INTERNAL_ERROR; 253 } 254 } 255 } 256 return ScanCallback.NO_ERROR; 257 } 258 259 /** 260 * Stops an ongoing Bluetooth LE scan. 261 * 262 * @param callback 263 */ 264 @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN) 265 public void stopScan(ScanCallback callback) { 266 BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter); 267 synchronized (mLeScanClients) { 268 BleScanCallbackWrapper wrapper = mLeScanClients.remove(callback); 269 if (wrapper == null) { 270 if (DBG) Log.d(TAG, "could not find callback wrapper"); 271 return; 272 } 273 wrapper.stopLeScan(); 274 } 275 } 276 277 /** 278 * Stops an ongoing Bluetooth LE scan started using a PendingIntent. 279 * 280 * @param callbackIntent The PendingIntent that was used to start the scan. 281 * @see #startScan(List, ScanSettings, PendingIntent) 282 */ 283 @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN) 284 public void stopScan(PendingIntent callbackIntent) { 285 BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter); 286 IBluetoothGatt gatt; 287 try { 288 gatt = mBluetoothManager.getBluetoothGatt(); 289 gatt.stopScanForIntent(callbackIntent, ActivityThread.currentOpPackageName()); 290 } catch (RemoteException e) { 291 } 292 } 293 294 /** 295 * Flush pending batch scan results stored in Bluetooth controller. This will return Bluetooth 296 * LE scan results batched on bluetooth controller. Returns immediately, batch scan results data 297 * will be delivered through the {@code callback}. 298 * 299 * @param callback Callback of the Bluetooth LE Scan, it has to be the same instance as the one 300 * used to start scan. 301 */ 302 public void flushPendingScanResults(ScanCallback callback) { 303 BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter); 304 if (callback == null) { 305 throw new IllegalArgumentException("callback cannot be null!"); 306 } 307 synchronized (mLeScanClients) { 308 BleScanCallbackWrapper wrapper = mLeScanClients.get(callback); 309 if (wrapper == null) { 310 return; 311 } 312 wrapper.flushPendingBatchResults(); 313 } 314 } 315 316 /** 317 * Start truncated scan. 318 * 319 * @hide 320 */ 321 @SystemApi 322 public void startTruncatedScan(List<TruncatedFilter> truncatedFilters, ScanSettings settings, 323 final ScanCallback callback) { 324 int filterSize = truncatedFilters.size(); 325 List<ScanFilter> scanFilters = new ArrayList<ScanFilter>(filterSize); 326 List<List<ResultStorageDescriptor>> scanStorages = 327 new ArrayList<List<ResultStorageDescriptor>>(filterSize); 328 for (TruncatedFilter filter : truncatedFilters) { 329 scanFilters.add(filter.getFilter()); 330 scanStorages.add(filter.getStorageDescriptors()); 331 } 332 startScan(scanFilters, settings, null, callback, null, scanStorages); 333 } 334 335 /** 336 * Cleans up scan clients. Should be called when bluetooth is down. 337 * 338 * @hide 339 */ 340 public void cleanup() { 341 mLeScanClients.clear(); 342 } 343 344 /** 345 * Bluetooth GATT interface callbacks 346 */ 347 private class BleScanCallbackWrapper extends IScannerCallback.Stub { 348 private static final int REGISTRATION_CALLBACK_TIMEOUT_MILLIS = 2000; 349 350 private final ScanCallback mScanCallback; 351 private final List<ScanFilter> mFilters; 352 private final WorkSource mWorkSource; 353 private ScanSettings mSettings; 354 private IBluetoothGatt mBluetoothGatt; 355 private List<List<ResultStorageDescriptor>> mResultStorages; 356 357 // mLeHandle 0: not registered 358 // -2: registration failed because app is scanning to frequently 359 // -1: scan stopped or registration failed 360 // > 0: registered and scan started 361 private int mScannerId; 362 363 public BleScanCallbackWrapper(IBluetoothGatt bluetoothGatt, 364 List<ScanFilter> filters, ScanSettings settings, 365 WorkSource workSource, ScanCallback scanCallback, 366 List<List<ResultStorageDescriptor>> resultStorages) { 367 mBluetoothGatt = bluetoothGatt; 368 mFilters = filters; 369 mSettings = settings; 370 mWorkSource = workSource; 371 mScanCallback = scanCallback; 372 mScannerId = 0; 373 mResultStorages = resultStorages; 374 } 375 376 public void startRegistration() { 377 synchronized (this) { 378 // Scan stopped. 379 if (mScannerId == -1 || mScannerId == -2) return; 380 try { 381 mBluetoothGatt.registerScanner(this, mWorkSource); 382 wait(REGISTRATION_CALLBACK_TIMEOUT_MILLIS); 383 } catch (InterruptedException | RemoteException e) { 384 Log.e(TAG, "application registeration exception", e); 385 postCallbackError(mScanCallback, ScanCallback.SCAN_FAILED_INTERNAL_ERROR); 386 } 387 if (mScannerId > 0) { 388 mLeScanClients.put(mScanCallback, this); 389 } else { 390 // Registration timed out or got exception, reset RscannerId to -1 so no 391 // subsequent operations can proceed. 392 if (mScannerId == 0) mScannerId = -1; 393 394 // If scanning too frequently, don't report anything to the app. 395 if (mScannerId == -2) return; 396 397 postCallbackError(mScanCallback, 398 ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED); 399 } 400 } 401 } 402 403 public void stopLeScan() { 404 synchronized (this) { 405 if (mScannerId <= 0) { 406 Log.e(TAG, "Error state, mLeHandle: " + mScannerId); 407 return; 408 } 409 try { 410 mBluetoothGatt.stopScan(mScannerId); 411 mBluetoothGatt.unregisterScanner(mScannerId); 412 } catch (RemoteException e) { 413 Log.e(TAG, "Failed to stop scan and unregister", e); 414 } 415 mScannerId = -1; 416 } 417 } 418 419 void flushPendingBatchResults() { 420 synchronized (this) { 421 if (mScannerId <= 0) { 422 Log.e(TAG, "Error state, mLeHandle: " + mScannerId); 423 return; 424 } 425 try { 426 mBluetoothGatt.flushPendingBatchResults(mScannerId); 427 } catch (RemoteException e) { 428 Log.e(TAG, "Failed to get pending scan results", e); 429 } 430 } 431 } 432 433 /** 434 * Application interface registered - app is ready to go 435 */ 436 @Override 437 public void onScannerRegistered(int status, int scannerId) { 438 Log.d(TAG, "onScannerRegistered() - status=" + status 439 + " scannerId=" + scannerId + " mScannerId=" + mScannerId); 440 synchronized (this) { 441 if (status == BluetoothGatt.GATT_SUCCESS) { 442 try { 443 if (mScannerId == -1) { 444 // Registration succeeds after timeout, unregister client. 445 mBluetoothGatt.unregisterClient(scannerId); 446 } else { 447 mScannerId = scannerId; 448 mBluetoothGatt.startScan(mScannerId, mSettings, mFilters, 449 mResultStorages, 450 ActivityThread.currentOpPackageName()); 451 } 452 } catch (RemoteException e) { 453 Log.e(TAG, "fail to start le scan: " + e); 454 mScannerId = -1; 455 } 456 } else if (status == ScanCallback.SCAN_FAILED_SCANNING_TOO_FREQUENTLY) { 457 // applicaiton was scanning too frequently 458 mScannerId = -2; 459 } else { 460 // registration failed 461 mScannerId = -1; 462 } 463 notifyAll(); 464 } 465 } 466 467 /** 468 * Callback reporting an LE scan result. 469 * 470 * @hide 471 */ 472 @Override 473 public void onScanResult(final ScanResult scanResult) { 474 if (VDBG) Log.d(TAG, "onScanResult() - " + scanResult.toString()); 475 476 // Check null in case the scan has been stopped 477 synchronized (this) { 478 if (mScannerId <= 0) return; 479 } 480 Handler handler = new Handler(Looper.getMainLooper()); 481 handler.post(new Runnable() { 482 @Override 483 public void run() { 484 mScanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_ALL_MATCHES, scanResult); 485 } 486 }); 487 } 488 489 @Override 490 public void onBatchScanResults(final List<ScanResult> results) { 491 Handler handler = new Handler(Looper.getMainLooper()); 492 handler.post(new Runnable() { 493 @Override 494 public void run() { 495 mScanCallback.onBatchScanResults(results); 496 } 497 }); 498 } 499 500 @Override 501 public void onFoundOrLost(final boolean onFound, final ScanResult scanResult) { 502 if (VDBG) { 503 Log.d(TAG, "onFoundOrLost() - onFound = " + onFound + " " + scanResult.toString()); 504 } 505 506 // Check null in case the scan has been stopped 507 synchronized (this) { 508 if (mScannerId <= 0) { 509 return; 510 } 511 } 512 Handler handler = new Handler(Looper.getMainLooper()); 513 handler.post(new Runnable() { 514 @Override 515 public void run() { 516 if (onFound) { 517 mScanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_FIRST_MATCH, 518 scanResult); 519 } else { 520 mScanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_MATCH_LOST, 521 scanResult); 522 } 523 } 524 }); 525 } 526 527 @Override 528 public void onScanManagerErrorCallback(final int errorCode) { 529 if (VDBG) { 530 Log.d(TAG, "onScanManagerErrorCallback() - errorCode = " + errorCode); 531 } 532 synchronized (this) { 533 if (mScannerId <= 0) { 534 return; 535 } 536 } 537 postCallbackError(mScanCallback, errorCode); 538 } 539 } 540 541 private int postCallbackErrorOrReturn(final ScanCallback callback, final int errorCode) { 542 if (callback == null) { 543 return errorCode; 544 } else { 545 postCallbackError(callback, errorCode); 546 return ScanCallback.NO_ERROR; 547 } 548 } 549 550 private void postCallbackError(final ScanCallback callback, final int errorCode) { 551 mHandler.post(new Runnable() { 552 @Override 553 public void run() { 554 callback.onScanFailed(errorCode); 555 } 556 }); 557 } 558 559 private boolean isSettingsConfigAllowedForScan(ScanSettings settings) { 560 if (mBluetoothAdapter.isOffloadedFilteringSupported()) { 561 return true; 562 } 563 final int callbackType = settings.getCallbackType(); 564 // Only support regular scan if no offloaded filter support. 565 if (callbackType == ScanSettings.CALLBACK_TYPE_ALL_MATCHES 566 && settings.getReportDelayMillis() == 0) { 567 return true; 568 } 569 return false; 570 } 571 572 private boolean isSettingsAndFilterComboAllowed(ScanSettings settings, 573 List<ScanFilter> filterList) { 574 final int callbackType = settings.getCallbackType(); 575 // If onlost/onfound is requested, a non-empty filter is expected 576 if ((callbackType & (ScanSettings.CALLBACK_TYPE_FIRST_MATCH 577 | ScanSettings.CALLBACK_TYPE_MATCH_LOST)) != 0) { 578 if (filterList == null) { 579 return false; 580 } 581 for (ScanFilter filter : filterList) { 582 if (filter.isAllFieldsEmpty()) { 583 return false; 584 } 585 } 586 } 587 return true; 588 } 589 590 private boolean isHardwareResourcesAvailableForScan(ScanSettings settings) { 591 final int callbackType = settings.getCallbackType(); 592 if ((callbackType & ScanSettings.CALLBACK_TYPE_FIRST_MATCH) != 0 593 || (callbackType & ScanSettings.CALLBACK_TYPE_MATCH_LOST) != 0) { 594 // For onlost/onfound, we required hw support be available 595 return (mBluetoothAdapter.isOffloadedFilteringSupported() 596 && mBluetoothAdapter.isHardwareTrackingFiltersAvailable()); 597 } 598 return true; 599 } 600 } 601