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 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17
} }
packaging {
jniLibs {
useLegacyPackaging = true
}
}
buildFeatures { buildFeatures {
compose = true compose = true
} }
@ -59,8 +64,7 @@ dependencies {
implementation("com.jakewharton.timber:timber:5.0.1") implementation("com.jakewharton.timber:timber:5.0.1")
implementation("androidx.cardview:cardview:1.0.0") implementation("androidx.cardview:cardview:1.0.0")
implementation("com.iqiyi.xcrash:xcrash-android-lib:3.1.0") implementation("com.iqiyi.xcrash:xcrash-android-lib:3.1.0")
implementation("io.github.justson:agentweb-core:v5.1.1-androidx") implementation("org.mozilla.geckoview:geckoview-omni:115.0.20230710165010")
implementation("com.github.Justson:Downloader:v5.0.4-androidx")
implementation("com.google.android.material:material:1.12.0") implementation("com.google.android.material:material:1.12.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0") implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.github.getActivity:XXPermissions:18.2") implementation("com.github.getActivity:XXPermissions:18.2")

View File

@ -1,22 +1,14 @@
# AgentWeb 混淆规则 # GeckoView 混淆规则
-keep class com.just.agentweb.** { *; } -dontwarn org.mozilla.geckoview.**
-dontwarn com.just.agentweb.** -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.** -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 混淆规则 # OkHttp 混淆规则
-keepattributes Signature -keepattributes Signature
-keepattributes InnerClasses -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 @Override
public void onClick(View v) { public void onClick(View v) {
// SendAtUtil.toLogin("gzzdweb01", "123456", "61.243.1.123", 1, 1); // 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.content.Context
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.webkit.JavascriptInterface
import android.widget.Toast import android.widget.Toast
import android.print.PrintAttributes
import android.print.PrintManager
import com.stand.standapp.R import com.stand.standapp.R
import com.stand.standapp.utils.LogManager import com.stand.standapp.utils.LogManager
import com.stand.standapp.printer.PrinterManager import com.stand.standapp.printer.PrinterManager
import timber.log.Timber import timber.log.Timber
import com.example.kingway.ptt.SendAtUtil import com.example.kingway.ptt.SendAtUtil
import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
class AndroidInterface(private val activity: MainActivity) { class AndroidInterface(private val activity: MainActivity) {
private var isSpeaking = false private var isSpeaking = false
private var lastSpeakTime = 0L 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() { fun pttSpeakPlay() {
if (!isSpeaking) { if (!isSpeaking) {
isSpeaking = true isSpeaking = true
@ -34,34 +105,20 @@ class AndroidInterface(private val activity: MainActivity) {
} }
} }
/**
* 获取 PTT 登录状态: "00"-未登录 "01"-登录中 "02"-已登录
*/
@JavascriptInterface
fun pttGetLoginStatus(): String { fun pttGetLoginStatus(): String {
return com.example.kingway.ptt.CallBackResolution.getPttLoginStatus() return com.example.kingway.ptt.CallBackResolution.getPttLoginStatus()
} }
/**
* 获取 PTT 登录 ID
*/
@JavascriptInterface
fun pttGetLoginId(): String { fun pttGetLoginId(): String {
return com.example.kingway.ptt.CallBackResolution.getPttLoginId() return com.example.kingway.ptt.CallBackResolution.getPttLoginId()
} }
/**
* PTT 登录
*/
@JavascriptInterface
fun pttSpeakRelease() { fun pttSpeakRelease() {
if (isSpeaking) { if (isSpeaking) {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
val duration = now - lastSpeakTime val duration = now - lastSpeakTime
if (duration < MIN_SPEAK_DURATION) { if (duration < MIN_SPEAK_DURATION) {
// 如果按得太快,延迟释放
val delay = MIN_SPEAK_DURATION - duration val delay = MIN_SPEAK_DURATION - duration
Timber.tag("PTT").d("Speak duration too short ($duration ms), delaying release by $delay ms") Timber.tag("PTT").d("Speak duration too short ($duration ms), delaying release by $delay ms")
Handler(Looper.getMainLooper()).postDelayed({ Handler(Looper.getMainLooper()).postDelayed({
@ -82,62 +139,34 @@ class AndroidInterface(private val activity: MainActivity) {
} }
} }
/**
* 强制释放 PTT (用于异常恢复)
*/
@JavascriptInterface
fun pttForceRelease() { fun pttForceRelease() {
isSpeaking = false isSpeaking = false
SendAtUtil.speakRelease() SendAtUtil.speakRelease()
Timber.tag("PTT").d("Force Release") Timber.tag("PTT").d("Force Release")
} }
/**
* PTT 获取群组
*/
@JavascriptInterface
fun pttGetGroupInfo() { fun pttGetGroupInfo() {
SendAtUtil.getGroupInfo() SendAtUtil.getGroupInfo()
} }
/**
* PTT 获取成员
*/
@JavascriptInterface
fun pttGetGroupMemberInfo(gid: String) { fun pttGetGroupMemberInfo(gid: String) {
SendAtUtil.getGroupMemberInfo(gid) SendAtUtil.getGroupMemberInfo(gid)
} }
/**
* 模拟 PTT 账号冲突 (测试用)
*/
@JavascriptInterface
fun simulatePttConflict() { fun simulatePttConflict() {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
MainActivity.showPttConflictDialog() MainActivity.showPttConflictDialog()
} }
} }
/**
* 发送原始 AT 指令 (调试用)
*/
@JavascriptInterface
fun pttSendAtCmd(cmd: String) { fun pttSendAtCmd(cmd: String) {
SendAtUtil.sendRawCmd(cmd) SendAtUtil.sendRawCmd(cmd)
} }
/**
* PTT 切换群组
*/
@JavascriptInterface
fun pttJumpGroup(gid: String) { fun pttJumpGroup(gid: String) {
SendAtUtil.jumpGroup(gid) SendAtUtil.jumpGroup(gid)
} }
/**
* JS 调用获取打印机当前连接状态
*/
@JavascriptInterface
fun getPrinterStatus(): String { fun getPrinterStatus(): String {
return try { return try {
val json = JSONObject() 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) { fun printText(text: String?, size: Int, align: Int, space: Int) {
if (text.isNullOrEmpty()) return if (text.isNullOrEmpty()) return
try { 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) { fun printBarcode(data: String?, size: Int, height: Int, align: Int, text_pos: Int, type: Int) {
if (data.isNullOrEmpty()) return if (data.isNullOrEmpty()) return
try { try {
@ -181,10 +202,6 @@ class AndroidInterface(private val activity: MainActivity) {
} }
} }
/**
* JS 调用打印二维码
*/
@JavascriptInterface
fun printQR(data: String?, size: Int, l_m: Int) { fun printQR(data: String?, size: Int, l_m: Int) {
if (data.isNullOrEmpty()) return if (data.isNullOrEmpty()) return
try { try {
@ -194,10 +211,6 @@ class AndroidInterface(private val activity: MainActivity) {
} }
} }
/**
* JS 调用打印图片 ( Assets 获取)
*/
@JavascriptInterface
fun printImage(fileName: String?, width: Int, align: Int, mode: Int) { fun printImage(fileName: String?, width: Int, align: Int, mode: Int) {
if (fileName.isNullOrEmpty()) return if (fileName.isNullOrEmpty()) return
try { 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) { fun printColumns(c1: String?, p1: Int, c2: String?, p2: Int, c3: String?, p3: Int, c4: String?, p4: Int) {
try { try {
PrinterManager.getFactory()?.Printcolumncontent(c1 ?: "", p1, c2 ?: "", p2, c3 ?: "", p3, c4 ?: "", p4) 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) { fun printColumnsWithHeight(c1: String?, p1: Int, c2: String?, p2: Int, c3: String?, p3: Int, c4: String?, p4: Int, height: Int) {
try { try {
PrinterManager.getFactory()?.Printcolumncontent(c1 ?: "", p1, c2 ?: "", p2, c3 ?: "", p3, c4 ?: "", p4, height) 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) { fun cutPaper(allCut: Boolean) {
try { try {
PrinterManager.getFactory()?.let { PrinterManager.getFactory()?.let {
@ -250,10 +251,6 @@ class AndroidInterface(private val activity: MainActivity) {
} }
} }
/**
* JS 调用打印自检页
*/
@JavascriptInterface
fun printTestPage() { fun printTestPage() {
try { try {
PrinterManager.getFactory()?.PrintTestPage() PrinterManager.getFactory()?.PrintTestPage()
@ -262,10 +259,6 @@ class AndroidInterface(private val activity: MainActivity) {
} }
} }
/**
* JS 调用主动检查纸张状态 (会触发回调)
*/
@JavascriptInterface
fun checkPaper() { fun checkPaper() {
try { try {
PrinterManager.getFactory()?.Check_Paper() PrinterManager.getFactory()?.Check_Paper()
@ -274,11 +267,6 @@ class AndroidInterface(private val activity: MainActivity) {
} }
} }
/**
* JS 调用压缩并导出日志
* 返回压缩后的文件路径
*/
@JavascriptInterface
fun exportLogs(): String { fun exportLogs(): String {
val zipPath = LogManager.zipLogs(activity) val zipPath = LogManager.zipLogs(activity)
if (zipPath != null) { if (zipPath != null) {
@ -294,10 +282,6 @@ class AndroidInterface(private val activity: MainActivity) {
} }
} }
/**
* JS 调用直接上传日志到后台
*/
@JavascriptInterface
fun uploadLogs() { fun uploadLogs() {
LogManager.uploadLogs(activity) { success, message -> LogManager.uploadLogs(activity) { success, message ->
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
@ -307,38 +291,15 @@ class AndroidInterface(private val activity: MainActivity) {
} }
/** /**
* JS 调用标准系统打印 (PDF/网络打印机/AirPrint) * 系统打印GeckoView 不支持 WebView.createPrintDocumentAdapter
* 适用于打印精美的 A4 单据或页面 * 如需此功能需用 GeckoSession 生成 PDF 后打印
*/ */
@JavascriptInterface
fun printPage() { fun printPage() {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
try { Toast.makeText(activity, "GeckoView 暂不支持系统打印,请使用热敏打印功能", Toast.LENGTH_SHORT).show()
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()
}
} }
} }
/**
* JS 调用原始数据打印
* 适用于热敏小票打印机 (蓝牙USB网络)
* @param data String 可以是 JSON 字符串包含打印的指令或文本
*/
@JavascriptInterface
fun printRawData(data: String?) { fun printRawData(data: String?) {
if (data.isNullOrEmpty()) return if (data.isNullOrEmpty()) return
try { try {
@ -355,10 +316,6 @@ class AndroidInterface(private val activity: MainActivity) {
} }
} }
/**
* JS 调用在线升级 (手动触发检查)
*/
@JavascriptInterface
fun startUpdate(apkUrl: String?) { fun startUpdate(apkUrl: String?) {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
try { try {
@ -369,10 +326,6 @@ class AndroidInterface(private val activity: MainActivity) {
} }
} }
/**
* JS 调用退出应用
*/
@JavascriptInterface
fun exitApp() { fun exitApp() {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
try { try {
@ -384,10 +337,6 @@ class AndroidInterface(private val activity: MainActivity) {
} }
} }
/**
* 获取版本号给 H5 判断是否需要升级
*/
@JavascriptInterface
fun getVersionName(): String { fun getVersionName(): String {
return try { return try {
val packageInfo = activity.packageManager.getPackageInfo(activity.packageName, 0) val packageInfo = activity.packageManager.getPackageInfo(activity.packageName, 0)
@ -397,10 +346,6 @@ class AndroidInterface(private val activity: MainActivity) {
} }
} }
/**
* JS 调用获取关于信息
*/
@JavascriptInterface
fun getAboutInfo(): String { fun getAboutInfo(): String {
return try { return try {
val packageInfo = activity.packageManager.getPackageInfo(activity.packageName, 0) 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("model", android.os.Build.MODEL)
json.put("osVersion", android.os.Build.VERSION.RELEASE) json.put("osVersion", android.os.Build.VERSION.RELEASE)
// 增加 CPU 架构信息
val abis = android.os.Build.SUPPORTED_ABIS val abis = android.os.Build.SUPPORTED_ABIS
json.put("cpuAbi", abis.joinToString(",")) json.put("cpuAbi", abis.joinToString(","))
// 增加当前进程是否为 64 位的判断
val is64Bit = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { val is64Bit = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
android.os.Process.is64Bit() android.os.Process.is64Bit()
} else { } else {
@ -428,17 +371,12 @@ class AndroidInterface(private val activity: MainActivity) {
} }
} }
/**
* JS 调用保存消息记录
*/
@JavascriptInterface
fun saveMessage(message: String?) { fun saveMessage(message: String?) {
if (message.isNullOrEmpty()) return if (message.isNullOrEmpty()) return
try { try {
val prefs = activity.getSharedPreferences("msg_records", Context.MODE_PRIVATE) val prefs = activity.getSharedPreferences("msg_records", Context.MODE_PRIVATE)
val currentMsgs = prefs.getString("history", "") ?: "" val currentMsgs = prefs.getString("history", "") ?: ""
val updatedMsgs = if (currentMsgs.isEmpty()) message else "$message|---|$currentMsgs" val updatedMsgs = if (currentMsgs.isEmpty()) message else "$message|---|$currentMsgs"
// 限制保存最近5000条
val list = updatedMsgs.split("|---|").take(5000) val list = updatedMsgs.split("|---|").take(5000)
prefs.edit().putString("history", list.joinToString("|---|")).apply() prefs.edit().putString("history", list.joinToString("|---|")).apply()
} catch (e: Exception) { } catch (e: Exception) {
@ -446,10 +384,6 @@ class AndroidInterface(private val activity: MainActivity) {
} }
} }
/**
* JS 调用获取消息记录
*/
@JavascriptInterface
fun getMessages(): String { fun getMessages(): String {
return try { return try {
val prefs = activity.getSharedPreferences("msg_records", Context.MODE_PRIVATE) val prefs = activity.getSharedPreferences("msg_records", Context.MODE_PRIVATE)
@ -459,10 +393,6 @@ class AndroidInterface(private val activity: MainActivity) {
} }
} }
/**
* JS 调用清空消息记录
*/
@JavascriptInterface
fun clearMessages() { fun clearMessages() {
try { try {
activity.getSharedPreferences("msg_records", Context.MODE_PRIVATE).edit().clear().apply() 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() { fun onPageFinished() {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
try { try {

View File

@ -3,52 +3,44 @@ package com.stand.standapp
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.KeyEvent 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 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 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() { class MainActivity : AppCompatActivity() {
companion object { companion object {
private var instance: MainActivity? = null private var instance: MainActivity? = null
// 待执行的 JS 代码(由 bridge.js 轮询获取)
@Volatile
private var pendingJsCode: String? = null
@JvmStatic @JvmStatic
fun executeJs(script: String) { fun executeJs(script: String) {
instance?.let { activity -> synchronized(pendingJsCode ?: Any()) {
activity.runOnUiThread { // 合并多次调用(用换行分隔)
activity.mAgentWeb?.jsAccessEntrace?.quickCallJs(script) pendingJsCode = if (pendingJsCode != null) "$pendingJsCode;$script" else script
}
} }
} }
@JvmStatic @JvmStatic
fun onPttLoginSuccess() { fun onPttLoginSuccess() {
Timber.tag("MainActivity").d("onPttLoginSuccess called from Native") Timber.tag("MainActivity").d("onPttLoginSuccess called from Native")
executeJs("onPttLoginSuccess") executeJs("onPttLoginSuccess()")
} }
// 硬件按键触发 // 硬件按键触发
@JvmStatic @JvmStatic
fun onHardwarePttDown() { fun onHardwarePttDown() {
instance?.runOnUiThread { instance?.runOnUiThread {
// 直接调用 JSBridge 对象的逻辑以复用保护机制
val jsInterface = instance?.mAgentWeb?.webCreator?.webView?.tag as? AndroidInterface
// 或者更直接一点,我们把逻辑抽离出来
instance?.pttInterface?.pttSpeakPlay() instance?.pttInterface?.pttSpeakPlay()
// 通知 H5 更新 UI executeJs("onHardwarePttDown()")
executeJs("onHardwarePttDown")
} }
} }
@ -56,7 +48,7 @@ class MainActivity : AppCompatActivity() {
fun onHardwarePttUp() { fun onHardwarePttUp() {
instance?.runOnUiThread { instance?.runOnUiThread {
instance?.pttInterface?.pttSpeakRelease() instance?.pttInterface?.pttSpeakRelease()
executeJs("onHardwarePttUp") executeJs("onHardwarePttUp()")
} }
} }
@ -83,10 +75,23 @@ class MainActivity : AppCompatActivity() {
.show() .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 pttInterface: AndroidInterface? = null
private var canGoBack = false
private lateinit var geckoView: GeckoView
private val mHandler = android.os.Handler(android.os.Looper.getMainLooper()) private val mHandler = android.os.Handler(android.os.Looper.getMainLooper())
private val TIMEOUT_MS = 30000L private val TIMEOUT_MS = 30000L
@ -99,64 +104,33 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
instance = this instance = this
try { try {
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
val container = findViewById<LinearLayout>(R.id.container) geckoView = findViewById<GeckoView>(R.id.geckoView)
if (container == null) return ?: return
findViewById<android.widget.Button>(R.id.btn_retry)?.setOnClickListener { findViewById<android.widget.Button>(R.id.btn_retry)?.setOnClickListener {
hideErrorPage() hideErrorPage()
mAgentWeb?.webCreator?.webView?.reload() geckoSession?.reload()
startTimeoutTimer() startTimeoutTimer()
} }
// 从配置中获取服务器地址 // 创建 GeckoRuntime
val serverUrl = AppConfig.getServerUrl(this) geckoRuntime = createGeckoRuntime()
val finalUrl = "$serverUrl/example/print_demo.html?t=${System.currentTimeMillis()}"
Timber.tag("MainActivity").d("Loading URL: $finalUrl")
mAgentWeb = AgentWeb.with(this) // 安装桥接扩展
.setAgentWebParent(container, LinearLayout.LayoutParams(-1, -1)) geckoRuntime?.webExtensionController?.installBuiltIn(
.useDefaultIndicator() "resource://android/assets/extensions/bridge/"
.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()
.ready() // 创建 AndroidInterface
.go(finalUrl) pttInterface = AndroidInterface(this)
// 创建 GeckoSession 并配置
createSessionAndLoad()
startTimeoutTimer() 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() UpdateManager(this).checkUpdate()
@ -165,13 +139,125 @@ class MainActivity : AppCompatActivity() {
} }
} }
fun getAgentWeb(): AgentWeb? { private fun createGeckoRuntime(): GeckoRuntime {
return mAgentWeb 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() { fun showExitDialog() {
// 调用 JS 函数 executeJs("addExitText()")
mAgentWeb?.jsAccessEntrace?.quickCallJs("addExitText")
androidx.appcompat.app.AlertDialog.Builder(this) androidx.appcompat.app.AlertDialog.Builder(this)
.setTitle("提示") .setTitle("提示")
@ -205,29 +291,24 @@ class MainActivity : AppCompatActivity() {
} }
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (mAgentWeb?.handleKeyEvent(keyCode, event) == true) {
return true
}
if (keyCode == KeyEvent.KEYCODE_BACK) { if (keyCode == KeyEvent.KEYCODE_BACK) {
val session = geckoSession
if (canGoBack && session != null) {
session.goBack()
return true
}
showExitDialog() showExitDialog()
return true return true
} }
return super.onKeyDown(keyCode, event) return super.onKeyDown(keyCode, event)
} }
override fun onPause() {
mAgentWeb?.webLifeCycle?.onPause()
super.onPause()
}
override fun onResume() {
mAgentWeb?.webLifeCycle?.onResume()
super.onResume()
}
override fun onDestroy() { override fun onDestroy() {
mAgentWeb?.webLifeCycle?.onDestroy()
cancelTimeoutTimer() cancelTimeoutTimer()
geckoView.releaseSession()
geckoSession?.close()
geckoSession = null
instance = null
super.onDestroy() super.onDestroy()
} }

View File

@ -3,11 +3,10 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<LinearLayout <org.mozilla.geckoview.GeckoView
android:id="@+id/container" android:id="@+id/geckoView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent" />
android:orientation="vertical" />
<!-- 加载等待页 --> <!-- 加载等待页 -->
<LinearLayout <LinearLayout

View File

@ -3,3 +3,14 @@ plugins {
alias(libs.plugins.android.application) apply false alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.compose) 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://maven.aliyun.com/repository/public") }
// 第三方开源库必备源 // 第三方开源库必备源
maven { url = uri("https://jitpack.io") } maven { url = uri("https://jitpack.io") }
// GeckoView (Mozilla)
maven { url = uri("https://maven.mozilla.org/maven2/") }
// 兜底源 // 兜底源
google() google()
mavenCentral() mavenCentral()