Android Bluetooth 与 Headset 通信

logo

本文主要记录 Android 与蓝牙设备通信过程的整个流程,并对流程中的一些坑给出相应的解决思路。
本文中的通信设备是蓝牙耳机,其他蓝牙设备整体思路及流程类似,视具体情况稍加调整

最近手上有个项目是基于移动端 App 与蓝牙耳机通信的,死磕一番发现蓝牙真的是...

for (int i = 0; i < 10000; i ++) {
    fuck("Android Bluetooth");
}

下图是项目完成后整理的一份流程表,希望对大家有帮助

流程图

通过图中所示流程相信大部分开发者都能清楚的了解到蓝牙的整个连接过程,但是为什么要画这张图呢?是因为在图中星标的这些位置需要引起大家注意

获取蓝牙适配器

这是所有蓝牙开发的第一步

在 Android API 17 及之前的版本中,需要通过 BluetoothAdapter 的 getDefaultAdapter() 函数进行获取

在 Android API 18 开始可以通过 BluetoothManager 这个类的 getAdapter() 函数获取到 BluetoothAdapter 对象

判断蓝牙开关

既然要用蓝牙,你得保证它开着啊

开关检测可以通过 BluetoothAdapter 的 isEnabled() 函数进行检测,返回 true 表示已开启,false 未开启,如果 BluetoothAdapter 为 null 则表示设备不支持蓝牙

如果蓝牙未开启,这个时候就需要申请开启蓝牙

这里特别说明,不要使用 mBluetoothAdapter.enable() 这句代码来开启蓝牙,在 Android 动态授权的机制出来后,这句代码在某些高版本的系统中无效,这对用户体验是致命的,就好像你一拳打出去打在空气中一样?当然,如果你觉得用户体验没个屌用就请原谅在下这顿逼逼叨

为了保证通过较好的用户体验来打开蓝牙,可以使用如下代码申请

// REQUEST_ENABLE_BT 是定义的局部变量,在 onActivityResult 会通过 requestCode 变量返回该值,用于识别操作类型
Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableIntent, REQUEST_ENABLE_BT);

然后在 Activity 的 onActivityResult() 函数中处理用户的决定

扫描设备

扫描设备可以通过 BluetoothAdapter 的 startDiscovery() 开启扫描设备,千万要记得在发现目标设备之后立即调用 cancelDiscovery() 关闭。Google 官方也强调过扫描蓝牙设备是一个比较耗的过程,所以能省则省

扫描到的设备会以广播的形式回调给设备,所以我们需要注册 BluetoothDevice.ACTION_FOUND 的监听器,在这里便可以获取到目标设备的 BluetoothDevice 类

设备校验

这里的设备校验我采用的是通过名称匹配的方式,我们的蓝牙耳机出厂的设备名称是有一定规律的,如:HeadsetHK_38913、HeadsetHK_81390、HeadsetHK_34698

那我只要找到以 HeadsetHK_ 开头的设备就尝试进入下面的流程

可能有些同学会觉得这里的处理不够严谨,那是得看使用场景来的

如果用户正常想要使用我们的蓝牙耳机的场景下,他周围刚好有一个名字同样以 HeadsetHK_ 开头的设备并且又不是我们的产品,这种概率大家自己掂量

为什么会选用这个不够严谨的方式来进行校验,是因为在此之前,我也是希望能够蓝牙设备蓝牙设备能够给我一个反馈,告诉 app 我是自己人,快拉我上车

但是通过 BluetoothSocket 去连接设备的时候发现一个问题,蓝牙不稳定,而且部分手机有些通信堵塞的感觉

在蓝牙设备开机的情况下,BluetoothSocket 连接可能 3~5s 便可以进行通信,但是如果设备关机了,BluetoothSocket 连接的过程可能需要等上15~30s 才能给出反馈

当然针对不同的使用场景需要采用不同的方案,这里只是将我的思路给出来分享给大家

设备配对

在 API 19 中,BluetoothDevice 类新增了 createBond() 函数,可用于主动配对设备,但是在此之前的版本中,我们需要通过反射的机制来进行主动配对。代码如下

// 传入的 device 就是希望配对的 BluetoothDevice 类
Method bond = BluetoothDevice.class.getMethod("createBond");
bond.invoke(device);

连接设备

再看看上面的流程图,为什么我要将扫描设备、设备校验、连接设备和数据通信用星号标出?

扫描设备是因为比较耗,需要强调;设备校验是为了提升能够成功建立连接的准确度;

那连接设备和数据通信为什么要单独拧出来说明?而且还分为两个步骤?

按照我没有了解之前,自以为连接成功就可以进行通信了,然并卵

大家记得 Android 系统自带的蓝牙设置界面中,为什么有些系统在点击扫描到的设备列表之后,点击目标设备第一次是设备配对,而第二次点击才是连接设备?

因为每个蓝牙设备都是由0或者多个组件构成的,这些组件有音视频、拍照、定位等各个功能,而且每个设备都有一个主要和次要的组件

像蓝牙耳机主要组件肯定就是音频,但是你不能肯定的说市面上所有蓝牙耳机都没有定位组件

说这些是为了告诉大家,既然蓝牙模块其实是N个功能组件的集合,那我们是可以单独与蓝牙设备中的某一个功能组件建立连接的

同样的,如果希望发起与蓝牙设备的连接,也要使用反射

Method connect = btHeadsetCls.getMethod("connect", BluetoothDevice.class);
connect.setAccessible(true);
connect.invoke(bluetoothHeadset, device);

不论是配对还是连接的反射返回,都会返回一个 boolean 值告诉我们成功或者失败,但是我可以确切的告诉大家,这两个值是不可靠的,那这个时候我们怎么才能知道蓝牙设备与我们的 App 连接成功呢?

需要监听 BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED,该广播会反馈蓝牙连接的当前状态

我们只需要在收到广播之后,作出对应的 UI 改变即可

数据通信

确保设备连接成功之后,就需要进行数据通信了

具体的通信方式这里不再赘述,主要使用到的是 BluetoothDevice 类中的 createRfcommSocketToServiceRecord() 创建连接然后进行读取

这里直接分享给大家一个网上找到工具类

BluetoothChatUtil 蓝牙通信连接工具类下载

后话

其实文中还有很多没有提及到的细节。例如对蓝牙广播的监听是否应该放到 service 中?当用户在下拉菜单或者是转到系统设置界面中进行蓝牙设置的时候,我们 app 是接收不到广播的。

本文没有对蓝牙权限获取的步骤进行说明,直接从获取蓝牙适配器开始。后面会陆续整理出相应的文章希望大家关注

最后修改:2017/10/26 20:13
如果觉得我的文章对你有用,请随意赞赏

1 条评论

  1. Bowen

    感谢作者梳理 非常清晰 另外想请教sco控制要如何处理比较好?<img src="https://ww4.sinaimg.cn/large/a15b4afegy1fcgqhwtenoj203902g743">

发表评论

颜文字