webview
This commit is contained in:
parent
d52dee56be
commit
786f077bd6
|
|
@ -49,6 +49,11 @@ android {
|
|||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
packaging {
|
||||
jniLibs {
|
||||
useLegacyPackaging = true
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
|
|
@ -59,8 +64,7 @@ dependencies {
|
|||
implementation("com.jakewharton.timber:timber:5.0.1")
|
||||
implementation("androidx.cardview:cardview:1.0.0")
|
||||
implementation("com.iqiyi.xcrash:xcrash-android-lib:3.1.0")
|
||||
implementation("io.github.justson:agentweb-core:v5.1.1-androidx")
|
||||
implementation("com.github.Justson:Downloader:v5.0.4-androidx")
|
||||
implementation("org.mozilla.geckoview:geckoview-omni:115.0.20230710165010")
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||
implementation("com.github.getActivity:XXPermissions:18.2")
|
||||
|
|
|
|||
|
|
@ -1,22 +1,14 @@
|
|||
# AgentWeb 混淆规则
|
||||
-keep class com.just.agentweb.** { *; }
|
||||
-dontwarn com.just.agentweb.**
|
||||
# GeckoView 混淆规则
|
||||
-dontwarn org.mozilla.geckoview.**
|
||||
-keep class org.mozilla.gecko.util.DebugConfig { *; }
|
||||
|
||||
# AndroidInterface 桥接类保护
|
||||
-keep class com.stand.standapp.AndroidInterface { *; }
|
||||
-keep class com.stand.standapp.AndroidInterface$Companion { *; }
|
||||
|
||||
# 忽略不存在的可选依赖报错
|
||||
-dontwarn com.alipay.sdk.**
|
||||
-dontwarn com.download.library.**
|
||||
-dontwarn com.google.android.material.snackbar.**
|
||||
|
||||
# 保护 JavaScript 交互接口 (非常重要!)
|
||||
-keepattributes *Annotation*
|
||||
-keepattributes *JavascriptInterface*
|
||||
-keepclassmembers class * {
|
||||
@android.webkit.JavascriptInterface <methods>;
|
||||
}
|
||||
|
||||
# 保护你自己的 JS 交互类
|
||||
-keep class com.stand.standapp.AndroidInterface { *; }
|
||||
|
||||
# OkHttp 混淆规则
|
||||
-keepattributes Signature
|
||||
-keepattributes InnerClasses
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
(function() {
|
||||
// 在页面上下文中注入桥接脚本,避免 cross-compartment 访问被拒
|
||||
var code = [
|
||||
'(function() {',
|
||||
' var _realAndroid = window.android;',
|
||||
' window.android = new Proxy({}, {',
|
||||
' get: function(_, method) {',
|
||||
' if (method === "then") return;',
|
||||
' return function() {',
|
||||
' var args = Array.prototype.slice.call(arguments);',
|
||||
' var result = prompt("bridge:" + method, JSON.stringify(args));',
|
||||
' try { return JSON.parse(result); } catch(e) { return result; }',
|
||||
' };',
|
||||
' }',
|
||||
' });',
|
||||
' if (_realAndroid) {',
|
||||
' Object.keys(_realAndroid).forEach(function(k) {',
|
||||
' try { window.android[k] = _realAndroid[k]; } catch(e) {}',
|
||||
' });',
|
||||
' }',
|
||||
' setInterval(function() {',
|
||||
' var code = prompt("bridge:_poll", "");',
|
||||
' if (code) { try { eval(code); } catch(e) {} }',
|
||||
' }, 500);',
|
||||
'})();'
|
||||
].join('\n');
|
||||
|
||||
var script = document.createElement('script');
|
||||
script.textContent = code;
|
||||
var parent = document.head || document.documentElement;
|
||||
if (parent) {
|
||||
parent.appendChild(script);
|
||||
script.remove();
|
||||
}
|
||||
})();
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"manifest_version": 2,
|
||||
"name": "standapp-bridge",
|
||||
"version": "1.0",
|
||||
"description": "JS-Native bridge for StandAPP",
|
||||
"content_scripts": [{
|
||||
"matches": ["*://*/*"],
|
||||
"js": ["bridge.js"],
|
||||
"run_at": "document_start",
|
||||
"all_frames": true
|
||||
}]
|
||||
}
|
||||
|
|
@ -53,7 +53,7 @@ public class pttActivity extends AppCompatActivity {
|
|||
@Override
|
||||
public void onClick(View v) {
|
||||
// SendAtUtil.toLogin("gzzdweb01", "123456", "61.243.1.123", 1, 1);
|
||||
SendAtUtil.toLogin("poc2", "poc123456", "220.154.131.231", 1, 1);
|
||||
SendAtUtil.toLogin("test004", "123456", "220.154.131.231", 1, 1);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,27 +3,98 @@ package com.stand.standapp
|
|||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.widget.Toast
|
||||
import android.print.PrintAttributes
|
||||
import android.print.PrintManager
|
||||
import com.stand.standapp.R
|
||||
import com.stand.standapp.utils.LogManager
|
||||
import com.stand.standapp.printer.PrinterManager
|
||||
import timber.log.Timber
|
||||
import com.example.kingway.ptt.SendAtUtil
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
class AndroidInterface(private val activity: MainActivity) {
|
||||
|
||||
private var isSpeaking = false
|
||||
private var lastSpeakTime = 0L
|
||||
private val MIN_SPEAK_DURATION = 200L // 最小通话时长 200ms
|
||||
private val MIN_SPEAK_DURATION = 200L
|
||||
|
||||
/**
|
||||
* PTT 开始说话 (带状态保护)
|
||||
* JS bridge 调度入口
|
||||
* @param method JS 调用的方法名
|
||||
* @param argsJson JSON 数组格式的参数
|
||||
* @return 返回值(JSON 字符串或空串)
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun dispatch(method: String, argsJson: String): String {
|
||||
return try {
|
||||
val args = if (argsJson.isNotEmpty()) JSONArray(argsJson) else JSONArray()
|
||||
when (method) {
|
||||
"pttSpeakPlay" -> { pttSpeakPlay(); "" }
|
||||
"pttGetLoginStatus" -> pttGetLoginStatus()
|
||||
"pttGetLoginId" -> pttGetLoginId()
|
||||
"pttSpeakRelease" -> { pttSpeakRelease(); "" }
|
||||
"pttForceRelease" -> { pttForceRelease(); "" }
|
||||
"pttGetGroupInfo" -> { pttGetGroupInfo(); "" }
|
||||
"pttGetGroupMemberInfo" -> { pttGetGroupMemberInfo(args.optString(0, "")); "" }
|
||||
"simulatePttConflict" -> { simulatePttConflict(); "" }
|
||||
"pttSendAtCmd" -> { pttSendAtCmd(args.optString(0, "")); "" }
|
||||
"pttJumpGroup" -> { pttJumpGroup(args.optString(0, "")); "" }
|
||||
"getPrinterStatus" -> getPrinterStatus()
|
||||
"printText" -> {
|
||||
printText(optStringOrNull(args, 0), args.optInt(1, 0), args.optInt(2, 0), args.optInt(3, 0)); ""
|
||||
}
|
||||
"printBarcode" -> {
|
||||
printBarcode(optStringOrNull(args, 0), args.optInt(1, 0), args.optInt(2, 0), args.optInt(3, 0), args.optInt(4, 0), args.optInt(5, 0)); ""
|
||||
}
|
||||
"printQR" -> {
|
||||
printQR(optStringOrNull(args, 0), args.optInt(1, 0), args.optInt(2, 0)); ""
|
||||
}
|
||||
"printImage" -> {
|
||||
printImage(optStringOrNull(args, 0), args.optInt(1, 0), args.optInt(2, 0), args.optInt(3, 0)); ""
|
||||
}
|
||||
"printColumns" -> {
|
||||
if (args.length() >= 8) {
|
||||
printColumns(optStringOrNull(args, 0), args.optInt(1, 0), optStringOrNull(args, 2), args.optInt(3, 0),
|
||||
optStringOrNull(args, 4), args.optInt(5, 0), optStringOrNull(args, 6), args.optInt(7, 0))
|
||||
}
|
||||
""
|
||||
}
|
||||
"printColumnsWithHeight" -> {
|
||||
if (args.length() >= 9) {
|
||||
printColumnsWithHeight(optStringOrNull(args, 0), args.optInt(1, 0), optStringOrNull(args, 2), args.optInt(3, 0),
|
||||
optStringOrNull(args, 4), args.optInt(5, 0), optStringOrNull(args, 6), args.optInt(7, 0), args.optInt(8, 0))
|
||||
}
|
||||
""
|
||||
}
|
||||
"cutPaper" -> { cutPaper(args.optBoolean(0, false)); "" }
|
||||
"printTestPage" -> { printTestPage(); "" }
|
||||
"checkPaper" -> { checkPaper(); "" }
|
||||
"exportLogs" -> exportLogs()
|
||||
"uploadLogs" -> { uploadLogs(); "" }
|
||||
"printPage" -> { printPage(); "" }
|
||||
"printRawData" -> { printRawData(optStringOrNull(args, 0)); "" }
|
||||
"startUpdate" -> { startUpdate(optStringOrNull(args, 0)); "" }
|
||||
"exitApp" -> { exitApp(); "" }
|
||||
"getVersionName" -> getVersionName()
|
||||
"getAboutInfo" -> getAboutInfo()
|
||||
"saveMessage" -> { saveMessage(optStringOrNull(args, 0)); "" }
|
||||
"getMessages" -> getMessages()
|
||||
"clearMessages" -> { clearMessages(); "" }
|
||||
"onPageFinished" -> { onPageFinished(); "" }
|
||||
else -> {
|
||||
Timber.tag("Bridge").w("Unknown method: $method")
|
||||
""
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.tag("Bridge").e(e, "Dispatch error: $method")
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
private fun optStringOrNull(args: JSONArray, index: Int): String? {
|
||||
return if (index < args.length() && !args.isNull(index)) args.getString(index) else null
|
||||
}
|
||||
|
||||
fun pttSpeakPlay() {
|
||||
if (!isSpeaking) {
|
||||
isSpeaking = true
|
||||
|
|
@ -34,34 +105,20 @@ class AndroidInterface(private val activity: MainActivity) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 PTT 登录状态: "00"-未登录 "01"-登录中 "02"-已登录
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun pttGetLoginStatus(): String {
|
||||
return com.example.kingway.ptt.CallBackResolution.getPttLoginStatus()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 PTT 登录 ID
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun pttGetLoginId(): String {
|
||||
return com.example.kingway.ptt.CallBackResolution.getPttLoginId()
|
||||
}
|
||||
|
||||
/**
|
||||
* PTT 登录
|
||||
*/
|
||||
|
||||
@JavascriptInterface
|
||||
fun pttSpeakRelease() {
|
||||
if (isSpeaking) {
|
||||
val now = System.currentTimeMillis()
|
||||
val duration = now - lastSpeakTime
|
||||
|
||||
if (duration < MIN_SPEAK_DURATION) {
|
||||
// 如果按得太快,延迟释放
|
||||
val delay = MIN_SPEAK_DURATION - duration
|
||||
Timber.tag("PTT").d("Speak duration too short ($duration ms), delaying release by $delay ms")
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
|
|
@ -82,62 +139,34 @@ class AndroidInterface(private val activity: MainActivity) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制释放 PTT (用于异常恢复)
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun pttForceRelease() {
|
||||
isSpeaking = false
|
||||
SendAtUtil.speakRelease()
|
||||
Timber.tag("PTT").d("Force Release")
|
||||
}
|
||||
|
||||
/**
|
||||
* PTT 获取群组
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun pttGetGroupInfo() {
|
||||
SendAtUtil.getGroupInfo()
|
||||
}
|
||||
|
||||
/**
|
||||
* PTT 获取成员
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun pttGetGroupMemberInfo(gid: String) {
|
||||
SendAtUtil.getGroupMemberInfo(gid)
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟 PTT 账号冲突 (测试用)
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun simulatePttConflict() {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
MainActivity.showPttConflictDialog()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送原始 AT 指令 (调试用)
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun pttSendAtCmd(cmd: String) {
|
||||
SendAtUtil.sendRawCmd(cmd)
|
||||
}
|
||||
|
||||
/**
|
||||
* PTT 切换群组
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun pttJumpGroup(gid: String) {
|
||||
SendAtUtil.jumpGroup(gid)
|
||||
}
|
||||
|
||||
/**
|
||||
* JS 调用:获取打印机当前连接状态
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun getPrinterStatus(): String {
|
||||
return try {
|
||||
val json = JSONObject()
|
||||
|
|
@ -151,10 +180,6 @@ class AndroidInterface(private val activity: MainActivity) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JS 调用:通用打印文本
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun printText(text: String?, size: Int, align: Int, space: Int) {
|
||||
if (text.isNullOrEmpty()) return
|
||||
try {
|
||||
|
|
@ -168,10 +193,6 @@ class AndroidInterface(private val activity: MainActivity) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JS 调用:打印条码
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun printBarcode(data: String?, size: Int, height: Int, align: Int, text_pos: Int, type: Int) {
|
||||
if (data.isNullOrEmpty()) return
|
||||
try {
|
||||
|
|
@ -181,10 +202,6 @@ class AndroidInterface(private val activity: MainActivity) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JS 调用:打印二维码
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun printQR(data: String?, size: Int, l_m: Int) {
|
||||
if (data.isNullOrEmpty()) return
|
||||
try {
|
||||
|
|
@ -194,10 +211,6 @@ class AndroidInterface(private val activity: MainActivity) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JS 调用:打印图片 (从 Assets 获取)
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun printImage(fileName: String?, width: Int, align: Int, mode: Int) {
|
||||
if (fileName.isNullOrEmpty()) return
|
||||
try {
|
||||
|
|
@ -210,10 +223,6 @@ class AndroidInterface(private val activity: MainActivity) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JS 调用:分栏打印 (4列)
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun printColumns(c1: String?, p1: Int, c2: String?, p2: Int, c3: String?, p3: Int, c4: String?, p4: Int) {
|
||||
try {
|
||||
PrinterManager.getFactory()?.Printcolumncontent(c1 ?: "", p1, c2 ?: "", p2, c3 ?: "", p3, c4 ?: "", p4)
|
||||
|
|
@ -222,10 +231,6 @@ class AndroidInterface(private val activity: MainActivity) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JS 调用:分栏打印 (带行高)
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun printColumnsWithHeight(c1: String?, p1: Int, c2: String?, p2: Int, c3: String?, p3: Int, c4: String?, p4: Int, height: Int) {
|
||||
try {
|
||||
PrinterManager.getFactory()?.Printcolumncontent(c1 ?: "", p1, c2 ?: "", p2, c3 ?: "", p3, c4 ?: "", p4, height)
|
||||
|
|
@ -234,10 +239,6 @@ class AndroidInterface(private val activity: MainActivity) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JS 调用:切纸
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun cutPaper(allCut: Boolean) {
|
||||
try {
|
||||
PrinterManager.getFactory()?.let {
|
||||
|
|
@ -250,10 +251,6 @@ class AndroidInterface(private val activity: MainActivity) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JS 调用:打印自检页
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun printTestPage() {
|
||||
try {
|
||||
PrinterManager.getFactory()?.PrintTestPage()
|
||||
|
|
@ -262,10 +259,6 @@ class AndroidInterface(private val activity: MainActivity) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JS 调用:主动检查纸张状态 (会触发回调)
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun checkPaper() {
|
||||
try {
|
||||
PrinterManager.getFactory()?.Check_Paper()
|
||||
|
|
@ -274,11 +267,6 @@ class AndroidInterface(private val activity: MainActivity) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JS 调用:压缩并导出日志
|
||||
* 返回压缩后的文件路径
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun exportLogs(): String {
|
||||
val zipPath = LogManager.zipLogs(activity)
|
||||
if (zipPath != null) {
|
||||
|
|
@ -294,10 +282,6 @@ class AndroidInterface(private val activity: MainActivity) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JS 调用:直接上传日志到后台
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun uploadLogs() {
|
||||
LogManager.uploadLogs(activity) { success, message ->
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
|
|
@ -307,38 +291,15 @@ class AndroidInterface(private val activity: MainActivity) {
|
|||
}
|
||||
|
||||
/**
|
||||
* JS 调用:标准系统打印 (PDF/网络打印机/AirPrint)
|
||||
* 适用于打印精美的 A4 单据或页面
|
||||
* 系统打印(GeckoView 不支持 WebView.createPrintDocumentAdapter)
|
||||
* 如需此功能,需用 GeckoSession 生成 PDF 后打印
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun printPage() {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
try {
|
||||
val printManager = activity.getSystemService(Context.PRINT_SERVICE) as PrintManager
|
||||
// 获取 WebView 的打印适配器
|
||||
val webView = activity.getAgentWeb()?.webCreator?.getWebView()
|
||||
if (webView == null) {
|
||||
Toast.makeText(activity, "WebView 未就绪", Toast.LENGTH_SHORT).show()
|
||||
return@post
|
||||
}
|
||||
|
||||
val printAdapter = webView.createPrintDocumentAdapter("Document")
|
||||
val jobName = activity.getString(R.string.app_name) + " Print Job"
|
||||
|
||||
printManager.print(jobName, printAdapter, PrintAttributes.Builder().build())
|
||||
} catch (e: Exception) {
|
||||
Timber.tag("Print").e(e, "系统打印失败")
|
||||
Toast.makeText(activity, "打印服务启动失败", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
Toast.makeText(activity, "GeckoView 暂不支持系统打印,请使用热敏打印功能", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JS 调用:原始数据打印
|
||||
* 适用于热敏小票打印机 (蓝牙、USB、网络)
|
||||
* @param data String 可以是 JSON 字符串,包含打印的指令或文本
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun printRawData(data: String?) {
|
||||
if (data.isNullOrEmpty()) return
|
||||
try {
|
||||
|
|
@ -355,10 +316,6 @@ class AndroidInterface(private val activity: MainActivity) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JS 调用:在线升级 (手动触发检查)
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun startUpdate(apkUrl: String?) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
try {
|
||||
|
|
@ -369,10 +326,6 @@ class AndroidInterface(private val activity: MainActivity) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JS 调用:退出应用
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun exitApp() {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
try {
|
||||
|
|
@ -384,10 +337,6 @@ class AndroidInterface(private val activity: MainActivity) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取版本号给 H5 判断是否需要升级
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun getVersionName(): String {
|
||||
return try {
|
||||
val packageInfo = activity.packageManager.getPackageInfo(activity.packageName, 0)
|
||||
|
|
@ -397,10 +346,6 @@ class AndroidInterface(private val activity: MainActivity) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JS 调用:获取“关于”信息
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun getAboutInfo(): String {
|
||||
return try {
|
||||
val packageInfo = activity.packageManager.getPackageInfo(activity.packageName, 0)
|
||||
|
|
@ -410,11 +355,9 @@ class AndroidInterface(private val activity: MainActivity) {
|
|||
json.put("model", android.os.Build.MODEL)
|
||||
json.put("osVersion", android.os.Build.VERSION.RELEASE)
|
||||
|
||||
// 增加 CPU 架构信息
|
||||
val abis = android.os.Build.SUPPORTED_ABIS
|
||||
json.put("cpuAbi", abis.joinToString(","))
|
||||
|
||||
// 增加当前进程是否为 64 位的判断
|
||||
val is64Bit = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
|
||||
android.os.Process.is64Bit()
|
||||
} else {
|
||||
|
|
@ -428,17 +371,12 @@ class AndroidInterface(private val activity: MainActivity) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JS 调用:保存消息记录
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun saveMessage(message: String?) {
|
||||
if (message.isNullOrEmpty()) return
|
||||
try {
|
||||
val prefs = activity.getSharedPreferences("msg_records", Context.MODE_PRIVATE)
|
||||
val currentMsgs = prefs.getString("history", "") ?: ""
|
||||
val updatedMsgs = if (currentMsgs.isEmpty()) message else "$message|---|$currentMsgs"
|
||||
// 限制保存最近5000条
|
||||
val list = updatedMsgs.split("|---|").take(5000)
|
||||
prefs.edit().putString("history", list.joinToString("|---|")).apply()
|
||||
} catch (e: Exception) {
|
||||
|
|
@ -446,10 +384,6 @@ class AndroidInterface(private val activity: MainActivity) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JS 调用:获取消息记录
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun getMessages(): String {
|
||||
return try {
|
||||
val prefs = activity.getSharedPreferences("msg_records", Context.MODE_PRIVATE)
|
||||
|
|
@ -459,10 +393,6 @@ class AndroidInterface(private val activity: MainActivity) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JS 调用:清空消息记录
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun clearMessages() {
|
||||
try {
|
||||
activity.getSharedPreferences("msg_records", Context.MODE_PRIVATE).edit().clear().apply()
|
||||
|
|
@ -474,10 +404,6 @@ class AndroidInterface(private val activity: MainActivity) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JS 回调:告知 App 页面加载完成
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun onPageFinished() {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -3,52 +3,44 @@ package com.stand.standapp
|
|||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.ConsoleMessage
|
||||
import android.webkit.WebResourceError
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.widget.LinearLayout
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import org.mozilla.geckoview.GeckoResult
|
||||
import org.mozilla.geckoview.GeckoRuntime
|
||||
import org.mozilla.geckoview.GeckoSession
|
||||
import org.mozilla.geckoview.GeckoSessionSettings
|
||||
import org.mozilla.geckoview.GeckoView
|
||||
import org.mozilla.geckoview.WebRequestError
|
||||
import timber.log.Timber
|
||||
import com.just.agentweb.AgentWeb
|
||||
import com.just.agentweb.WebChromeClient
|
||||
import com.just.agentweb.WebViewClient
|
||||
import com.stand.standapp.R
|
||||
import com.stand.standapp.AppConfig
|
||||
import com.stand.standapp.AndroidInterface
|
||||
import com.stand.standapp.UpdateManager
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
private var instance: MainActivity? = null
|
||||
|
||||
// 待执行的 JS 代码(由 bridge.js 轮询获取)
|
||||
@Volatile
|
||||
private var pendingJsCode: String? = null
|
||||
|
||||
@JvmStatic
|
||||
fun executeJs(script: String) {
|
||||
instance?.let { activity ->
|
||||
activity.runOnUiThread {
|
||||
activity.mAgentWeb?.jsAccessEntrace?.quickCallJs(script)
|
||||
}
|
||||
synchronized(pendingJsCode ?: Any()) {
|
||||
// 合并多次调用(用换行分隔)
|
||||
pendingJsCode = if (pendingJsCode != null) "$pendingJsCode;$script" else script
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun onPttLoginSuccess() {
|
||||
Timber.tag("MainActivity").d("onPttLoginSuccess called from Native")
|
||||
executeJs("onPttLoginSuccess")
|
||||
executeJs("onPttLoginSuccess()")
|
||||
}
|
||||
|
||||
// 硬件按键触发
|
||||
@JvmStatic
|
||||
fun onHardwarePttDown() {
|
||||
instance?.runOnUiThread {
|
||||
// 直接调用 JSBridge 对象的逻辑以复用保护机制
|
||||
val jsInterface = instance?.mAgentWeb?.webCreator?.webView?.tag as? AndroidInterface
|
||||
// 或者更直接一点,我们把逻辑抽离出来
|
||||
instance?.pttInterface?.pttSpeakPlay()
|
||||
// 通知 H5 更新 UI
|
||||
executeJs("onHardwarePttDown")
|
||||
executeJs("onHardwarePttDown()")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +48,7 @@ class MainActivity : AppCompatActivity() {
|
|||
fun onHardwarePttUp() {
|
||||
instance?.runOnUiThread {
|
||||
instance?.pttInterface?.pttSpeakRelease()
|
||||
executeJs("onHardwarePttUp")
|
||||
executeJs("onHardwarePttUp()")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -83,10 +75,23 @@ class MainActivity : AppCompatActivity() {
|
|||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取并清除待执行的 JS 代码(由 bridge.js 轮询)
|
||||
@JvmStatic
|
||||
fun pollPendingJs(): String {
|
||||
synchronized(pendingJsCode ?: Any()) {
|
||||
val code = pendingJsCode ?: ""
|
||||
pendingJsCode = null
|
||||
return code
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var mAgentWeb: AgentWeb? = null
|
||||
private var geckoSession: GeckoSession? = null
|
||||
private var geckoRuntime: GeckoRuntime? = null
|
||||
private var pttInterface: AndroidInterface? = null
|
||||
private var canGoBack = false
|
||||
private lateinit var geckoView: GeckoView
|
||||
|
||||
private val mHandler = android.os.Handler(android.os.Looper.getMainLooper())
|
||||
private val TIMEOUT_MS = 30000L
|
||||
|
|
@ -99,64 +104,33 @@ class MainActivity : AppCompatActivity() {
|
|||
super.onCreate(savedInstanceState)
|
||||
instance = this
|
||||
try {
|
||||
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
val container = findViewById<LinearLayout>(R.id.container)
|
||||
if (container == null) return
|
||||
geckoView = findViewById<GeckoView>(R.id.geckoView)
|
||||
?: return
|
||||
|
||||
findViewById<android.widget.Button>(R.id.btn_retry)?.setOnClickListener {
|
||||
hideErrorPage()
|
||||
mAgentWeb?.webCreator?.webView?.reload()
|
||||
geckoSession?.reload()
|
||||
startTimeoutTimer()
|
||||
}
|
||||
|
||||
// 从配置中获取服务器地址
|
||||
val serverUrl = AppConfig.getServerUrl(this)
|
||||
val finalUrl = "$serverUrl/example/print_demo.html?t=${System.currentTimeMillis()}"
|
||||
Timber.tag("MainActivity").d("Loading URL: $finalUrl")
|
||||
// 创建 GeckoRuntime
|
||||
geckoRuntime = createGeckoRuntime()
|
||||
|
||||
mAgentWeb = AgentWeb.with(this)
|
||||
.setAgentWebParent(container, LinearLayout.LayoutParams(-1, -1))
|
||||
.useDefaultIndicator()
|
||||
.setWebChromeClient(object : WebChromeClient() {
|
||||
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
|
||||
Timber.tag("WebConsole").d("${consoleMessage?.messageLevel()}: ${consoleMessage?.message()} -- From line ${consoleMessage?.lineNumber()} of ${consoleMessage?.sourceId()}")
|
||||
return super.onConsoleMessage(consoleMessage)
|
||||
}
|
||||
})
|
||||
.setWebViewClient(object : WebViewClient() {
|
||||
// ...
|
||||
})
|
||||
.addJavascriptInterface("android", AndroidInterface(this).also { pttInterface = it })
|
||||
.createAgentWeb()
|
||||
// 安装桥接扩展
|
||||
geckoRuntime?.webExtensionController?.installBuiltIn(
|
||||
"resource://android/assets/extensions/bridge/"
|
||||
)
|
||||
|
||||
.ready()
|
||||
.go(finalUrl)
|
||||
// 创建 AndroidInterface
|
||||
pttInterface = AndroidInterface(this)
|
||||
|
||||
// 创建 GeckoSession 并配置
|
||||
createSessionAndLoad()
|
||||
|
||||
startTimeoutTimer()
|
||||
|
||||
// 配置 WebView 属性以支持 SSE 和跨域
|
||||
|
||||
mAgentWeb?.let { agent ->
|
||||
try {
|
||||
val webView = agent.webCreator.webView
|
||||
val settings = webView.settings
|
||||
// 允许混合内容 (HTTP/HTTPS)
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
|
||||
settings.mixedContentMode = android.webkit.WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
|
||||
}
|
||||
// 允许跨域和存储
|
||||
settings.allowUniversalAccessFromFileURLs = true
|
||||
settings.allowFileAccessFromFileURLs = true
|
||||
settings.domStorageEnabled = true
|
||||
settings.javaScriptEnabled = true
|
||||
} catch (e: Exception) {
|
||||
Timber.tag("MainActivity").e(e, "WebView settings error")
|
||||
}
|
||||
}
|
||||
// .go("http://152.32.186.199/local/print_demo.html")
|
||||
|
||||
// 启动时检查更新
|
||||
UpdateManager(this).checkUpdate()
|
||||
|
||||
|
|
@ -165,13 +139,125 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
fun getAgentWeb(): AgentWeb? {
|
||||
return mAgentWeb
|
||||
private fun createGeckoRuntime(): GeckoRuntime {
|
||||
val runtime = GeckoRuntime.getDefault(this)
|
||||
runtime.settings.aboutConfigEnabled = true
|
||||
runtime.settings.consoleOutputEnabled = true
|
||||
return runtime
|
||||
}
|
||||
|
||||
private fun createSessionAndLoad() {
|
||||
val session = GeckoSession()
|
||||
val sessionSettings = session.settings
|
||||
|
||||
// 配置 GeckoSession 设置(对应原 WebView settings)
|
||||
sessionSettings.allowJavascript = true
|
||||
sessionSettings.userAgentMode = GeckoSessionSettings.USER_AGENT_MODE_MOBILE
|
||||
|
||||
val runtime = geckoRuntime ?: return
|
||||
|
||||
// 设置各种代理
|
||||
setupDelegates(session)
|
||||
|
||||
// 打开会话
|
||||
session.open(runtime)
|
||||
|
||||
geckoSession = session
|
||||
|
||||
// 将 session 绑定到 GeckoView
|
||||
geckoView.setSession(session)
|
||||
|
||||
// 加载页面
|
||||
val serverUrl = AppConfig.getServerUrl(this)
|
||||
val finalUrl = "$serverUrl/example/print_demo.html?t=${System.currentTimeMillis()}"
|
||||
Timber.tag("MainActivity").d("Loading URL: $finalUrl")
|
||||
session.loadUri(finalUrl)
|
||||
}
|
||||
|
||||
private fun setupDelegates(session: GeckoSession) {
|
||||
// NavigationDelegate - 处理页面加载错误
|
||||
session.navigationDelegate = object : GeckoSession.NavigationDelegate {
|
||||
override fun onLoadError(
|
||||
session: GeckoSession,
|
||||
uri: String?,
|
||||
error: WebRequestError
|
||||
): GeckoResult<String>? {
|
||||
Timber.tag("MainActivity").e("Page load error: ${error.code} - ${error.category}")
|
||||
runOnUiThread { showErrorPage() }
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) {
|
||||
this@MainActivity.canGoBack = canGoBack
|
||||
}
|
||||
|
||||
override fun onCanGoForward(session: GeckoSession, canGoForward: Boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
// ProgressDelegate - 页面加载进度
|
||||
session.progressDelegate = object : GeckoSession.ProgressDelegate {
|
||||
override fun onPageStart(session: GeckoSession, url: String) {
|
||||
Timber.tag("MainActivity").d("Page started: $url")
|
||||
}
|
||||
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
||||
Timber.tag("MainActivity").d("Page stopped, success: $success")
|
||||
}
|
||||
}
|
||||
|
||||
// PromptDelegate - 处理 JS bridge 和 console 消息
|
||||
session.promptDelegate = object : GeckoSession.PromptDelegate {
|
||||
override fun onTextPrompt(
|
||||
session: GeckoSession,
|
||||
prompt: GeckoSession.PromptDelegate.TextPrompt
|
||||
): GeckoResult<GeckoSession.PromptDelegate.PromptResponse>? {
|
||||
val message = prompt.message ?: ""
|
||||
|
||||
if (message.startsWith("bridge:")) {
|
||||
// JS bridge 消息
|
||||
val payload = message.substring(7)
|
||||
if (payload == "_poll") {
|
||||
// 轮询待执行的 JS 代码
|
||||
val code = pollPendingJs()
|
||||
return GeckoResult.fromValue(prompt.confirm(code))
|
||||
}
|
||||
val result = handleBridgeCall(payload, prompt.defaultValue ?: "")
|
||||
return GeckoResult.fromValue(prompt.confirm(result))
|
||||
}
|
||||
// 默认处理(显示对话框)
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onAlertPrompt(
|
||||
session: GeckoSession,
|
||||
prompt: GeckoSession.PromptDelegate.AlertPrompt
|
||||
): GeckoResult<GeckoSession.PromptDelegate.PromptResponse>? {
|
||||
Timber.tag("WebConsole").d("Alert: ${prompt.message}")
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBridgeCall(method: String, argsJson: String): String {
|
||||
return try {
|
||||
pttInterface?.dispatch(method, argsJson) ?: ""
|
||||
} catch (e: Exception) {
|
||||
Timber.tag("Bridge").e(e, "Bridge call failed: $method")
|
||||
"{\"error\":\"${e.message}\"}"
|
||||
}
|
||||
}
|
||||
|
||||
fun getGeckoSession(): GeckoSession? {
|
||||
return geckoSession
|
||||
}
|
||||
|
||||
fun getAndroidInterface(): AndroidInterface? {
|
||||
return pttInterface
|
||||
}
|
||||
|
||||
fun showExitDialog() {
|
||||
// 调用 JS 函数
|
||||
mAgentWeb?.jsAccessEntrace?.quickCallJs("addExitText")
|
||||
executeJs("addExitText()")
|
||||
|
||||
androidx.appcompat.app.AlertDialog.Builder(this)
|
||||
.setTitle("提示")
|
||||
|
|
@ -205,29 +291,24 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
if (mAgentWeb?.handleKeyEvent(keyCode, event) == true) {
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
val session = geckoSession
|
||||
if (canGoBack && session != null) {
|
||||
session.goBack()
|
||||
return true
|
||||
}
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
showExitDialog()
|
||||
return true
|
||||
}
|
||||
return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
mAgentWeb?.webLifeCycle?.onPause()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
mAgentWeb?.webLifeCycle?.onResume()
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
mAgentWeb?.webLifeCycle?.onDestroy()
|
||||
cancelTimeoutTimer()
|
||||
geckoView.releaseSession()
|
||||
geckoSession?.close()
|
||||
geckoSession = null
|
||||
instance = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,10 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/container"
|
||||
<org.mozilla.geckoview.GeckoView
|
||||
android:id="@+id/geckoView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical" />
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<!-- 加载等待页 -->
|
||||
<LinearLayout
|
||||
|
|
|
|||
|
|
@ -3,3 +3,14 @@ plugins {
|
|||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.kotlin.compose) apply false
|
||||
}
|
||||
|
||||
// 处理 GeckoView Glean 依赖冲突:强制使用统一版本
|
||||
subprojects {
|
||||
project.configurations.all {
|
||||
resolutionStrategy.eachDependency {
|
||||
if (requested.group == "org.mozilla.telemetry" && requested.name.contains("glean")) {
|
||||
useVersion("52.7.0")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ dependencyResolutionManagement {
|
|||
maven { url = uri("https://maven.aliyun.com/repository/public") }
|
||||
// 第三方开源库必备源
|
||||
maven { url = uri("https://jitpack.io") }
|
||||
// GeckoView (Mozilla)
|
||||
maven { url = uri("https://maven.mozilla.org/maven2/") }
|
||||
// 兜底源
|
||||
google()
|
||||
mavenCentral()
|
||||
|
|
|
|||
Loading…
Reference in New Issue