package com.stand.standapp import android.content.Intent import android.os.Bundle import android.view.KeyEvent import androidx.appcompat.app.AppCompatActivity import android.widget.FrameLayout 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.StorageController.ClearFlags import org.mozilla.geckoview.WebRequestError import timber.log.Timber class MainActivity : AppCompatActivity() { companion object { private var instance: MainActivity? = null // 防止对话框堆叠 @Volatile private var isConflictDialogShowing = false // 待显示的弹窗(instance 为 null 时暂存) @Volatile private var pendingDialog: Pair? = null // 待执行的 JS 代码(由 bridge.js 轮询获取) @Volatile private var pendingJsCode: String? = null @JvmStatic fun executeJs(script: String) { Timber.tag("ExecuteJs").d(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()") } // 硬件按键触发 @JvmStatic fun onHardwarePttDown() { instance?.runOnUiThread { instance?.pttInterface?.pttSpeakPlay() executeJs("onHardwarePttDown()") } } @JvmStatic fun onHardwarePttUp() { instance?.runOnUiThread { instance?.pttInterface?.pttSpeakRelease() executeJs("onHardwarePttUp()") } } @JvmStatic fun showPttConflictDialog() { if (isConflictDialogShowing) return isConflictDialogShowing = true instance?.runOnUiThread { val ctx = instance ?: return@runOnUiThread androidx.appcompat.app.AlertDialog.Builder(ctx) .setTitle("账号冲突") .setMessage("您的 PTT 账号已在其他设备登录,当前设备已被迫下线。\n请点击确认重启应用。") .setCancelable(false) .setPositiveButton("确认") { _, _ -> com.stand.standapp.utils.AppKiller.killApp(ctx, restart = true) } .setOnDismissListener { isConflictDialogShowing = false } .show() } } @JvmStatic fun showPttUpdateDialog(msg: String, title: String) { if (isConflictDialogShowing) return val ctx = instance if (ctx == null) { pendingDialog = title to msg return } isConflictDialogShowing = true ctx.runOnUiThread { androidx.appcompat.app.AlertDialog.Builder(ctx) .setTitle(title) .setMessage(msg) .setCancelable(false) .setPositiveButton("确认") { _, _ -> com.stand.standapp.utils.AppKiller.killApp(ctx, restart = true) } .setOnDismissListener { isConflictDialogShowing = false } .show() } } @JvmStatic fun showPendingDialogIfNeeded() { val pending = pendingDialog ?: return pendingDialog = null showPttUpdateDialog(pending.second, pending.first) } // 获取并清除待执行的 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 var officialMode = false private var loginData = "" private val mHandler = android.os.Handler(android.os.Looper.getMainLooper()) private val TIMEOUT_MS = 10000L private val timeoutRunnable = Runnable { showErrorPage() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) instance = this showPendingDialogIfNeeded() // 读取登录模式和数据 officialMode = intent.getBooleanExtra("official_mode", false) loginData = intent.getStringExtra("login_data") ?: "" 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() // 创建 AndroidInterface pttInterface = AndroidInterface(this) // 创建 GeckoSession 并配置(但不加载页面) val session = createSession() geckoSession = session geckoView.setSession(session) startTimeoutTimer() // 安装桥接扩展,等安装完成后再加载页面 geckoRuntime?.webExtensionController?.installBuiltIn( "resource://android/assets/extensions/bridge/" )?.accept { extension -> Timber.tag("MainActivity").d("WebExtension installed: ${extension?.metaData?.name}") runOnUiThread { val serverUrl = AppConfig.getServerUrl(this) val finalUrl = if (officialMode) { "$serverUrl/sysdispatch/home?t=${System.currentTimeMillis()}" } else { "$serverUrl/example/print_demo.html?t=${System.currentTimeMillis()}" } Timber.tag("MainActivity").d("Loading URL: $finalUrl (officialMode=$officialMode)") session.loadUri(finalUrl) } } // 启动时检查更新 UpdateManager(this).checkUpdate() // 👈 动态注入 Compose 容器,让 FloatingChatWidget 呈现在最上层! val composeView = androidx.compose.ui.platform.ComposeView(this).apply { setContent { com.stand.standapp.ui.chat.FloatingChatWidget() } } findViewById(android.R.id.content)?.addView( composeView, FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT ) ) } 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 // 强制清缓存,确保 JS/CSS 等资源获取最新版本 try { // runtime.storageController.clearData(ClearFlags.ALL) Timber.tag("MainActivity").i("GeckoView cache cleared") } catch (e: Exception) { Timber.tag("MainActivity").w(e, "Failed to clear GeckoView cache") } return runtime } private fun createSession(): GeckoSession { val session = GeckoSession() val sessionSettings = session.settings // 配置 GeckoSession 设置(对应原 WebView settings) sessionSettings.allowJavascript = true sessionSettings.userAgentMode = GeckoSessionSettings.USER_AGENT_MODE_MOBILE val runtime = geckoRuntime ?: return session // 设置各种代理 setupDelegates(session) // 打开会话 session.open(runtime) return session } 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") // 正式界面模式下,页面加载完成后传递登录数据给前端 // if (success && officialMode && loginData.isNotEmpty()) { // // 延迟 500ms 确保 JS 上下文就绪 // mHandler.postDelayed({ // val escaped = loginData.replace("'", "\\'").replace("\n", "\\n") // executeJs("if(window.appLogin){window.appLogin('$escaped');}") // Timber.tag("MainActivity").d("Called appLogin with loginData") // }, 500) // } } } // 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)) } // 默认处理(显示对话框) val res = GeckoResult() runOnUiThread { val input = android.widget.EditText(this@MainActivity) input.setText(prompt.defaultValue) androidx.appcompat.app.AlertDialog.Builder(this@MainActivity) .setTitle(prompt.title ?: "") .setMessage(prompt.message) .setView(input) .setPositiveButton("确定") { _, _ -> res.complete(prompt.confirm(input.text.toString())) } .setNegativeButton("取消") { _, _ -> res.complete(prompt.dismiss()) } .setOnCancelListener { res.complete(prompt.dismiss()) } .show() } return res } override fun onAlertPrompt( session: GeckoSession, prompt: GeckoSession.PromptDelegate.AlertPrompt ): GeckoResult? { val res = GeckoResult() runOnUiThread { androidx.appcompat.app.AlertDialog.Builder(this@MainActivity) .setMessage(prompt.message) .setPositiveButton("确定") { _, _ -> res.complete(prompt.dismiss()) } .setOnCancelListener { res.complete(prompt.dismiss()) } .show() } return res } override fun onAuthPrompt( session: GeckoSession, prompt: GeckoSession.PromptDelegate.AuthPrompt ): GeckoResult? { return null } override fun onChoicePrompt( session: GeckoSession, prompt: GeckoSession.PromptDelegate.ChoicePrompt ): GeckoResult? { val res = GeckoResult() runOnUiThread { val choices = prompt.choices.map { it.label }.toTypedArray() androidx.appcompat.app.AlertDialog.Builder(this@MainActivity) .setTitle(prompt.title) .setSingleChoiceItems(choices, -1) { dialog, which -> dialog.dismiss() res.complete(prompt.confirm(prompt.choices[which])) } .setOnCancelListener { res.complete(prompt.dismiss()) } .show() } return res } override fun onButtonPrompt( session: GeckoSession, prompt: GeckoSession.PromptDelegate.ButtonPrompt ): GeckoResult? { val res = GeckoResult() runOnUiThread { androidx.appcompat.app.AlertDialog.Builder(this@MainActivity) .setTitle(prompt.title ?: "提示") .setMessage(prompt.message) .setPositiveButton("确定") { _, _ -> res.complete(prompt.confirm(GeckoSession.PromptDelegate.ButtonPrompt.Type.POSITIVE)) } .setNegativeButton("取消") { _, _ -> res.complete(prompt.dismiss()) } .setOnCancelListener { res.complete(prompt.dismiss()) } .show() } return res } } } 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("确定") { _, _ -> com.stand.standapp.utils.AppKiller.killApp(this) } .setNegativeButton("取消", null) .show() } private fun pttLogoutAndDestroy() { try { com.example.kingway.ptt.SendAtUtil.toCancelLogin() } catch (e: Exception) { Timber.tag("LoginActivity").e(e, "PTT cancel failed") } try { com.example.kingway.ptt.SendAtUtil.toLogout() Timber.tag("LoginActivity").i("PTT logout sent") } catch (e: Exception) { Timber.tag("LoginActivity").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.stand.standapp.utils.GpioManager.stopListening() } catch (e: Exception) { Timber.tag("LoginActivity").e(e, "GPIO stop failed") } try { // com.example.kingway.ptt.MyApplication.destroy() Timber.tag("LoginActivity").i("PTT native destroyed") } catch (e: Exception) { Timber.tag("LoginActivity").e(e, "PTT destroy failed") } try { // PrinterManager.shutdown() Timber.tag("LoginActivity").i("Printer shut down") } catch (e: Exception) { Timber.tag("LoginActivity").e(e, "Printer shutdown failed") } try { com.stand.standapp.utils.LogManager.shutdown() Timber.tag("LoginActivity").i("LogManager shut down") } catch (e: Exception) { Timber.tag("LoginActivity").e(e, "LogManager shutdown 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) } fun isOfficialMode(): Boolean = officialMode fun getLoginData(): String = loginData 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 } }