package com.shukria.kiosklauncher.util import android.content.Context import android.os.Environment import android.util.Log import com.shukria.kiosklauncher.network.RetrofitClient import com.shukria.kiosklauncher.service.LogPushService import com.shukria.kiosklauncher.service.RemoteControlService import com.shukria.kiosklauncher.service.ScreenShareService import com.shukria.kiosklauncher.ui.ScreenCaptureActivity import com.shukria.kiosklauncher.util.YSDKManager import okhttp3.Request as OkRequest import java.io.File import org.json.JSONObject import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import java.util.TimeZone object CommandDispatcher { private const val TAG = "CommandDispatcher" private var appContext: Context? = null fun dispatch(context: Context, serialNumber: String, command: String, payload: JSONObject) { appContext = context.applicationContext Log.i(TAG, "TMS command received: '$command'") when (command) { // Phase 2 — log streaming "debug_log_upload_start" -> handleLogStart(serialNumber, payload) "debug_log_upload_stop" -> handleLogStop(serialNumber, payload) // Phase 4 — screen share "screen_share_start" -> handleScreenShareStart(context, serialNumber, payload) "screen_share_stop" -> handleScreenShareStop(context, serialNumber, payload) // Phase 5 — remote touch injection "touch_event" -> handleTouchEvent(serialNumber, payload) // Phase 3 — app marketplace / OTA install "UPDATE_APPLICATION" -> handleAppUpdate(context, serialNumber, payload) else -> { Log.w(TAG, "Unrecognised command: '$command'") ack(serialNumber, payload.optString("request_id"), "error", "Unknown command: $command") } } } // ------------------------------------------------------------------------- // Phase 2 — Log streaming // ------------------------------------------------------------------------- private fun handleLogStart(sn: String, payload: JSONObject) { val reqId = payload.optString("request_id") val mode = payload.optString("mode", "REALTIME") val lastLinesCount = payload.optInt("last_lines_count", 100) val frequencySend = payload.optInt("frequency_send", 15) val logLevel = payload.optString("log_level", "ALL") Log.d(TAG, "Log start: mode=$mode lastLines=$lastLinesCount freq=${frequencySend}s") ack(sn, reqId, "success", "Log streaming started in $mode mode") LogPushService.start( sn = sn, mode = mode, lastLinesCount = lastLinesCount, frequencySend = frequencySend, requestId = reqId ) } private fun handleLogStop(sn: String, payload: JSONObject) { val reqId = payload.optString("request_id") Log.d(TAG, "Log stop requested") LogPushService.stop(sn) ack(sn, reqId, "success", "Log streaming stopped") } // ------------------------------------------------------------------------- // Phase 4 — Screen share // ------------------------------------------------------------------------- private fun handleScreenShareStart(context: Context, sn: String, payload: JSONObject) { val reqId = payload.optString("request_id") if (ScreenShareService.isRunning()) { Log.d(TAG, "Screen share already running") ack(sn, reqId, "success", "Screen share already active") return } Log.i(TAG, "Starting ScreenShareService FGS, then launching consent activity") ack(sn, reqId, "success", "Screen share starting") // Step 1: start FGS first (Android 14 requires FGS running before getMediaProjection) ScreenShareService.prepare(context, sn) // Step 2: show consent dialog ScreenCaptureActivity.start(context, sn) } private fun handleScreenShareStop(context: Context, sn: String, payload: JSONObject) { val reqId = payload.optString("request_id") Log.i(TAG, "Stopping screen share") ScreenShareService.stop(context) ack(sn, reqId, "success", "Screen share stopped") } // ------------------------------------------------------------------------- // Phase 5 — Remote touch injection // ------------------------------------------------------------------------- private fun handleTouchEvent(sn: String, payload: JSONObject) { val x = payload.optDouble("x", -1.0).toFloat() val y = payload.optDouble("y", -1.0).toFloat() if (x < 0f || x > 1f || y < 0f || y > 1f) { Log.w(TAG, "touch_event: coords out of range x=$x y=$y") return } Log.d(TAG, "touch_event: injecting tap at x=${"%.3f".format(x)} y=${"%.3f".format(y)}") RemoteControlService.injectTap(x, y) } // ------------------------------------------------------------------------- // Phase 3 — App OTA install // ------------------------------------------------------------------------- private fun handleAppUpdate(context: Context, sn: String, payload: JSONObject) { val reqId = payload.optString("request_id") val downloadUrl = payload.optString("downloadUrl").ifBlank { Log.e(TAG, "UPDATE_APPLICATION missing downloadUrl") ack(sn, reqId, "error", "Missing downloadUrl") return } val version = payload.optString("version", "unknown") val packageName = payload.optString("packageName", "") val fileName = "${packageName.ifBlank { "app" }}-${version}.apk" val destDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) val destFile = File(destDir, "ota/$fileName").also { it.parentFile?.mkdirs() } Log.i(TAG, "UPDATE_APPLICATION: pkg=$packageName ver=$version url=$downloadUrl") ack(sn, reqId, "success", "Downloading $packageName v$version") Thread { try { val response = RetrofitClient.okHttpClient.newCall(OkRequest.Builder().url(downloadUrl).build()).execute() if (!response.isSuccessful) { Log.e(TAG, "HTTP ${response.code()} downloading $packageName") ack(sn, reqId, "error", "Download failed: HTTP ${response.code()}") return@Thread } response.body()?.byteStream()?.use { input -> destFile.outputStream().use { output -> input.copyTo(output) } } ?: run { ack(sn, reqId, "error", "Download failed: empty response") return@Thread } Log.i(TAG, "Downloaded $packageName to ${destFile.absolutePath} (${destFile.length()} bytes)") installApk(context, destFile.absolutePath, packageName, sn, reqId) } catch (e: Exception) { Log.e(TAG, "Download error for $packageName: ${e.message}") ack(sn, reqId, "error", "Download error: ${e.message}") } }.start() } private fun installApk(context: Context, path: String, packageName: String, sn: String, reqId: String) { val ok = YSDKManager.installApp(path, packageName) if (ok) { Log.i(TAG, "YSDK installApp called for $packageName — awaiting PACKAGE_ADDED broadcast for confirmation") ack(sn, reqId, "success", "Install initiated for $packageName") // Register one-shot receiver to confirm actual install success val receiver = object : android.content.BroadcastReceiver() { override fun onReceive(ctx: android.content.Context, intent: android.content.Intent) { val installedPkg = intent.data?.schemeSpecificPart ?: return if (installedPkg == packageName) { Log.i(TAG, "Install confirmed by system: $packageName") ack(sn, reqId, "success", "Installed $packageName — confirmed by system") ctx.unregisterReceiver(this) } } } val filter = android.content.IntentFilter().apply { addAction(android.content.Intent.ACTION_PACKAGE_ADDED) addAction(android.content.Intent.ACTION_PACKAGE_REPLACED) addDataScheme("package") } context.applicationContext.registerReceiver(receiver, filter) } else { Log.e(TAG, "YSDK not connected — cannot install $packageName") ack(sn, reqId, "error", "Install unavailable: YSDK not connected") } } // ------------------------------------------------------------------------- // ACK helper — matches TMS ota/ack/+ handler: expects "oid" and "sn" fields // ------------------------------------------------------------------------- private fun ack(serialNumber: String, requestId: String, status: String, message: String) { val json = JSONObject().apply { put("oid", requestId) put("sn", serialNumber) put("status", status) put("message", message) put("timestamp", utcNow()) } MQTTManager.publishJson(MQTTConfig.ackTopic(serialNumber), json, qos = 1) } private fun utcNow(): String = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US) .apply { timeZone = TimeZone.getTimeZone("UTC") } .format(Date()) }