4. 低功耗蓝牙扫描

任务说明

本案例实现一个低功耗蓝牙(BLE)扫描器,能够扫描周围的BLE广播设备,并按照指定的服务UUID进行过滤,仅显示包含目标服务的设备。最终通过列表界面呈现扫描结果,支持开始/停止扫描操作。

实现方案

使用Android系统提供的BluetoothLeScanner API完成扫描。核心逻辑包括:申请必要权限、初始化扫描器、构造包含服务UUID过滤条件的ScanFilter、通过ScanCallback接收结果并更新UI。过滤条件在扫描请求层生效,系统底层会优先丢弃不匹配的广播包,从而降低功耗与回调频率。

代码实现


// BleScannerActivity.java
public class BleScannerActivity extends AppCompatActivity {
    private static final int REQ_PERM = 100;
    private static final UUID TARGET_SERVICE_UUID =
            UUID.fromString("0000180f-0000-1000-8000-00805f9b34fb"); // Battery Service

    private BluetoothAdapter bluetoothAdapter;
    private BluetoothLeScanner bleScanner;
    private ScanCallback scanCallback;
    private ArrayAdapter<String> adapter;
    private boolean isScanning = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_ble_scan);

        adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1);
        ((ListView) findViewById(R.id.list_devices)).setAdapter(adapter);

        BluetoothManager manager = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
        bluetoothAdapter = manager.getAdapter();
        bleScanner = bluetoothAdapter.getBluetoothLeScanner();

        findViewById(R.id.btn_scan).setOnClickListener(v -> toggleScan());
        requestPermissions();
    }

    private void requestPermissions() {
        if (Build.VERSION.SDK_INT >= 31) {
            requestPermissions(new String[]{
                    Manifest.permission.BLUETOOTH_SCAN,
                    Manifest.permission.BLUETOOTH_CONNECT,
                    Manifest.permission.ACCESS_FINE_LOCATION
            }, REQ_PERM);
        } else {
            requestPermissions(new String[]{
                    Manifest.permission.ACCESS_FINE_LOCATION
            }, REQ_PERM);
        }
    }

    private void toggleScan() {
        if (isScanning) {
            stopScan();
        } else {
            startScan();
        }
    }

    private void startScan() {
        if (bleScanner == null) return;

        ScanFilter filter = new ScanFilter.Builder()
                .setServiceUuid(new ParcelUuid(TARGET_SERVICE_UUID))
                .build();

        ScanSettings settings = new ScanSettings.Builder()
                .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
                .build();

        scanCallback = new ScanCallback() {
            @Override
            public void onScanResult(int callbackType, ScanResult result) {
                BluetoothDevice device = result.getDevice();
                String info = device.getName() + " [" + device.getAddress() + "] RSSI:" + result.getRssi();
                runOnUiThread(() -> {
                    if (adapter.getPosition(info) < 0) {
                        adapter.add(info);
                    }
                });
            }

            @Override
            public void onScanFailed(int errorCode) {
                runOnUiThread(() ->
                        Toast.makeText(BleScannerActivity.this,
                                "扫描失败,错误码:" + errorCode, Toast.LENGTH_SHORT).show());
            }
        };

        bleScanner.startScan(Collections.singletonList(filter), settings, scanCallback);
        isScanning = true;
        ((Button) findViewById(R.id.btn_scan)).setText("停止扫描");
    }

    private void stopScan() {
        if (bleScanner != null && scanCallback != null) {
            bleScanner.stopScan(scanCallback);
        }
        isScanning = false;
        ((Button) findViewById(R.id.btn_scan)).setText("开始扫描");
    }
}

// activity_ble_scan.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <Button
        android:id="@+id/btn_scan"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="开始扫描" />

    <ListView
        android:id="@+id/list_devices"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:layout_marginTop="12dp" />
</LinearLayout>

// AndroidManifest.xml 关键权限声明
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
    android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />

关键逻辑说明

扫描过滤器使用setServiceUuid()指定目标服务UUID(示例为Battery Service 0x180F)。当BLE外设广播中包含该服务UUID时,系统才会将结果回调给应用。列表项显示设备名称、MAC地址和RSSI信号强度。重复设备通过getPosition()检查避免重复添加。

SVG:BLE扫描过滤流程

BLE外设A 广播含0x180F BLE外设B 广播不含0x180F BLE外设C 广播含0x180F ScanFilter UUID = 0x180F ✓ 通过 ✗ 丢弃 ✓ 通过 回调 onScanResult 回调 onScanResult 丢弃

落地与验证

运行方式:在Android 8.0及以上设备安装应用,确保手机蓝牙已开启。点击“开始扫描”按钮,系统会弹出权限请求,全部允许后扫描自动开始。

验证方法:准备一个广播包含Battery Service(0x180F)的BLE外设(如某些心率计、蓝牙温湿度计),放置于手机附近。观察列表是否出现该设备名称与MAC地址。再准备一个不包含该服务的普通BLE设备(如蓝牙耳机),确认其不会出现在列表中。

判定标准:包含目标服务UUID的设备在数秒内出现在列表且RSSI值合理(-30~-90 dBm);不包含目标服务的设备始终不出现;点击“停止扫描”后列表不再新增设备,即判定通过。