Appearance
开发插件
集成使用
插件本质是一个 apk 文件。所以在一个空的 android 项目上开发即可。
创建一个空的Android项目
如果已经有空项目请忽略
使用 AndroidStudio 创建一个空的项目。
这边 AGP(Android Gradle Plugin) 使用的是 8.0 版本
编译脚本使用 Kotlin DSL(build.gradle.kts)
这边的主要目录结构
shell
MyPlugin/
├── app/
│ ├── src/main/java/com.sample.myplugin/
│ │ └──MyJSViewPlugin.java
│ └──build.gradle.kts
├── gradle/
├── settings.gradle.kts
└── build.gradle.kts接下来的文档使用以上环境来讲解。如果自身项目环境有差异请自行调整,随机应变。
添加 Maven 仓库
插件的 SDK 被上传到一个私有的 Maven 上,所以要额外添加一个仓库地址。
kotlin
maven {
// 仓库地址
url = uri("http://nexus.cluster.qcast.cn/repository/maven-releases/")
// 高版本 gradle 要求地址是 https 协议,添加这个忽略协议要求
isAllowInsecureProtocol = true
}文件./MyPlugin/settings.gradle.kts:
kotlin
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
maven {
url = uri("http://nexus.cluster.qcast.cn/repository/maven-releases/")
isAllowInsecureProtocol = true
}
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven {
url = uri("http://nexus.cluster.qcast.cn/repository/maven-releases/")
isAllowInsecureProtocol = true
}
}
}
rootProject.name = "MyPlugin"
include(":app")添加gradle插件依赖
文件./MyPlugin/build.gradle.kts:
kotlin
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.application") version "8.1.1" apply false
id("jsview-plugin-asm") version "1.0.0" apply false
}对于老版本的 Gradle 是这样配置:
groovy
classpath "com.qcode.jsviewplugin:jsview-plugin-asm:1.0.0"在文件./MyPlugin/app/build.gradle.kts中添加:
kotlin
plugins {
id("com.android.application")
id("jsview-plugin-asm")
}
...添加插件 sdk 依赖
kotlin
// JSView插件依赖
annotationProcessor("com.qcode.jsviewplugin:jsview-plugin-annotation-processor:1.0.1")
implementation("com.qcode.jsviewplugin:jsview-plugin-base:1.0.1")文件./MyPlugin/app/build.gradle.kts:
kotlin
plugins {
id("com.android.application")
id("jsview-plugin-asm")
}
android {
namespace = "com.sample.myplugin"
compileSdk = 33
...
}
dependencies {
...
// JSView插件依赖
annotationProcessor("com.qcode.jsviewplugin:jsview-plugin-annotation-processor:1.0.0")
implementation("com.qcode.jsviewplugin:jsview-plugin-base:1.0.0")
}创建插件
创建一个类继承JSViewPluginBase 并且使用 JSViewPlugin注解这个类。
java
package com.sample.myplugin;
import com.qcode.jsviewpluginbase.JSViewPlugin;
import com.qcode.jsviewpluginbase.JSViewPluginBase;
import com.qcode.jsviewpluginbase.JsviewFunctionEnc;
import java.util.Map;
@JSViewPlugin(
// 插件名称
pluginName = "我的插件",
// 插件版本名
versionName = "1.0",
// 插件版本号
versionCode = 1,
// 插件包名,类似 apk 的包名,用于与其他插件区分。
packageName = "com.abc.myplugin"
)
public class MyJSViewPlugin extends JSViewPluginBase {
public MyJSViewPlugin(Map<String, Object> param, JsviewFunctionEnc funcEncRef, String pluginName) {
super(param, funcEncRef, pluginName);
}
// TODO 添加自己的接口
}添加给 js 调用的接口
在MyJSViewPlugin.java创建自己的方法,用@JavascriptInterface注解,
参数和返回值类型只支持基本数据类型。
java
@JavascriptInterface
public void testFunction(String text){
Log.d("MyPlugin", "testFunction: "+text);
}编译插件
点击编译项目后就会得到两个文件: ./MyPlugin/app/build/generated/ap_generated_sources/debug/out/js/MyJSViewPlugin.js./MyPlugin/app/build/outputs/apk/release/app-release-unsigned.apk
使用插件
- 将 apk 文件部署到 http 服务器上。
- 修改
MyJSViewPlugin.js文件里的 apk 文件下载地址跟apk 文件 md5.
javascript
let PluginInfo={
//插件下载地址
downloadUrl:"http://192.168.2.179:8092/app/build/outputs/apk/release/app-release-unsigned.apk",
// apk 文件的md5
md5:"e7c9ef87c1bd283c2ab4be10852e2358",
packageName:"com.abc.myplugin", // 插件包名
name:"我的插件", // 插件名
version:"1.0", //插件需要的版本号
versionCodeMin:1,
versionCodeMax:1,
bridgeName:"jPMyJSViewPlugin", //插件bridge注册到jsview的名称
className:"com.sample.myplugin.MyJSViewPluginCreator", //插件初始化类名称
initMethod:"createInstance", //插件初始化方法
listener:"__MyJSViewPluginPluginLoadResult", //插件加载结果回调
listener2: "__MyJSViewPluginPluginStatus",
};
...注意: $\color{red}{正式上线一般不需要配置下载地址和md5}$ ,插件会根据包名和版本自动去对应后台下载。具体插件管理后台联系相关人员。
- 小程序集成
MyJSViewPlugin.js
把MyJSViewPlugin.js放到自己的小程序项目中并引用。
javascript
import { MyJSViewPlugin } from "./MyJSViewPlugin";- 加载插件
javascript
// 调用globalLoadPlugin接口加载插件
MyJSViewPlugin.globalLoadPlugin((obj)=>{
/**
* 插件加载状态回调。
* 回调函数的参数定义如下:
* object结构,包含status和code两个变量:
* 1)开始加载插件:
* obj.status=1;
* obj.code表示是否是首次加载:1:首次加载,此时插件需要经历下载、解压等过程,用时较长,可以考虑给用户相关提示;
* 2:非首次加载,此时插件加载过程很短,可以不用出现用户提示界面。
* 2)插件加载中,加载新插件时上报此状态完整状态,加载旧插件时,只上报dexload完成状态(code=3):
* obj.status=2;
* obj.code表示加载过程中的状态:1:插件下载进度,目前只上报下载结束;2:插件解压完成;3:插件dexload完成。
* obj.progress:当code=1时(下载进度),progress为实际下载进度,百分制,目前只有100(100%,完成状态)。
* 3)插件加载成功
* obj.status=3;
* obj.code无效。
* 4)插件加载失败
* obj.status=4;
* obj.code表示插件加载失败原因:1:插件管理模块不存在(未真正开始加载插件),下面的负值为插件管理模块返回的错误;
* -1:请求插件加载的参数不正确,需要确认构造的PluginInfo内容;
* -2:未找到插件更新链接;
* -3:插件HTTP请求失败;
* -4:插件下载失败;
* -5:插件MD5校验失败;
* -6:解压失败;
* -7:文件大小为0;
* -8:未找到dex文件;
* -9:dex文件load失败;
* -10:插件初始化失败;
* -11:取消插件下载;
* -12:版本检查失败,比如已经加载了其他版本,此版本不能再加载。
*
*/
})- 调用插件接口
javascript
// 插件加载成功后可以调用 java 中定义好的接口
MyJSViewPlugin.testFunction("来自javascript的调用!")Android 如何调用 js 的方法
如果 java 部分有些事件发生想通知到 js 可以在插件的 java 代码里添加一个方法然后用@JavascriptFunction注解:
java
/**
* 调用此方法会通知到 js 层
* 方法名不限制,但返回值和参数只支持基本数据类型,
* 方法里不要写逻辑
*/
@JavascriptFunction
public String javaEvent(int event){
return null;
}在 js 代码中实现这个方法:
javascript
// 注意:方法名要跟 java 一样,参数数量也要对应上
MyJSViewPlugin.nativeCallback.javaEvent = (event)=>{
console.log("来自 java 的调用:"+event)
return "这是来自 js 的字符串";
}最后在 java 中调用javaEvent接口即可:
java
String str = javaEvent(123);插件管理模块功能
JsviewFunctionEnc.java 封装了所有插件管理模块支持的功能
java
/******************************************************************************
* 本文件封装了插件管理模块支持的接口。使用者调用前需要先初始化,把插件管理模块初始化插件时
* 传入的参数传给构造函数。
******************************************************************************/
package com.qcode.jsvplayer;
import android.os.Bundle;
import android.util.Log;
import android.widget.FrameLayout;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
public class JsviewFunctionEnc {
private String TAG = "JsviewFunctionEnc";
private Map mParam;
public interface HoleStyleChangeListener {
void onEvent(Bundle data);
}
public interface NetStateChangeListener {
void onEvent(int status);
}
JsviewFunctionEnc(Map param){
mParam = param;
}
/***************************************
* 注册js interface
* @param bridge js interface对象
* @return {int} 0表示成功,-1表示失败
***************************************/
public int addJsvBridge(Object bridge){
if(mParam.containsKey("jsView") && mParam.containsKey("bridgeName") && mParam.containsKey("addJsvBridge")) {
String bridge_name = (String) mParam.get("bridgeName");
Method add_bridge = (Method) mParam.get("addJsvBridge");
Object js_view = (Object) mParam.get("jsView");
try {
add_bridge.setAccessible(true);//调用方法前,设置访问标志
add_bridge.invoke(js_view, bridge_name, bridge);//使用方法
return 0;
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}else {
Log.d(TAG, "Add bridge failed, no enough params!");
}
return -1;
}
/*********************************************
* 发送事件给js端
* @param key {string} 事件名称
* @param value {string} json string,事件值
* @return {int} 0表示成功,-1表示失败
*********************************************/
public int emitEvent(String key, String value){
if(mParam!=null && mParam.containsKey("jsView") && mParam.containsKey("emitEvent")) {
Method emit_event = (Method) mParam.get("emitEvent");
Object js_view = (Object) mParam.get("jsView");
try {
emit_event.setAccessible(true);
emit_event.invoke(js_view, key, value);
return 0;
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}else {
Log.d(TAG, "Send event failed, no enough params!");
}
return -1;
}
/*************************************************
* 调用js回调函数
* @param callback {string} js注册下来的回调函数名称
* @param value {string} json string,回调函数参数
* @return {int} 0表示成功,-1表示失败
*************************************************/
public int evaluateJsFunction(String callback, String value){
if(mParam!=null && mParam.containsKey("jsView") && mParam.containsKey("evaluateJsFunction")) {
Method evaluateJsFunction = (Method) mParam.get("evaluateJsFunction");
Object js_view = (Object) mParam.get("jsView");
try {
evaluateJsFunction.setAccessible(true);
evaluateJsFunction.invoke(js_view, callback, value);
return 0;
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}else {
Log.d(TAG, "Send callback failed, no enough params!");
}
return -1;
}
/*********************************************************************
* 获取jsview(小程序所有在view)的back view,插件支持back view和front view,
* 其中back view在jsview后面,需要给jsview打个洞,才可以透出来,一般用于播放
* 器插件等场景,front view在jsview前面,直接盖在jsview上面。
* @return {FrameLayout} back view对象
*********************************************************************/
public FrameLayout getBackgroundRootView(){
if(mParam.containsKey("jsView") && mParam.containsKey("getRootView")) {
Method get_rootview = (Method) mParam.get("getRootView");
Object js_view = (Object) mParam.get("jsView");
try {
get_rootview.setAccessible(true);
return (FrameLayout) get_rootview.invoke(js_view, true);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}else {
Log.d(TAG, "Get background root view failed, no enough params!");
}
return null;
}
/*********************************************************************
* 获取jsview(小程序所有在view)的front view,插件支持back view和front view,
* 其中back view在jsview后面,需要给jsview打个洞,才可以透出来,一般用于播放
* 器插件等场景,front view在jsview前面,直接盖在jsview上面。
* @return {FrameLayout} front view对象
*********************************************************************/
public FrameLayout getFrontRootView(){
if(mParam.containsKey("jsView") && mParam.containsKey("getRootView")) {
Method get_rootview = (Method) mParam.get("getRootView");
Object js_view = (Object) mParam.get("jsView");
try {
get_rootview.setAccessible(true);
return (FrameLayout) get_rootview.invoke(js_view, false);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}else {
Log.d(TAG, "Get front root view failed, no enough params!");
}
return null;
}
/*************************************
* 通知jsview释放焦点,用于back view或者
* front view需要焦点的场景
*************************************/
public void releaseFocus(){
if(mParam.containsKey("jsView") && mParam.containsKey("releaseFocus")) {
Method release_focus = (Method) mParam.get("releaseFocus");
Object js_view = (Object) mParam.get("jsView");
try {
release_focus.setAccessible(true);
release_focus.invoke(js_view);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}else {
Log.d(TAG, "Release focus failed, no enough params!");
}
}
/***********************************************
* 监听在jsview上打的洞的状态变化,主要是尺寸的变化
* @param track_id {string} 打的洞的索引
* @param listener 回调函数
* @return {int} 0表示成功,-1表示失败
***********************************************/
public int registerHoleStyleChange(String track_id, HoleStyleChangeListener listener){
if(mParam.containsKey("jsView") && mParam.containsKey("registerHoleStyleChange")) {
Method register_hole_style_change = (Method) mParam.get("registerHoleStyleChange");
Object js_view = (Object) mParam.get("jsView");
try {
register_hole_style_change.setAccessible(true);
try {
register_hole_style_change.invoke(js_view, track_id, listener.getClass().getMethod("onEvent", Bundle.class), listener);
return 0;
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}else {
Log.d(TAG, "Register hole style change failed, no enough params!");
}
return -1;
}
/**************************************
* 取消对所打的洞的变化的监听
* @param track_id {string} 打的洞的索引
* @return {int} 0表示成功,-1表示失败
**************************************/
public int unregisterHoleStyleChange(String track_id){
if(mParam.containsKey("jsView") && mParam.containsKey("unregisterHoleStyleChange")) {
Method unregister_hole_style_change = (Method) mParam.get("unregisterHoleStyleChange");
Object js_view = (Object) mParam.get("jsView");
try {
unregister_hole_style_change.setAccessible(true);
unregister_hole_style_change.invoke(js_view, track_id);
return 0;
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}else {
Log.d(TAG, "Unregister hole style change failed, no enough params!");
}
return -1;
}
/*****************************************
* 注册网络状态变化监听
* @param name {string} key,用于注销时传入
* @param listener 回调函数
* @return {int} 0表示成功,-1表示失败
*****************************************/
public int registerNetStateChange(String name, NetStateChangeListener listener){
if(mParam.containsKey("jsView") && mParam.containsKey("registerNetStateChange")) {
Method register_net_state_change = (Method) mParam.get("registerNetStateChange");
Object js_view = (Object) mParam.get("jsView");
try {
register_net_state_change.setAccessible(true);
try {
register_net_state_change.invoke(js_view, name, listener.getClass().getMethod("onEvent", Integer.class), listener);
return 0;
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}else {
Log.d(TAG, "Register net state change failed, no enough params!");
}
return -1;
}
/**********************************************
* 注销网络状态变化监听
* @param name {string }key,使用注册时传入的值
* @return {int} 0表示成功,-1表示失败
**********************************************/
public int unregisterNetStateChange(String name){
if(mParam.containsKey("jsView") && mParam.containsKey("unregisterNetStateChange")) {
Method unregister_net_state_change = (Method) mParam.get("unregisterNetStateChange");
Object js_view = (Object) mParam.get("jsView");
try {
unregister_net_state_change.setAccessible(true);
unregister_net_state_change.invoke(js_view, name);
return 0;
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}else {
Log.d(TAG, "Unregister net state change failed, no enough params!");
}
return -1;
}
/*********************************
* 获取电视小程序渠道号
* @return {string} 渠道号
*********************************/
public String getMarketCode(){
if(mParam.containsKey("jsView") && mParam.containsKey("getMarketCode")) {
Method get_market_code = (Method) mParam.get("getMarketCode");
Object js_view = (Object) mParam.get("jsView");
try {
get_market_code.setAccessible(true);
return (String)get_market_code.invoke(js_view);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}else {
Log.d(TAG, "Get market code failed, no enough params!");
}
return null;
}
/*****************************************
* 获取电视小程序唯一标识,uuid
* @return {string} uuid
*****************************************/
public String getUUID(){
if(mParam.containsKey("jsView") && mParam.containsKey("getUUID")) {
Method get_uuid = (Method) mParam.get("getUUID");
Object js_view = (Object) mParam.get("jsView");
try {
get_uuid.setAccessible(true);
return (String)get_uuid.invoke(js_view);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}else {
Log.d(TAG, "Get uuid failed, no enough params!");
}
return null;
}
/***************************
* 获取以太网mac地址
* @return {string} mac地址
***************************/
public String getEthMac(){
if(mParam.containsKey("jsView") && mParam.containsKey("getEthMac")) {
Method get_eth_mac = (Method) mParam.get("getEthMac");
Object js_view = (Object) mParam.get("jsView");
try {
get_eth_mac.setAccessible(true);
return (String)get_eth_mac.invoke(js_view);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}else {
Log.d(TAG, "Get ethernet mac failed, no enough params!");
}
return null;
}
/******************************
* 获取wifi mac地址
* @return {string} mac地址
******************************/
public String getWifiMac(){
if(mParam.containsKey("jsView") && mParam.containsKey("getWifiMac")) {
Method get_wifi_mac = (Method) mParam.get("getWifiMac");
Object js_view = (Object) mParam.get("jsView");
try {
get_wifi_mac.setAccessible(true);
return (String)get_wifi_mac.invoke(js_view);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}else {
Log.d(TAG, "Get wifi mac failed, no enough params!");
}
return null;
}
/*********************************************************
* 获取调用小程序的信息,值为小程序的appname+"|"+小程序链接的md5;
* 主要用于识别加载插件的小程序的信息。
* @return {string} refeffer,加载者信息
*********************************************************/
public String getReferrer(){
if(mParam.containsKey("jsView") && mParam.containsKey("getReferrer")) {
Method get_referrer = (Method) mParam.get("getReferrer");
Object js_view = (Object) mParam.get("jsView");
try {
get_referrer.setAccessible(true);
return (String)get_referrer.invoke(js_view);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}else {
Log.d(TAG, "Get referrer failed, no enough params!");
}
return null;
}
/**********************************************
* 校验小程序签名
* @param signKey {string} 支持的小程序签名
* @return {boolean} true表示通过,false表示失败
**********************************************/
public boolean checkSignKey(String signKey){
if(mParam.containsKey("jsView") && mParam.containsKey("checkSignKey")) {
Method check_sign_key = (Method) mParam.get("checkSignKey");
Object js_view = (Object) mParam.get("jsView");
try {
check_sign_key.setAccessible(true);
return (boolean)check_sign_key.invoke(js_view, signKey);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}else {
Log.d(TAG, "Unregister net state change failed, no enough params!");
}
return false;
}
}支持的接口如下
addJsvBridge
注册js interface,插件模块将js interface接口进行注册,以便js端可以正确访问插件提供的js穿透接口。
emitEvent
插件发送事件给js端,一般用于状态的通报。
evaluateJsFunction
调用插件js端注册的回调函数,一般用于异步调用完成后的结果通知。
getBackgroundRootView
插件管理模块给插件提供了back view和front view,其中back view显示在jsview(加载小程序的view)后面,front view显示在jsview的前面。插件如果需要进行界面展示,可以根据不同场景需求,选择对应的view。
本接口获取back view
getFrontRootView
获取front view
releaseFocus
如果back view或者front view需要获取焦点,通知jsview失去焦点。
registerHoleStyleChange
对于back view,需要在jsview上打洞,back view才可见,本接口用于通知洞的状态变化。
unregisterHoleStyleChange
注销jsview洞的状态变化通知函数。
registerNetStateChange
注册网络状态变化通知接口。
unregisterNetStateChange
注销网络状态变化通知接口。
getMarketCode
获取电视小程序apk的渠道号。
getUUID
获取终端唯一标识。
getEthMac
获取终端以太网(有线)mac地址。
getWifiMac
获取终端wifi mac(无线mac)地址。
getReferrer
获取加载插件的小程序信息,值为小程序的appname+"|"+小程序链接(url)的md5。
checkSignKey
校验小程序签名。
插件加载的 UI 显示
插件在下载和加载的时候SDK 会负责显示 UI。 如果不想显示 UI 可以使用setDefaultLoadUI 去设置。
java
// 加载插件不要有 UI 显示。
PluginEntity.setDefaultLoadUI(PluginEntity.LOAD_UI_NONE);
// 或者只允许显示插件加载错误的 UI
PluginEntity.setDefaultLoadUI(PluginEntity.LOAD_UI_ERRORS_ONLY);