引擎接口

    更新时间:

    该文档为快应用开发者提供订阅消息相关的接口以及说明

    日期 说明
    2021-01-04 创建文档
    2021-03-04 个别参数调整
    2021-03-05 更新兼容方案
    2021-03-06 补充 UserId 加密方法 strKey2SecretKey
    2021-03-08 补充参数来源
    2020-04-01 补充一个接口 getstate
    2021-04-11 补充 manifest.json 中的必要参数
    2021-05-18 补充测试环境与正式环境的切换说明
    2021-05-19 增加 Q&A
    2021-06-08 消息类型增加一个值 3:长期服务消息
    2021-06-10 更新订阅参数scene的说明

    rpk 订阅

    manifest.json

    接入消息订阅功能前,需要在 manifest.json 新增两个字段 appId 和 appKey,如下图所示:

    manifest.json 配置

    接口声明

    { "name" : "system.vivopush"}
    

    导入模块

    import vivopush from '@system.vivopush'
    

    方法说明

    是否支持订阅 (重要)

    vivopush.getstate()
    

    由于服务消息推送消息的功能依赖于系统其他模块,使用前需要调用本接口方法来判断其他模块的支持情况,如不支持,则无法下发消息通知给用户

    参数
    参数名 类型 必填 说明
    success function Y 支持时回调
    fail function Y 不支持时回调
    示例
    vivopush.getstate({
      success: () => {
        prompt.showToast({
          message: `依赖模块支持订阅消息`,
        })
      },
    
      fail: function (code) {
        prompt.showToast({
          message: '依赖模块不支持订阅消息',
        })
      },
    })
    

    订阅

    vivopush.subscribe(OBJECT)
    

    用于订阅消息

    参数
    参数名 类型 必填 说明
    params object Y 订阅所需要的参数
    success function Y 订阅成功的回调方法
    fail function Y 订阅失败的回调方法
    params 参数说明
    参数名 类型 必填 来源 说明
    templateIds Array Y 在服务后台申请获取 订阅的服务消息模板 id 列表:[‘aaaaaa’, ‘bbbbbb’]
    clientId String Y 开发者在 vivo 开放平台上创建快应用时所得的 clientId 快应用 id(clientId 不是 appId)
    userId String Y 开发者传入 调用方自己维护的用户标识
    scene String Y 开发者传入 场景标识,调用方传递的标识场景的字段(每次订阅的 scene 必须不同,票务类型建议传入订单号,以防止重复)
    type Number Y 每个 templateId 都有一个 type,与 templateId 同时获取 消息模板类型,1:服务消息 2:订阅消息 3:长期服务消息
    subDesc String Y 开发者传入 订阅消息的的描述
    fail 返回错误信息
    错误信息 data 错误码 code 说明
    templateIds is empty 1001 用户没有勾选模板
    get openId fail 1002 获取 openId 异常
    checkTemplateId onFailure 1003 网络或服务器异常
    onResponse data parse exception 1004 服务器返回数据解析异常
    Not logged in 1005 用户没有登陆 vivo 账号
    The user cancels by clicking close 1060 用户点击关闭取消订阅
    The user cancels by clicking on the blank space 1061 用户点击对话框以外取消订阅
    activity is finish 1007 快应用已销毁
    checkTemplateId fail... 1008 模板检查失败
    get account info get net error 1090 获取账号信息时网络异常
    verifyResult state : 0 1091 vivo 账号失效(重新登陆 vivo 账号可以解决)
    subscribe fail... 10060 重复订阅
    subscribe fail... 20000 订阅参数错误
    示例
    vivopush.subscribe({
      params: {
      templateIds:[‘aaaaaa’, ‘bbbbbb’],
        clientId: ‘cccccc’,
        userId: ‘eeeeee’,
        scene: ‘123’,
        subDesc: ‘中超联赛’,
    
        type: ‘1’
      },
      success: function() {
      },
      fail: function(data,code) {
      }
    })
    

    取消订阅

    vivopush.unsubscribe(OBJECT)
    

    用于取消订阅

    参数
    参数名 类型 必填 说明
    params object Y 取消订阅所需要的参数
    success function Y 订阅成功的回调方法
    fail function Y 订阅失败的回调方法
    params 参数说明
    参数名 类型 必填 来源 说明
    templateIds Array Y 在服务后台申请获取 订阅的服务消息模板 id 列表:[‘aaaaaa’, ‘bbbbbb’](必须和订阅时的值一致
    userId String Y 开发者传入 调用方自己维护的用户标识(必须和订阅时的值一致
    scene String Y 开发者传入 场景标识,调用方传递的标识场景的字段(每次订阅的 scene 必须不同,票务类型建议传入订单号,以防止重复,必须和订阅时的值一致
    type Number Y 每个 templateId 都有一个 type,与 templateId 同时获取 消息模板类型(必须和订阅时的值一致
    subDesc String Y 开发者传入 订阅消息的的描述(必须和订阅时的值一致
    clientId String Y 开发者在 vivo 开放平台上创建快应用时所得的 clientId 快应用 id(必须和订阅时的值一致
    fail 返回错误代码
    错误原因 说明
    unsubscribe fail, code:10040 没有订阅或者已经取消了订阅
    示例
    vivopush.unsubscribe({
      params: {
        templateIds:[‘aaaaaa’, ‘bbbbbb’],
        userId:’ cccccc’,
        clientId: ‘cccccc’,
        subDesc: ‘中超联赛’,
        scene:’ 123’,
        type:’1’
      },
      success: function() {
      },
    
      fail: function(data) {
      }
    })
    

    查询订阅关系

    vivopush.isRelationExist(OBJECT)
    

    用于查询订阅关系是否已存在

    参数
    参数名 类型 必填 说明
    params object Y 查询订阅关系所需要的参数
    success function Y 查询订阅关系成功的回调方法
    fail function Y 查询订阅关系失败的回调方法
    params 参数说明
    参数名 类型 必填 来源 说明
    templateIds Array Y 支持查询多个模板 id 订阅的服务消息模板 id 列表:[‘aaaaaa’](必须和订阅时的值一致
    userId String Y 开发者传入 调用方自己维护的用户标识(必须和订阅时的值一致
    scene String Y 开发者传入 场景标识,调用方传递的标识场景的字段(每次订阅的 scene 必须不同,票务类型建议传入订单号,以防止重复,必须和订阅时的值一致
    type Number Y 每个 templateId 都有一个 type,与 templateId 同时获取 消息模板类型(必须和订阅时的值一致
    subDesc String Y 开发者传入 订阅消息的的描述(必须和订阅时的值一致
    clientId String Y 开发者在 vivo 开放平台上创建快应用时所得的 clientId 快应用 id(必须和订阅时的值一致
    返回值
    参数名 说明
    data 单个模板 ID 时:true:已存在订阅关系;false:不存在订阅关系;多个模板 ID 时:{"aaaaaaa":true,"bbbbbbbb":false}
    示例
    vivopush.isRelationExist({
      params: {
        templateIds:[‘aaaaaa’, ‘bbbbbb’],
        userId:’ cccccc’,
        clientId: ‘cccccc’,
        subDesc: ‘中超联赛’,
        scene:’ 123’,
        type:’1’
      },
      success: function(data,code) {
        console.info('查询成功:' + data)
      },
    
      fail: function(data) {
      }
    })
    

    兼容性说明

    该接口是 vivo 手机独有的接口,在其他厂商手机上无法使用,考虑到开发者可能会用一个 rpk 发布在多个厂商渠道,或者因其他原因不能升级快应用的最小版本,特增加兼容说明

    多厂商兼容

    开发者需要发布 rpk 到不同的厂商渠道时,可以统一在 manifest 文件中声明接口,仅声明接口是不会报错的,代码中 import 或者使用时可以根据厂商信息进行判断。

    示例如下:

    导入时:

    import device from '@system.device
    let vivopush
    try {
      vivopush = require("@system.vivopush")
      console.log('import ok ')
    } catch(e) {
      console.log('import fail : ' + e)
    }
    

    调用时:

    device.getInfo({
      success: function(ret) {
        //判断厂商信息后调用
        if(ret.brand == 'vivo') {
          vivopush.subscribe(...)
        }
      }
    })
    

    低版本兼容

    支持该接口的引擎版本为 1091 以上,如果开发者不能将最小支持版本提高到 1091,则需要在 import 或者使用时根据运行平台版本来判断

    Q&A

    1 接口调用报错,比如 import 是报找不到或接口不存在?

    检查引擎版本,需要大于等于 1091(内置的引擎版本不够,可以在微信群里要一个最新的);检查调试器运行平台,运行平台要选择“快应用(com.vivo.hybrid)”


    2 订阅弹窗拉起后,没有显示模板信息?

    检查参数与引擎环境,如果是测试环境的参数,引擎也需要切换到测试环境;同样的,正式环境下的参数,引擎需要切换到正式环境;环境没有问题的话,重新登陆 vivo 账号后再尝试下。


    3 收不到通知?

    调用 vivopush.getState 进行检查,如果没有返回成功,检查 appId 和 appKey 是否正确,是否安装了联调提供的发送通知的客户端


    4 上传日志?

    拨号*#*#112##,进入日志管理,打开日志开关,退出后,复现异常,重新拨号进入日志管理,上传日志,然后提取码告知给 vivo 的技术支持

    APP 订阅

    本文档用于开发者在 app 中接入 v 订阅功能,请结合服务端接入文档一起使用,app 端接入 v 订阅,主要分为两步:获取参数,使用参数。工具方法章节提供了必要的方法实现,工具方法可以直接使用。

    获取参数

    接入 v 订阅需要的参数及获取方式如下:

    参数名 类型 必填 来源 说明
    templateIds Y 查阅服务端文档,在服务后台获取 订阅的服务消息模板 id 列表,例如 [‘aaaaaa’, ‘bbbbbb’]
    type int Y 每个 templateId 都有一个 type,与 templateId 同时获取 消息模板类型,1:服务消息 2:订阅消息 3:长期服务消息
    appPackage String Y app 包名 应用包名,只允许订阅 app 自身,不允许订阅其他 app
    appName String Y app 包名 应用包名,只允许订阅 app 自身,不允许订阅其他 app
    appVersion String Y app 名称 应用名称
    clientId String Y 开发者在 vivo 开放平台上创建应用时所得的 clientId 应用 id
    userId String Y 用于标识用户的唯一标识,开发者自行传入 调用方自己维护的用户标识(使用时需要加密,参考后文中示例代码的使用方式)
    scene String Y 用户标识场景,开发者自行传入 场景标识,调用方传递的标识场景的字段
    subDesc String Y 开发者自定义 订阅消息的描述(比如“订单信息”等)
    appId String Y 消息管理后台-设置中获取 开发者平台注册得到的 appId
    appKey String Y 消息管理后台-设置中获取 开发者平台注册得到的 appKey
    appUserId int Y 用户 id,使用工具方法中的 getAppUserId 获取 参考工具方法中的 getAppUserId
    fromApp boolean Y 传入 true 订阅发起的来源是否为 app

    接入准备

    获取到以上参数后,请先创建好通知渠道,创建通知渠道请在应用初始化的时候进行,避免收不到通知,参考工具方法中的“创建通知渠道”,并确认测试用的 vivo 手机已经登陆了 vivo 账号

    功能前置判断

    由于 V 订阅功能仅支持在国内销售的 vivo 手机上使用,其他手机上调用该功能的代码可能引起应用崩溃等异常,且该功能对手机内部模块版本存在依赖。请在 V 订阅的代码块前加上以下判断

    /**
     * 判断手机是否支持app订阅功能 * * @return
     */
    public boolean isSupport() {
        if (!Build.BRAND.equals("vivo")) {
            Log.i(TAG, "not vivo phone");
            return false;
        }
        try {
            PackageManager manager = this.getPackageManager();
            PackageInfo HybridInfo = manager.getPackageInfo("com.vivo.hybrid", 0);
            int versionCode1 = HybridInfo.versionCode;
            if (versionCode1 < 11010701 && versionCode1 != 11010630) {
                Log.i(TAG, "hybrid Engine not support ");
                return false;
            }
            PackageInfo AbeInfo = manager.getPackageInfo("com.vivo.abe", 0);
            int versionCode2 = AbeInfo.versionCode;
            if (versionCode2 < 5030005) {
                Log.i(TAG, "abe not support");
                return false;
            }
        } catch (Exception e) {
            Log.i(TAG, "check version fail : " + e);
            return false;
        }
        Log.i(TAG, "support vivo subscribe");
        return true;
    }
    

    订阅(使用参数)

    拉起订阅弹窗

    在使用参数前,请先将工具方法添加到代码中,subscribe 方法用于拉起订阅弹窗

    private void subscribe() {
        //templateIds请替换为服务后台申请到的值,同时type传入与templateIds对应的值(1:服务消息 2:订阅消息 3:长期服务消息)
        String templateIds = "[534df9c929434bbd977f9904ddb41a41]";
        String type = "2";
    
        //请替换为vivo开放平台上创建应用时所得的clientId
        String clientId = "474775";
    
        //自行定义
        String scene = "testScene";
    
        //userId请参考此处的使用方式先进行加密
        String userId = "testUserId"; // 需加密
        String encryptUserId;
        try {
            encryptUserId = encryptAESGCM(userId, SERVICE_SECRET);
        } catch (Exception e) {
            encryptUserId = null;
        }
        //自行定义
        String subDesc = "订阅测试";
    
        //appId和appKey可以在代码中定义为常量,直接拼接到下面的builder中
        String appId = "10201";
        String appKey = "7435d171-d1eb-4ebc-ae91-41778ed935df";
    
        //appPackage  appName appVersion请传入应用的实际值
        String appPackage = "com.example.appsubscriberdemo";
        String appName = "应用订阅测试";
        String appVersion = "1";
    
        //使用工具方法中的getAppUserId获取
        int appUserId = getAppUserId();
    
        Uri.Builder builder = new Uri.Builder();
        builder.scheme("quickapp")
                .authority("hybrid.vivo.com")
                .appendPath("subscribe")
                .appendQueryParameter("templateIds", templateIds)
                .appendQueryParameter("clientId", clientId)
                .appendQueryParameter("scene", scene)
                .appendQueryParameter("type", type)
                .appendQueryParameter("userId", encryptUserId)
                .appendQueryParameter("subDesc", subDesc)
                .appendQueryParameter("appId", appId)
                .appendQueryParameter("appKey", appKey)
                .appendQueryParameter("appPackage", appPackage)
                .appendQueryParameter("appName", appName)
                .appendQueryParameter("appVersion", appVersion)
                .appendQueryParameter("appUserId", String.valueOf(appUserId))
                .appendQueryParameter("fromApp", "true");
        String myUrl = builder.build().toString();
        Intent intent = new Intent();
        intent.setData(Uri.parse(myUrl));
        startActivity(intent);
    }
    

    拉起弹窗并成功订阅后,可以参考服务端文档,尝试推送通知

    获取订阅回调

    订阅回调通过 onActivityResult 获取,resultCode 为 123,示例代码如下:

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        // 123是订阅回调的resultCode
        if (resultCode == 123) {
            if (data != null) {
                int code = data.getIntExtra("code", -1);
                String content = data.getStringExtra("data");
                Log.e(TAG, "subscribe result ,code : " + code + " , content :" + content);
            }
        }
    }
    
    返回数据说明
    data code 说明
    success 0 订阅成功
    templateIds is empty 1001 用户没有勾选模板
    get openId fail 1002 获取 openId 异常
    checkTemplateId onFailure 1003 网络或服务器异常
    onResponse data parse exception 1004 服务器返回数据解析异常
    Not logged in 1005 用户没有登陆 vivo 账号
    The user cancels by clicking close 1060 用户点击关闭取消订阅
    The user cancels by clicking on the blank space 1061 用户点击对话框以外取消订阅
    activity is finish 1007 快应用/APP 已销毁
    checkTemplateId fail... 1008 模板检查失败
    get account info get net error 1090 获取账号信息时网络异常
    verifyResult state : 0 1091 vivo 账号失效(重新登陆 vivo 账号可以解决)
    subscribe fail... 10060 重复订阅
    subscribe fail... 20000 订阅参数错误

    取消订阅

    取消订阅和查询订阅关系需要客户端绑定引擎的服务,通过 aidl 调用相关接口获取结果,接入方式如下:

    创建 aidl 文件

    在 com.vivo.hybrid.msgcenter 路径下创建以下两个 aidl 文件用于通信

    创建aidl文件

    文件内容如下:

    // ICallback.aidl
    package com.vivo.hybrid.msgcenter;
    
    // Declare any non-default types here with import statements
    
    interface ICallback {
        void callback(int code, String data);
    }
    
    //ISubscribe.aidl
    package com.vivo.hybrid.msgcenter;
    
    // Declare any non-default types here with import statements
    
    import com.vivo.hybrid.msgcenter.ICallback;
    
    interface ISubscribe {
        void unSubscribe(String aString, com.vivo.hybrid.msgcenter.ICallback callback);
    
        void isRelationExist(String aString, com.vivo.hybrid.msgcenter.ICallback callback);
    }
    

    绑定服务

    获取 binder 对象并赋值给 ISubscribe,示例代码如下:

    private void bindService() {
        Intent intent = new Intent();
        intent.setPackage("com.vivo.hybrid");
        intent.setAction("com.vivo.hybrid.msgcenter.SubscribeService");
        this.bindService(intent, mServiceConnection, BIND_AUTO_CREATE);
    }
    
    private void unBindService() {
        this.unbindService(mServiceConnection);
    }
    
    private ISubscribe mService;
    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.i(TAG, "onServiceConnected");
            mService = ISubscribe.Stub.asInterface(service);
        }
    
        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.i(TAG, "onServiceDisconnected");
            mService = null;
        }
    };
    

    bindService 方法可以放在 activity 的 onResume 方法中。unBindService 方法可以放在 activity 的 onPause 方法中。

    调用取消订阅的接口

    示例代码如下:

    private void unSubscribe() {
        Log.i(TAG, "unSubscribe");
        if (mService != null) {
            try {
                JSONObject jsonObject = new JSONObject();
    
                //templateIds请替换需要查询的值,同时type传入与templateIds对应的值
                String template1 = "534df9c929434bbd977f9904ddb41a41";
                JSONArray templateIdsJson = new JSONArray();
                templateIdsJson.put(template1);
                jsonObject.put("templateIds", templateIdsJson);
                String type = "2";
                jsonObject.put("type", type);
    
                //订阅时传入的值
                String scene = "testScene";
                jsonObject.put("scene", scene);
    
                //请替换为vivo开放平台上创建应用时所得的clientId
                String clientId = "474775";
                jsonObject.put("clientId", clientId);
    
                // userId请使用需要查询的用户的userId,并进行加密
                String userId = "";
                String encryptUserId;
                try {
                    encryptUserId = encryptAESGCM(userId, SERVICE_SECRET);
                } catch (Exception e) {
                    encryptUserId = null;
                }
                jsonObject.put("userId", encryptUserId);
    
                //和订阅时保持一致
                String appPackage = "";
                jsonObject.put("package", appPackage);
    
                //和订阅时保持一致
                String subDesc = "";
                jsonObject.put("subDesc", subDesc);
                mService.unSubscribe(jsonObject.toString(), new ICallback.Stub() {
                    @Override
                    public void callback(int code, String data) throws RemoteException {
                        Log.e(TAG, "unSubscribe result: " + code + " , data : " + data);
                        if (code == 0) {
                            Log.i(TAG, "取消订阅成功");
                        } else {
                            Log.i(TAG, "取消订阅失败 :" + data);
                        }
                    }
                });
            } catch (RemoteException e) {
                e.printStackTrace();
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
    }
    

    查询订阅关系

    调用接口前需要创建 aidl 文件和绑定服务,同上文“取消订阅”。

    调用查询接口

    示例代码如下:

    private void checkRelation() {
        Log.i(TAG, "checkRelation");
        if (mService != null) {
            try {
                JSONObject jsonObject = new JSONObject();
    
                //templateIds请替换需要查询的值,同时type传入与templateIds对应的值
                String template1 = "534df9c929434bbd977f9904ddb41a41";
                JSONArray templateIdsJson = new JSONArray();
                templateIdsJson.put(template1);
                jsonObject.put("templateIds", templateIdsJson);
                String type = "2";
                jsonObject.put("type", type);
    
                //订阅时传入的值
                String scene = "testScene";
                jsonObject.put("scene", scene);
    
                //请替换为vivo开放平台上创建应用时所得的clientId
                String clientId = "474775";
                jsonObject.put("clientId", clientId);
    
                // userId请使用需要查询的用户的userId,并进行加密
                String userId = "";
                String encryptUserId;
                try {
                    encryptUserId = encryptAESGCM(userId, SERVICE_SECRET);
                } catch (Exception e) {
                    encryptUserId = null;
                }
                jsonObject.put("userId", encryptUserId);
    
                //和订阅时保持一致
                String appPackage = "";
                jsonObject.put("package", appPackage);
                //和订阅时保持一致
                String subDesc = "";
                jsonObject.put("subDesc", subDesc);
                mService.isRelationExist(jsonObject.toString(), new ICallback.Stub() {
                    @Override
                    public void callback(int code, String data) throws RemoteException {
                        Log.e(TAG, "checkRelation result: " + code + " , data : " + data);
                        if (code == 0) {
                            if ("true".equals(data)) {
                                Log.e(TAG, "查询成功: 已订阅");
                            } else {
                                Log.e(TAG, "查询成功: 未订阅");
                                Toast.makeText(MainActivity.this, "查询成功: 未订阅", Toast.LENGTH_LONG).show();
                            }
                        } else {
                            Log.e(TAG, "查询失败: " + data);
                        }
                    }
                });
            } catch (RemoteException e) {
                e.printStackTrace();
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
    }
    

    工具方法

    加密

    userId 需要加密后传输使用,加密方法如下:

    const crypto = require('crypto')
    
    /**
     * aes加密
     * @param data
     * @param secretKey
     */
    const aesEncrypt = function (data, secretKey, iv) {
      var md5 = crypto.createHash('md5')
      var result = md5.update(secretKey).digest()
      var cipher = crypto.createCipheriv('aes-128-gcm', result, iv)
      const encrypted = cipher.update(data)
      const final = cipher.final()
      const tag = cipher.getAuthTag()
      const res = Buffer.concat([encrypted, final, tag])
      return encodeURI(res.toString('base64'))
    }
    

    创建通知渠道

    在 Android 8 以上的版本需要创建对应的 channel 才能响应通知 channel 注册代码示例如下:

    /**
     * 创建不同等级的channel
     *
     * @param context
     */
    private void createChannel(Context context) {
        String channelIdPrefix = getDeleteShort(context.getPackageName());
        String channelNamePrefix = getString(R.string.channel_name);//channel_name
        NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
        if (Build.VERSION.SDK_INT >= 26 && manager != null) {
            NotificationChannel channel1 = new NotificationChannel(channelIdPrefix + "_1",
                    channelNamePrefix + "1", NotificationManager.IMPORTANCE_MIN);
            manager.createNotificationChannel(channel1);
    
            NotificationChannel channel2 = new NotificationChannel(channelIdPrefix + "_2",
                    channelNamePrefix + "2", NotificationManager.IMPORTANCE_LOW);
            manager.createNotificationChannel(channel2);
    
            NotificationChannel channel3 = new NotificationChannel(channelIdPrefix + "_3",
                    channelNamePrefix + "3", NotificationManager.IMPORTANCE_DEFAULT);
            manager.createNotificationChannel(channel3);
    
            NotificationChannel channel4 = new NotificationChannel(channelIdPrefix + "_4",
                    channelNamePrefix + "4", NotificationManager.IMPORTANCE_HIGH);
            manager.createNotificationChannel(channel4);
        }
    }
    
    public static String getDeleteShort(String string) {
        try {
            return string.replace(".", "").toLowerCase();
        } catch (Exception e) {
            return "";
        }
    }
    

    获取 appUserId

    用于区分程序运行所在的用户 id 可通过以下方式获取:

    import android.os.Process;
    /**
     * 获取userId
     *
     * @param
     * @return userId
     */
    private int getAppUserId() {
        return Process.myUid() / 100000;
    }
    

    打开消息中心快应用

    消息中心快应用里面可以查询历史订阅记录

    private static void openMsgCenter(){
        Intent intent = new Intent();
        intent.setClassName("com.vivo.hybrid", "com.vivo.hybrid.main.DispatcherActivity");
        intent.setData(Uri.parse("hap://app/com.quickapp.msg"));
        Bundle bundle = new Bundle();
        bundle.putInt("EXTRA_MODE", 4);
        intent.putExtras(bundle);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        sContext.startActivity(intent);
    }
    

    Q&A

    订阅失败时的常见原因:
    1 检查 vivo 账号是否登陆,如果已经登陆,重新登陆后再尝试。
    2 检查传入的 app 包名信息是否已替换为实际的包名,包名不一致会订阅失败。
    3 检查 clientId 是否正确,templateId 和 type 是否对应。
    4 订阅成功后,服务端推送了消息,手机没有收到消息。请检查所开发的 app 的通知消息的功能是否打开。确保此功能是打开状态。
    5 开启混淆不影响上述代码的功能。
    6 安全检测问题,如果端侧被检测出来泄露密钥的安全风险,建议将密钥和加密过程放在服务器,采用请求返回密文的方式来获取加密后的 userID。
    7 查看日志关键字 SubscribeHelper,SubscribeActivity。

    H5 订阅

    引擎通过提供主动拉起 V 订阅的组件,实现 H5 场景下的订阅。

    V 订阅组件 SDK

    首先在网页中嵌入如下 jssdk

    <script type="text/javascript" src="//h5.vivo.com.cn/qa/vmsg/dist/qa_subscribe.min.js"></script>
    

    使用订阅组件前,需要满足以下多个前置条件

    前置条件一:判断是否是 vivo 手机

    该功能仅支持在 vivo 手机上使用,电脑端或者其他品牌手机无法生效,可使用以下方法进行判断。

    方法定义
    function isVivoManufacturer() {
      const ua = navigator.userAgent
      let l1 = ua.match(/(vivo|iqoo|v\d{4}(?:a|t|ba|ca|bt|ct|et|ea|ga|dt|da|a0))/i) || []
      return l1.length > 1
    }
    

    前置条件二:判断当前环境是否支持订阅组件

    该方法会在 SDK 中注入到浏览器,用于判断当前环境是否支持订阅组件。

    使用该方法,页面 dom 树中必须真实存在订阅组件 。开发者需要自行通过该方法的回调控制组件的显示跟隐藏,true 则展示订阅组件,false 则隐藏订阅。

    使用回调函数方式调用 channelReady(callback)
    参数:
    参数名 类型 必填 说明
    callback function 平台上快应用能力检测的回调函数,如支持快应用服务则返回 true 值,否则则返回 false 值
    callback 参数
    参数名 类型 说明
    bAvailable boolean 当前环境支持订阅组件,则该值为 true,否则该值为 false
    示例
    channelReady(function (bAvailable) {
      alert('是否存在框架服务:' + bAvailable)
    })
    

    订阅组件使用方法

    在页面<body>合适位置插入<qa-subscribe-button>标签, 示例如下:

    注意,订阅组件是需要用户主动点击组件后才会拉起订阅弹窗。


    <head>
      <script type='text/javascript' src='//h5.vivo.com.cn/qa/vmsg/dist/qa_subscribe.min.js'></script>
      <title>订阅测试1</title>
    </head>
    <body>
      <qa-subscribe-button
        style='height: 2rem; display: none'
        data-package-name='com.quickapp.msg'
        data-page='/subscribe'
        data-params='{"templateIds": "[2420149388d94ec3b32a602da23a79da,2420149388d94ec3b32a602da23a79da]"
                            ,"scene":"场景测试"
                            ,"type":"2"
                            ,"clientId":"90000000017"
                            ,"userId":"ygiAdzTGWVi/I7T69/khgZPwt20N04UtkBh4H0NxK3B21hw="
                            ,"subDesc":"测试subdesc"
                            ,"package":"com.quickapp.center"
                            ,"subject":"rpk"
                            ,"mode":"debug"
                            ,"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIyT213eGVHMW1UQzh0NFNScnRqdW9EdlB1ZHBpbE9CQk03TE1NZVJTUkZZPSIsImV4cCI6MTY2ODE1MDg5NH0.A5HhaFXXuyToukNJVOec9k1K8miXG4RPgyg_D0uegvg"
                            ,"channelCode":"other"}'
        data-click-event='{"eventName": "handleClickEvent", "eventParams": "this is a click"}'
        data-subscribe-event='{"eventName": "subscribeHandler", "eventParams": "this is a subscribe"}'
      >
        <templates>
          <!-- <templates> 中插入布局元素,以下仅为示例,开发者可根据业务需要定制 DOM 及 样式 -->
          <div id='container'>
            <button class="btn">订阅测试</button>
          </div>
        </templates>
          <styles>
        <!-- <styles> 中插入样式 -->
        #container .btn{margin-left: 30px;background-color: rgb(65, 95, 255);font-size: 28px;color:
          #fff;padding: 5px 14px;border: 1px solid rgb(65, 95, 255);border-radius: 5px;}
      </styles>
      </qa-subscribe-button>
      <script>
        //判断是否是 vivo 手机
        function isVivoManufacturer() {
          const ua = navigator.userAgent
          let l1 = ua.match(/(vivo|iqoo|v\d{4}(?:a|t|ba|ca|bt|ct|et|ea|ga|dt|da|a0))/i) || []
            return l1.length > 1
      }
        //判断当前环境是否支持订阅,支持订阅,展示订阅组件
        channelReady(function (bAvailable) {
          const $button = document.getElementById('targetButton')
    
          if (bAvailable) {
            $button.style.display = 'block'
          } else {
            $button.style.display = 'none'
          }
        })
    
        // 绑定的方法名,必须是全局方法
        function handleClickEvent(params) {
           console.log(params) // this is a click
        }
    
        function subscribeHandler(params, callbackParams) {
          console.log(params) // this is a subscribe
          console.log(`测试订阅回调结果 ${callbackParams}`) // callbackParams:订阅回调object
        }
      </script>
    </body>
    

    订阅组件属性

    属性名称 类型 必填 描述
    data-package-name string Y 必须传入 com.quickapp.msg
    data-page string Y 必须传入 /subscribe
    data-params string Y 订阅组件参数,详情见下表
    design-params object N 用于指定设计稿宽度及默认 font-size 大小,以适配使用不同宽度屏幕。开发者可根据设计稿所标尺寸与默认 font-size 计算出元素 rem 单位值。点击组件会根据实际屏幕宽度、 designWidth 值及开发者指定的元素 rem 值重新计算元素实际大小。不填默认 fontSize 为 16、designWidth 为 1080。
    data-click-event String N 配置用户点击后回调执行的方法,可用于数据上报;eventName: click 事件回调执行的方法名称;eventParams: 回调方法传入的参数
    data-expose-event String N 配置点击组件曝光时回调执行的方法,eventName: 曝光时回调执行的方法名称;eventParams: 回调方法传入的参数
    data-subscribe-event string N 配置订阅回调,用于接收订阅结果返回;eventName: 订阅事件回调方法;eventParams: 回调方法传入的参数;callbackParams: 订阅回调结果,非用户定义,订阅事件方法回调时,以第二个参数的形式返回,参数详情请见下表 callbackParams 描述

    订阅参数 data-params

    订阅参数全部为必传。

    参数名 类型 描述
    templateIds array 期望订阅的消息模板, 查阅服务端文档,在服务后台获取
    type string 模板对应的 type,在申请模板时获取
    clientId string 应用 id,开发者在 vivo 开放平台上注册后分配
    userId string 用户 id,用于标识用户身份,需要加密后传输,建议在服务端加密,参考下文的加密方法;如果业务场景不需要使用用户 id,该值可传入“ ”
    subDesc string 订阅消息的描述, 开发者自定义
    package string 包名,如果是 rpk,就传入 rpk 包名,如果是 app,就传入 app 包名
    subject string 订阅主体的类型,值为 rpk 或 app,如果是 rpk,在订阅时会同时为 rpk 创建桌面图标
    token string 用于安全校验的 token,通过服务端接口获取,详见下文获取 token
    channelCode string 渠道编码,用于 CP 对站外渠道进行区分,以方便分类计费,V 订阅只对这个参数进行透传,无其他场景使用,取值可自定义例如投放了百度和 vivo 两个站外渠道,则可定义为:baidu 和 vivo,不建议使用中文,可使用拼音进行代替。如果不进行站外投放或者确认不进行区分也可以不传,V 订阅会对所有订阅赋予默认值“other”
    mode string 可选,用于开发阶段,值为 debug,加入这个参数后,订阅完成时会返回真实的订阅回传结果,如果失败会包含具体的失败原因;如果不加入,则只返回订阅成功或订阅失败。

    订阅回调结果 callbackParams

    参数名 类型 必填 描述
    msg string Y 返回结果描述
    code number Y 订阅接口状态码。0: 成功 1: 失败 2: 超时

    订阅组件可以使用的标签和样式列表请参考 快应用官方点击组件 相关约束描述


    获取 token

    同服务端接入文档中获取 token 的接口 https://quickappmsg.vivo.com.cn/docs/development/server/#%E9%89%B4%E6%9D%83



    安全规范

    由于 h5 订阅直接暴露在开放的公域 h5 页面上,存在潜在的安全隐患,相应的安全规范及实施方案见:h5 接入安全规范及实施方案



    工具方法

    加密 userId

    用于加密 userId,未加密的 userId 将无法订阅成功。建议将加密放在服务端处理,客户端仅获取结果,可以避免密钥泄露方法如下:

    /**
    * 加密
    *
    * @param content 待加密内容
    * @param key 加密使用的 AES 密钥,使用从服务后台获取到的service secret
    * @return 加密后的密文
    */
    public static String encryptAESGCM(String content, String key) throws Exception {
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        SecretKey secretKey = strKey2SecretKey(key);
        cipher.init(Cipher.ENCRYPT_MODE, secretKey);
        byte[] iv = cipher.getIV();
        assert iv.length == 12;
        byte[] encryptData = cipher.doFinal(content.getBytes());
        assert encryptData.length == content.getBytes().length + 16;
        byte[] message = new byte[12 + content.getBytes().length + 16];
        System.arraycopy(iv, 0, message, 0, 12);
        System.arraycopy(encryptData, 0, message, 12, encryptData.length);
        String encryptContent = android.util.Base64.encodeToString(message, android.util.Base64.NO_WRAP);
        return encryptContent;
    }
    
    /**
    * 将使用 Base64 加密后的字符串类型的 secretKey 转为 SecretKey
    *
    * @param strKey
    * @return SecretKey
    */
    public static SecretKey strKey2SecretKey(String strKey) {
        byte[] bytes = android.util.Base64.decode(strKey, android.util.Base64.NO_WRAP);
        SecretKeySpec secretKey = new SecretKeySpec(bytes, "AES");
        return secretKey;
    }
    

    其他编码语言加密 userId 代码参考
    PHP
    // 加密数据
    function encrypt($data, $key) {
      $method = 'aes-256-gcm'
      $options = OPENSSL_RAW_DATA
      $ivlen = openssl_cipher_iv_length($method)
      $iv = openssl_random_pseudo_bytes($ivlen) // 生成随机的IV
      $ciphertext = openssl_encrypt($data, $method, $key, $options, $iv, '')
      return base64_encode($iv.$ciphertext.$tag)
    }
    
    Node.js
    const crypto = require('crypto')
    // 加密函数
    function encrypt(text, key) {
      const iv = crypto.randomBytes(12) // 生成 12 字节的随机初始化向量
      const cipher = crypto.createCipheriv('aes-256-gcm', Buffer.from(key, 'base64'), iv, {
        authTagLength: 16,
      }) // 创建加密器
      const encrypted = Buffer.concat([cipher.update(text), cipher.final()]) // 加密数据
      const tag = cipher.getAuthTag() // 获取 Tag 值
      const result = Buffer.concat([iv, encrypted, tag]) // 合并 IV、Tag 和加密数据
      return result.toString('base64') // 返回 base64 编码后的结果
    }