Ionic2Cordova蓝牙插件封装

首先有Ionic2以及Cordova环境
如果没有在命令行执行以下命令

    npm install -g ionic cordova  //全局安装ionic 和cordova指令
    ionic start myApp tabs --v1  创建ionic2项目之前文章讲到了 “--v1&&--v2代表ionic不同版本”
    npm install -g plugman  //全局安装cordova插件命令 
    cordova platform add android //添加android平台 默认创建的项目只包含ios平台 其他的平台需要使用命令添加

现在基本基本开发环境已经就绪如果需要具体安装环境请跳转到Ionic2探索总结

首先我们查看一下plugman的帮助命令 直接输入plugman回车就能看到帮助,从帮助中找到下面这一句话

    plugman create --name <pluginName> --plugin_id <pluginID> --plugin_version <version> [--path <directory>] [--variable NAME=VALUE]

这句话就是创建插件的命令,下面我给出一个创建的事例代码 最好是到刚才创建好的项目跟目录执行

    plugman create --name bluetooths --plugin_id bluetooths --plugin_version 0.0.1 
    /*
    [--path <directory>]这个是可选的,如果你想创建到别的目录下的话可以通过这个可选的指定路径,如果仅仅想创建到当前路径下的话 可以省略这个可选的指令  
    */

来看一下插件创建完毕后的目录以及文件

    .
    ├── plugin.xml
    ├── src       
    └── www
        └── bluetooths.js

这是当前的目录,然而感觉缺点什么?缺的是各平台的代码。我们再回头看看plugman这个命令的帮助 有这样的命令

    Add a Platform to a Plugin
    --------------------------

        $ plugman platform add --platform_name <platform>

    Parameters:

    - <platform>: One of android, ios

    Remove a Platform from a Plugin
    -------------------------------

        $ plugman platform remove --platform_name <platform>

    Parameters:

    - <platform>: One of android, ios

看到这应该就知道了 我们添加平台

    cd bluetooths
    plugman platform add --platform_name android
    plugman platform add --platform_name ios

我们查看目录

    .
    ├── plugin.xml
    ├── src
    │   ├── android
    │   │   └── bluetooths.java
    │   └── ios
    │       └── bluetooths.m
    └── www
        └── bluetooths.js

    4 directories, 4 files

分析文件

plugin.xml

    <?xml version='1.0' encoding='utf-8'?>
    <plugin id="bluetooths" version="0.0.1" xmlns="http://apache.org/cordova/ns/plugins/1.0"   @1 xmlns:android="http://schemas.android.com/apk/res/android">
        <name>bluetooths</name>           @2
        <js-module name="bluetooths" src="www/bluetooths.js">   @3
            <clobbers target="cordova.plugins.bluetooths" />    @4
        </js-module>                                            @5
        <platform name="android">                               @6
            <config-file parent="/*" target="res/xml/config.xml">
                <feature name="bluetooths">
                    <param name="android-package" value="bluetooths.bluetooths" />
                </feature>
            </config-file>
            <config-file parent="/*" target="AndroidManifest.xml" />
            <source-file src="src/android/bluetooths.java" target-dir="src/bluetooths/bluetooths" />
        </platform>
        <platform name="ios">                                 @7
            <config-file parent="/*" target="config.xml">
                <feature name="bluetooths">
                    <param name="ios-package" value="bluetooths" />
                </feature>
            </config-file>
            <source-file src="src/ios/bluetooths.m" />
        </platform>
    </plugin>

1, 这个行指定了这个插件的id 版本

2, 插件名字

3, 4,5, 这个是插件的js部分 src说明js插件的文件的位置 target代表在怎么在全局中引用这个插件如果在es5中可以直接使用cordova.plugins.bluetooths这个对象上的各个方法,如果在es6以及typescript中使用的时候得先在代码的最上边加入这个

    declare var cordova:any
    ...
    cordova.plugins.bluetooths.function...

6, android平台的配置

7, ios平台的配置

bluetooths.js

    var exec = require('cordova/exec');  //引入cordova内部已经实现与原生交互的接口

    exports.coolMethod = function(arg0, success, error) {
        exec(success, error, "bluetooths", "coolMethod", [arg0]);
    };

上面三行代码实现了 导出一个名为coolMethod的方法 这个方法有三个参数 arg0为我们调用这个方法的时候参数,以及回调方法。

