Android蓝牙开发流程实践

概述

工作需要用到Android蓝牙开发,所以在这里对Android蓝牙开发做一个整理。

先了解下Android蓝牙开发的基础知识:官方文档戳这里

我们需要知道,现在的蓝牙分为经典蓝牙和BLE(低功耗蓝牙)两种,经典蓝牙和低功耗蓝牙的区别戳这里 ,经典蓝牙适合传输数据量比较大的传输,比如图像、视频、音乐等,耗电也大;BLE适合耗电小,实时性要求高和数据量小的传输,比如智能穿戴设备、遥控类、鼠标、键盘、传感设备如心跳带,血压计,温度传感器等。

对于Android开发者来说,我们要知道 Android 4.3 及以上版本才支持BLE,常说的蓝牙单模和双模指的是仅支持BLE和同时支持BLE和经典蓝牙,经典蓝牙和BLE之间不能通信,Android手机同时支持经典蓝牙和BLE,但是扫描蓝牙的时候,只能扫描其中一种,如果是Android手机跟其他设备通过蓝牙通信,首先要确认设备支持的蓝牙协议。下面记录的是经典蓝牙开发步骤。

蓝牙开发步骤

通常Android蓝牙开发包含以下5个步骤:

  • 开启
  • 扫描
  • 配对
  • 连接
  • 通信

开启蓝牙

  • 权限(需要蓝牙和GPS权限,Android 6.0以上要加上运行时权限授权)

在 AndroidManifest 中声明权限:

    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

获取定位授权:

    //要模糊定位权限才能搜索到蓝牙
    PermissionUtil.requestEach(this, new PermissionUtil.OnPermissionListener() {
        @Override
        public void onSucceed() {
            //授权成功后打开蓝牙
            openBlueTooth();
        }
        @Override
        public void onFailed(boolean showAgain) {

        }
    }, PermissionUtil.LOCATION);

  • 判断设备是否支持蓝牙
    BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    if (mBluetoothAdapter == null) {
        //如果mBluetoothAdapter为null,该设备不支持蓝牙,不过基本上都支持的
    }

  • 判断蓝牙是否开启,没有就开启
    if (!mBluetoothAdapter.isEnabled()) {
        //若没打开则打开蓝牙
        mBluetoothAdapter.enable();
    }

扫描蓝牙

  • 可以扫描有指定 UUID 服务的蓝牙设备,UUID 不是设备的标识,而是某个服务的标识, 什么是UUID戳这里
  • 可以扫描全部蓝牙设备
  • 注意:蓝牙设备被某设备(包括当前的设备)配对/连接后,可能不会再被扫描到
    //扫描:经典蓝牙
    mBluetoothAdapter.startDiscovery();

    //扫描:低功耗蓝牙,需要加上停止扫描规则,扫描到指定设备或者一段时间后,这里设置10秒后停止扫描
    new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            mBluetoothAdapter.stopLeScan(MainActivity.this);
            Log.i(TAG, "=== 停止扫描了 === ");
        }
    }, SCAN_PERIOD);

    mBluetoothAdapter.startLeScan(this);

通过广播来监听扫描结果:

    //广播接收器
    BroadcastReceiver receiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            switch (action) {
                case BluetoothAdapter.ACTION_DISCOVERY_STARTED:
                    //扫描开始
                    break;
                case BluetoothDevice.ACTION_FOUND:
                    //发现蓝牙
                    BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                    break;
                case BluetoothAdapter.ACTION_DISCOVERY_FINISHED:
                    //扫描结束
                    break;
            }
        }
    };

