diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8e3e137..655e1ac 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 6f74ca9..b691c76 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -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 ; -} - -# 保护你自己的 JS 交互类 --keep class com.stand.standapp.AndroidInterface { *; } - # OkHttp 混淆规则 -keepattributes Signature -keepattributes InnerClasses diff --git a/app/src/main/assets/extensions/bridge/bridge.js b/app/src/main/assets/extensions/bridge/bridge.js new file mode 100644 index 0000000..696f4cb --- /dev/null +++ b/app/src/main/assets/extensions/bridge/bridge.js @@ -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(); + } +})(); diff --git a/app/src/main/assets/extensions/bridge/manifest.json b/app/src/main/assets/extensions/bridge/manifest.json new file mode 100644 index 0000000..26bf027 --- /dev/null +++ b/app/src/main/assets/extensions/bridge/manifest.json @@ -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 + }] +} diff --git a/app/src/main/java/com/example/kingway/ptt/pttActivity.java b/app/src/main/java/com/example/kingway/ptt/pttActivity.java index 0256d02..47b29fb 100644 --- a/app/src/main/java/com/example/kingway/ptt/pttActivity.java +++ b/app/src/main/java/com/example/kingway/ptt/pttActivity.java @@ -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); } }); diff --git a/app/src/main/java/com/stand/standapp/AndroidInterface.kt b/app/src/main/java/com/stand/standapp/AndroidInterface.kt index a97b239..93a980a 100644 --- a/app/src/main/java/com/stand/standapp/AndroidInterface.kt +++ b/app/src/main/java/com/stand/standapp/AndroidInterface.kt @@ -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) @@ -409,36 +354,29 @@ class AndroidInterface(private val activity: MainActivity) { json.put("versionName", packageInfo.versionName) 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 { abis.any { it.contains("64") } } json.put("is64Bit", is64Bit) - + json.toString() } catch (e: Exception) { "{ \"appName\": \"消防值守台\", \"versionName\": \"1.0.0\" }" } } - /** - * 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 { diff --git a/app/src/main/java/com/stand/standapp/MainActivity.kt b/app/src/main/java/com/stand/standapp/MainActivity.kt index 20943ed..77d7215 100644 --- a/app/src/main/java/com/stand/standapp/MainActivity.kt +++ b/app/src/main/java/com/stand/standapp/MainActivity.kt @@ -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(R.id.container) - if (container == null) return + geckoView = findViewById(R.id.geckoView) + ?: return findViewById(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,14 +139,126 @@ 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? { + 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? { + 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? { + 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("提示") .setMessage("确定要退出应用吗?") @@ -205,29 +291,24 @@ class MainActivity : AppCompatActivity() { } override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { - if (mAgentWeb?.handleKeyEvent(keyCode, event) == true) { - return true - } if (keyCode == KeyEvent.KEYCODE_BACK) { + val session = geckoSession + if (canGoBack && session != null) { + session.goBack() + return true + } 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() } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 9df1cb5..d841402 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -3,11 +3,10 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + android:layout_height="match_parent" />