02 开发新的米家扩展程序 - MiEcosystem/NewXmPluginSDK GitHub Wiki

开发新的米家扩展程序


开发APP插件前的必须步骤

步骤一: 创建硬件

选择我的硬件中,点击开发进入app插件开发界面开发之前应先进行基础配置

步骤二:了解米家扩展程序

https://iot.mi.com/new/guide.html?file=05-米家扩展程序开发指南/01-Android开发指南/01-米家扩展程序整体简介

步骤三:配置扩展程序

基础配置:

1.通用配置:

1.通用功能:

产品名字: 目前至少需要配置中文英文两种语言

设备配网方式:有多种模式可选,针对自自己产品

  1. AP配网模式:针对wifi设备通过连接设备热点传递配置信息
  2. AP直连,是指类似与运动相机、米家后视镜这样的点对点WiFi连接的局域网查看设备
  3. 设备扫码配网模式:会把配置信息生成二维码方式展示给设备,摄像机

通用物料:主要是产品的图标,根据详细描述配置产

  1. 拟物图标常态:设备列表界面,添加设备界面需要显示的设备图标
  2. MIUI弹窗图:miui发现设备时候需要弹窗提示连接设备的图其余图标后面会优化不在使用

2. 自动化配置:如果设备要支持自动化,请自行配置自动化

https://iot.mi.com/new/guide.html?file=03-平台使用指南/02-智能硬件直连接入/08-配置自动化

3.卡片配置:设备需要支持卡片操作,请参考文档配置文件,验证通过后提测米家

https://iot.mi.com/new/guide.html?file=05-米家扩展程序开发指南/01-Android开发指南/20-米家卡片配置说明

如需要米家开发人员协助配置需要邮寄设备到小米武汉

4.首页快捷开关配置:如设备有开关的功能,必须要支持快捷开关操作

目前由MIOT开发人员配置,厂商需要提供自己设备的开关的属性值

5.摄像机预览:如果是摄像机设备需要支持摄像机预览的功能

必须已经支持视频流的传输:

https://iot.mi.com/new/guide.html?file=05-米家扩展程序开发指南/01-Android开发指南/09-摄像机预览功能API

需要在插件中实现以上功能方可使用摄像机预览相关功能

1.摄像机落地页
  1. 需要支持视频流功能
  2. 配置manifest文件,目前model必须包含camera

2.摄像机卡片预览

如果已经支持摄像机预览的功能,发送邮件MIOT这边开发人员会配置。可以通过落地页,悬浮窗测试是否已经支持预览功能。

3.摄像机悬浮窗

如果插件已经实现摄像机预览的接口,在插件中调用

XmPluginHostApi.openCameraFloatingWindow 显示悬浮窗

注意在进入插件的时候需要调用 XmPluginHostApi.closeCameraFloatingWindow 关闭


