最近在进行一些 Android app 蓝牙相关的开发,需要前台长时间高频率的扫描 BLE 设备,使用了 Android 提供的 SCAN_MODE_LOW_LATENCY 扫描模式,扫描 30 分钟后出现了扫描不到 BLE 设备的情况。

问题

使用自己开发的 Android app ,蓝牙扫描代码为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanSettings;

public class BluetoothScanManager {
    private static BluetoothLeScanner bluetoothLeScanner;
    private static ScanCallback scanCallback;
    private static ScanSettings scanSettings;

    public BluetoothScanManager(BluetoothLeScanner bluetoothLeScanner, ScanCallback scanCallback) {
        BluetoothScanManager.bluetoothLeScanner = bluetoothLeScanner;
        BluetoothScanManager.scanCallback = scanCallback;
        BluetoothScanManager.scanSettings = new ScanSettings.Builder()
                .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
		.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
                .build();
    }

    private static void startScan() {
        bluetoothLeScanner.startScan(null, scanSettings, scanCallback);
    }

    private static void stopScan() {
        bluetoothLeScanner.stopScan(scanCallback);
    }
}
   

使用的测试手机的 Android 版本为 7.0 。程序开始扫描的前 30 分钟可以正常的高频扫描到 BLE 设备,30 分钟后一个 BLE 设备也扫描不到了。

查找原因

根据搜索引擎检索到的相关信息查看 Android 蓝牙相关源码,在 180 行找到了处理 BLE 扫描操作的类:

1
2
    // Handler class that handles BLE scan operations.
    private class ClientHandler extends Handler {

其中用来接收消息的函数为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
        @Override
        public void handleMessage(Message msg) {
            ScanClient client = (ScanClient) msg.obj;
            switch (msg.what) {
                case MSG_START_BLE_SCAN:
                    handleStartScan(client);
                    break;
                case MSG_STOP_BLE_SCAN:
                    handleStopScan(client);
                    break;
                case MSG_FLUSH_BATCH_RESULTS:
                    handleFlushBatchResults(client);
                    break;
                case MSG_SCAN_TIMEOUT:
                    mScanNative.regularScanTimeout();
                    break;
                default:
                    // Shouldn't happen.
                    Log.e(TAG, "received an unkown message : " + msg.what);
            }
        }

当收到开始 BLE 扫描的信号后会执行 handleStartScan 函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
        void handleStartScan(ScanClient client) {
            Utils.enforceAdminPermission(mService);
            logd("handling starting scan");
            if (!isScanSupported(client)) {
                Log.e(TAG, "Scan settings not supported");
                return;
            }
            if (mRegularScanClients.contains(client) || mBatchClients.contains(client)) {
                Log.e(TAG, "Scan already started");
                return;
            }
            // Begin scan operations.
            if (isBatchClient(client)) {
                mBatchClients.add(client);
                mScanNative.startBatchScan(client);
            } else {
                mRegularScanClients.add(client);
                mScanNative.startRegularScan(client);
                if (!mScanNative.isOpportunisticScanClient(client)) {
                    mScanNative.configureRegularScanParams();
                    if (!mScanNative.isFirstMatchScanClient(client)) {
                        Message msg = mHandler.obtainMessage(MSG_SCAN_TIMEOUT);
                        msg.obj = client;
                        // Only one timeout message should exist at any time
                        mHandler.removeMessages(SCAN_TIMEOUT_MS);
                        mHandler.sendMessageDelayed(msg, SCAN_TIMEOUT_MS);
                    }
                }
                // Update BatteryStats with this workload.
                try {
                    mBatteryStats.noteBleScanStarted(client.workSource);
                } catch (RemoteException e) {
                    /* ignore */
                }
            }
        }

其中重要的是 // Begin scan operations 的 else 代码块中的第二个 if 代码块 ,首先会进行一个 isFirstMatchScanClient 函数的判断。函数在 531 行:

1
2
3
        private boolean isFirstMatchScanClient(ScanClient client) {
            return (client.settings.getCallbackType() & ScanSettings.CALLBACK_TYPE_FIRST_MATCH) != 0;
        }

这个函数会将 client 的 CallbackType 和 ScanSettings.CALLBACK_TYPE_FIRST_MATCH 进行比对看是否一致,不一致的话会创建一个超时消息并使用 sendMessageDelayed 函数延迟 SCAN_TIMEOUT_MS 后发送。SCAN_TIMEOUT_MS 的值在 72 行:

1
2
    // Maximum msec before scan gets downgraded to opportunistic
    private static final int SCAN_TIMEOUT_MS = 30 * 60 * 1000;

这个值就和遇到的 30 分钟后扫描不到 BLE 设备对上了。这个延迟 30 分钟后发送的消息会触发上面的 handleMessage 中的

1
2
3
                case MSG_SCAN_TIMEOUT:
                    mScanNative.regularScanTimeout();
                    break;

regularScanTimeout 函数在 674 行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
        void regularScanTimeout() {
            for (ScanClient client : mRegularScanClients) {
                if (!isOpportunisticScanClient(client) && !isFirstMatchScanClient(client)) {
                    logd("clientIf set to scan opportunisticly: " + client.clientIf);
                    setOpportunisticScanClient(client);
                    client.stats.setScanTimeout();
                }
            }
            // The scan should continue for background scans
            configureRegularScanParams();
            if (numRegularScanClients() == 0) {
                logd("stop scan");
                gattClientScanNative(false);
            }
        }
        void setOpportunisticScanClient(ScanClient client) {
            // TODO: Add constructor to ScanSettings.Builder
            // that can copy values from an existing ScanSettings object
            ScanSettings.Builder builder = new ScanSettings.Builder();
            ScanSettings settings = client.settings;
            builder.setScanMode(ScanSettings.SCAN_MODE_OPPORTUNISTIC);
            builder.setCallbackType(settings.getCallbackType());
            builder.setScanResultType(settings.getScanResultType());
            builder.setReportDelay(settings.getReportDelayMillis());
            builder.setNumOfMatches(settings.getNumOfMatches());
            client.settings = builder.build();
        }

可以看到 regularScanTimeout 会把我们的蓝牙扫描模式设置为 ScanSettings.SCAN_MODE_OPPORTUNISTIC 。Android developer 的文档对这个扫描模式的描述为:

A special Bluetooth LE scan mode. Applications using this scan mode will passively listen for other scan results without starting BLE scans themselves.

根据文档,这种模式仅在其他应用已经启用了蓝牙扫描的情况下才会工作。也就是说,扫描不会主动进行,只会依赖其他应用的扫描行为。

至此,问题基本查清了。总结一下,当我们设置的 BLE 扫描的 CallbackType 不为 ScanSettings.CALLBACK_TYPE_FIRST_MATCH 时,BLE 的 ScanManager 会自动为我们 BLE 扫描的 client 增加一个 30 分钟后的超时消息发送,这个超时消息会改变我们的蓝牙扫描模式并把我们 BLE 扫描的 client 的状态设置为超时,导致我们搜索不到 BLE 设备。

解决方式

由于这个 30 分钟的限制只针对我们 BLE 扫描的 client 。所以我们可以在开始扫描的同时创建一个定时任务,在一段时间(<30 分钟),比如 29 分钟后停止蓝牙扫描,等待一秒,然后再开始蓝牙扫描。大概代码为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanSettings;
import android.os.Handler;
import android.os.Looper;

public class BluetoothScanManager {
    private static BluetoothLeScanner bluetoothLeScanner;
    private static ScanCallback scanCallback;
    private static ScanSettings scanSettings;
    private static Handler handler;
    private static final long SCAN_DURATION = 29 * 60 * 1000; // 29 minutes in milliseconds

    public BluetoothScanManager(BluetoothLeScanner bluetoothLeScanner, ScanCallback scanCallback) {
        BluetoothScanManager.bluetoothLeScanner = bluetoothLeScanner;
        BluetoothScanManager.scanCallback = scanCallback;
        BluetoothScanManager.handler = new Handler(Looper.getMainLooper());
        BluetoothScanManager.scanSettings = new ScanSettings.Builder()
                .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
                .build();
    }

    private static void startScan() {
        bluetoothLeScanner.startScan(null, scanSettings, scanCallback);
        handler.postDelayed(() -> {
            stopScan();
            handler.postDelayed(BluetoothScanManager::startScan, 1000); // Wait 1 second before restarting scan
        }, SCAN_DURATION);
    }

    private static void stopScan() {
        bluetoothLeScanner.stopScan(scanCallback);
    }
}

修改后测试了两个小时,可以不间断的搜索到 BLE 设备。具体的 SCAN_DURATION 可能会根据不同厂商或不同安卓版本来修改。目前最新的 main 分支上的相关代码做了修改,但这个参数的值并没有变,详见 AdapterService.java

为什么会有这个限制

AOSP code review system 上增加这个限制的 commit 消息为:

Add protection against LE scanning abuse

Added two checks to prevent abuse. The first check ensures that an app doesn’t scan too frequently in a certain time period. It is allowed to scan again after its oldest scan exceedes said time period. The second check ensures that an app doesn’t scan for too long. Upon starting a scan, this code waits a certain amount of time. If the app is still scanning by that point, this code stops the scan and forces the app to use opportunistic scanning instead.

看来是为了防止 BLE 扫描的滥用。而且一开始的超时时间设置的为 5 分钟,后边的一次提交才改为 30 分钟。但这些限制并没有在开发者文档中相关的地方标注出来。

参考链接