關于BLE相關的知識,借助于之前做的一個應用(防丟器),在此做一個整理筆記。怕自己懶,先立個FLAG,這將是一個系列的文章:
1.準備知識:
Github上的庫:Bluetooth-LE-Library---Android(雖然沒有引入,但是他這個demo里有另外幾個類寫的很好,可直接用)
其他介紹BLE原理和工作機制的文章(本文主要從代碼角度入手)
應用簡要邏輯介紹: a.掃描并篩選出特定的設備,不是所有藍牙設備都展示,只顯示自家的;b.連接并寫入認證信息到ble設備,該認證視定制情況,實現設備綁定;c.定時檢測已綁定設備連接狀態,未連接的由應用發起主動連接;d.監聽藍牙設備狀態,比如設備發過來的指令、斷開事件、信號強度;e.根據設備狀態進行對應提醒,如斷開報警,強度弱時提示距離偏大,接收到ble設備的單擊事件時執行拍照(這里其實還充當了自拍器按鍵,但是我并沒有找到直接控制系統相機快門的方法,采用的是自己搞了一個拍照界面⊙﹏⊙‖∣,有人會的話,請留言,萬分感謝),接收到雙擊事件時手機報警(相當于尋找一時不知道丟到哪里的手機);f.設備以及應用的一些設置,如勿擾區域、勿擾時段、記錄設備定位信息等;g.最后還有一個設備升級,可自行參考CRS相關support文檔。
2.權限申請以及最低API設置
這一步是很容易被忽略但又非常關鍵的,被這個坑到過(┬_┬)
- 在
AndroidManifest.xml
主配置文件里添加:
<!-- 藍牙相關 -->
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="true"/>
- 在
build.gradle
里如下設置:
defaultConfig {
applicationId "com.powerstick.beaglepro"
minSdkVersion 18
targetSdkVersion 19
versionCode 8
versionName "1.0.8"
}
3.初始化藍牙掃描相關類并掃描、解析、展示(用到android.bluetooth.*)
1.BluetoothLeScanner.java
如下
import android.bluetooth.BluetoothAdapter;
import android.os.Handler;
import com.tencent.bugly.crashreport.BuglyLog;
public class BluetoothLeScanner {
private final String TAG = "BluetoothLeScanner";
private final Handler mHandler;
private final BluetoothAdapter.LeScanCallback mLeScanCallback;
private final BluetoothUtils mBluetoothUtils;
private boolean mScanning;
public BluetoothLeScanner(final BluetoothAdapter.LeScanCallback leScanCallback, final BluetoothUtils
bluetoothUtils) {
mHandler = new Handler();
mLeScanCallback = leScanCallback;
mBluetoothUtils = bluetoothUtils;
}
public boolean isScanning() {
return mScanning;
}
public void scanLeDevice(final int duration, final boolean enable) {
if (enable) {
if (mScanning) {
return;
}
BuglyLog.d(TAG, "~ Starting Scan");
if (duration > 0) {
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
BuglyLog.d(TAG, "~ Stopping Scan (timeout)");
mScanning = false;
mBluetoothUtils.getBluetoothAdapter().stopLeScan(mLeScanCallback);
}
}, duration);
}
mScanning = true;
mBluetoothUtils.getBluetoothAdapter().startLeScan(mLeScanCallback);
} else {
BuglyLog.d(TAG, "~ Stopping Scan");
mScanning = false;
mBluetoothUtils.getBluetoothAdapter().stopLeScan(mLeScanCallback);
}
}
}
2.在Activity中實例化并進行掃描
// 藍牙信息相關
private BluetoothUtils mBluetoothUtils;
private BluetoothLeScanner mScanner;
mBluetoothUtils = new BluetoothUtils(this);
mScanner = new BluetoothLeScanner(mLeScanCallback, mBluetoothUtils);
startScan();
private void startScan() {
final boolean mIsBluetoothOn = mBluetoothUtils.isBluetoothOn();
final boolean mIsBluetoothLePresent = mBluetoothUtils.isBluetoothLeSupported();
mBluetoothUtils.askUserToEnableBluetoothIfNeeded();
if (mIsBluetoothOn && mIsBluetoothLePresent) {
mScanner.scanLeDevice(-1, true);
}
}
*以上代碼沒有注釋應該沒問題,這其中還有判斷手機是否打開藍牙的代碼行,需注意。
3.解析掃描得到的結果,并根據實際需要在頁面內展示
上面用到的mLeScanCallback
如下:
private final BluetoothAdapter.LeScanCallback mLeScanCallback = new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(final BluetoothDevice device, final int rssi, final byte[] scanRecord) {
runOnUiThread(new Runnable() {
@Override
public void run() {
BuglyLog.i("--------->", "name:"+device.getName());
// 這里刪減了業務邏輯,只做部分知識介紹
// 對于掃描到的設備,進行額外信息的獲取,大多是為了只在掃描配對頁顯示自家產品,剔除一般設備
List<AdRecord> adRecord = AdRecordUtils.parseScanRecordAsList(scanRecord);
for (int i = 0; i < adRecord.size(); i++) {
AdRecord record = adRecord.get(i);
if (record.getType() == 0xFF) {
String mac = ByteUtils.byteArrayToHexString(record.getData());
// 直接獲取出來的字符串形如:[AA,BB,CC...]處理成AA:BB:CC...的樣子
mac = mac.replace("[", "").replace("]", "").replace(", ", ":");
BuglyLog.e("****", mac);
// 我們的設備額外信息就是MAC
if (TextUtils.equals(mac, device.getAddress())) {
// 添加到UI頁面,等著點擊配對
}
}
}
}
});
}
};
4.相關工具類
1.ByteUtils.java
字節處理,在后面的手機與BLE設備通信處經常需要用到
import java.nio.ByteBuffer;
public class ByteUtils {
private static final String HEXES = "0123456789ABCDEF";
private ByteUtils() {
}
/**
* 字節數組轉換成16進制的字符串表示: [01, 30, FF, AA]
*/
public static String byteArrayToHexString(final byte[] array) {
final StringBuilder sb = new StringBuilder();
boolean firstEntry = true;
sb.append('[');
for (final byte b : array) {
if (!firstEntry) {
sb.append(", ");
}
sb.append(HEXES.charAt((b & 0xF0) >> 4));
sb.append(HEXES.charAt((b & 0x0F)));
firstEntry = false;
}
sb.append(']');
return sb.toString();
}
/**
* 檢查一個字節數組是否以另一個字節數組為開始
*
* @param array the array
* @param prefix the prefix
* @return boolean
*/
public static boolean doesArrayBeginWith(final byte[] array, final byte[] prefix) {
if (array.length < prefix.length) {
return false;
}
for (int i = 0; i < prefix.length; i++) {
if (array[i] != prefix[i]) {
return false;
}
}
return true;
}
/**
* 將一個長度為2的字節數組轉為int
*/
public static int getIntFrom2ByteArray(final byte[] input) {
final byte[] result = new byte[4];
result[0] = 0;
result[1] = 0;
result[2] = input[0];
result[3] = input[1];
return ByteUtils.getIntFromByteArray(result);
}
/**
* Converts a byte to an int, preserving the sign.
* <p/>
* For example, FF will be converted to 255 and not -1.
*
* @param bite the byte
* @return the int from byte
*/
public static int getIntFromByte(final byte bite) {
return bite & 0xFF;
}
/**
* Converts a byte array to an int.
*
* @param bytes the bytes
* @return the int from byte array
*/
public static int getIntFromByteArray(final byte[] bytes) {
return ByteBuffer.wrap(bytes).getInt();
}
/**
* Converts a byte array to a long.
*
* @param bytes the bytes
* @return the long from byte array
*/
public static long getLongFromByteArray(final byte[] bytes) {
return ByteBuffer.wrap(bytes).getLong();
}
/**
* Inverts an byte array in place.
*
* @param array the array
*/
public static void invertArray(final byte[] array) {
final int size = array.length;
byte temp;
for (int i = 0; i < size / 2; i++) {
temp = array[i];
array[i] = array[size - 1 - i];
array[size - 1 - i] = temp;
}
}
}
2.AdRecordUtils.java
從掃描到的設備中解析各自的額外信息
package com.powerstick.beaglepro.util;
import android.annotation.SuppressLint;
import android.util.SparseArray;
import com.afap.utils.ByteUtils;
import com.powerstick.beaglepro.model.AdRecord;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public final class AdRecordUtils {
private AdRecordUtils(){
// TO AVOID INSTANTIATION
}
public static String getRecordDataAsString(final AdRecord nameRecord) {
if (nameRecord == null) {
return "";
}
return new String(nameRecord.getData());
}
public static byte[] getServiceData(final AdRecord serviceData) {
if (serviceData == null) {
return null;
}
if (serviceData.getType() != AdRecord.TYPE_SERVICE_DATA) return null;
final byte[] raw = serviceData.getData();
//Chop out the uuid
return Arrays.copyOfRange(raw, 2, raw.length);
}
public static int getServiceDataUuid(final AdRecord serviceData) {
if (serviceData == null) {
return -1;
}
if (serviceData.getType() != AdRecord.TYPE_SERVICE_DATA) return -1;
final byte[] raw = serviceData.getData();
//Find UUID data in byte array
int uuid = (raw[1] & 0xFF) << 8;
uuid += (raw[0] & 0xFF);
return uuid;
}
/*
* Read out all the AD structures from the raw scan record
*/
public static List<AdRecord> parseScanRecordAsList(final byte[] scanRecord) {
final List<AdRecord> records = new ArrayList<>();
int index = 0;
while (index < scanRecord.length) {
final int length = scanRecord[index++];
//Done once we run out of records
if (length == 0) break;
final int type = ByteUtils.getIntFromByte(scanRecord[index]);
//Done if our record isn't a valid type
if (type == 0) break;
final byte[] data = Arrays.copyOfRange(scanRecord, index + 1, index + length);
records.add(new AdRecord(length, type, data));
//Advance
index += length;
}
return Collections.unmodifiableList(records);
}
@SuppressLint("UseSparseArrays")
public static Map<Integer, AdRecord> parseScanRecordAsMap(final byte[] scanRecord) {
final Map<Integer, AdRecord> records = new HashMap<>();
int index = 0;
while (index < scanRecord.length) {
final int length = scanRecord[index++];
//Done once we run out of records
if (length == 0) break;
final int type = ByteUtils.getIntFromByte(scanRecord[index]);
//Done if our record isn't a valid type
if (type == 0) break;
final byte[] data = Arrays.copyOfRange(scanRecord, index + 1, index + length);
records.put(type, new AdRecord(length, type, data));
//Advance
index += length;
}
return Collections.unmodifiableMap(records);
}
public static SparseArray<AdRecord> parseScanRecordAsSparseArray(final byte[] scanRecord) {
final SparseArray<AdRecord> records = new SparseArray<>();
int index = 0;
while (index < scanRecord.length) {
final int length = scanRecord[index++];
//Done once we run out of records
if (length == 0) break;
final int type = ByteUtils.getIntFromByte(scanRecord[index]);
//Done if our record isn't a valid type
if (type == 0) break;
final byte[] data = Arrays.copyOfRange(scanRecord, index + 1, index + length);
records.put(type, new AdRecord(length, type, data));
//Advance
index += length;
}
return records;
}
}
以上,基本涵蓋了藍牙設備掃描與篩選,頁面如何顯示要根據實際需要了。