引擎接口
更新时间:
该文档为快应用开发者提供订阅消息相关的接口以及说明
日期 | 说明 |
---|---|
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,如下图所示:
接口声明
{ "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 文件用于通信
文件内容如下:
// 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 树中必须真实存在订阅组件
使用回调函数方式调用 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 编码后的结果
}