无需手动调参!这款 Android 智能蓝牙扫描工具,自动平衡效率与功耗

9 阅读8分钟

BleScanManager 功能介绍

BleScanManager 是一款适用于 Android 设备的智能蓝牙扫描管理工具,支持灵活扫描控制与环境自适应调节,简化 BLE 设备扫描开发流程。

核心功能

  • 三种扫描模式可选,满足不同场景需求:激进模式(快速发现设备)、平衡模式(性能与功耗兼顾)、低功耗模式(节能优先)。
  • 支持周期性扫描,可自定义单次扫描时长和扫描间隔,减少不必要的资源占用。
  • 智能环境适配,根据设备屏幕状态、电量水平、充电状态及连接阶段,自动切换扫描模式,平衡扫描效率与功耗。
  • 丰富事件回调,实时反馈设备发现、扫描模式变更、连接 / 断开状态及扫描失败等信息,便于业务层处理。
  • 灵活过滤配置,支持按设备名称、MAC 地址、服务 UUID 创建扫描过滤器,精准筛选目标设备。
  • 适配 Android 不同系统版本权限要求,兼容低版本与 Android 12+ 蓝牙权限机制。

完整代码(BleScanManager.java)

package com.example.ble;

import android.Manifest;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.bluetooth.le.*;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.os.BatteryManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.ParcelUuid;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.*;

/**
 * BleScanManager
 *
 * 功能:
 * - 智能切换 ScanSettings(AGGRESSIVE / BALANCED / LOW_POWER)
 * - 周期性(burst)扫描(可配置:burstDuration / cycleInterval)
 * - 根据屏幕(on/off)与电量自动降级或升级扫描模式
 * - 简单事件总线(Listener)回调:设备发现、模式变更、连接/断开
 *
 * 使用方式示例:
 *   BleScanManager manager = new BleScanManager(appContext);
 *   manager.addListener(myListener);
 *   manager.start(); // 注册接收器,允许智能策略生效(但不会自动开始扫描,需调用 startScan())
 *   manager.startScan(BleScanManager.MODE_AGGRESSIVE, 10000, null);
 *   ...
 *   manager.stopScan();
 *   manager.stop(); // 注销接收器,停止定时任务
 */ 
public class BleScanManager {

    private static final String TAG = "BleScanManager";

    // Scan modes
    public static final int MODE_AGGRESSIVE = 1;
    public static final int MODE_BALANCED = 2;
    public static final int MODE_LOW_POWER = 3;

    private final Context context;
    private final BluetoothManager bluetoothManager;
    private final BluetoothAdapter bluetoothAdapter;
    private final Handler mainHandler = new Handler(Looper.getMainLooper());

    // BLE scanner
    private BluetoothLeScanner scanner;
    private ScanCallback scanCallback;

    // state
    private volatile boolean isScanning = false;
    private volatile int currentMode = MODE_BALANCED;
    private volatile boolean isConnectedPhase = false; // true 表示正在连接/已连接阶段
    private volatile boolean screenOn = true;
    private volatile int batteryPercent = 100;
    private volatile boolean charging = false;

    // periodic scanning (burst scan) config & scheduler
    private ScheduledExecutorService scheduler;
    private ScheduledFuture<?> periodicFuture;
    private long burstDurationMs = 5_000L;   // 扫描持续时长(默认5s)
    private long cycleIntervalMs = 30_000L;  // 扫描周期间隔(默认30s)

    // listeners (simple event bus)
    private final List<BleScanListener> listeners = new CopyOnWriteArrayList<>();

    // scan filters
    private List<ScanFilter> activeFilters = null;

