package com.stand.standapp import android.content.Context import android.os.Handler import android.os.Looper import android.widget.Toast import com.stand.standapp.R import com.stand.standapp.utils.LogManager import com.stand.standapp.printer.PrinterManager import timber.log.Timber import com.example.kingway.ptt.SendAtUtil import org.json.JSONArray import org.json.JSONObject class AndroidInterface(private val activity: MainActivity) { private var isSpeaking = false private var lastSpeakTime = 0L private val MIN_SPEAK_DURATION = 200L /** * JS bridge 调度入口 * @param method JS 调用的方法名 * @param argsJson JSON 数组格式的参数 * @return 返回值(JSON 字符串或空串) */ fun dispatch(method: String, argsJson: String): String { return try { val args = if (argsJson.isNotEmpty()) JSONArray(argsJson) else JSONArray() when (method) { "pttSpeakPlay" -> { pttSpeakPlay(); "" } "pttGetLoginStatus" -> pttGetLoginStatus() "pttGetLoginId" -> pttGetLoginId() "pttGetCurrentGroup" -> pttGetCurrentGroup() "pttSpeakRelease" -> { pttSpeakRelease(); "" } "pttForceRelease" -> { pttForceRelease(); "" } "pttGetGroupInfo" -> { pttGetGroupInfo(); "" } "pttGetGroupMemberInfo" -> { pttGetGroupMemberInfo(args.optString(0, "")); "" } "simulatePttConflict" -> { simulatePttConflict(); "" } "pttSendAtCmd" -> { pttSendAtCmd(args.optString(0, "")); "" } "pttJumpGroup" -> { pttJumpGroup(args.optString(0, "")); "" } "getPrinterStatus" -> getPrinterStatus() "printText" -> { printText(optStringOrNull(args, 0), args.optInt(1, 0), args.optInt(2, 0), args.optInt(3, 0)); "" } "printBarcode" -> { printBarcode(optStringOrNull(args, 0), args.optInt(1, 0), args.optInt(2, 0), args.optInt(3, 0), args.optInt(4, 0), args.optInt(5, 0)); "" } "printQR" -> { printQR(optStringOrNull(args, 0), args.optInt(1, 0), args.optInt(2, 0)); "" } "printImage" -> { printImage(optStringOrNull(args, 0), args.optInt(1, 0), args.optInt(2, 0), args.optInt(3, 0)); "" } "printColumns" -> { if (args.length() >= 8) { printColumns(optStringOrNull(args, 0), args.optInt(1, 0), optStringOrNull(args, 2), args.optInt(3, 0), optStringOrNull(args, 4), args.optInt(5, 0), optStringOrNull(args, 6), args.optInt(7, 0)) } "" } "printColumnsWithHeight" -> { if (args.length() >= 9) { printColumnsWithHeight(optStringOrNull(args, 0), args.optInt(1, 0), optStringOrNull(args, 2), args.optInt(3, 0), optStringOrNull(args, 4), args.optInt(5, 0), optStringOrNull(args, 6), args.optInt(7, 0), args.optInt(8, 0)) } "" } "cutPaper" -> { cutPaper(args.optBoolean(0, false)); "" } "printTestPage" -> { printTestPage(); "" } "checkPaper" -> { checkPaper(); "" } "exportLogs" -> exportLogs() "uploadLogs" -> { uploadLogs(); "" } "printPage" -> { printPage(); "" } "printRawData" -> { printRawData(optStringOrNull(args, 0)); "" } "startUpdate" -> { startUpdate(optStringOrNull(args, 0)); "" } "exitApp" -> { exitApp(); "" } "forceExit" -> { forceExit(); "" } "getVersionName" -> getVersionName() "getAboutInfo" -> getAboutInfo() "saveMessage" -> { saveMessage(optStringOrNull(args, 0)); "" } "getMessages" -> getMessages() "clearMessages" -> { clearMessages(); "" } "onPageFinished" -> { onPageFinished(); "" } "showRestartDialog" -> { showRestartDialog(args.optString(0, ""), args.optString(1, "提示")); "" } "showJsonDialog" -> { showJsonDialog(args.optString(0, "")); "" } "getTempCallStatus" -> getTempCallStatus() "maximizeTempCall" -> { maximizeTempCall(); "" } "logout" -> { logout(); "" } else -> { Timber.tag("Bridge").w("Unknown method: $method") "" } } } catch (e: Exception) { Timber.tag("Bridge").e(e, "Dispatch error: $method") "" } } private fun optStringOrNull(args: JSONArray, index: Int): String? { return if (index < args.length() && !args.isNull(index)) args.getString(index) else null } fun pttSpeakPlay() { if (!isSpeaking) { isSpeaking = true lastSpeakTime = System.currentTimeMillis() SendAtUtil.speakPlay() Timber.tag("PTT").d("Speak Play") MainActivity.executeJs("updatePttTalkStatus(true)") } else { Timber.tag("PTT").w("Speak Play ignored: already speaking") } } fun pttGetLoginStatus(): String { return com.example.kingway.ptt.CallBackResolution.getPttLoginStatus() } fun pttGetLoginId(): String { return com.example.kingway.ptt.CallBackResolution.getPttLoginId() } fun pttGetCurrentGroup(): String { return try { val json = JSONObject() json.put("groupId", com.example.kingway.ptt.CallBackResolution.getCurrentGroupId()) json.put("groupName", com.example.kingway.ptt.CallBackResolution.getCurrentGroupName()) json.toString() } catch (e: Exception) { Timber.tag("PTT").e(e, "pttGetCurrentGroup failed") "{\"groupId\":\"\",\"groupName\":\"\"}" } } fun pttSpeakRelease() { Timber.tag("PTT").d("pttSpeakRelease called, isSpeaking=%s", isSpeaking) if (isSpeaking) { val now = System.currentTimeMillis() val duration = now - lastSpeakTime if (duration < MIN_SPEAK_DURATION) { val delay = MIN_SPEAK_DURATION - duration Timber.tag("PTT").d("Speak duration too short ($duration ms), delaying release by $delay ms") Handler(Looper.getMainLooper()).postDelayed({ realRelease() }, delay) } else { realRelease() } } } private fun realRelease() { if (isSpeaking) { isSpeaking = false SendAtUtil.speakRelease() Timber.tag("PTT").d("Real Speak Release") MainActivity.executeJs("updatePttTalkStatus(false)") } else { Timber.tag("PTT").w("realRelease skipped: already released") } } fun pttForceRelease() { val wasSpeaking = isSpeaking isSpeaking = false SendAtUtil.speakRelease() Timber.tag("PTT").d("Force Release (wasSpeaking=%s)", wasSpeaking) } fun pttGetGroupInfo() { Timber.tag("PTT").d("pttGetGroupInfo called") SendAtUtil.getGroupInfo() } fun pttGetGroupMemberInfo(gid: String) { Timber.tag("PTT").d("pttGetGroupMemberInfo: gid=%s", gid) SendAtUtil.getGroupMemberInfo(gid) } fun simulatePttConflict() { Timber.tag("PTT").w("simulatePttConflict: showing conflict dialog via JS bridge") Handler(Looper.getMainLooper()).post { MainActivity.showPttConflictDialog() } } fun pttSendAtCmd(cmd: String) { Timber.tag("PTT").d("pttSendAtCmd: %s", cmd) SendAtUtil.sendRawCmd(cmd) } fun pttJumpGroup(gid: String) { Timber.tag("PTT").d("pttJumpGroup: gid=%s", gid) SendAtUtil.jumpGroup(gid) if (gid.isNotEmpty()) { AppConfig.setLastGroupId(activity, gid) } } fun getPrinterStatus(): String { return try { val json = JSONObject() val connected = PrinterManager.isConnected() json.put("connected", connected) json.put("hasPaper", PrinterManager.isPaperReady()) json.put("port", AppConfig.getSerialPort(activity)) json.toString() } catch (e: Exception) { "{\"connected\":false, \"hasPaper\":false, \"error\":\"${e.message}\"}" } } fun printText(text: String?, size: Int, align: Int, space: Int) { if (text.isNullOrEmpty()) return try { PrinterManager.getFactory()?.let { if (it.isConnection) { it.PrintText(text, size, align, space) } } } catch (e: Throwable) { Timber.tag("JS_Interface").e(e, "printText failed") } } fun printBarcode(data: String?, size: Int, height: Int, align: Int, text_pos: Int, type: Int) { if (data.isNullOrEmpty()) return try { PrinterManager.getFactory()?.PrintBarcode(data, size, height, align, text_pos, type) } catch (e: Throwable) { Timber.tag("JS_Interface").e(e, "printBarcode failed") } } fun printQR(data: String?, size: Int, l_m: Int) { if (data.isNullOrEmpty()) return try { PrinterManager.getFactory()?.PrintQR(data, size, l_m) } catch (e: Throwable) { Timber.tag("JS_Interface").e(e, "printQR failed") } } fun printImage(fileName: String?, width: Int, align: Int, mode: Int) { if (fileName.isNullOrEmpty()) return try { val stream = activity.assets.open(fileName) val bitmap = android.graphics.BitmapFactory.decodeStream(stream) PrinterManager.getFactory()?.PrintImage(bitmap, width, align, mode) stream.close() } catch (e: Exception) { Timber.tag("JS_Interface").e(e, "printImage failed: $fileName") } } fun printColumns(c1: String?, p1: Int, c2: String?, p2: Int, c3: String?, p3: Int, c4: String?, p4: Int) { try { PrinterManager.getFactory()?.Printcolumncontent(c1 ?: "", p1, c2 ?: "", p2, c3 ?: "", p3, c4 ?: "", p4) } catch (e: Throwable) { Timber.tag("JS_Interface").e(e, "printColumns failed") } } fun printColumnsWithHeight(c1: String?, p1: Int, c2: String?, p2: Int, c3: String?, p3: Int, c4: String?, p4: Int, height: Int) { try { PrinterManager.getFactory()?.Printcolumncontent(c1 ?: "", p1, c2 ?: "", p2, c3 ?: "", p3, c4 ?: "", p4, height) } catch (e: Throwable) { Timber.tag("JS_Interface").e(e, "printColumnsWithHeight failed") } } fun cutPaper(allCut: Boolean) { try { if (allCut){ PrinterManager.getFactory()?.PaperAllCut(); }else{ PrinterManager.getFactory()?.PaperCut(); } } catch (e: Throwable) { Timber.tag("JS_Interface").e(e, "cutPaper failed") } } fun printTestPage() { try { PrinterManager.getFactory()?.PrintTestPage() } catch (e: Throwable) { Timber.tag("JS_Interface").e(e, "printTestPage failed") } } fun checkPaper() { try { PrinterManager.getFactory()?.Check_Paper() } catch (e: Throwable) { Timber.tag("JS_Interface").e(e, "checkPaper failed") } } fun exportLogs(): String { val zipPath = LogManager.zipLogs(activity) if (zipPath != null) { Handler(Looper.getMainLooper()).post { Toast.makeText(activity.applicationContext, "日志已打包: $zipPath", Toast.LENGTH_LONG).show() } return zipPath } else { Handler(Looper.getMainLooper()).post { Toast.makeText(activity.applicationContext, "暂无日志或打包失败", Toast.LENGTH_SHORT).show() } return "" } } fun uploadLogs() { LogManager.uploadLogs(activity) { success, message -> Handler(Looper.getMainLooper()).post { Toast.makeText(activity.applicationContext, if (success) "日志上传成功" else "上传失败: $message", Toast.LENGTH_SHORT).show() } } } /** * 系统打印(GeckoView 不支持 WebView.createPrintDocumentAdapter) * 如需此功能,需用 GeckoSession 生成 PDF 后打印 */ fun printPage() { Handler(Looper.getMainLooper()).post { Toast.makeText(activity.applicationContext, "GeckoView 暂不支持系统打印,请使用热敏打印功能", Toast.LENGTH_SHORT).show() } } fun printRawData(data: String?) { if (data.isNullOrEmpty()) return try { Timber.tag("Print").d("收到 H5 原始打印数据: $data") Handler(Looper.getMainLooper()).post { try { Toast.makeText(activity.applicationContext, "H5指令打印: $data", Toast.LENGTH_SHORT).show() } catch (e: Exception) { Timber.tag("Print").e(e, "Toast显示失败") } } } catch (e: Exception) { Timber.tag("Print").e(e, "指令解析失败") } } fun startUpdate(apkUrl: String?) { Handler(Looper.getMainLooper()).post { try { UpdateManager(activity).checkUpdate() } catch (e: Exception) { Timber.tag("Update").e(e, "启动更新检查失败") } } } fun exitApp() { Handler(Looper.getMainLooper()).post { try { activity.showExitDialog() } catch (e: Exception) { Timber.tag("App").e(e, "显示退出弹窗失败") activity.finish() } } } fun forceExit() { Handler(Looper.getMainLooper()).post { try { com.stand.standapp.utils.AppKiller.killApp(activity) } catch (e: Exception) { Timber.tag("App").e(e, "强制退出失败") activity.finish() } } } fun getVersionName(): String { return try { val packageInfo = activity.packageManager.getPackageInfo(activity.packageName, 0) packageInfo.versionName ?: "1.0.0" } catch (e: Exception) { Timber.tag("AndroidInterface").e(e, "getVersionName failed") "1.0.0" } } fun getAboutInfo(): String { return try { val packageInfo = activity.packageManager.getPackageInfo(activity.packageName, 0) val json = com.stand.standapp.utils.DeviceInfoUtils.getDeviceInfoJson(activity) json.put("appName", activity.getString(R.string.app_name)) json.put("versionName", packageInfo.versionName ?: "1.0.0") json.toString() } catch (e: Exception) { Timber.tag("AndroidInterface").e(e, "getAboutInfo failed") "{ \"appName\": \"消防值守台\", \"versionName\": \"1.0.0\" }" } } fun saveMessage(message: String?) { if (message.isNullOrEmpty()) return try { val prefs = activity.getSharedPreferences("msg_records", Context.MODE_PRIVATE) val currentMsgs = prefs.getString("history", "") ?: "" val updatedMsgs = if (currentMsgs.isEmpty()) message else "$message|---|$currentMsgs" val list = updatedMsgs.split("|---|").take(5000) prefs.edit().putString("history", list.joinToString("|---|")).apply() } catch (e: Exception) { Timber.tag("Storage").e(e, "消息保存异常") } } fun getMessages(): String { return try { val prefs = activity.getSharedPreferences("msg_records", Context.MODE_PRIVATE) prefs.getString("history", "") ?: "" } catch (e: Exception) { Timber.tag("Storage").e(e, "getMessages failed") "" } } fun clearMessages() { try { activity.getSharedPreferences("msg_records", Context.MODE_PRIVATE).edit().clear().apply() Handler(Looper.getMainLooper()).post { Toast.makeText(activity.applicationContext, "记录已清空", Toast.LENGTH_SHORT).show() } } catch (e: Exception) { Timber.tag("Storage").e(e, "清空记录异常") } } fun onPageFinished() { Handler(Looper.getMainLooper()).post { try { activity.cancelTimeoutTimer() activity.findViewById(R.id.loading_layout)?.visibility = android.view.View.GONE // 正式界面模式下,页面加载完成后传递登录数据给前端 val isOfficial = activity.isOfficialMode() val data = activity.getLoginData() Timber.tag("AndroidInterface").d("onPageFinished: isOfficial=$isOfficial, data=$data") if (isOfficial && data.isNotEmpty()) { val escaped = data.replace("'", "\\'").replace("\n", "\\n") com.stand.standapp.MainActivity.executeJs("if(window.appLogin){window.appLogin('$escaped');}") Timber.tag("AndroidInterface").d("Called appLogin") } // 页面加载并初始化完成后触发状态 com.stand.standapp.MainActivity.onPageLoadFinished() } catch (e: Exception) { Timber.tag("UI").e(e, "隐藏加载布局失败") } } } fun showRestartDialog(message: String, title: String) { Timber.tag("AndroidInterface").w("showRestartDialog from JS: title=%s msg=%s", title, message) Handler(Looper.getMainLooper()).post { com.stand.standapp.MainActivity.showPttUpdateDialog(message, title) } } fun showJsonDialog(jsonStr: String) { Handler(Looper.getMainLooper()).post { try { val formatted = try { val obj = org.json.JSONObject(jsonStr) obj.toString(2) } catch (_: Exception) { jsonStr } androidx.appcompat.app.AlertDialog.Builder(activity) .setTitle("JSON 数据") .setMessage(formatted) .setPositiveButton("确认", null) .show() } catch (e: Exception) { Timber.tag("UI").e(e, "showJsonDialog error") } } } fun getTempCallStatus(): String { return try { val json = JSONObject() json.put("isOpen", TempCallActivity.isOpen()) json.put("isMinimized", TempCallActivity.isMinimizedState()) json.toString() } catch (e: Exception) { "{\"isOpen\":false,\"isMinimized\":false}" } } fun maximizeTempCall() { Handler(Looper.getMainLooper()).post { try { TempCallActivity.maximize() } catch (e: Exception) { Timber.tag("TempCall").e(e, "maximizeTempCall error") } } } fun logout() { activity.logout() } }