本来只想使用HBuilder在线方式开发一个管理BLE蓝牙的小型APP,研究了下dcloud上所有和蓝牙有关的文档,发现经典蓝牙的可以实现,而对于BLE蓝牙并没有适当的文档。
经典蓝牙使用下述方式即可连接:
bluetoothSocket = device.createInsecureRfcommSocketToServiceRecord(uuid);
bluetoothSocket.connect();
但是对于BLE蓝牙,会报错如下:
Uncaught java.io.IOException: read failed, socket might closed or timeout, read ret: -1;at android.bluetooth.BluetoothSocket.connect
原因如下:
The problem is with the socket.mPort parameter. When you create your socket using socket = device.createRfcommSocketToServiceRecord(SERIAL_UUID); , the mPort gets integer value "-1", and this value seems doesn't work for android >=4.2 , so you need to set it to "1". The bad news is that createRfcommSocketToServiceRecord only accepts UUID as parameter and not mPort so we have to use other aproach : socket =(BluetoothSocket) device.getClass().getMethod("createRfcommSocket", new Class[] {int.class}).invoke(device,1);. We need to use both socket attribs , the second one as a fallback.
我们可以使用:
socket =(BluetoothSocket) device.getClass().getMethod("createRfcommSocket", new Class[] {int.class}).invoke(device,1);
socket.connect();但这种使用方式目前用html5plus的 Native.js for Android 方式应该是无法实现的,Native.js无法反射抽象类。
请注意:Android 4.3(API Level 18)才开始引入Bluetooth Low Energy(BLE,低功耗蓝牙4.0)的核心功能并提供了相应的 API, 应用程序通过这些 API 扫描蓝牙设备、查询 services、读写设备的 characteristics(属性特征)等操作。也就是说蓝牙4.0只有android4.3或4.3以上才支持。
Android使用蓝牙之前先引入权限:
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/> <uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> <uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED"/>
另外要注意,蓝牙也需要 Geolocation(位置信息)权限,加上否则搜不出来设备列表。
说到安卓的权限问题,飘易特别提醒一下:
安卓6.0(API >= 23)开始实行权限的动态管理,而目前5+SDK离线版并未实现动态授权管理,因此建议安卓离线打包时设置较低的targetSdkVersion来解决这个问题,目前建议编译目标设置为Android 5.0(API 21)。
如果需要开发插件, io.dcloud.PandoraEntry作为apk入口时,必须设置 targetSDKVersion>=21 沉浸式才生效。
以下为危险的权限,需要动态授权:
身体传感器/日历/摄像头/通讯录/地理位置/麦克风/电话/短信/存储空间
其他的权限不受影响,所以在做这些危险操作的时候需要提示用户授权。否则报错如下:
java.lang.SecurityException:Permission Denial: starting Intent { act=android.media.action.IMAGE_CAPTUREflg=0x3 cmp=com.android.camera/.Camera clip={text/uri-listU:file:///storage/emulated/0/bdc97b284f5549d5b9d89fe6f7fcc7ba.jpg} (has extras)} from ProcessRecord{382b57 16353:cn.xzkj.chihuo/u0a189} (pid=16353, uid=10189)with revoked permission android.permission.CAMERA
飘易使用的Android Studio 3.0进行插件开发,过程中需要安装不少依赖包,而GOOGLE的安卓资源在国内基本是被qiang的,因此要开发安卓,先把路打通:
先挂上v-p-n(建议思科的anyConnect),然后必须在系统的hosts文件里手动绑定dl.google.com的ip地址到正确的海外ip(国内ip污染严重):
172.217.*.* dl.google.com
可以到 http://ping.chinaz.com/ 获取dl.google.com 的海外ip。如何判断绑定的ip有效呢?在浏览器中打开:
https://dl.google.com/android/repository/repository2-1.xml,如果该网址可以打开,说明绑定的ip是有效的。
在Android Studio 3.0 里build.gradle添加阿里云的国内镜像:
allprojects {
repositories {
maven{ url 'http://maven.aliyun.com/nexus/content/groups/public/'}
jcenter()
google()
}
}其中,google() 默认使用了google自家的maven库:https://maven.google.com,而该地址默认301跳转到https://dl.google.com/dl/android/maven2/ 了。
蓝牙原生部分,飘易使用的是开源项目FastBle:https://github.com/Jasonchenlijian/FastBle
使用Gradle安装方式:
compile 'com.clj.fastble:FastBleLib:2.2.2'
下面就是如何编写原生代码实现蓝牙相关的状态获取,扫描,连接,读写等操作了:
package com.plugin.bluetooth;
...
//蓝牙状态
public void state(IWebview pWebview, JSONArray array){
boolean ble_support = BleManager.getInstance().isSupportBle();//是否支持蓝牙BLE
boolean ble_enable = BleManager.getInstance().isBlueEnable();//是否打开蓝牙
final String CallBackID = array.optString(0);//回调通知id
String ReturnString = null;
if(ble_support){
if(ble_enable){
// 调用方法将原生代码的执行结果返回给js层并触发相应的JS层回调函数
JSUtil.execCallback(pWebview, CallBackID, "Bluetooth is open", JSUtil.OK, false);
}else{
// 调用方法将原生代码的执行结果返回给js层并触发相应的JS层回调函数
JSUtil.execCallback(pWebview, CallBackID, "Bluetooth is closed", JSUtil.ERROR, false);
// 可以手动打开蓝牙
//BleManager.getInstance().enableBluetooth();
}
}else{
// 调用方法将原生代码的执行结果返回给js层并触发相应的JS层回调函数
JSUtil.execCallback(pWebview, CallBackID, "Bluetooth is not supported", JSUtil.ERROR, false);
}
}
//scan扫描 第1个参数是:回调id ;
public void scan(final IWebview pWebview, JSONArray array){
final String CallBackID = array.optString(0);
//扫描规则
BleScanRuleConfig scanRuleConfig = new BleScanRuleConfig.Builder()
//.setServiceUuids(serviceUuids) // 只扫描指定的服务的设备,可选
//.setDeviceName(true, names) // 只扫描指定广播名的设备,可选
//.setDeviceMac(mac) // 只扫描指定mac的设备,可选
.setAutoConnect(false) // 连接时的autoConnect参数,可选,默认false
.setScanTimeOut(30000) // 扫描超时时间,可选,默认10秒;小于等于0表示不限制扫描时间
.build();
BleManager.getInstance().initScanRule(scanRuleConfig);
BleManager.getInstance().scan(new BleScanCallback() {
@Override
public void onScanStarted(boolean success) {
// 开始(主线程)
deviceMap = new HashMap<String, JSONArray>();//传递给js层
bleDeviceMap = new HashMap<String, BleDevice>();//内部持有
deviceMap.clear();
bleDeviceMap.clear();
Log.d("BLE","scan started");
JSUtil.execCallback(pWebview, CallBackID, "scan started", JSUtil.OK, true);//回调通知js层
}
@Override
public void onScanning(BleDevice bleDevice) {
// 扫描到一个符合扫描规则的BLE设备(主线程)
JSONArray newArray = new JSONArray();
newArray.put(bleDevice.getName());
newArray.put(bleDevice.getRssi());
newArray.put(bleDevice.getMac());
if(!deviceMap.containsKey(bleDevice.getMac())){
deviceMap.put(bleDevice.getMac(), newArray);
bleDeviceMap.put(bleDevice.getMac(), bleDevice);
}
JSONObject jsonObj = new JSONObject(deviceMap);//转JSONObject
//日志
Log.d("BLE", jsonObj.toString());
JSUtil.execCallback(pWebview, CallBackID, jsonObj, JSUtil.OK, true);//回调通知js层
}
@Override
public void onScanFinished(List<BleDevice> scanResultList) {
// 扫描结束,列出所有扫描到的符合扫描规则的BLE设备(主线程)
Log.d("BLE", "scan finished");
JSUtil.execCallback(pWebview, CallBackID, "scan finished", JSUtil.OK, true);//回调通知js层
}
});
}
//连接 第1个参数是:回调id ; 第2个参数:设备的 mac 地址
public void connect(final IWebview pWebview, JSONArray array) {
final String CallBackID = array.optString(0);
String mac = array.optString(1);//这里是指 mac address
if(!bleDeviceMap.containsKey(mac)) {
JSUtil.execCallback(pWebview, CallBackID, "device not found", JSUtil.ERROR, false);//回调通知js层
}
BleDevice bleDevice = bleDeviceMap.get(mac);
if(BleManager.getInstance().isConnected(bleDevice)){
JSUtil.execCallback(pWebview, CallBackID, "device is already connected", JSUtil.OK, true);//回调通知js层
}else {
//先取消扫描
BleManager.getInstance().cancelScan();
//初始化变量
final HashMap<String, HashMap> mapS = new HashMap<String, HashMap>();
//连接
BleManager.getInstance().connect(bleDevice, new BleGattCallback() {
@Override
public void onStartConnect() {
// 开始连接
JSUtil.execCallback(pWebview, CallBackID, "start connect", JSUtil.OK, true);//回调通知js层
}
@Override
public void onConnectFail(BleException exception) {
// 连接失败
JSUtil.execCallback(pWebview, CallBackID, "connect fail: "+exception.getDescription()+" Code:"+String.valueOf(exception.getCode()), JSUtil.ERROR, true);//回调通知js层
}
@Override
public void onConnectSuccess(BleDevice bleDevice, final BluetoothGatt gatt, int status) {
// 连接成功,BleDevice即为所连接的BLE设备
JSUtil.execCallback(pWebview, CallBackID, "device connected", JSUtil.OK, true);//回调通知js层
//安卓下蓝牙操作必须延时
new Timer().schedule(new TimerTask() {
@Override
public void run() {
//延时操作的run方法
mapS.clear();
for (BluetoothGattService service : gatt.getServices()) {
List<BluetoothGattCharacteristic> characteristicList = service.getCharacteristics();
HashMap<String, JSONArray> mapC = new HashMap<String, JSONArray>();
for(BluetoothGattCharacteristic c : service.getCharacteristics()){// 特征
JSONArray arr = new JSONArray();
arr.put(String.valueOf(c.getProperties()));
arr.put(c.getUuid().toString());
mapC.put(c.getUuid().toString(), arr);
}
if(!mapS.containsKey(service.getUuid().toString())){
mapS.put(service.getUuid().toString(), mapC);
}
}
JSONObject jsonObj = new JSONObject(mapS);//转JSONObject
//日志
Log.d("BLE", jsonObj.toString());
JSUtil.execCallback(pWebview, CallBackID, jsonObj, JSUtil.OK, true);//回调通知js层
}
},600);//延时多少ms执行
}
@Override
public void onDisConnected(boolean isActiveDisConnected, BleDevice bleDevice, BluetoothGatt gatt, int status) {
// 连接中断,isActiveDisConnected表示是否是主动调用了断开连接方法
//gatt.connect();//断开自动重新连接
JSUtil.execCallback(pWebview, CallBackID, "device disconnected", JSUtil.ERROR, true);//回调通知js层
}
});
}
}
//断开某个连接 第1个参数是:回调id ; 第2个参数:设备的 mac 地址
public void cancelConnect(final IWebview pWebview, JSONArray array) {
final String CallBackID = array.optString(0);
String mac = array.optString(1);//这里是指 mac address
if(!bleDeviceMap.containsKey(mac)) {
JSUtil.execCallback(pWebview, CallBackID, "device not found", JSUtil.ERROR, false);//回调通知js层
}
BleDevice bleDevice = bleDeviceMap.get(mac);
if(BleManager.getInstance().isConnected(bleDevice)) {
BleManager.getInstance().disconnect(bleDevice);
}
JSUtil.execCallback(pWebview, CallBackID, "connect cancelled", JSUtil.OK, false);//回调通知js层
}
//断开所有连接 第1个参数是:回调id ;
public void cancelAllConnect(final IWebview pWebview, JSONArray array) {
final String CallBackID = array.optString(0);
BleManager.getInstance().disconnectAllDevice();
JSUtil.execCallback(pWebview, CallBackID, "all connect cancelled", JSUtil.OK, false);//回调通知js层
}
//取消扫描 第1个参数是:回调id ;
public void cancelScan(final IWebview pWebview, JSONArray array) {
final String CallBackID = array.optString(0);
BleManager.getInstance().cancelScan();
JSUtil.execCallback(pWebview, CallBackID, "scan cancelled", JSUtil.OK, false);//回调通知js层
}
//读取特征 - 订阅通知notify | 第1个参数是:回调id ; Argus1:设备mac地址 Argus2:服务uuid Argus3:特征uuid
public void notify(final IWebview pWebview, JSONArray array) {
final String CallBackID = array.optString(0);
String mac = array.optString(1);//这里是指 mac address
String uuid_service = array.optString(2);//service uuid
String uuid_characteristic_notify = array.optString(3);//characteristic uuid
if(!bleDeviceMap.containsKey(mac)) {
JSUtil.execCallback(pWebview, CallBackID, "device not found", JSUtil.ERROR, false);//回调通知js层
}
BleDevice bleDevice = bleDeviceMap.get(mac);
if(!BleManager.getInstance().isConnected(bleDevice)){
JSUtil.execCallback(pWebview, CallBackID, "device not connected", JSUtil.ERROR, false);//回调通知js层
}
BleManager.getInstance().notify(
bleDevice, uuid_service, uuid_characteristic_notify,
new BleNotifyCallback() {
@Override
public void onNotifySuccess() {
// 打开通知操作成功
Log.d("BLE", "notify success");
}
@Override
public void onNotifyFailure(BleException exception) {
// 打开通知操作失败
JSUtil.execCallback(pWebview, CallBackID, "notify fail: "+exception.getDescription(), JSUtil.ERROR, false);//回调通知js层
}
@Override
public void onCharacteristicChanged(byte[] data) {
// 打开通知后,设备发过来的数据将在这里出现
String s = HexUtil.formatHexString(data, false);
Log.d("BLE", s);
JSUtil.execCallback(pWebview, CallBackID, s, JSUtil.OK, true);//回调通知js层
}
}
);
//over
}
//取消读取特征 - 取消订阅通知notify | 第1个参数是:回调id ; Argus1:设备mac地址 Argus2:服务uuid Argus3:特征uuid
public void cancelNotify(final IWebview pWebview, JSONArray array) {
final String CallBackID = array.optString(0);
String mac = array.optString(1);//这里是指 mac address
String uuid_service = array.optString(2);//service uuid
String uuid_characteristic_notify = array.optString(3);//characteristic uuid
if(!bleDeviceMap.containsKey(mac)) {
JSUtil.execCallback(pWebview, CallBackID, "device not found", JSUtil.ERROR, false);//回调通知js层
}
BleDevice bleDevice = bleDeviceMap.get(mac);
if(!BleManager.getInstance().isConnected(bleDevice)){
JSUtil.execCallback(pWebview, CallBackID, "device not connected", JSUtil.ERROR, false);//回调通知js层
}
//取消订阅通知notify,并移除数据接收的回调监听
if(BleManager.getInstance().stopNotify(bleDevice, uuid_service, uuid_characteristic_notify)){
JSUtil.execCallback(pWebview, CallBackID, "notify cancelled", JSUtil.OK, false);//回调通知js层
}else{
JSUtil.execCallback(pWebview, CallBackID, "notify cancel fail", JSUtil.ERROR, false);//回调通知js层
}
}
//写入特征 | 第1个参数是:回调id ; Argus1:设备mac地址 Argus2:服务uuid Argus3:特征uuid;Argus4:写入的16进制字符串
public void write(final IWebview pWebview, JSONArray array) {
final String CallBackID = array.optString(0);
String mac = array.optString(1);//这里是指 mac address
String uuid_service = array.optString(2);//service uuid
String uuid_characteristic_notify = array.optString(3);//characteristic uuid
String dataHex = array.optString(4);//要写入的16进制字符串
if(!bleDeviceMap.containsKey(mac)) {
JSUtil.execCallback(pWebview, CallBackID, "device not found", JSUtil.ERROR, false);//回调通知js层
}
BleDevice bleDevice = bleDeviceMap.get(mac);
if(!BleManager.getInstance().isConnected(bleDevice)){
JSUtil.execCallback(pWebview, CallBackID, "device not connected", JSUtil.ERROR, false);//回调通知js层
}
//写入
byte[] data = HexUtil.hexStringToBytes(dataHex);
BleManager.getInstance().write(
bleDevice, uuid_service, uuid_characteristic_notify, data,
new BleWriteCallback() {
@Override
public void onWriteSuccess() {
// 发送数据到设备成功
JSUtil.execCallback(pWebview, CallBackID, "write success", JSUtil.OK, false);//回调通知js层
}
@Override
public void onWriteFailure(BleException exception) {
// 发送数据到设备失败
JSUtil.execCallback(pWebview, CallBackID, "write fail", JSUtil.ERROR, false);//回调通知js层
}
}
);
//over
}接下来编写原生类和js层的对应关系:
开发者在实现JS层API时首先要定义一个插件类的别名,并需要在Android工程的assets\data\dcloud_properties.xml文件中声明插件类别名和Native层扩展插件类的对应关系:
<properties> <features> <feature name="bluetooth" value="com.plugin.bluetooth.Bluetooth"></feature> </features> </properties>
再接下来就是js层的事了:
document.addEventListener( "plusready", function()
{
var PluginJSName = 'bluetooth', B = window.plus.bridge;
var bluetooth =
{
//判断蓝牙状态是否可用
state : function (successCallback, errorCallback )
{
var success = typeof successCallback !== 'function' ? null : function(args)
{
successCallback(args);
},
fail = typeof errorCallback !== 'function' ? null : function(code)
{
errorCallback(code);
};
callbackID = B.callbackId(success, fail);
return B.exec(PluginJSName, "state", [callbackID]);
},
//扫描
scan : function (successCallback, errorCallback )
{
var success = typeof successCallback !== 'function' ? null : function(args)
{
successCallback(args);
},
fail = typeof errorCallback !== 'function' ? null : function(code)
{
errorCallback(code);
};
callbackID = B.callbackId(success, fail);
return B.exec(PluginJSName, "scan", [callbackID]);
},
//停止扫描
cancelScan : function (successCallback, errorCallback )
{
var success = typeof successCallback !== 'function' ? null : function(args)
{
successCallback(args);
},
fail = typeof errorCallback !== 'function' ? null : function(code)
{
errorCallback(code);
};
callbackID = B.callbackId(success, fail);
return B.exec(PluginJSName, "cancelScan", [callbackID]);
},
//选择一个设备连接
connect : function (Argus1, successCallback, errorCallback )
{
var success = typeof successCallback !== 'function' ? null : function(args)
{
successCallback(args);
},
fail = typeof errorCallback !== 'function' ? null : function(code)
{
errorCallback(code);
};
callbackID = B.callbackId(success, fail);
return B.exec(PluginJSName, "connect", [callbackID, Argus1]);
},
//断开一个设备连接
cancelConnect : function (Argus1, successCallback, errorCallback )
{
var success = typeof successCallback !== 'function' ? null : function(args)
{
successCallback(args);
},
fail = typeof errorCallback !== 'function' ? null : function(code)
{
errorCallback(code);
};
callbackID = B.callbackId(success, fail);
return B.exec(PluginJSName, "cancelConnect", [callbackID, Argus1]);
},
//断开所有设备连接
cancelAllConnect : function (successCallback, errorCallback )
{
var success = typeof successCallback !== 'function' ? null : function(args)
{
successCallback(args);
},
fail = typeof errorCallback !== 'function' ? null : function(code)
{
errorCallback(code);
};
callbackID = B.callbackId(success, fail);
return B.exec(PluginJSName, "cancelAllConnect", [callbackID]);
},
//读数据 Argus1:设备mac地址 Argus2:服务uuid Argus3:特征uuid
notify : function (Argus1, Argus2, Argus3, successCallback, errorCallback )
{
var success = typeof successCallback !== 'function' ? null : function(args)
{
successCallback(args);
},
fail = typeof errorCallback !== 'function' ? null : function(code)
{
errorCallback(code);
};
callbackID = B.callbackId(success, fail);
return B.exec(PluginJSName, "notify", [callbackID, Argus1, Argus2, Argus3]);
},
//写数据 Argus1:设备mac地址 Argus2:服务uuid Argus3:特征uuid;Argus4:写入的16进制字符串
write : function (Argus1, Argus2, Argus3, Argus4, successCallback, errorCallback )
{
var success = typeof successCallback !== 'function' ? null : function(args)
{
successCallback(args);
},
fail = typeof errorCallback !== 'function' ? null : function(code)
{
errorCallback(code);
};
callbackID = B.callbackId(success, fail);
return B.exec(PluginJSName, "write", [callbackID, Argus1, Argus2, Argus3, Argus4]);
},
//取消读数据 Argus1:设备mac地址 Argus2:服务uuid Argus3:特征uuid
cancelNotify : function (Argus1, Argus2, Argus3, successCallback, errorCallback )
{
var success = typeof successCallback !== 'function' ? null : function(args)
{
successCallback(args);
},
fail = typeof errorCallback !== 'function' ? null : function(code)
{
errorCallback(code);
};
callbackID = B.callbackId(success, fail);
return B.exec(PluginJSName, "cancelNotify", [callbackID, Argus1, Argus2, Argus3]);
},
};
window.plus.bluetooth = bluetooth;
}, true );【离线打包注意点】:
1,配置应用的包名及版本号
打开AndroidManifest.xml文件,在代码视图中修改根节点的package属性值,其中package为应用的包名,采用反向域名格式,为应用的标识;versionCode为应用的版本号(整数值),用于各应用市场的升级判断,建议与manifest.json中version -> code值一致;versionName为应用的版本名称(字符串),在系统应用管理程序中显示的版本号,建议与manifest.json中version -> name值一致。
2,配置应用名称
打开res -> values -> strings.xml文件,修改“app_name”字段值,该值为安装到手机上桌面显示的应用名称。
3,配置应用图标和启动界面
将应用的图标(文件名为icon.png)和启动图片按照对应的尺寸拷贝到工程的res -> drawable-XXX目录下。
4,更新应用资源
打开assets -> apps 目录,将下面“HelloH5”目录名称修改为应用manifest.json中的id名称(这步非常重要,否则会导致应用无法正常启动),并将所有应用资源拷贝到其下的www目录中。
5,配置应用信息
打开assets -> data下的control.xml文件,修改appid值;其中appid值为HBuilder应用的appid,必须与应用manifest.json中的id值完全一致;appver为应用的版本号,用于应用资源的升级,必须保持与manifest.json中的version -> name值完全一致;version值为应用基座版本号(plus.runtime.innerVersion返回的值),不要随意修改。
【参考】:
2,IOException: read failed, socket might closed - Bluetooth on Android 4.3
3,安卓离线打包