创建新的扩展程序

  1. github更新SDK代码

    米家扩展程序工程放置于plugProject目录下,如下图,可以放置多个米家扩展程序工程:

  2. 创建新的米家扩展程序工程

    在SDK根目录下执行python脚本gen_plug.py(执行完后会在plugProject目录下自动生成最简单的米家扩展程序工程):

    注意:

    1)model为创建新硬件时配置的

    2)developerId为申请的小米开发者账号(小米账号),不是手机号码

    3)packageName为创建Android扩展程序时配置的包名(针对Android包名,禁止出现Java关键字(关键字列表),开关类产品需特别注意

    4)只支持python2.7版本

    python gen_plug.py model developerId packageName
    

    创建了米家扩展程序工程后,需要同步下NewXmPluginSDK工程的gradle信息,不然新的米家扩展程序模块有可能没刷新显示出来:

  3. 配置米家扩展程序签名文件

    通过gen_plug.py脚本生成新的米家扩展程序工程时,会在keystore目录下生成一个示例的key.keystore,这个只是作为示例使用,为了安全,实际扩展程序开发中需要替换成自己的签名文件。

    所有米家扩展程序在米家APP上运行时需要进行签名验证,修改米家扩展程序工程build.gradle的签名信息:

    defaultConfig {
        // minSdkVersion和targetSdkVersion必须与米家APP保持一致,如果minSdkVersion设置过高,则米家扩展程序无法在低版本Android平台下载安装
        minSdkVersion 15
        targetSdkVersion 19
    }
    signingConfigs {
        release {
            storeFile new File("${project.projectDir}/keystore/key.keystore")
            storePassword 'mihome'
            keyAlias 'mihome-demo-key'
            keyPassword 'mihome'
        }
    }
    
    buildTypes {
        debug {
            debuggable true
            signingConfig signingConfigs.release
        }
        release {
            minifyEnabled true
            shrinkResources false
            zipAlignEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
        }
    }
    
  4. 配置米家扩展程序编译脚本

    修改米家扩展程序工程build.gradle,末尾添加:

    apply from: "${project.rootDir.absolutePath}/plug.gradle"
    
    
  5. 添加米家扩展程序依赖项目

    如果米家扩展程序有其他的项目依赖,添加到complieProject属性中,如下:

    project.ext.set("complieProject",[":demolib"])
    

    依赖项目结构如下:

    依赖jar库和native so放置于libs目录下。

  6. 关联pluglib库源码

    米家扩展程序开发的时候,发现pluglib中的函数变量名被重命名,没有注释,可以通过下面方法关联pluglib源码看到源码中的注释和变量名。

    先点击系统反编译源码文件的右上角:

    然后选择libs_ex下的pluglib-sources.jar:

  7. 依赖其他jar包

    如果想依赖其他jar的话,不需要修改gradle,直接将jar放入./libs目录下面。

    扩展程序中引入的第三方库,如ButterKnife等,需要使用implementation方式引入进来,不要使用compileOnly/provided方式引入。避免使用米家app引入的第三方库,否则米家app升级依赖库后,扩展程序相关接口调用可能会运行异常。

开发注意事项

  1. 米家扩展程序默认需要支持中英文:

    1)英文资源放到/res/values下

    2)中文资源放到/res/values-zh/下。

  2. 米家扩展程序里边尽量避免和common_ui里边同名资源名,否则米家扩展程序中的资源会替换掉common_ui里边资源

  3. 暂时不支持Serializable,相关需求的话请用Parcelable代替

  4. androi-support-*.jar库不需要米家扩展程序引入,已经在公共配置中加入

AndroidManifest.xml文件application下添加米家扩展程序配置和入口

  • 必须设置model和message_handler,提供米家扩展程序运行的米家APP最低API Level版本

  • 米家扩展程序不支持在AndroidManifest中自定义Application入口

  • 米家扩展程序不支持在AndroidManifest中自定义Activity、Service、Receiver、Provider

  • 米家扩展程序不支持自定义style

     <!-- 支持米家扩展程序的米家APP最低API Level版本,必须 -->
     <meta-data android:name="minPluginSdkApiVersion" android:value="1" />
     <!-- 设备model,必须 -->
     <meta-data android:name="model" android:value="xiaomi.demo.v1" />
     <!-- 设备处理消息入口,必须 -->
     <meta-data android:name="message_handler" android:value="com.xiaomi.xmplugindemo.MessageReceiver" />
     <!-- 开发者Id, 必须 -->
     <meta-data android:name="MiHomeDeveloperId" android:value="id_894148746" />
     <!-- 支持的运行平台,必须 -->
     <meta-data android:name="MiHomePlatform" android:value="phone" />
    
  • 如何获取开发者Id

打开米家智能平台,点击“开发平台”,然后:

创建MessageReceiver类,继承IXmPluginMessageReceiver,入口函数

类名必须保持和AndroidManifest.xml中配置message_handler一致

public class MessageReceiver implements IXmPluginMessageReceiver {
    public static final String MODEL = "xiaomi.demo.v1";

