【Android BLE】蓝牙开发「防丢器」的相关知识点(一):扫描并识别设备

关于BLE相关的知识,借助于之前做的一个应用(防丢器),在此做一个整理笔记。怕自己懒,先立个FLAG,这将是一个系列的文章:

1.准备知识:

  • BLE:Bluetooth Low Energy

  • Github上的库:Bluetooth-LE-Library---Android(虽然没有引入,但是他这个demo里有另外几个类写的很好,可直接用)

  • 其他介绍BLE原理和工作机制的文章(本文主要从代码角度入手)

  • 应用简要逻辑介绍: a.扫描并筛选出特定的设备,不是所有蓝牙设备都展示,只显示自家的;b.连接并写入认证信息到ble设备,该认证视定制情况,实现设备绑定;c.定时检测已绑定设备连接状态,未连接的由应用发起主动连接;d.监听蓝牙设备状态,比如设备发过来的指令、断开事件、信号强度;e.根据设备状态进行对应提醒,如断开报警,强度弱时提示距离偏大,接收到ble设备的单击事件时执行拍照(这里其实还充当了自拍器按键,但是我并没有找到直接控制系统相机快门的方法,采用的是自己搞了一个拍照界面⊙﹏⊙‖∣,有人会的话,请留言,万分感谢),接收到双击事件时手机报警(相当于寻找一时不知道丢到哪里的手机);f.设备以及应用的一些设置,如勿扰区域、勿扰时段、记录设备定位信息等;g.最后还有一个设备升级,可自行参考CRS相关support文档。

2.权限申请以及最低API设置

这一步是很容易被忽略但又非常关键的,被这个坑到过(┬_┬)

  1. 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"/>
  1. 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;
    }
}

以上,基本涵盖了蓝牙设备扫描与筛选,页面如何显示要根据实际需要了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容