package com.stand.standapp import android.content.Intent import android.os.Bundle import android.view.KeyEvent 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 class MainActivity : AppCompatActivity() { companion object { private var instance: MainActivity? = null // 待执行的 JS 代码(由 bridge.js 轮询获取) @Volatile private var pendingJsCode: String? = null @JvmStatic fun executeJs(script: String) { synchronized(pendingJsCode ?: Any()) { // 合并多次调用(用换行分隔) pendingJsCode = if (pendingJsCode != null) "$pendingJsCode;$script" else script } } @JvmStatic fun onPttLoginSuccess() { Timber.tag("MainActivity").d("onPttLoginSuccess called from Native") executeJs("onPttLoginSuccess()") } // 硬件按键触发 @JvmStatic fun onHardwarePttDown() { instance?.runOnUiThread { instance?.pttInterface?.pttSpeakPlay() executeJs("onHardwarePttDown()") } } @JvmStatic fun onHardwarePttUp() { instance?.runOnUiThread { instance?.pttInterface?.pttSpeakRelease() executeJs("onHardwarePttUp()") } } @JvmStatic fun showPttConflictDialog() { instance?.runOnUiThread { val ctx = instance ?: return@runOnUiThread androidx.appcompat.app.AlertDialog.Builder(ctx) .setTitle("账号冲突") .setMessage("您的 PTT 账号已在其他设备登录,当前设备已被迫下线。\n请点击确认退出并重新登录。") .setCancelable(false) .setPositiveButton("确认") { _, _ -> try { com.example.kingway.ptt.SendAtUtil.toLogout() com.example.kingway.ptt.MyApplication.destroy() com.stand.standapp.utils.GpioManager.stopListening() } catch (_: Exception) {} val pm = ctx.packageManager val intent = pm.getLaunchIntentForPackage(ctx.packageName) intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) ctx.startActivity(intent) android.os.Process.killProcess(android.os.Process.myPid()) } .show() } } // 获取并清除待执行的 JS 代码(由 bridge.js 轮询) @JvmStatic fun pollPendingJs(): String { synchronized(pendingJsCode ?: Any()) { val code = pendingJsCode ?: "" pendingJsCode = null return code } } } 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 private val timeoutRunnable = Runnable { showErrorPage() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) instance = this try { setContentView(R.layout.activity_main) geckoView = findViewById(R.id.geckoView) ?: return findViewById(R.id.btn_retry)?.setOnClickListener { hideErrorPage() geckoSession?.reload() startTimeoutTimer() } // 创建 GeckoRuntime geckoRuntime = createGeckoRuntime() // 安装桥接扩展 geckoRuntime?.webExtensionController?.installBuiltIn( "resource://android/assets/extensions/bridge/" ) // 创建 AndroidInterface pttInterface = AndroidInterface(this) // 创建 GeckoSession 并配置 createSessionAndLoad() startTimeoutTimer() // 启动时检查更新 UpdateManager(this).checkUpdate() } catch (e: Exception) { Timber.tag("MainActivity").e(e, "Fatal Crash") } } 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() { executeJs("addExitText()") androidx.appcompat.app.AlertDialog.Builder(this) .setTitle("提示") .setMessage("确定要退出应用吗?") .setPositiveButton("确定") { _, _ -> pttLogoutAndDestroy() finish() } .setNegativeButton("取消", null) .show() } private fun pttLogoutAndDestroy() { try { com.example.kingway.ptt.SendAtUtil.toLogout() Timber.tag("MainActivity").i("PTT logout sent") } catch (e: Exception) { Timber.tag("MainActivity").e(e, "PTT logout failed") } try { com.stand.standapp.utils.GpioManager.stopListening() } catch (e: Exception) { Timber.tag("MainActivity").e(e, "GPIO stop failed") } try { com.example.kingway.ptt.MyApplication.destroy() Timber.tag("MainActivity").i("PTT native destroyed") } catch (e: Exception) { Timber.tag("MainActivity").e(e, "PTT destroy failed") } } override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { 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 onDestroy() { cancelTimeoutTimer() geckoView.releaseSession() geckoSession?.close() geckoSession = null instance = null super.onDestroy() } private fun startTimeoutTimer() { cancelTimeoutTimer() mHandler.postDelayed(timeoutRunnable, TIMEOUT_MS) } fun cancelTimeoutTimer() { mHandler.removeCallbacks(timeoutRunnable) } private fun showErrorPage() { findViewById(R.id.loading_layout)?.visibility = android.view.View.GONE findViewById(R.id.error_layout)?.visibility = android.view.View.VISIBLE } private fun hideErrorPage() { findViewById(R.id.error_layout)?.visibility = android.view.View.GONE findViewById(R.id.loading_layout)?.visibility = android.view.View.VISIBLE } }