package com.shukria.kiosklauncher.service import android.os.Handler import android.os.Looper import android.util.Log import com.shukria.kiosklauncher.util.MQTTConfig import com.shukria.kiosklauncher.util.MQTTManager import org.json.JSONObject import java.io.BufferedReader import java.io.InputStreamReader import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import java.util.TimeZone import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.concurrent.Future /** * Collects logcat output and streams it to the TMS server over MQTT. * * REALTIME mode: continuous logcat stream flushed every ~4 KB. * WITH_DELAY mode: periodic snapshot of the last N lines. * * Without READ_LOGS permission (normal for non-system apps) logcat returns * only this app's own PID logs — sufficient for kiosk debugging. */ object LogPushService { private const val TAG = "LogPushService" private const val FLUSH_THRESHOLD_BYTES = 4096 private const val FLUSH_INTERVAL_MS = 3_000L // time-based flush: don't wait for 4 KB @Volatile private var active = false @Volatile private var serialNumber = "" @Volatile private var requestId = "" private var logcatProcess: Process? = null private val executor: ExecutorService = Executors.newSingleThreadExecutor() private var realtimeFuture: Future<*>? = null private val delayHandler = Handler(Looper.getMainLooper()) private var delayRunnable: Runnable? = null // ------------------------------------------------------------------------- // Public API — called by CommandDispatcher // ------------------------------------------------------------------------- fun start( sn: String, mode: String, lastLinesCount: Int, frequencySend: Int, requestId: String ) { stop(sn) // clean up any existing session first active = true serialNumber = sn this.requestId = requestId Log.i(TAG, "Starting log session [$mode] for $sn requestId=$requestId") when (mode.uppercase()) { "REALTIME" -> startRealtime(sn, requestId) "WITH_DELAY" -> startWithDelay(sn, lastLinesCount, frequencySend, requestId) else -> startRealtime(sn, requestId) } } fun stop(sn: String) { if (!active) return active = false Log.i(TAG, "Stopping log session for $sn") logcatProcess?.destroy() logcatProcess = null realtimeFuture?.cancel(true) realtimeFuture = null delayRunnable?.let { delayHandler.removeCallbacks(it) } delayRunnable = null } val isActive: Boolean get() = active // ------------------------------------------------------------------------- // REALTIME mode — stream logcat output continuously // ------------------------------------------------------------------------- private fun startRealtime(sn: String, reqId: String) { realtimeFuture = executor.submit { try { val process = Runtime.getRuntime().exec( arrayOf("logcat", "-v", "time", "*:D") ) logcatProcess = process val reader = BufferedReader(InputStreamReader(process.inputStream)) val buffer = StringBuilder() var lastFlushMs = System.currentTimeMillis() while (active) { if (reader.ready()) { val line = reader.readLine() ?: break buffer.append(line).append("\n") } val now = System.currentTimeMillis() val hitSizeLimit = buffer.length >= FLUSH_THRESHOLD_BYTES val hitTimeLimit = buffer.isNotEmpty() && (now - lastFlushMs) >= FLUSH_INTERVAL_MS if (hitSizeLimit || hitTimeLimit) { publishLogs(sn, reqId, buffer.toString()) buffer.clear() lastFlushMs = now } else if (!reader.ready()) { Thread.sleep(200) } } if (buffer.isNotEmpty()) publishLogs(sn, reqId, buffer.toString()) } catch (e: InterruptedException) { Log.d(TAG, "Realtime log thread interrupted") } catch (e: Exception) { Log.e(TAG, "Realtime logcat error: ${e.message}") } } } // ------------------------------------------------------------------------- // WITH_DELAY mode — periodic snapshot of last N lines // ------------------------------------------------------------------------- private fun startWithDelay(sn: String, lastLinesCount: Int, frequencySend: Int, reqId: String) { val interval = frequencySend.coerceAtLeast(5) * 1000L // minimum 5s val runnable = object : Runnable { override fun run() { if (!active) return captureSnapshot(sn, reqId, lastLinesCount) delayHandler.postDelayed(this, interval) } } delayRunnable = runnable delayHandler.post(runnable) } private fun captureSnapshot(sn: String, reqId: String, lastLines: Int) { executor.submit { try { val process = Runtime.getRuntime().exec( arrayOf("logcat", "-d", "-t", lastLines.toString(), "-v", "time", "*:D") ) val logs = process.inputStream.bufferedReader().readText() process.waitFor() process.destroy() if (logs.isNotBlank()) publishLogs(sn, reqId, logs) } catch (e: Exception) { Log.e(TAG, "Snapshot logcat error: ${e.message}") } } } // ------------------------------------------------------------------------- // Publish — matches TMS RemoteLogService parse_log_message/1 exactly // ------------------------------------------------------------------------- private fun publishLogs(sn: String, reqId: String, logs: String) { Log.d(TAG, "Publishing ${logs.length} bytes to logpush topic") val payload = JSONObject().apply { put("logs", logs) put("session_info", JSONObject().apply { put("timestamp", utcNow()) put("session_id", reqId) put("status", "streaming") }) } MQTTManager.publishJson(MQTTConfig.logPushTopic(sn), payload, qos = 0) } private fun utcNow(): String = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US) .apply { timeZone = TimeZone.getTimeZone("UTC") } .format(Date()) }