Home | History | Annotate | Download | only in le
      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.RequiresPermission;
     21 import android.annotation.SystemApi;
     22 import android.app.ActivityThread;
     23 import android.bluetooth.BluetoothAdapter;
     24 import android.bluetooth.BluetoothGatt;
     25 import android.bluetooth.BluetoothGattCallbackWrapper;
     26 import android.bluetooth.IBluetoothGatt;
     27 import android.bluetooth.IBluetoothManager;
     28 import android.os.Handler;
     29 import android.os.Looper;
     30 import android.os.ParcelUuid;
     31 import android.os.RemoteException;
     32 import android.os.WorkSource;
     33 import android.util.Log;
     34 
     35 import java.util.ArrayList;
     36 import java.util.HashMap;
     37 import java.util.List;
     38 import java.util.Map;
     39 import java.util.UUID;
     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     private final IBluetoothManager mBluetoothManager;
     61     private final Handler mHandler;
     62     private BluetoothAdapter mBluetoothAdapter;
     63     private final Map<ScanCallback, BleScanCallbackWrapper> mLeScanClients;
     64 
     65     /**
     66      * Use {@link BluetoothAdapter#getBluetoothLeScanner()} instead.
     67      *
     68      * @param bluetoothManager BluetoothManager that conducts overall Bluetooth Management.
     69      * @hide
     70      */
     71     public BluetoothLeScanner(IBluetoothManager bluetoothManager) {
     72         mBluetoothManager = bluetoothManager;
     73         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
     74         mHandler = new Handler(Looper.getMainLooper());
     75         mLeScanClients = new HashMap<ScanCallback, BleScanCallbackWrapper>();
     76     }
     77 
     78     /**
     79      * Start Bluetooth LE scan with default parameters and no filters. The scan results will be
     80      * delivered through {@code callback}.
     81      * <p>
     82      * Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission.
     83      * An app must hold
     84      * {@link android.Manifest.permission#ACCESS_COARSE_LOCATION ACCESS_COARSE_LOCATION} or
     85      * {@link android.Manifest.permission#ACCESS_FINE_LOCATION ACCESS_FINE_LOCATION} permission
     86      * in order to get results.
     87      *
     88      * @param callback Callback used to deliver scan results.
     89      * @throws IllegalArgumentException If {@code callback} is null.
     90      */
     91     @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
     92     public void startScan(final ScanCallback callback) {
     93         startScan(null, new ScanSettings.Builder().build(), callback);
     94     }
     95 
     96     /**
     97      * Start Bluetooth LE scan. The scan results will be delivered through {@code callback}.
     98      * <p>
     99      * Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission.
    100      * An app must hold
    101      * {@link android.Manifest.permission#ACCESS_COARSE_LOCATION ACCESS_COARSE_LOCATION} or
    102      * {@link android.Manifest.permission#ACCESS_FINE_LOCATION ACCESS_FINE_LOCATION} permission
    103      * in order to get results.
    104      *
    105      * @param filters {@link ScanFilter}s for finding exact BLE devices.
    106      * @param settings Settings for the scan.
    107      * @param callback Callback used to deliver scan results.
    108      * @throws IllegalArgumentException If {@code settings} or {@code callback} is null.
    109      */
    110     @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
    111     public void startScan(List<ScanFilter> filters, ScanSettings settings,
    112             final ScanCallback callback) {
    113         startScan(filters, settings, null, callback, null);
    114     }
    115 
    116     /**
    117      * Start Bluetooth LE scan. Same as {@link #startScan(ScanCallback)} but allows the caller to
    118      * specify on behalf of which application(s) the work is being done.
    119      *
    120      * @param workSource {@link WorkSource} identifying the application(s) for which to blame for
    121      *                   the scan.
    122      * @param callback Callback used to deliver scan results.
    123      * @hide
    124      */
    125     @SystemApi
    126     @RequiresPermission(allOf = {
    127             Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.UPDATE_DEVICE_STATS })
    128     public void startScanFromSource(final WorkSource workSource, final ScanCallback callback) {
    129         startScanFromSource(null, new ScanSettings.Builder().build(), workSource, callback);
    130     }
    131 
    132     /**
    133      * Start Bluetooth LE scan. Same as {@link #startScan(List, ScanSettings, ScanCallback)} but
    134      * allows the caller to specify on behalf of which application(s) the work is being done.
    135      *
    136      * @param filters {@link ScanFilter}s for finding exact BLE devices.
    137      * @param settings Settings for the scan.
    138      * @param workSource {@link WorkSource} identifying the application(s) for which to blame for
    139      *                   the scan.
    140      * @param callback Callback used to deliver scan results.
    141      * @hide
    142      */
    143     @SystemApi
    144     @RequiresPermission(allOf = {
    145             Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.UPDATE_DEVICE_STATS })
    146     public void startScanFromSource(List<ScanFilter> filters, ScanSettings settings,
    147                                     final WorkSource workSource, final ScanCallback callback) {
    148         startScan(filters, settings, workSource, callback, null);
    149     }
    150 
    151     private void startScan(List<ScanFilter> filters, ScanSettings settings,
    152                            final WorkSource workSource, final ScanCallback callback,
    153                            List<List<ResultStorageDescriptor>> resultStorages) {
    154         BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter);
    155         if (callback == null) {
    156             throw new IllegalArgumentException("callback is null");
    157         }
    158         if (settings == null) {
    159             throw new IllegalArgumentException("settings is null");
    160         }
    161         synchronized (mLeScanClients) {
    162             if (mLeScanClients.containsKey(callback)) {
    163                 postCallbackError(callback, ScanCallback.SCAN_FAILED_ALREADY_STARTED);
    164                 return;
    165             }
    166             IBluetoothGatt gatt;
    167             try {
    168                 gatt = mBluetoothManager.getBluetoothGatt();
    169             } catch (RemoteException e) {
    170                 gatt = null;
    171             }
    172             if (gatt == null) {
    173                 postCallbackError(callback, ScanCallback.SCAN_FAILED_INTERNAL_ERROR);
    174                 return;
    175             }
    176             if (!isSettingsConfigAllowedForScan(settings)) {
    177                 postCallbackError(callback,
    178                         ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED);
    179                 return;
    180             }
    181             if (!isHardwareResourcesAvailableForScan(settings)) {
    182                 postCallbackError(callback,
    183                         ScanCallback.SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES);
    184                 return;
    185             }
    186             if (!isSettingsAndFilterComboAllowed(settings, filters)) {
    187                 postCallbackError(callback,
    188                         ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED);
    189                 return;
    190             }
    191             BleScanCallbackWrapper wrapper = new BleScanCallbackWrapper(gatt, filters,
    192                     settings, workSource, callback, resultStorages);
    193             wrapper.startRegisteration();
    194         }
    195     }
    196 
    197     /**
    198      * Stops an ongoing Bluetooth LE scan.
    199      * <p>
    200      * Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission.
    201      *
    202      * @param callback
    203      */
    204     @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
    205     public void stopScan(ScanCallback callback) {
    206         BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter);
    207         synchronized (mLeScanClients) {
    208             BleScanCallbackWrapper wrapper = mLeScanClients.remove(callback);
    209             if (wrapper == null) {
    210                 if (DBG) Log.d(TAG, "could not find callback wrapper");
    211                 return;
    212             }
    213             wrapper.stopLeScan();
    214         }
    215     }
    216 
    217     /**
    218      * Flush pending batch scan results stored in Bluetooth controller. This will return Bluetooth
    219      * LE scan results batched on bluetooth controller. Returns immediately, batch scan results data
    220      * will be delivered through the {@code callback}.
    221      *
    222      * @param callback Callback of the Bluetooth LE Scan, it has to be the same instance as the one
    223      *            used to start scan.
    224      */
    225     public void flushPendingScanResults(ScanCallback callback) {
    226         BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter);
    227         if (callback == null) {
    228             throw new IllegalArgumentException("callback cannot be null!");
    229         }
    230         synchronized (mLeScanClients) {
    231             BleScanCallbackWrapper wrapper = mLeScanClients.get(callback);
    232             if (wrapper == null) {
    233                 return;
    234             }
    235             wrapper.flushPendingBatchResults();
    236         }
    237     }
    238 
    239     /**
    240      * Start truncated scan.
    241      *
    242      * @hide
    243      */
    244     @SystemApi
    245     public void startTruncatedScan(List<TruncatedFilter> truncatedFilters, ScanSettings settings,
    246             final ScanCallback callback) {
    247         int filterSize = truncatedFilters.size();
    248         List<ScanFilter> scanFilters = new ArrayList<ScanFilter>(filterSize);
    249         List<List<ResultStorageDescriptor>> scanStorages =
    250                 new ArrayList<List<ResultStorageDescriptor>>(filterSize);
    251         for (TruncatedFilter filter : truncatedFilters) {
    252             scanFilters.add(filter.getFilter());
    253             scanStorages.add(filter.getStorageDescriptors());
    254         }
    255         startScan(scanFilters, settings, null, callback, scanStorages);
    256     }
    257 
    258     /**
    259      * Cleans up scan clients. Should be called when bluetooth is down.
    260      *
    261      * @hide
    262      */
    263     public void cleanup() {
    264         mLeScanClients.clear();
    265     }
    266 
    267     /**
    268      * Bluetooth GATT interface callbacks
    269      */
    270     private class BleScanCallbackWrapper extends BluetoothGattCallbackWrapper {
    271         private static final int REGISTRATION_CALLBACK_TIMEOUT_MILLIS = 2000;
    272 
    273         private final ScanCallback mScanCallback;
    274         private final List<ScanFilter> mFilters;
    275         private final WorkSource mWorkSource;
    276         private ScanSettings mSettings;
    277         private IBluetoothGatt mBluetoothGatt;
    278         private List<List<ResultStorageDescriptor>> mResultStorages;
    279 
    280         // mLeHandle 0: not registered
    281         // -1: scan stopped or registration failed
    282         // > 0: registered and scan started
    283         private int mClientIf;
    284 
    285         public BleScanCallbackWrapper(IBluetoothGatt bluetoothGatt,
    286                 List<ScanFilter> filters, ScanSettings settings,
    287                 WorkSource workSource, ScanCallback scanCallback,
    288                 List<List<ResultStorageDescriptor>> resultStorages) {
    289             mBluetoothGatt = bluetoothGatt;
    290             mFilters = filters;
    291             mSettings = settings;
    292             mWorkSource = workSource;
    293             mScanCallback = scanCallback;
    294             mClientIf = 0;
    295             mResultStorages = resultStorages;
    296         }
    297 
    298         public void startRegisteration() {
    299             synchronized (this) {
    300                 // Scan stopped.
    301                 if (mClientIf == -1) return;
    302                 try {
    303                     UUID uuid = UUID.randomUUID();
    304                     mBluetoothGatt.registerClient(new ParcelUuid(uuid), this);
    305                     wait(REGISTRATION_CALLBACK_TIMEOUT_MILLIS);
    306                 } catch (InterruptedException | RemoteException e) {
    307                     Log.e(TAG, "application registeration exception", e);
    308                     postCallbackError(mScanCallback, ScanCallback.SCAN_FAILED_INTERNAL_ERROR);
    309                 }
    310                 if (mClientIf > 0) {
    311                     mLeScanClients.put(mScanCallback, this);
    312                 } else {
    313                     // Registration timed out or got exception, reset clientIf to -1 so no
    314                     // subsequent operations can proceed.
    315                     if (mClientIf == 0) mClientIf = -1;
    316                     postCallbackError(mScanCallback,
    317                             ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED);
    318                 }
    319             }
    320         }
    321 
    322         public void stopLeScan() {
    323             synchronized (this) {
    324                 if (mClientIf <= 0) {
    325                     Log.e(TAG, "Error state, mLeHandle: " + mClientIf);
    326                     return;
    327                 }
    328                 try {
    329                     mBluetoothGatt.stopScan(mClientIf, false);
    330                     mBluetoothGatt.unregisterClient(mClientIf);
    331                 } catch (RemoteException e) {
    332                     Log.e(TAG, "Failed to stop scan and unregister", e);
    333                 }
    334                 mClientIf = -1;
    335             }
    336         }
    337 
    338         void flushPendingBatchResults() {
    339             synchronized (this) {
    340                 if (mClientIf <= 0) {
    341                     Log.e(TAG, "Error state, mLeHandle: " + mClientIf);
    342                     return;
    343                 }
    344                 try {
    345                     mBluetoothGatt.flushPendingBatchResults(mClientIf, false);
    346                 } catch (RemoteException e) {
    347                     Log.e(TAG, "Failed to get pending scan results", e);
    348                 }
    349             }
    350         }
    351 
    352         /**
    353          * Application interface registered - app is ready to go
    354          */
    355         @Override
    356         public void onClientRegistered(int status, int clientIf) {
    357             Log.d(TAG, "onClientRegistered() - status=" + status +
    358                     " clientIf=" + clientIf + " mClientIf=" + mClientIf);
    359             synchronized (this) {
    360                 if (status == BluetoothGatt.GATT_SUCCESS) {
    361                     try {
    362                         if (mClientIf == -1) {
    363                             // Registration succeeds after timeout, unregister client.
    364                             mBluetoothGatt.unregisterClient(clientIf);
    365                         } else {
    366                             mClientIf = clientIf;
    367                             mBluetoothGatt.startScan(mClientIf, false, mSettings, mFilters,
    368                                     mWorkSource, mResultStorages,
    369                                     ActivityThread.currentOpPackageName());
    370                         }
    371                     } catch (RemoteException e) {
    372                         Log.e(TAG, "fail to start le scan: " + e);
    373                         mClientIf = -1;
    374                     }
    375                 } else {
    376                     // registration failed
    377                     mClientIf = -1;
    378                 }
    379                 notifyAll();
    380             }
    381         }
    382 
    383         /**
    384          * Callback reporting an LE scan result.
    385          *
    386          * @hide
    387          */
    388         @Override
    389         public void onScanResult(final ScanResult scanResult) {
    390             if (VDBG) Log.d(TAG, "onScanResult() - " + scanResult.toString());
    391 
    392             // Check null in case the scan has been stopped
    393             synchronized (this) {
    394                 if (mClientIf <= 0) return;
    395             }
    396             Handler handler = new Handler(Looper.getMainLooper());
    397             handler.post(new Runnable() {
    398                 @Override
    399                 public void run() {
    400                     mScanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_ALL_MATCHES, scanResult);
    401                 }
    402             });
    403 
    404         }
    405 
    406         @Override
    407         public void onBatchScanResults(final List<ScanResult> results) {
    408             Handler handler = new Handler(Looper.getMainLooper());
    409             handler.post(new Runnable() {
    410                 @Override
    411                 public void run() {
    412                     mScanCallback.onBatchScanResults(results);
    413                 }
    414             });
    415         }
    416 
    417         @Override
    418         public void onFoundOrLost(final boolean onFound, final ScanResult scanResult) {
    419             if (VDBG) {
    420                 Log.d(TAG, "onFoundOrLost() - onFound = " + onFound +
    421                         " " + scanResult.toString());
    422             }
    423 
    424             // Check null in case the scan has been stopped
    425             synchronized (this) {
    426                 if (mClientIf <= 0)
    427                     return;
    428             }
    429             Handler handler = new Handler(Looper.getMainLooper());
    430             handler.post(new Runnable() {
    431                     @Override
    432                 public void run() {
    433                     if (onFound) {
    434                         mScanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_FIRST_MATCH,
    435                                 scanResult);
    436                     } else {
    437                         mScanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_MATCH_LOST,
    438                                 scanResult);
    439                     }
    440                 }
    441             });
    442         }
    443 
    444         @Override
    445         public void onScanManagerErrorCallback(final int errorCode) {
    446             if (VDBG) {
    447                 Log.d(TAG, "onScanManagerErrorCallback() - errorCode = " + errorCode);
    448             }
    449             synchronized (this) {
    450                 if (mClientIf <= 0)
    451                     return;
    452             }
    453             postCallbackError(mScanCallback, errorCode);
    454         }
    455     }
    456 
    457     private void postCallbackError(final ScanCallback callback, final int errorCode) {
    458         mHandler.post(new Runnable() {
    459             @Override
    460             public void run() {
    461                 callback.onScanFailed(errorCode);
    462             }
    463         });
    464     }
    465 
    466     private boolean isSettingsConfigAllowedForScan(ScanSettings settings) {
    467         if (mBluetoothAdapter.isOffloadedFilteringSupported()) {
    468             return true;
    469         }
    470         final int callbackType = settings.getCallbackType();
    471         // Only support regular scan if no offloaded filter support.
    472         if (callbackType == ScanSettings.CALLBACK_TYPE_ALL_MATCHES
    473                 && settings.getReportDelayMillis() == 0) {
    474             return true;
    475         }
    476         return false;
    477     }
    478 
    479     private boolean isSettingsAndFilterComboAllowed(ScanSettings settings,
    480                         List <ScanFilter> filterList) {
    481         final int callbackType = settings.getCallbackType();
    482         // If onlost/onfound is requested, a non-empty filter is expected
    483         if ((callbackType & (ScanSettings.CALLBACK_TYPE_FIRST_MATCH
    484                         | ScanSettings.CALLBACK_TYPE_MATCH_LOST)) != 0) {
    485             if (filterList == null) {
    486                 return false;
    487             }
    488             for (ScanFilter filter : filterList) {
    489                 if (filter.isAllFieldsEmpty()) {
    490                     return false;
    491                 }
    492             }
    493         }
    494         return true;
    495     }
    496 
    497     private boolean isHardwareResourcesAvailableForScan(ScanSettings settings) {
    498         final int callbackType = settings.getCallbackType();
    499         if ((callbackType & ScanSettings.CALLBACK_TYPE_FIRST_MATCH) != 0
    500                 || (callbackType & ScanSettings.CALLBACK_TYPE_MATCH_LOST) != 0) {
    501             // For onlost/onfound, we required hw support be available
    502             return (mBluetoothAdapter.isOffloadedFilteringSupported() &&
    503                     mBluetoothAdapter.isHardwareTrackingFiltersAvailable());
    504         }
    505         return true;
    506     }
    507 }
    508