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% 强制低功耗)。