This commit is contained in:
844143714@qq,com 2026-04-29 10:21:38 +08:00
parent d52dee56be
commit 786f077bd6
10 changed files with 334 additions and 272 deletions

View File

@ -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")

View File

@ -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

View File

@ -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();
}
})();

View File

@ -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
}]
}

View File

@ -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);
}
});

View File

@ -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 {

View File

@ -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()
}

View File

@ -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

View File

@ -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")
}
}
}
}

View File

@ -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()