    // public constructor
    public BleScanManager(@NonNull Context context) {
        this.context = context.getApplicationContext();
        this.bluetoothManager = (BluetoothManager) this.context.getSystemService(Context.BLUETOOTH_SERVICE);
        this.bluetoothAdapter = bluetoothManager != null ? bluetoothManager.getAdapter() : null;
        this.scanner = bluetoothAdapter != null ? bluetoothAdapter.getBluetoothLeScanner() : null;
        this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
            Thread t = new Thread(r);
            t.setName("BleScanManager-Scheduler");
            t.setDaemon(true);
            return t;
        });

        // 尝试读取当前屏幕/电量状态(非必需,接收器会及时更新)
        queryInitialBatteryAndScreenState();
    }

    // ---------------------------
    // Lifecycle: start / stop manager (register receivers etc.)
    // ---------------------------
    public void start() {
        registerReceivers();
    }

    public void stop() {
        stopPeriodicScan();
        stopScan();
        unregisterReceivers();
        if (scheduler != null && !scheduler.isShutdown()) {
            scheduler.shutdownNow();
        }
    }

    // ---------------------------
    // Permission checker: caller must request runtime permissions
    // ---------------------------
    private boolean hasPermission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            // Android 12+
            return ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED &&
                    ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED;
        } else {
            // Pre-Android 12: location may be required for scan (depending on target SDK)
            return ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
                    ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;
        }
    }

    // ---------------------------
    // Public API: startScan / stopScan / startPeriodicScan / stopPeriodicScan
    // ---------------------------

    /**
     * 直接开始一次扫描(非周期性),会在 durationMs 后自动停止。
     *
     * @param mode       想要的扫描模式(MODE_AGGRESSIVE / MODE_BALANCED / MODE_LOW_POWER)
     * @param durationMs 扫描持续时间(ms)
     * @param filters    可选过滤器(null 表示不使用过滤器)
     */
    @SuppressLint("MissingPermission")
    public synchronized void startScan(int mode, long durationMs, List<ScanFilter> filters) {
        if (!hasPermission()) {
            Log.w(TAG, "Missing BLE scan permissions. Caller should request runtime permissions.");
            return;
        }

        if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
            Log.w(TAG, "Bluetooth adapter not available or disabled.");
            return;
        }

        stopScan(); // 若已有扫描则先停止(切换设置)
        updateScannerIfNeeded();

        this.currentMode = mode;
        this.activeFilters = filters;

        ScanSettings settings = buildSettingsForMode(mode);

        if (scanCallback == null) {
            scanCallback = new InternalScanCallback();
        }

        try {
            if (filters == null || filters.isEmpty()) {
                scanner.startScan(null, settings, scanCallback);
            } else {
                scanner.startScan(filters, settings, scanCallback);
            }
            isScanning = true;
            for (BleScanListener l : listeners) {
                l.onScanModeChanged(mode);
            }
            Log.i(TAG, "startScan: mode=" + mode + " durationMs=" + durationMs);
            // 自动停止
            mainHandler.postDelayed(this::stopScan, Math.max(1000L, durationMs));
        } catch (Exception e) {
            Log.e(TAG, "startScan failed: " + e.getMessage(), e);
        }
    }

    /**
     * 停止当前扫描(无论周期性还是一次性)
     */
    @SuppressLint("MissingPermission")
    public synchronized void stopScan() {
        if (!isScanning) return;
        try {
            if (scanner != null && scanCallback != null) {
                scanner.stopScan(scanCallback);
            }
        } catch (Exception e) {
            Log.e(TAG, "stopScan error: " + e.getMessage(), e);
        } finally {
            isScanning = false;
        }
    }

    /**
     * 启动周期性(burst)扫描:每 cycleIntervalMs 周期执行一次 burstDurationMs 的扫描
     * 如果已启动周期性扫描,会先停止旧的再启动新的。
     *
     * @param mode            期望扫描模式
     * @param burstDurationMs 扫描持续时长(ms)
     * @param cycleIntervalMs 周期间隔(ms)
     * @param filters         过滤器(可空)
     */
    public synchronized void startPeriodicScan(int mode, long burstDurationMs, long cycleIntervalMs, List<ScanFilter> filters) {
        // 保存参数
        this.burstDurationMs = burstDurationMs;
        this.cycleIntervalMs = cycleIntervalMs;
        this.activeFilters = filters;

        // 停止旧的任务
        stopPeriodicScan();

        // 启动新的周期任务
        periodicFuture = scheduler.scheduleAtFixedRate(() -> {
            try {
                // 在主线程启动扫描(避免多线程操作 Android BLE API)
                mainHandler.post(() -> startScan(mode, this.burstDurationMs, this.activeFilters));
            } catch (Exception e) {
                Log.e(TAG, "periodicScan error: " + e.getMessage(), e);
            }
        }, 0, cycleIntervalMs, TimeUnit.MILLISECONDS);

        Log.i(TAG, "startPeriodicScan: mode=" + mode + " burst=" + burstDurationMs + " cycle=" + cycleIntervalMs);
    }

    /**
     * 停止周期性扫描任务(但不会停止正在进行的扫描)
     */
    public synchronized void stopPeriodicScan() {
        if (periodicFuture != null && !periodicFuture.isCancelled()) {
            periodicFuture.cancel(true);
            periodicFuture = null;
            Log.i(TAG, "stopPeriodicScan");
        }
    }

    // ---------------------------
    // Smart switching: 根据屏幕、电量、连接阶段决定最佳mode
    // ---------------------------

    /**
     * 设置是否处于连接/配置阶段(连接阶段会强制转到低功耗模式以避免干扰)
     */
    public void setConnectingPhase(boolean connectingPhase) {
        this.isConnectedPhase = connectingPhase;
        adaptScanModeByEnvironment();
    }

    /**
     * 外部也可以显式调用以强制进入某一策略(例如 UI 上用户手动切换)
     */
    public void forceScanMode(int forcedMode) {
        this.currentMode = forcedMode;
        adaptScanModeByEnvironment();
    }

    /**
     * 基于当前 screenOn / batteryPercent / charging / isConnectedPhase 等信息做智能决策
     * 规则示例(可调整):
     * - 如果正在连接阶段 -> 低功耗(避免干扰)
     * - else if 电量 < 20% && 非充电 -> 低功耗
     * - else if 屏幕熄灭 -> 平衡(或低功耗,视场景)
     * - else 屏幕点亮 && 电量充足 -> 激进
     */
    private void adaptScanModeByEnvironment() {
        int decidedMode = MODE_BALANCED;

        if (isConnectedPhase) {
            decidedMode = MODE_LOW_POWER;
        } else if (!screenOn) {
            // 屏幕熄灭:更倾向于节能
            if (batteryPercent < 25 && !charging) {
                decidedMode = MODE_LOW_POWER;
            } else {
                decidedMode = MODE_BALANCED;
            }
        } else {
            // 屏幕点亮
            if (!charging && batteryPercent < 15) {
                decidedMode = MODE_LOW_POWER;
            } else {
                decidedMode = MODE_AGGRESSIVE;
            }
        }

        if (decidedMode != currentMode) {
            Log.i(TAG, "adaptScanModeByEnvironment: " + currentMode + " -> " + decidedMode);
            currentMode = decidedMode;
            // 重启扫描(如果正在扫描)
            if (isScanning) {
                // 停止并重新start以应用新的设置
                mainHandler.post(() -> {
                    stopScan();
                    // 如果周期扫描正在运行,周期任务会在下个周期自动重新启动扫描,避免重复启动,这里直接启动一次短扫描来切换即时效果
                    startScan(currentMode, burstDurationMs, activeFilters);
                });
            }
            for (BleScanListener l : listeners) {
                l.onScanModeChanged(currentMode);
            }
        }
    }

    // ---------------------------
    // Build ScanSettings 根据 mode
    // ---------------------------
    private ScanSettings buildSettingsForMode(int mode) {
        ScanSettings.Builder builder = new ScanSettings.Builder();
        // 默认实时回调
        builder.setReportDelay(0);

        // Match mode / num of matches:可根据 mode 进一步微调
        switch (mode) {
            case MODE_AGGRESSIVE:
                builder.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY);
                builder.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE);
                builder.setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT);
                builder.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES);
                break;
            case MODE_BALANCED:
                builder.setScanMode(ScanSettings.SCAN_MODE_BALANCED);
                builder.setMatchMode(ScanSettings.MATCH_MODE_STICKY);
                builder.setNumOfMatches(ScanSettings.MATCH_NUM_FEW_ADVERTISEMENT);
                builder.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES);
                break;
            case MODE_LOW_POWER:
            default:
                builder.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER);
                builder.setMatchMode(ScanSettings.MATCH_MODE_STICKY);
                builder.setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT);
                builder.setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH);
                // 把上报延迟设为较大以便批量处理也可选(根据需要)
                // builder.setReportDelay(10000);
                break;
        }

        // 兼容 flag
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            builder.setLegacy(true);
        }

        return builder.build();
    }

    // ---------------------------
    // Internal scan callback and event forwarding
    // ---------------------------
    private class InternalScanCallback extends ScanCallback {
        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            // forward event
            for (BleScanListener l : listeners) {
                l.onDeviceFound(result);
            }
        }

        @Override
        public void onBatchScanResults(List<ScanResult> results) {
            for (ScanResult r : results) {
                for (BleScanListener l : listeners) {
                    l.onDeviceFound(r);
                }
            }
        }

        @Override
        public void onScanFailed(int errorCode) {
            Log.e(TAG, "Scan failed, code=" + errorCode);
            for (BleScanListener l : listeners) {
                l.onScanFailed(errorCode);
            }
        }
    }

    // ---------------------------
    // Listeners (simple event bus)
    // ---------------------------
    public interface BleScanListener {
        /**
         * 发现设备(会频繁回调)
         */
        void onDeviceFound(ScanResult result);

        /**
         * 扫描模式改变
         */
        void onScanModeChanged(int newMode);

        /**
         * 连接事件(可由外部调用 postDeviceConnected/postDeviceDisconnected)
         */
        void onDeviceConnected(BluetoothDevice device);

        void onDeviceDisconnected(BluetoothDevice device);

        /**
         * 扫描失败回调
         */
        void onScanFailed(int errorCode);
    }

    public void addListener(BleScanListener listener) {
        if (listener != null) listeners.add(listener);
    }

    public void removeListener(BleScanListener listener) {
        if (listener != null) listeners.remove(listener);
    }

    // Helper for external modules to post connection events into manager's event bus
    public void postDeviceConnected(BluetoothDevice device) {
        for (BleScanListener l : listeners) {
            l.onDeviceConnected(device);
        }
        // 在连接阶段自动降到低功耗,避免干扰
        setConnectingPhase(true);
    }

    public void postDeviceDisconnected(BluetoothDevice device) {
        for (BleScanListener l : listeners) {
            l.onDeviceDisconnected(device);
        }
        setConnectingPhase(false);
    }

    // ---------------------------
    // Receivers for battery and screen
    // ---------------------------
    private final BroadcastReceiver batteryReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context ctx, Intent intent) {
            if (intent == null) return;
            int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
            int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
            int percent = level >= 0 && scale > 0 ? (int) ((level * 100L) / scale) : -1;
            batteryPercent = percent >= 0 ? percent : batteryPercent;

            int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
            charging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL;

            adaptScanModeByEnvironment();
        }
    };

    private final BroadcastReceiver screenReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context ctx, Intent intent) {
            if (intent == null) return;
            String action = intent.getAction();
            if (Intent.ACTION_SCREEN_ON.equals(action)) {
                screenOn = true;
            } else if (Intent.ACTION_SCREEN_OFF.equals(action)) {
                screenOn = false;
            }
            adaptScanModeByEnvironment();
        }
    };

    private void registerReceivers() {
        try {
            // battery
            IntentFilter batteryFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
            context.registerReceiver(batteryReceiver, batteryFilter);

            // screen on/off
            IntentFilter screenFilter = new IntentFilter();
            screenFilter.addAction(Intent.ACTION_SCREEN_ON);
            screenFilter.addAction(Intent.ACTION_SCREEN_OFF);
            context.registerReceiver(screenReceiver, screenFilter);
        } catch (Exception e) {
            Log.w(TAG, "registerReceivers error: " + e.getMessage(), e);
        }
    }

    private void unregisterReceivers() {
        try {
            context.unregisterReceiver(batteryReceiver);
        } catch (Exception ignored) {}
        try {
            context.unregisterReceiver(screenReceiver);
        } catch (Exception ignored) {}
    }

    private void queryInitialBatteryAndScreenState() {
        // battery
        try {
            Intent battery = context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
            if (battery != null) {
                int level = battery.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
                int scale = battery.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
                batteryPercent = level >= 0 && scale > 0 ? (int) ((level * 100L) / scale) : batteryPercent;
                int status = battery.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
                charging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL;
            }
        } catch (Exception ignored) {}

        // screen: best-effort -> assume on; will be updated by receiver when registered
        screenOn = true;
    }

    // ---------------------------
    // Helper: update scanner reference (in case adapter changes)
    // ---------------------------
    private void updateScannerIfNeeded() {
        if (bluetoothAdapter != null) {
            BluetoothLeScanner s = bluetoothAdapter.getBluetoothLeScanner();
            if (s != null) scanner = s;
        }
    }

    // ---------------------------
    // Utility: build common filter helpers
    // ---------------------------
    public static ScanFilter buildFilterByDeviceName(String deviceName) {
        if (deviceName == null) return null;
        return new ScanFilter.Builder().setDeviceName(deviceName).build();
    }

    public static ScanFilter buildFilterByAddress(String mac) {
        if (mac == null) return null;
        return new ScanFilter.Builder().setDeviceAddress(mac).build();
    }

    public static ScanFilter buildFilterByServiceUuid(UUID uuid) {
        if (uuid == null) return null;
        return new ScanFilter.Builder().setServiceUuid(new ParcelUuid(uuid)).build();
    }

    // ---------------------------
    // Cleanup finalize (just in case)
    // ---------------------------
    @Override
    protected void finalize() throws Throwable {
        try {
            stop();
        } catch (Exception ignored) {}
        super.finalize();
    }
}