    @Override
    public boolean handleMessage(Context context, XmPluginPackage xmPluginPackage, int type,
                                 Intent intent,
                                 DeviceStat deviceStat) {
        switch (type) {
            case LAUNCHER: {// 启动入口
                XmPluginHostApi.instance().startActivity(context, xmPluginPackage, intent,
                        deviceStat.did, MainActivity.class);
                return true;
            }

            default:
                break;
        }
        return false;
    }

    @Override
    public boolean handleMessage(Context context, XmPluginPackage xmPluginPackage, int type,
                                 Intent intent, DeviceStat deviceStat, MessageCallback callback) {
        // TODO Auto-generated method stub
        return false;
    }

}

handleMessage中除了可以处理LAUNCHER消息外,还可以处理其他类型的消息,这个根据自己的产品需求来进行。目前支持的所有消息类型如下:

  • LAUNCHER
    • 入口消息,APILevel:1
  • PUSH_MESSAGE
    • push消息,APILevel:2
  • MSG_PAGE_NAVIGATE
    • 页面跳转,APILevel:3
  • MSG_BROADCAST_BLUETOOTH_DEVICE_ACTION_ACL_CONNECTED
    • 系统蓝牙广播ACL_CONNECTED,APILevel:6
  • MSG_BROADCAST_BLUETOOTH_DEVICE_ACTION_ACL_DISCONNECTED
    • 系统蓝牙广播ACL_DISCONNECTED,APILevel:7
  • MSG_BLUETOOTH_PAIRING
    • 蓝牙配对,APILevel:8
  • MSG_BLUETOOTH_DISCONNECT
    • 蓝牙断开连接,APILevel:9
  • MSG_SCENE_GET_CONDITION_EXTRA
    • 获取场景自定义条件中的extra字段,APILevel:10
  • MSG_UPNP_CONNECT
    • UPNP设备已连接,APILevel:12
  • MSG_UPNP_DISCONNECT
    • UPNP设备连接断开,APILevel:13
  • MSG_SET_SCENE_LARGE_EXTRA
    • 设置超长场景extra,APILevel:14
  • MSG_BROADCAST
    • 接收broadcast消息,APILevel:15
  • MSG_UPNP_EVENT
    • UPNP设备事件,APILevel:16
  • MSG_NOTIFICATION_PENDING_INTENT
    • 通知栏点击事件,APILevel:17
  • MSG_INIT_CAMERA_FRAME_SENDER
    • 初始化请求通道,APILevel:18
  • MSG_STAR_REQUEST_CAMERA_FRAME
    • 请求相机数据,APILevel:19
  • MSG_STOP_REQUEST_CAMERA_FRAME
    • 停止发送数据,APILevel:20
  • MSG_DESTROY_REQUEST_CAMERA_FRAME
    • 销毁发送通道,APILevel:21
  • MSG_BLE_CHARACTER_CHANGED
    • 注册底层ble蓝牙数据监听,当收到数据时发送此msg给米家扩展程序,APILevel:22

如何创建Activity和Fragment

米家扩展程序不支持直接继承Android原生的Activity和Fragment。如果要使用Activity的话,可以继承提供的XmPluginBaseActivity类。如果要使用Fragment的话,可以继承提供的BaseFragment。

如何调用自定义的Activity

通过XmPluginBaseActivity.startActivity(Intent intent, String className)方法调用,或者是XmPluginBaseActivity.startActivityForResult(Intent intent, String className, int requestCode)。