配对蓝牙

  • 配对与连接之间的区别:配对意味着两个设备之间知道彼此的存在,通过配对密钥,建立一个加密的连接。而连接意味着设备之间共享一个通信通道(UUID),能够彼此传输数据
    /**
     * 蓝牙配对,配对结果通过广播返回
     * @param device
     */
    public void pin(BluetoothDevice device) {
        if (device == null || !mBluetoothAdapter.isEnabled()) {
            return;
        }

        //配对之前把扫描关闭
        if (mBluetoothAdapter.isDiscovering()) {
            mBluetoothAdapter.cancelDiscovery();
        }

        //判断设备是否配对,没有就进行配对
        if (device.getBondState() == BluetoothDevice.BOND_NONE) {
            try {
                Method createBondMethod = device.getClass().getMethod("createBond");
                Boolean returnValue = (Boolean) createBondMethod.invoke(device);
                returnValue.booleanValue();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

广播监听配对结果:

    case BluetoothDevice.ACTION_BOND_STATE_CHANGED:
        //配对状态变化广播
        BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice
                .EXTRA_DEVICE);
        switch (device.getBondState()) {
            case BluetoothDevice.BOND_NONE:
                Log.i(TAG, "--- 配对失败 ---");
                break;
            case BluetoothDevice.BOND_BONDING:
                Log.i(TAG, "--- 配对中... ---");
                break;
            case BluetoothDevice.BOND_BONDED:
                Log.i(TAG, "--- 配对成功 ---");
                break;
        }
        break;

连接蓝牙

  • 配对成功之后,两台设备之间建立连接,一台充当client的角色,另一台充当server的角色,由client发起连接;
  • 通过 UUID 创建 BluetoothSocket 进行连接,两个端的UUID要一致,client和server都自己开发的话,可以由服务端创建一个UUID
  • 发起连接和监听连接都要在子线程中执行

在client端的子线程中发起连接:

/**
 *
 * 发起蓝牙连接的线程
 * 作者: 代码来自于Google官方 -> API指南 -> 蓝牙模块
 * 日期: 18/12/14
 */

public class ConnectThread extends Thread {
    private final BluetoothSocket mmSocket;
    private final BluetoothDevice mmDevice;
    private final BluetoothAdapter mBluetoothAdapter;
    private ConnectCallBack callBack;

    public ConnectThread(BluetoothDevice device, BluetoothAdapter bluetoothAdapter, ConnectCallBack callBack) {
        // Use a temporary object that is later assigned to mmSocket,
        // because mmSocket is final
        BluetoothSocket tmp = null;
        mmDevice = device;
        mBluetoothAdapter = bluetoothAdapter;
        this.callBack = callBack;

        // Get a BluetoothSocket to connect with the given BluetoothDevice
        try {
            // MY_UUID is the app's UUID string, also used by the server code
            tmp = device.createRfcommSocketToServiceRecord(UUID.fromString(ServerActivity.uuidStr));
        } catch (IOException e) { }
        mmSocket = tmp;
    }

    public void run() {
        // Cancel discovery because it will slow down the connection
        mBluetoothAdapter.cancelDiscovery();

        try {
            // Connect the device through the socket. This will block
            // until it succeeds or throws an exception
            mmSocket.connect();
        } catch (IOException connectException) {
            // Unable to connect; close the socket and get out
            try {
                mmSocket.close();
            } catch (IOException closeException) { }
            return;
        }

        // Do work to manage the connection (in a separate thread)
//        manageConnectedSocket(mmSocket); //启动数据传输的线程
        if(callBack != null) {
            callBack.onConnectSucceed(mmSocket);
        }

    }

    /** Will cancel an in-progress connection, and close the socket */
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) { }
    }

    public interface ConnectCallBack {
        public void onConnectSucceed(BluetoothSocket serverSocket);
    }
}

在server端的子线程中监听连接:

/**
 *
 * 监听蓝牙连接线程
 * 作者: 代码来自于Google官方 -> API指南 -> 蓝牙模块
 * 日期: 18/12/14
 */

public class AcceptThread extends Thread {

    private static final String TAG = "BluetoothDemo";

    private final BluetoothServerSocket mmServerSocket;

    private AcceptCallBack callBack;

    public AcceptThread(BluetoothAdapter bluetoothAdapter, AcceptCallBack callBack) {

        this.callBack = callBack;

        // Use a temporary object that is later assigned to mmServerSocket,
        // because mmServerSocket is final
        BluetoothServerSocket tmp = null;
        try {
            // MY_UUID is the app's UUID string, also used by the client code
            tmp = bluetoothAdapter.listenUsingRfcommWithServiceRecord("bluetoothdemo", UUID
                    .fromString(ServerActivity.uuidStr));
        } catch (IOException e) {
        }
        mmServerSocket = tmp;
    }