这个方法的内部是调用cordova插件的接口前两个是回调的方法,第三个是指定会响应到那个插件,第四个表面看是调用指定的方法,其实cordova插件的工作只是把coolMethod这个值传了过去(这个在android代码的时候说明) 以及后面的参数。

我们现在来看android的代码

    public class bluetooths extends CordovaPlugin {

        @Override
        public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
            if (action.equals("coolMethod")) {
                String message = args.getString(0);
                this.coolMethod(message, callbackContext);
                return true;
            }
            return false;
        }

        private void coolMethod(String message, CallbackContext callbackContext) {
            if (message != null && message.length() > 0) {
                callbackContext.success(message);
            } else {
                callbackContext.error("Expected one non-empty string argument.");
            }
        }
    }

这个插件类继承CordovaPlugin类,重写execute这个方法
action:触发的事件名 String类型
args:之前用户调用插件穿的值 JSONArray类型
callbackContext:回调类 CallbackContext类型

为什么说只是传了个方法的值过来,先看action是一个字符串类型的,在下面我们通过action.equals("coolMethod")来判断是否相等 这个类似于 ==。如果相等就调用this.coolMethod(message, callbackContext);这个内部方法可以随意更改。

当我们完成这插件后,我们就需要把这个插件应用到我们的项目中(ios因为没接触过暂时不讲)

    cd ../
    cordova plugin add ./bluetooths/

添加完成后点开plugins目录看到有bluetoots这个文件夹说明插件添加成功,调用在前面已经说了,下面写一下具体的用法

    declare var cordova:any
    ...
    cordova.plugins.bluetooths.coolMethod("message",(res)=>{
        alert(res)
    },(err)=>{
        alert(err)
    })

虽然cordvoa提供的插件库比较丰富,但是我们的业务需要监听蓝牙被用户手动在设置里更改后的信息,因为cordova提供的插件并没有这样的监听,所以踩这个坑了。

蓝牙监听插件js代码

js这边的代码js这边的代码非常简单

    var arg0="message"
    exports.registerReceiver = function(success,error){   //注册监听的js方法
        exec(success,error,"bluetooths","registerReceiver",[arg0]);
    }
    exports.unregisterReceiver = function(success,error){
        exec(success,error,"bluetooths","unregisterReceiver",[arg0]);  //取消监听的js方法
    }

蓝牙监听插件android代码

添加权限

在android代码里面基本都是纯android api里的方法,类。所以熟悉android的很容易写出下面的代码,当然我是个纯web前端开发出生的,android代码写的不好请见谅。

因为是要操作蓝牙所以需要取得权限在AndroidManifest.xml文件中的manifest标签里添加下面的代码

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

分发执行函数

在js代码中我们传递的是五个参数 但是经过CordovaPlugin这个类的接受处理后我们在adnroid看到的只有三个参数。
它把js这端的成功和失败回调通过CallbackContext处理,使得我们可以通过类型为CallbackContext传进来的参数,调用js端的方法,同时传参给js方法。第三个参数也是就“bluetooths”是给CordovaPlugin的标志,调用的是哪一个插件,最后两个参数分别为调用的方法名以及参数。对应到execute方法中的参数为action,args。

先匹配方法名调用不同的方法或者做不同的事代码如下

    public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
            if(action.equals("registerReceiver")){  //如果action==“registerReceiver”注册
                String message = args.getString(0);
                this.registerReceiver(callbackContext);   //自定义方法后面讲
                return true;
            }else if(action.equals("unregisterReceiver")){ 如果action==“unregisterReceiver” 取消
                this.unregisterReceiver(callbackContext);  //自定义方法后面讲
            }
            return true;
        }

device对象转化为JSONObject

这段代码是从cordova-plugin-bluetooth-serial这个插件的328-338行代码拷贝过来的

    private JSONObject deviceToJSON(BluetoothDevice device) throws JSONException {
        JSONObject json = new JSONObject();
        json.put("name", device.getName());
        json.put("address", device.getAddress());
        json.put("id", device.getAddress());
        if (device.getBluetoothClass() != null) {
            json.put("class", device.getBluetoothClass().getDeviceClass());
        }
        return json;
    }