创建Device继承BaseDevice,封装设备的状态和rpc调用

  • 创建Device类:

     // 封装设备功能处理
     public class Device extends BaseDevice {
         // 缓存设备状态数据,每次进入不需要立即更新数据
         private static ArrayList<Device> DEVICE_CACHE = new ArrayList<Device>();
     
         // 先从缓存中获取Device,并更新DeviceStat
         public static synchronized Device getDevice(DeviceStat deviceStat) {
             for (Device device : DEVICE_CACHE) {
                 if (deviceStat.did.equals(device.getDid())) {
                     device.mDeviceStat = deviceStat;
                     return device;
                 }
             }
     
             Device device = new Device(deviceStat);
             DEVICE_CACHE.add(device);
             return device;
         }
     
         // 通过did获取Device
         public static synchronized Device getDevice(String did) {
             for (Device device : DEVICE_CACHE) {
                 if (did.equals(device.getDid())) {
                     return device;
                 }
             }
             return null;
         }
     
         public Device(DeviceStat deviceStat) {
             super(deviceStat);
         }
     }
    
  • 在Activity中初始化device

     Device mDevice = Device.getDevice(mDeviceStat);
    
  • Device提供的方法

     /**
      * 添加状态监听
      * 
      * @param listener
      */
     public void addStateChangedListener(StateChangedListener listener);
         
     /**
      * 移除状态监听
      * 
      * @param listener
      */
     public void removeStateChangedListener(StateChangedListener listener)
         
     /**
      * 设备方法调用
      * 
      * @param method 方法名
      * @param param 参数,可以是一个集合Collection子类
      * @param callback 回调结果
      * @param parser
      */
     public <T> void callMethod(String method, Object[] params,
             final Callback<T> callback, final Parser<T> parser) {
         XmPluginHostApi.instance().callMethod(getDid(), method, params, callback, parser);
     }
     
     /**
      * 设备方法调用
      * 
      * @param method 方法名
      * @param param
      * @param callback 回调结果
      * @param parser
      */
     public <T> void callMethod(String method, JSONArray params,
             final Callback<T> callback, final Parser<T> parser) {
         XmPluginHostApi.instance().callMethod(getDid(), method, params, callback, parser);
     }
         
     /**
      * 设备方法调用
      * API level: 29
      *
      * @param method 方法名
      * @param params
      * @param callback 回调结果
      * @param parser
      */
     public <T> void callMethod(String method, JSONObject params,
                                final Callback<T> callback, final Parser<T> parser);
                                
      /**
      * 获取设备固件升级信息
      */
     public void checkDeviceUpdateInfo(final Callback<DeviceUpdateInfo> callback);
         
     /**
      * 返回设备id
      */
     public String getDid();
         
     /**
      * 返回设备名字
      */
     public String getName();
         
     /**
      * 返回设备ip
      */
     public String getIp();
         
     /**
      * 返回设备mac地址
      */
     public String getMac();
         
     /**
      * 返回model
      */
     public String getModel();
         
     /**
      * 返回分享设备的parentId
      */
     public String getParentId();
         
     /**
      * 返回分享设备的parentModel
      */
     public String getParentModel();
         
     /**
      * 获取绑定标志,BIND_FLAG_UNSET: 0,BIND_FLAG_SET: 1
      */
     public int getBindFlag();
         
     /**
      * 获取认证标志,AUTH_FLAG_SET: 0,AUTH_FLAG_SET: 1
      */
     public int getAuthFlag();
         
     /**
      * 是否是子设备,仅支持二级子设备
      */
     public boolean isSubDevice();
         
     /**
      * 获取当前设备token
      */
     public String getToken();
         
     /**
      * ApiLevel:10 是否主人设备
      */
     public boolean isOwner();
         
     /**
      * ApiLevel:10 是否家庭设备
      */
     public boolean isFamily();
         
     /**
      * ApiLevel:10 是否是分享权限,默认是可以控制
      */
     public boolean isShared();
         
         
     /**
      * ApiLevel:10 是否绑定设备,无论哪种权限,主人,分享,家庭都算
      */
     public boolean isBinded2();
         
     /**
      * ApiLevel:20 是否是只读分享权限,如果设备支持微信分享,必须要检查这个选项,在米家扩展程序中不能让用户控制设备
      */
     public boolean isReadOnlyShared();
         
    

