经过一个多月的时间蓝牙多设备连接的重构终于告一段落了,这次的重构不止是代码方面的完善,还结合了一些用户的使用场景,另外增加一些离线操作,使手机端对蓝牙的操作更加的便捷,对蓝牙设备的管理更加统一。
现在一台手机可以连接多个设备,例如连接蓝牙耳机,智能手环等。既然手机可以连接多个设备,那么移动应用也是可以连接多个设备的(血压计、心率计等),下面就是移动应用 App 实现多设备连接的思路方法。
关于蓝牙连接,主要是 BluetoothGatt 这个类型,每个蓝牙的连接都需要用独立且唯一的 BluetoothGatt 。开始的想法是每个蓝牙都重新创建一个 Service, 在新的 Service 内使用 BluetoothGatt 进行连接,然而这个方法是可以实现多设备连接,但是创建多个 Service 对手机消耗比较大。之后,想到把 BluetoothGatt 保存起来不就可以了么,那用什么保存呢,既可以临时保存多个,又可以按照需要获取相对应的 BluetoothGatt 。在 java 里面有个类型 Map(String, Object) ,它是以 key-value 的形式存储到 Map 中。可以根据当时的 Key 来取相应的 Value 值,而且在关掉进程时相应的变量也就释放了。
private Map mBluetoothGattMap = new HashMap<>(); //临时保存 BluetoothGatt
private Map mGattCharacteristicMap = new HashMap<>();// 临时保存蓝牙的特征值 Characteristic
private Map mBluetoothInfoMap = new HashMap<>();// 临时保存自己设置的蓝牙信息(deviceName、deviceType、startCMD、stopCMD 等)
private Map mDeviceTypeMap = new HashMap<>();// 临时存储 deviceType
private Map mConnectModelMap = new HashMap<>();// 临时存储 已连接的设备
//...
/**
* 连接设备
*
* @param deviceType 设备类型
* @return true 连接成功,false连接失败
*/
boolean connectBluetooth(Context context, String deviceType, String deviceAddress) {
//...
BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(deviceAddress);//根据 mac 地址获取蓝牙设备
//...
BluetoothGatt bluetoothGatt = device.connectGatt(this, false, mGattCallback);// 对蓝牙进行连接操作
//...
mBluetoothGattMap.put(deviceType, bluetoothGatt);//把 BluetoothGatt 已 key-value 的形式临时保存起来
return true;
}
要有良好的用户操作体验,我们应该避免对一些无关的操作重复进行。例如我们第一次打开应用连接了蓝牙设备,以后再打开 App 不需要重复操作连接过程,用户就可以少打开一个页面,少点击两次按钮。减少用户重复操作,让用户直接进入正题,提高主功能的使用率。
在 Android 中连接蓝牙的方法是
public BluetoothGatt connectGatt(Context context, boolean autoConnect,
BluetoothGattCallback callback) {
return (connectGatt(context, autoConnect,callback, TRANSPORT_AUTO));
}
其中 BluetoothGatt 是每个连接成功的蓝牙返回唯一的属性。当蓝牙设备连接成功后会返回唯一的 BluetoothGatt ,并用它进行对蓝牙的命令操作;autoConnect 是设置是否为自动连接的一个属性,然而根据我自己的测试,当 autoConnect 的属性设置为 true 时,是有可能自动连接的,但是有时也会失效,所以不采用;BluetoothGattCallback 是蓝牙连接、命令操作,数据返回等成功时的回调。
因为 autoConnect 的设置具有不确定性,所以我们采取另一种方式:当我们第一次连接蓝牙成功的时候,把蓝牙的 Mac 地址存储起来;在第二次启动 App 的时候,先把蓝牙的 Mac 地址从 SharedPreferences 中取出来,用 Mac 地址进行连接,如果连接失败(可能结果是设备不对或者设备没有打开),我们就开启蓝牙扫描功能,进行重新扫描设备,打开设备进行连接。
/**
*保存 mac 地址到 SharedPreferences
*/
public void saveMac(Context context, String macAddress){
if (null != context) {
SharedPreferences sharedPreferences = context.getSharedPreferences(BLUETOOTH_MAC_TABLE, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(deviceType, bluetoothAddress);
editor.apply();
GGLog.i(TAG, "save success");
}
}
void autoConnect(Context context, final String deviceType) {
/*防止蓝牙 adapter 为空,程序崩溃*/
if (null == mBluetoothAdapter) {
GGLog.e(TAG, "method discoveryBluetooth \n mBluetoothAdapter is null");
return;
}
//根据 deviceType 获取 蓝牙 mac 地址
SharedPreferences sharedPreferences = context.getSharedPreferences(BLUETOOTH_MAC_TABLE, Context.MODE_PRIVATE);
String deviceAddress = sharedPreferences.getString(deviceType, "");
if (!"".equals(deviceAddress)) {
if (!deviceAddressList.contains(deviceAddress)) {
deviceAddressList.add(deviceAddress);
}
connectBluetooth(context, deviceType, deviceAddress);// 连接设备
}
}
void connectBluetooth(Context context, String deviceType, String deviceAddress){
//...
// 连接失败 开启扫描功能
if (!bluetoothGatt.connect() || bluetoothGatt.getServices().size() == 0) {
startScan();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
stopScan();//十秒后停止扫描
}
}, 10000);
return false;
}
}
在移动端我们很多时候,同一份数据要在多个页面上显示,又不想多次调用接口,我们只好把数据保存到本地,来达到数据共享的目的。
在我这蓝牙的开发中,是使用 SQL 对数据进行保存。蓝牙测量中的数据因为是实时更新,我们不需要进行缓存,只要把测量的数据结果和蓝牙的信息存储下来就可以了。
使用 SQL 有一点不好的是使用 ContentValues 以 key-value 的形式进行存储,那么带来一个问题就是 key 我们手写很容易写错,所以可能一不小心就会万劫不复。在 Git 上有个轻量级的数据库第三方 Litepal (https://github.com/LitePalFramework/LitePal) ,它操作简单,不需要手写 key ,对一些常用的增删改查都进行了封装,而且还支持手写 SQL 语句,在 CSDN 上博客专家郭霖有详细的介绍 Litepal 的说明和使用方法。在这里我使用的是原生的 SQL ,防止与库外的第三方冲突。
蓝牙设备信息的保存,我存储了以下信息:
存储字段 | 类型 | 功能说明 |
---|---|---|
deviceType | String | 设备类型,更具设备类型进行蓝牙操作 |
deviceAddress | String | 蓝牙的 mac 地址,用来连接蓝牙 |
deviceName | String | 蓝牙名称,扫描蓝牙时,进行校验 |
connectStatus | int | 蓝牙连接状态 |
对测量结果的存储内容是:
存储字段 | 类型 | 功能说明 |
---|---|---|
deviceType | String | 设备类型,更具设备类型进行蓝牙操作 |
measuredData | String | 测量数据,把测量完成的数据进行保存,又 int[] 转成 String 进行保存 |
measuredTime | long | 测量时间 |
isUpdate | boolean | 上传状态,判断是否上传服务区,如果没有,则在有网络的情况下自动上传 |
/**
* 插入一条测量结果。因为数据库是封装在蓝牙库里面,我们获取不到 Application的 Context 所以传递一个 context
*/
void insertComplete(Context context, CompleteModel entity) {
SQLiteTemplate sqLiteTemplate = SQLiteTemplate.getInstance(context, instance.mBLEDBManager, false);
ContentValues values = new ContentValues();
values.put("deviceType", entity.getDeviceType()); //保存蓝牙设备类型
values.put("measuredData", Arrays.toString(entity.getMeasuredData()));//保存测量结果
values.put("measuredTime", entity.getMeasuredTime());// 保存测量时间
values.put("isUpdate", entity.isUpdate());//保存是否已经上传完毕
sqLiteTemplate.insert("ble_complete_table", values);// 数据插入表中
}
这些都是移动端对数据库的简单操作,剩下的 更新、删除、查找的方法就不都一一列举了。对于蓝牙设备信息的存储与保存测量结果相似,也不列举了。
既然我们有单独蓝牙列表页面,那么就要有对蓝牙的一些基本的操作。在列表页面我们可以对蓝牙进行搜索,发现周围打开的蓝牙设备;点击链接,连接我们需要使用的血压计、厨房秤等;长安断开链接并删除相应的蓝牙设备,当我们设备不再使用或者更换新设备的时候,我们可以删除多余的设备,使页面看起来更加简洁。
首先,我们在BluetoothService里面对扫描到的蓝牙进行区分,检查是否是我们设定的的蓝牙设备(extends BaseBluetoothAdapter
的类),根据设定的蓝牙名称(deviceName)筛选扫描到的设备,之后把设备通过 Listener 监听传递到 Activity 中,并添加到列表里显示出来。
以下是 BluetoothService 中蓝牙扫描结果的处理
private void scanResult(BluetoothDevice device, int type) {
if (null != device) {
String name = device.getName();//获取扫描到的设备名称
if (null != name) {
//获取我们自己设定的蓝牙详情(deviceType、deviceName等)
for (BluetoothInfo bluetoothInterface : mInterfaceList) {
String deviceName = bluetoothInterface.getDeviceName();
//判断设备名称是否与我们自己设定的名称相同
if (!deviceName.equals(name)) {
continue;
}
//判断设备是否已经添加到列表中
if (deviceNameList.contains(deviceName)) {
continue;
}
// 把设备名称添加到列表中
if (!deviceNameList.contains(deviceName)) {
deviceNameList.add(deviceName);
}
//如果扫描到的设备是上次连接过的设备,则自动连接。
if (null != deviceAddressList && deviceAddressList.size() > 0) {
for (String address : deviceAddressList) {
if (address.equals(device.getAddress())) {
connectBluetooth(null, bluetoothInterface.getDeviceType(), address);
}
}
}
GGBLEDeviceEntity entity = BLEDeviceToGGBLEEntity(bluetoothInterface.getDeviceType(), device);//把bluetoothDevice 转换成我们自定义的BLEEntity。
if (null != mResultListener) {
mResultListener.onScanResult(entity);//扫描结果监听赋值
} else {
setError(bluetoothInterface.getDeviceType(), HHCBluetoothProfile.ERROR_NULL, "mResultListener is null");
}
}
}
}
}
//根据设备类型获取连接的蓝牙设备
GGBLEDeviceEntity getConnectedDevice(String deviceType) {
return mConnectModelMap.get(deviceType);
}
//获取所有的连接设备
List getAllConnectedDevice() {
List connectModelList = new ArrayList<>();
for (String type : mConnectModelMap.keySet()) {
connectModelList.add(mConnectModelMap.get(type));
}
return connectModelList;
}
mBluetoothAdapter.startLeScan(mLeScanCallback);
; mBluetoothAdapter.stopLeScan(mLeScanCallback);
mBluetoothAdapter.getBluetoothLeScanner().startScan(mScanCallback);
; mBluetoothAdapter.getBluetoothLeScanner().stopScan(mScanCallback);
mBluetoothAdapter.startDiscovery();
; mBluetoothAdapter.cancelDiscovery();
* demo 下载地址:https://github.com/wdmxzf/BluetoothDemo