334 lines
11 KiB
Kotlin
334 lines
11 KiB
Kotlin
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
|
||
}
|
||
}
|