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