蓝牙显示通信构建方法(IntentFilter)

    private IntentFilter makeFilter() {
        IntentFilter filter = new IntentFilter();
        filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); //添加连接action
        filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);//添加断开action
        return filter;
    }

注册和注销

先在bluetooths对象中创建两个私有对象

    BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    BroadcastReceiver mReceiver =null;

创建私方法 registerReceiver

    private void registerReceiver(final CallbackContext callbackContext) throws JSONException {
         final JSONArray unpairedDevices = new JSONArray(); //new  JSONArray对象
         mReceiver = new BroadcastReceiver(){
             public void onReceive(Context context, Intent intent) {
                 ...
             }
         }        //new广播对象
         Activity activity = cordova.getActivity();
         activity.registerReceiver(mReceiver, makeFilter());
    }

在bluetooths对象中我们创建了一个蓝牙对象和一个广播对象,
在registerReceiver方法创建类型为JSONArray的对象unpairedDevices是为了等会储存device转化后的JSONArray类型的对象。
重点来了!!! 重点就在我们的mReceiver这个对象上,这是一个广播对象,这个对象需要实现onReceive方法,这个方法会在广播被接收到的时候调用。那么什么时候广播会被接受呢?
这个mReceiver接收到广播和 activity.registerReceiver(mReceiver, makeFilter()); 这一句代码有关。
这句代码是注册这个监听到指定广播,那些广播被指定了呢?看上面的 蓝牙显示通信构建方法(IntentFilter)

        filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); //添加连接action
        filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);//添加断开action

就是这,添加了两个事件,这两个事件触发后都会被通过这个显示通信的注册的监听捕获到。

处理监听结果

    if(intent.getAction().equals(BluetoothDevice.ACTION_ACL_CONNECTED)){  
        BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);//获取设备对象
        try {
                JSONObject o = deviceToJSON(device,"CONNECTED");  //生成json格式的device信息
                unpairedDevices.put(o);
                callbackContext.success(o);
            } catch (JSONException e) {}
        }else if(intent.getAction().equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)){
            ...
    } 

这边只展示了监听连接后的处理结果,断开后的处理方式基本一样,不过这有一点要说:我们的监听又不是监听一次,我们需要一直监听下去,所以我找到pltform/android/CordovaLib/src/org/apache/cordova/CallbackContext.java这个Cordova回调的源文件结合cordova-plugin-bluetooth-serial这个插件的BluetoothSerial.java文件中discoverUnpairedDevices方法里面不断返回查找到的设备的信息
o
首先看一下PluginResult这个类简要属性

    public class PluginResult {
        private final int status;  //当前结果的的状态
        private final int messageType;  信息类型
        private boolean keepCallback = false;    是否继续发送
        private String encodedMessage; 
       ...
       public PluginResult(Status status, JSONObject message) { //构造函数
            this.status = status.ordinal();
            this.messageType = MESSAGE_TYPE_JSON;
            encodedMessage = message.toString();  //JSONObject转化为Stirng储存
        }
        public void setKeepCallback(boolean b) {
            this.keepCallback = b;  //更改是否保持回调
        }
    }

再看一下 CallbackCintext.java的一个方法

    public void sendPluginResult(PluginResult pluginResult) { //传入一个PluginResult类的实例对象
        synchronized (this) {
            if (finished) {  //先判断是不是已经结束了
                LOG.w(LOG_TAG, "Attempted to send a second callback for ID: " + callbackId + "\nResult was: " + pluginResult.getMessage());
                return;
            } else {
                finished = !pluginResult.getKeepCallback();
                //如果没有结束取出pluginResult.keepCallback作为下一轮的判断
            }
        }
        webView.sendPluginResult(pluginResult, callbackId); //向js发送消息
    }

通过上面一些分析android如何保持持续向js发送回调已经明了。

先判断回调是否还存在,如果存在说明需要回调 。new PluginResult类的实例,设置下一回合还需要发送信息,然后发送消息到js的回调,代码如下。

    if (callbackContext != null) {
        PluginResult res = new PluginResult(PluginResult.Status.OK, o);//将信息写入 同时设置后续还有返回信息
        res.setKeepCallback(true);
        callbackContext.sendPluginResult(res); 
    }

