1 掃描工具類
package com.example.yjh_erji.bluetooth;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import androidx.core.app.ActivityCompat;
import com.tjf.lib_utils.LogsUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* BLE 掃描工具類,兼容Android 5.0+版本,修復(fù)了掃描不穩(wěn)定問題
*/
public class BleScanner {
private static final String TAG = "BleScanner";
private final Context context;
private final BluetoothAdapter bluetoothAdapter;
private final Handler handler = new Handler(Looper.getMainLooper());
private boolean isScanning = false;
private final Map<String, BluetoothDevice> scannedDevices = new HashMap<>();
private BleScanCallback callback;
// 掃描配置參數(shù)
private long scanPeriod = 10000; // 掃描10秒
private boolean isDuplicateFilterEnabled = true;
private int scanMode = ScanSettings.SCAN_MODE_BALANCED; // 默認(rèn)平衡模式
// 回調(diào)實(shí)例
private android.bluetooth.le.ScanCallback leScanCallback;
private BluetoothAdapter.LeScanCallback oldLeScanCallback;
public BleScanner(Context context) {
this.context = context.getApplicationContext();
// 獲取藍(lán)牙管理器和適配器
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
bluetoothAdapter = bluetoothManager.getAdapter();
} else {
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
}
}
/**
* 設(shè)置掃描結(jié)果回調(diào)
*/
public void setScanCallback(BleScanCallback callback) {
this.callback = callback;
}
/**
* 設(shè)置掃描時(shí)間(毫秒)
*/
public void setScanPeriod(long scanPeriod) {
this.scanPeriod = scanPeriod;
}
/**
* 設(shè)置是否過濾重復(fù)設(shè)備
*/
public void setDuplicateFilterEnabled(boolean enabled) {
isDuplicateFilterEnabled = enabled;
}
/**
* 設(shè)置掃描模式
* @param scanMode 參考ScanSettings中的掃描模式常量
*/
public void setScanMode(int scanMode) {
this.scanMode = scanMode;
}
/**
* 檢查設(shè)備是否支持BLE
*/
public boolean isBleSupported() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 &&
context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);
}
/**
* 檢查藍(lán)牙是否已啟用
*/
public boolean isBluetoothEnabled() {
return bluetoothAdapter != null && bluetoothAdapter.isEnabled();
}
/**
* 檢查是否擁有必要的權(quán)限
*/
public boolean hasRequiredPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Android 12+ 權(quán)限檢查
boolean hasScanPermission = ActivityCompat.checkSelfPermission(context,
android.Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED;
boolean hasConnectPermission = ActivityCompat.checkSelfPermission(context,
android.Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED;
if (!hasScanPermission || !hasConnectPermission) {
return false;
}
// 檢查是否需要位置權(quán)限(根據(jù)BLUETOOTH_SCAN權(quán)限的聲明)
if (!hasNeverForLocationFlag()) {
return ActivityCompat.checkSelfPermission(context,
android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
}
return true;
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Android 6.0-11 需要位置權(quán)限
return ActivityCompat.checkSelfPermission(context,
android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
}
return true; // Android 6.0以下無需特殊權(quán)限
}
/**
* 檢查清單文件中BLUETOOTH_SCAN是否包含neverForLocation標(biāo)記
*/
private boolean hasNeverForLocationFlag() {
try {
PackageInfo packageInfo = context.getPackageManager()
.getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS);
if (packageInfo.requestedPermissions != null) {
for (String perm : packageInfo.requestedPermissions) {
if (perm.equals(android.Manifest.permission.BLUETOOTH_SCAN)) {
// 檢查權(quán)限標(biāo)記(實(shí)際需要解析AndroidManifest.xml)
// 這里簡化處理,實(shí)際項(xiàng)目中可通過PackageManager更精確檢查
return true;
}
}
}
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Package not found", e);
}
return false;
}
/**
* 開始掃描BLE設(shè)備
*/
public void startScan() {
if (!isBleSupported()) {
notifyScanFailed(BleScanCallback.ERROR_BLE_NOT_SUPPORTED);
return;
}
if (!isBluetoothEnabled()) {
notifyScanFailed(BleScanCallback.ERROR_BLUETOOTH_DISABLED);
return;
}
if (!hasRequiredPermissions()) {
notifyScanFailed(BleScanCallback.ERROR_MISSING_PERMISSIONS);
return;
}
if (isScanning) {
stopScan();
}
scannedDevices.clear();
isScanning = true;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
startScanWithNewApi();
} else {
// 對于Android 4.3-4.4,建議提示用戶升級系統(tǒng)
Log.w(TAG, "Old Android version may have limited BLE support");
startScanWithOldApi();
}
// 設(shè)置掃描超時(shí)
handler.postDelayed(this::stopScan, scanPeriod);
}
/**
* 使用新API (Android 5.0+) 開始掃描
*/
private void startScanWithNewApi() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return;
android.bluetooth.le.BluetoothLeScanner scanner = bluetoothAdapter.getBluetoothLeScanner();
if (scanner == null) {
notifyScanFailed(BleScanCallback.ERROR_SCAN_FAILED);
return;
}
// 配置掃描設(shè)置
ScanSettings.Builder settingsBuilder = new ScanSettings.Builder()
.setScanMode(scanMode);
// 兼容Android 6.0+的報(bào)告延遲設(shè)置
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
settingsBuilder.setReportDelay(0); // 實(shí)時(shí)報(bào)告
}
// 兼容Android 8.0+的匹配模式設(shè)置
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
settingsBuilder.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
.setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT);
}
ScanSettings settings = settingsBuilder.build();
// 創(chuàng)建掃描過濾器(默認(rèn)空列表,不過濾任何設(shè)備)
List<ScanFilter> filters = new ArrayList<>();
// 初始化掃描回調(diào)
leScanCallback = new android.bluetooth.le.ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
if (result != null) {
processScanResult(
result.getDevice(),
result.getRssi(),
result.getScanRecord() != null ? result.getScanRecord().getBytes() : null
);
}
}
@Override
public void onBatchScanResults(List<ScanResult> results) {
super.onBatchScanResults(results);
for (ScanResult result : results) {
processScanResult(
result.getDevice(),
result.getRssi(),
result.getScanRecord() != null ? result.getScanRecord().getBytes() : null
);
}
}
@Override
public void onScanFailed(int errorCode) {
super.onScanFailed(errorCode);
Log.e(TAG, "Scan failed with error code: " + errorCode);
isScanning = false;
notifyScanFailed(errorCode);
}
};
// 開始掃描,增加權(quán)限異常捕獲
try {
scanner.startScan(filters, settings, leScanCallback);
Log.d(TAG, "Started BLE scan with new API");
} catch (SecurityException e) {
Log.e(TAG, "Permission denied while starting scan", e);
isScanning = false;
notifyScanFailed(BleScanCallback.ERROR_MISSING_PERMISSIONS);
}
}
/**
* 使用舊API (Android 4.3-4.4) 開始掃描
*/
private void startScanWithOldApi() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) return;
oldLeScanCallback = new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
processScanResult(device, rssi, scanRecord);
}
};
bluetoothAdapter.startLeScan(oldLeScanCallback);
Log.d(TAG, "Started BLE scan with old API");
}
/**
* 處理掃描結(jié)果
*/
private void processScanResult(BluetoothDevice device, int rssi, byte[] scanRecord) {
if (device == null || !isScanning) return;
String deviceAddress = device.getAddress();
// 處理重復(fù)設(shè)備(可更新信息而非完全過濾)
boolean isNewDevice = !scannedDevices.containsKey(deviceAddress);
if (isDuplicateFilterEnabled && !isNewDevice) {
return; // 過濾重復(fù)設(shè)備
}
scannedDevices.put(deviceAddress, device);
// 解析廣播數(shù)據(jù)
BleAdvertisementData advertisementData = new BleAdvertisementData();
advertisementData.rssi = rssi;
advertisementData.rawBytes = scanRecord;
parseRawAdvertisementData(advertisementData);
LogsUtils.i("processScanResult",
"device: " + device.getName() + " - " + device.getAddress(),
"rssi: " + rssi,
"advertisementData: " + advertisementData);
// 回調(diào)通知
if (callback != null && isNewDevice) {
callback.onDeviceFound(device, rssi, advertisementData);
}
}
/**
* 解析原始廣播數(shù)據(jù)(根據(jù)藍(lán)牙規(guī)范)
*/
private void parseRawAdvertisementData(BleAdvertisementData data) {
if (data.rawBytes == null) return;
int offset = 0;
while (offset < data.rawBytes.length - 1) {
int len = data.rawBytes[offset++] & 0xFF;
if (len == 0) break;
if (offset + len > data.rawBytes.length) break; // 防止數(shù)組越界
int type = data.rawBytes[offset] & 0xFF;
byte[] fieldData = Arrays.copyOfRange(data.rawBytes, offset + 1, offset + len);
// 根據(jù)類型解析數(shù)據(jù)
switch (type) {
case 0x01: // Flags
data.flags = fieldData[0];
break;
case 0x02: // Incomplete List of 16-bit Service Class UUIDs
case 0x03: // Complete List of 16-bit Service Class UUIDs
parseServiceUuids(fieldData, data);
break;
case 0x09: // Complete Local Name
data.deviceName = new String(fieldData);
break;
case 0x0A: // TX Power Level
data.txPower = fieldData[0];
break;
case 0xFF: // Manufacturer Specific Data
if (fieldData.length >= 2) {
int manufacturerId = ((fieldData[1] & 0xFF) << 8) | (fieldData[0] & 0xFF);
byte[] manufacturerSpecificData = Arrays.copyOfRange(fieldData, 2, fieldData.length);
data.manufacturerData.put(manufacturerId, manufacturerSpecificData);
}
break;
}
offset += len;
}
}
/**
* 解析服務(wù)UUID
*/
private void parseServiceUuids(byte[] data, BleAdvertisementData advertisementData) {
if (advertisementData.serviceUuids == null) {
advertisementData.serviceUuids = new ArrayList<>();
}
int uuidLength = data.length;
for (int i = 0; i < uuidLength; i += 2) {
if (i + 1 >= uuidLength) break; // 防止數(shù)組越界
// 16-bit UUID
long uuidValue = ((data[i + 1] & 0xFF) << 8) | (data[i] & 0xFF);
String uuidString = String.format("%08X-0000-1000-8000-00805F9B34FB", uuidValue);
try {
advertisementData.serviceUuids.add(UUID.fromString(uuidString));
} catch (IllegalArgumentException e) {
Log.e(TAG, "Invalid UUID: " + uuidString);
}
}
}
/**
* 停止掃描BLE設(shè)備
*/
public void stopScan() {
if (isScanning) {
isScanning = false;
handler.removeCallbacksAndMessages(null); // 移除所有延遲任務(wù)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// 新API停止掃描
android.bluetooth.le.BluetoothLeScanner scanner = bluetoothAdapter.getBluetoothLeScanner();
if (scanner != null && leScanCallback != null) {
try {
scanner.stopScan(leScanCallback);
} catch (Exception e) {
Log.e(TAG, "Error stopping scan", e);
}
}
} else {
// 舊API停止掃描
if (oldLeScanCallback != null) {
bluetoothAdapter.stopLeScan(oldLeScanCallback);
}
}
// 釋放回調(diào)引用
leScanCallback = null;
oldLeScanCallback = null;
Log.d(TAG, "Stopped BLE scan. Found " + scannedDevices.size() + " devices");
if (callback != null) {
callback.onScanCompleted(new ArrayList<>(scannedDevices.values()));
}
}
}
/**
* 通知掃描失敗
*/
private void notifyScanFailed(int errorCode) {
if (callback != null) {
callback.onScanFailed(errorCode);
}
}
/**
* 判斷掃描是否正在進(jìn)行
*/
public boolean isScanning() {
return isScanning;
}
/**
* 獲取已掃描到的設(shè)備列表
*/
public List<BluetoothDevice> getScannedDevices() {
return new ArrayList<>(scannedDevices.values());
}
/**
* 掃描結(jié)果回調(diào)接口
*/
public interface BleScanCallback {
int ERROR_BLE_NOT_SUPPORTED = 1001;
int ERROR_BLUETOOTH_DISABLED = 1002;
int ERROR_MISSING_PERMISSIONS = 1003;
int ERROR_SCAN_FAILED = 1004;
/**
* 發(fā)現(xiàn)新設(shè)備
*/
void onDeviceFound(BluetoothDevice device, int rssi, BleAdvertisementData advertisementData);
/**
* 掃描完成
*/
void onScanCompleted(List<BluetoothDevice> devices);
/**
* 掃描失敗
*/
void onScanFailed(int errorCode);
}
/**
* 封裝BLE廣播數(shù)據(jù)的類
*/
public static class BleAdvertisementData {
public String deviceName;
public int rssi;
public int txPower;
public byte flags;
public List<UUID> serviceUuids;
public Map<Integer, byte[]> manufacturerData = new HashMap<>();
public byte[] rawBytes;
/**
* 將字節(jié)數(shù)組轉(zhuǎn)換為十六進(jìn)制字符串
*/
public static String bytesToHex(byte[] bytes) {
if (bytes == null) return "";
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02X ", b));
}
return result.toString().trim();
}
@Override
public String toString() {
return "BleAdvertisementData{" +
"deviceName='" + deviceName + '\'' +
", rssi=" + rssi +
", txPower=" + txPower +
", serviceUuids=" + serviceUuids +
", manufacturerData size=" + manufacturerData.size() +
", rawBytes length=" + (rawBytes != null ? rawBytes.length : 0) +
'}';
}
}
}
2 使用
// 初始化 BLE 掃描器
bleScanner = new BleScanner(this);
bleScanner.setScanCallback(new BleScanner.BleScanCallback() {
@Override
public void onDeviceFound(BluetoothDevice device, int rssi, BleScanner.BleAdvertisementData advertisementData) {
// 處理發(fā)現(xiàn)的設(shè)備
runOnUiThread(() -> {
LogsUtils.d("BleScanner", "發(fā)現(xiàn)設(shè)備: " + device.getName() + " (" + device.getAddress() + ")");
LogsUtils.d("BleScanner", "RSSI: " + rssi);
if (advertisementData.deviceName != null) {
LogsUtils.d("BleScanner", "設(shè)備名稱: " + advertisementData.deviceName);
}
// 處理服務(wù) UUID
if (advertisementData.serviceUuids != null && !advertisementData.serviceUuids.isEmpty()) {
StringBuilder uuidStr = new StringBuilder();
for (UUID uuid : advertisementData.serviceUuids) {
uuidStr.append(uuid.toString()).append("\n");
}
LogsUtils.d("BleScanner", "服務(wù) UUID:\n" + uuidStr);
}
// 處理制造商數(shù)據(jù)(如 iBeacon)
if (advertisementData.manufacturerData != null) {
for (int manufacturerId : advertisementData.manufacturerData.keySet()) {
byte[] data = advertisementData.manufacturerData.get(manufacturerId);
LogsUtils.d("BleScanner", "制造商 ID: 0x" + String.format("%04X", manufacturerId));
LogsUtils.d("BleScanner", "制造商數(shù)據(jù): " + BleScanner.BleAdvertisementData.bytesToHex(data));
// 示例:解析 iBeacon 數(shù)據(jù)
if (manufacturerId == 0x004C) { // Apple 公司 ID
parseIBeaconData(data);
}
}
}
});
}
@Override
public void onScanCompleted(List<BluetoothDevice> devices) {
// 掃描完成回調(diào)
runOnUiThread(() -> {
Toast.makeText(MainActivity.this, "掃描完成,發(fā)現(xiàn) " + devices.size() + " 個(gè)設(shè)備", Toast.LENGTH_SHORT).show();
LogsUtils.d("BleScanner", "掃描完成,共發(fā)現(xiàn) " + devices.size() + " 個(gè)設(shè)備");
});
}
@Override
public void onScanFailed(int errorCode) {
// 掃描失敗回調(diào)
runOnUiThread(() -> {
String errorMsg = "掃描失敗,錯(cuò)誤碼: " + errorCode;
Toast.makeText(MainActivity.this, errorMsg, Toast.LENGTH_SHORT).show();
Log.e("BleScanner", errorMsg);
});
}
});
/**
* 開始 BLE 掃描
*/
private void startBleScan() {
// 配置掃描參數(shù)(可選)
bleScanner.setScanPeriod(15000); // 掃描15秒
bleScanner.setDuplicateFilterEnabled(true); // 過濾重復(fù)設(shè)備
// 開始掃描
bleScanner.startScan();
Toast.makeText(this, "正在掃描 BLE 設(shè)備...", Toast.LENGTH_SHORT).show();
}
/**
* 停止 BLE 掃描
*/
private void stopBleScan() {
if (bleScanner.isScanning()) {
bleScanner.stopScan();
Toast.makeText(this, "已停止掃描", Toast.LENGTH_SHORT).show();
}
}
3 權(quán)限配置
<!-- BLE基礎(chǔ)權(quán)限 -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<!-- Android 12+ 權(quán)限 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- 位置權(quán)限(根據(jù)需要) -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"
android:maxSdkVersion="30" />
<!-- 藍(lán)牙和BLE功能聲明 -->
<uses-feature android:name="android.hardware.bluetooth" android:required="true" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />