StandAPP/app/src/main/java/com/stand/standapp/MainActivity.kt

334 lines
11 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<GeckoView>(R.id.geckoView)
?: return
findViewById<android.widget.Button>(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<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() {
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<android.view.View>(R.id.loading_layout)?.visibility = android.view.View.GONE
findViewById<android.view.View>(R.id.error_layout)?.visibility = android.view.View.VISIBLE
}
private fun hideErrorPage() {
findViewById<android.view.View>(R.id.error_layout)?.visibility = android.view.View.GONE
findViewById<android.view.View>(R.id.loading_layout)?.visibility = android.view.View.VISIBLE
}
}