    public void run() {
        BluetoothSocket socket = null;
        // Keep listening until exception occurs or a socket is returned
        while (true) {
            Log.i(TAG, "AcceptThread监听中...");

            try {
                socket = mmServerSocket.accept();
            } catch (IOException e) {
                break;
            }
            // If a connection was accepted
            if (socket != null) {

                try {
                    // Do work to manage the connection (in a separate thread)
//                manageConnectedSocket(socket); //启动数据传输的线程
                    if(callBack != null) {
                        callBack.onAcceptSucceed(socket);
                    }


                    Log.i(TAG, "AcceptThread连接成功");
                    mmServerSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                break;
            }
        }
    }

    /**
     * Will cancel the listening socket, and cause the thread to finish
     */
    public void cancel() {
        try {
            mmServerSocket.close();
        } catch (IOException e) {
        }
    }

    public interface AcceptCallBack {
        public void onAcceptSucceed(BluetoothSocket serverSocket);
    }

}

通信

  • 连接成功后,通过socket得到I/O流,读取数据和写入数据,在两个设备之间传输数据。
/**
 * 发送和接收数据
 * 作者: 代码来自于Google官方 -> API指南 -> 蓝牙模块
 * 日期: 18/12/14
 */

public class ConnectedThread extends Thread {
    private static final String TAG = "ConnectedThread";
    private final BluetoothSocket mmSocket;
    private final InputStream mmInStream;
    private final OutputStream mmOutStream;
    private Handler mHandler;

    public ConnectedThread(BluetoothSocket socket, Handler handler) {
        mmSocket = socket;
        InputStream tmpIn = null;
        OutputStream tmpOut = null;
        mHandler = handler;

        // Get the input and output streams, using temp objects because
        // member streams are final
        try {
            tmpIn = socket.getInputStream();
            tmpOut = socket.getOutputStream();
        } catch (IOException e) { }

        mmInStream = tmpIn;
        mmOutStream = tmpOut;
    }

    public void run() {
        byte[] buffer = new byte[1024];  // buffer store for the stream
        int bytes; // bytes returned from read()

        // Keep listening to the InputStream until an exception occurs
        while (true) {
            try {
                // Read from the InputStream
                bytes = mmInStream.read(buffer);
                // Send the obtained bytes to the UI activity
                if(mHandler != null) {
                    mHandler.obtainMessage(ServerActivity.MESSAGE_READ, bytes, -1, buffer)
                            .sendToTarget();
                }

            } catch (IOException e) {
                break;
            }
        }
    }

    /* Call this from the main activity to send data to the remote device */
    public void write(byte[] bytes) {
        try {
            mmOutStream.write(bytes);
        } catch (IOException e) { }
    }

    /* Call this from the main activity to shutdown the connection */
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) { }
    }
}

ConnectThread、AcceptThread 和 ConnectedThread 都是Google官方文档里面的示例,我自己加了些回调方法而已,如果觉得比较乱,可以直接去看官方文档。官方文档戳这里

代码上传到了GitHub, 功能比较简单,两台手机一个充当client,一个充当server,严格按照 开启 >> 扫描 >> 配对 >> 连接 >> 通信 5个步骤走才能实现通信。仅供参考。

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

推荐阅读更多精彩内容

  • 最近项目使用蓝牙,之前并没有接触,还是发现了很多坑,查阅了很多资料,说的迷迷糊糊,今天特查看官方文档。 说下遇到的...
    King9527阅读 1,794评论 0 1
  • 公司的项目最近需要用到蓝牙开发的相关内容,因此特地查阅了Google官方文档的内容并进行二次整理,希望能对需要学习...
    Chuckiefan阅读 32,446评论 44 123
  • Android平台支持蓝牙网络协议栈,实现蓝牙设备之间数据的无线传输。本文档描述了怎样利用android平台提供的...
    Camming阅读 3,312评论 0 3
  • 前言: 本文主要描述Android BLE的一些基础知识及相关操作流程,不牵扯具体的业务实现,其中提供了针对广播包...
    幻影宇寰阅读 5,334评论 6 19
  • 今年本来安排1月17号回家过年,结果中间出了一点小插曲,我媳妇的妹妹他们喊我们一起开车回家。 时间定在了1月14,...
    冢虎欢喜哥阅读 1,139评论 7 58