使用建议与说明

1. 权限说明(必看)

  • BleScanManager 仅做权限校验,不主动触发权限弹窗。调用者需自行处理运行时权限申请,申请通过后再调用 startScan(...)startPeriodicScan(...)

2. 管理器初始化与生命周期

// 初始化(建议使用 Application 上下文,避免内存泄漏)
BleScanManager manager = new BleScanManager(getApplicationContext());
// 添加事件监听(接收设备发现、模式变更等回调)
manager.addListener(myListener);
// 启动管理器:注册电量/屏幕状态广播,让智能策略生效
manager.start();

// 无需使用时(如页面销毁),务必停止管理器
manager.stop(); // 自动停止扫描、注销广播、关闭调度线程

3. 快速启动扫描

  • 一次性扫描(指定时长后自动停止):
// 激进模式扫描 10 秒,无过滤条件
manager.startScan(BleScanManager.MODE_AGGRESSIVE, 10_000L, null);
  • 周期性扫描(循环执行“扫描-暂停”):
// 平衡模式,每 30 秒启动一次扫描,每次扫描持续 5 秒,无过滤条件
manager.startPeriodicScan(BleScanManager.MODE_BALANCED, 5_000L, 30_000L, null);

// 停止周期性扫描(不影响当前正在进行的单次扫描)
manager.stopPeriodicScan();

4. 连接状态同步(触发智能降级)

  • 设备连接成功时:调用 manager.postDeviceConnected(device),会自动切换到低功耗模式,避免扫描干扰连接。
  • 设备断开连接时:调用 manager.postDeviceDisconnected(device),恢复正常扫描策略。

5. 自定义扫描策略

默认通过 adaptScanModeByEnvironment() 实现“屏幕+电量+连接状态”的智能适配。
可根据业务需求修改该方法逻辑,例如:

  • 依据设备信号强度(RSSI)动态切换模式。
  • 结合电池温度、厂商白名单调整扫描参数。
  • 增加自定义阈值(如电量低于 10% 强制低功耗)。