本来只想使用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,安卓离线打包