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 com.example.kingway.ptt.CallBackResolution 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 private val jsLock = Any() @JvmStatic fun executeJs(script: String) { Timber.tag("ExecuteJs").d(script) synchronized(jsLock) { // 合并多次调用(用换行分隔) pendingJsCode = if (pendingJsCode != null) "$pendingJsCode;$script" else script } } @JvmStatic fun onPttLoginSuccess() { Timber.tag("MainActivity").d("onPttLoginSuccess called from Native") executeJs("onPttLoginSuccess()") } // 硬件按键触发 @JvmStatic fun onHardwarePttDown() { Timber.tag("MainActivity").d("onHardwarePttDown triggered") instance?.runOnUiThread { instance?.pttInterface?.pttSpeakPlay() executeJs("onHardwarePttDown()") } } @JvmStatic fun onHardwarePttUp() { Timber.tag("MainActivity").d("onHardwarePttUp triggered") instance?.runOnUiThread { instance?.pttInterface?.pttSpeakRelease() executeJs("onHardwarePttUp()") } } @JvmStatic fun showPttConflictDialog() { Timber.tag("PTT").w("showPttConflictDialog: account conflict detected") 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) { Timber.tag("PTT").w("showPttUpdateDialog: title=%s msg=%s", title, msg) if (isConflictDialogShowing) return val ctx = instance if (ctx == null) { pendingDialog = title to msg Timber.tag("PTT").d("Pending dialog stored (instance null): title=%s", title) 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 Timber.tag("PTT").d("Flushing pending dialog: %s", pending.second) pendingDialog = null showPttUpdateDialog(pending.second, pending.first) } // 获取并清除待执行的 JS 代码(由 bridge.js 轮询) @JvmStatic fun pollPendingJs(): String { synchronized(jsLock) { 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() } fun logout() { runOnUiThread { Timber.tag("MainActivity").i("用户注销,清理程序") // 1. PTT Logout and cancel login try { com.example.kingway.ptt.SendAtUtil.toCancelLogin() } catch (e: Exception) { Timber.tag("MainActivity").e(e, "PTT cancel failed") } 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") } // 2. Stop GPIO listening try { com.stand.standapp.utils.GpioManager.stopListening() Timber.tag("MainActivity").i("GPIO stop listening") } catch (e: Exception) { Timber.tag("MainActivity").e(e, "GPIO stop failed") } // 3. Destroy/release AudioTrack in PTT Native try { com.stand.standapp.MyApplication.pNative?.destory() com.stand.standapp.MyApplication.pNative = null Timber.tag("MainActivity").i("PTT native AudioTrack destroyed") } catch (e: Exception) { Timber.tag("MainActivity").e(e, "PTT native destroy failed") } // 4. Stop and destroy AudioRecord try { com.example.kingway.ptt.AudioRecordManager.getInstance().destroyAudio() Timber.tag("MainActivity").i("AudioRecordManager destroyed") } catch (e: Exception) { Timber.tag("MainActivity").e(e, "AudioRecordManager destroy failed") } // 5. Close printer connection try { com.stand.standapp.printer.PrinterManager.getFactory()?.ClosePort() Timber.tag("MainActivity").i("Printer port closed") } catch (e: Exception) { Timber.tag("MainActivity").e(e, "Printer close failed") } // 6. Clear Access Token try { AppConfig.setAccessToken(this, "") Timber.tag("MainActivity").i("Cleared access token") } catch (e: Exception) { Timber.tag("MainActivity").e(e, "Failed to clear access token") } // 7. Redirect back to LoginActivity and clear task stack try { val intent = Intent(this, LoginActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK startActivity(intent) finish() } catch (e: Exception) { Timber.tag("MainActivity").e(e, "Failed to redirect to LoginActivity") } try { CallBackResolution.resetPttLoginStatus(); }catch (e: Exception) { Timber.tag("MainActivity").e(e, "Failed to clear public value") } } } 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 onPause() { super.onPause() geckoSession?.setActive(false) } override fun onResume() { super.onResume() geckoSession?.setActive(true) } override fun onDestroy() { cancelTimeoutTimer() geckoView.releaseSession() geckoSession?.close() geckoSession = null clearCompanionState() instance = null super.onDestroy() } private fun clearCompanionState() { synchronized(jsLock) { pendingJsCode = null } pendingDialog = null isConflictDialogShowing = false } 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 } }