基本已经完成了,我相信如果有同学完整的学习完了这一篇,基本cordova插件的封装没有问题了,下面为完整android代码

    package bluetooths;

    import org.apache.cordova.CordovaPlugin;
    import org.apache.cordova.CallbackContext;
    import android.app.Activity;
    import org.json.JSONArray;
    import org.json.JSONException;
    import org.json.JSONObject;
    import android.bluetooth.BluetoothAdapter;
    import android.bluetooth.BluetoothDevice;
    import android.content.BroadcastReceiver;
    import android.content.Context;
    import android.content.Intent;
    import android.content.IntentFilter;
    import android.widget.Toast;
    import org.apache.cordova.PluginResult;
    import org.json.JSONObject;

    /**
    * This class echoes a string called from JavaScript.
    */
    public class bluetooths extends CordovaPlugin  {
        BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        BroadcastReceiver mReceiver =null;
        @Override
        public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
            if(action.equals("registerReceiver")){
                String message = args.getString(0);
                this.registerReceiver(callbackContext);
                return true;
            }else if(action.equals("unregisterReceiver")){
                this.unregisterReceiver(callbackContext);
            }
            return true;
        }
        private void registerReceiver(final CallbackContext callbackContext) throws JSONException {
            final JSONArray unpairedDevices = new JSONArray(); //new  JSONArray对象
            mReceiver = new BroadcastReceiver() {           //new广播对象
            @Override
            public void onReceive(Context context, Intent intent) {
            // Toast.makeText(context,"BroadcastReceiver",Toast.LENGTH_SHORT).show();
                if(intent.getAction().equals(BluetoothDevice.ACTION_ACL_CONNECTED)){  
                    BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);//获取设备对象
                    try {
                            JSONObject o = deviceToJSON(device,"CONNECTED");  //生成json格式的device信息
                            unpairedDevices.put(o);
                            if (callbackContext != null) {
                                PluginResult res = new PluginResult(PluginResult.Status.OK, o);//将信息写入 同时设置后续还有返回信息
                                res.setKeepCallback(true);
                                callbackContext.sendPluginResult(res); 
                            }
                        } catch (JSONException e) {}
                // Toast.makeText(context,"接受到已连接,消息为:"+device.getName()+"address: "+device.getAddress(),Toast.LENGTH_LONG).show();
                }else if(intent.getAction().equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)){  
                    BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);//获取设备对象
                    try {
                            JSONObject o = deviceToJSON(device,"DISCONNECTED");  //生成json格式的device信息
                            unpairedDevices.put(o);
                            if (callbackContext != null) {
                                PluginResult res = new PluginResult(PluginResult.Status.OK, o);//将信息写入 同时设置后续还有返回信息
                                res.setKeepCallback(true);
                                callbackContext.sendPluginResult(res); 
                            }
                        } catch (JSONException e) {}
                //    Toast.makeText(context,"接受到断开连接,消息为:"+device.getName()+"address: "+device.getAddress(),Toast.LENGTH_LONG).show();
                    }
                }
            };
            Activity activity = cordova.getActivity();
            activity.registerReceiver(mReceiver, makeFilter());
        }
        public void unregisterReceiver(final CallbackContext callbackContext){
            Activity activity = cordova.getActivity();
            activity.unregisterReceiver(mReceiver);
        }
        /*
        @deviceToJSON 将收到的设备对象转化为JSONObject对象方便与js交互数据
        @device 设备对象,当监听到设备变化后接受到的设备对象
        @connectType如果是连接发出的消息值为 CONNECTED 如果是断开连接发出的消息为 DISCONNECTED
        */
        private final JSONObject deviceToJSON(BluetoothDevice device,String connectType) throws JSONException {
            JSONObject json = new JSONObject();    //创建JSONObject对象
            json.put("name", device.getName());     //设备名字
            json.put("address", device.getAddress());   //设备地址
            json.put("id", device.getAddress());   //设备唯一编号使用地址表示
            json.put("connectType",connectType);
            if (device.getBluetoothClass() != null) {
                json.put("class", device.getBluetoothClass().getDeviceClass());  //设备类型 主要分别设备是哪一种设备
            }
            return json;
        }
        private IntentFilter makeFilter() {
            IntentFilter filter = new IntentFilter();
            filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
            filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
            return filter;
        }

    }

推荐阅读更多精彩内容