设置titlebar在顶部透明显示时的顶部padding

mHostActivity.setTitleBarPadding(findViewById(R.id.title_bar));

activiy中需要返回调用结果必须如下调用

activity().setResult(RESULT_OK);

在米家扩展程序Activity中凡是需要使用Context上下文对象参数的,必须使用activity()

显示Toast

Toast.makeText(activity(), “test”, Toast.LENGTH_SHORT).show();

显示Dialog

MLAlertDialog.Builder builder = new MLAlertDialog.Builder(activity());
builder.setTitle("测试Dialog");
builder.setPositiveButton("Ok", new MLAlertDialog.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialog, int which) {
                            
    }
});

页面右上角调用公共设备菜单界面

    ArrayList<IXmPluginHostActivity.MenuItemBase> menus = new ArrayList<>();

    ////插件自定义菜单,可以在public void onActivityResult(int requestCode, int resultCode, Intent data) 中接收用户点击的菜单项,String result = data.getStringExtra("menu");
    IXmPluginHostActivity.StringMenuItem stringMenuItem = new
            IXmPluginHostActivity.StringMenuItem();
    stringMenuItem.name = "test string menu";
    menus.add(stringMenuItem);

    //跳转到插件下一个activity的菜单
    IXmPluginHostActivity.IntentMenuItem intentMenuItem = new
            IXmPluginHostActivity.IntentMenuItem();
    intentMenuItem.name = "test intent menu";
    intentMenuItem.intent =
            mHostActivity.getActivityIntent(null,
                    ApiDemosActivity.class.getName());
    menus.add(intentMenuItem);

    //带开关按钮的菜单,可以自动调用设备rpc
    IXmPluginHostActivity.SlideBtnMenuItem slideBtnMenuItem = new
            IXmPluginHostActivity.SlideBtnMenuItem();
    slideBtnMenuItem.name = "test slide menu";
    slideBtnMenuItem.isOn = mDevice.getRgb() > 0;
    slideBtnMenuItem.onMethod = "set_rgb";
    JSONArray onparams = new JSONArray();
    onparams.put(0xffffff);
    slideBtnMenuItem.onParams = onparams.toString();
    slideBtnMenuItem.offMethod = "set_rgb";
    JSONArray offparams = new JSONArray();
    offparams.put(0);
    slideBtnMenuItem.offParams =
            offparams.toString();
    menus.add(slideBtnMenuItem);

    Intent intent = new Intent();
    intent.putExtra("security_setting_enable",true);
    mHostActivity.openMoreMenu2(menus, true, REQUEST_MENU, intent);

更详细的使用说明见:设备更多菜单API

调起创建场景界面

mHostActivity.startCreateSceneByCondition(mDevice.getDid(), "device");

米家扩展程序finish 模拟singleTop和回退到设备列表

XmPluginBaseActivity.java

/**ApiLevel:14
 * 按照调用栈一直回退finish,直到lastActivityClass为止,当lastActivityClass为null会一直回退到设备列表
 * @param lastActivityClass
 */
public void finishParent(String lastActivityClass) {
  
}

加载native so

必须用下面的接口加载

XmPluginHostApi.instance().loadLibrary(“model”,”h264decoder",
                            getClassLoader());

更多其他接口

提供接口的几个基本文件

  • XmPluginHostApi.java
    • 封装了米家APP提供给扩展程序的大部分功能接口
    • 使用XmPluginHostApi.instance()调用其中方法
  • IXmPluginHostActivity.java
    • 封装了米家APP提供的Activity或View相关的接口
    • 米家扩展程序的所有Activity都需要继承XmPluginBaseActivity,XmPluginBaseActivity里有个mHostActivity可以用来访问IXmPluginHostActivity类提供的各种方法
  • XmBluetoothManager.java
    • 封装了所有蓝牙相关的接口
    • 使用XmBluetoothManager.instance()调用其中方法
⚠️ **GitHub.com Fallback** ⚠️