package com.shukria.kiosklauncher.service import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.Service import android.content.Context import android.content.Intent import android.net.ConnectivityManager import android.os.Build import android.os.Handler import android.os.IBinder import android.os.Looper import android.util.Log import androidx.core.app.NotificationCompat import com.shukria.kiosklauncher.R import com.shukria.kiosklauncher.util.CommandDispatcher import com.shukria.kiosklauncher.util.DeviceInfo import com.shukria.kiosklauncher.util.MQTTConfig import com.shukria.kiosklauncher.util.MQTTManager import com.shukria.kiosklauncher.util.YSDKManager import org.json.JSONArray import org.json.JSONObject import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import java.util.TimeZone import java.util.UUID class MQTTService : Service() { private val TAG = "MQTTService" private val CHANNEL_ID = "mqtt_service_channel" private val NOTIFICATION_ID = 1001 private lateinit var serialNumber: String private val heartbeatHandler = Handler(Looper.getMainLooper()) private val heartbeatRunnable = object : Runnable { override fun run() { sendHeartbeat() heartbeatHandler.postDelayed(this, MQTTConfig.HEARTBEAT_INTERVAL_MS) } } // ------------------------------------------------------------------------- // Service lifecycle // ------------------------------------------------------------------------- override fun onCreate() { super.onCreate() serialNumber = DeviceInfo.getSerialNumber(this) createNotificationChannel() startForeground(NOTIFICATION_ID, buildNotification("Connecting…")) RemoteControlService.autoEnable(this) YSDKManager.bind(this) initMQTT() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { // Restart automatically if the OS kills the service return START_STICKY } override fun onDestroy() { heartbeatHandler.removeCallbacksAndMessages(null) LogPushService.stop(serialNumber) MQTTManager.shutdown() super.onDestroy() } override fun onBind(intent: Intent?): IBinder? = null // ------------------------------------------------------------------------- // MQTT setup // ------------------------------------------------------------------------- private fun initMQTT() { MQTTManager.onCommandReceived = { command, payload -> CommandDispatcher.dispatch(this, serialNumber, command, payload) } MQTTManager.init(this, serialNumber) // Start heartbeat — first beat immediately, then every 30 s heartbeatHandler.post(heartbeatRunnable) } // ------------------------------------------------------------------------- // Heartbeat — matches TmsTerminalStatusLog / TmsTerminalStatusItem schema // ------------------------------------------------------------------------- private fun sendHeartbeat() { if (!MQTTManager.isConnected) return val timestamp = utcNow() val activityManager = getSystemService(ACTIVITY_SERVICE) as android.app.ActivityManager val memInfo = android.app.ActivityManager.MemoryInfo() activityManager.getMemoryInfo(memInfo) val ramFreeMb = memInfo.availMem / (1024 * 1024) @Suppress("DEPRECATION") val networkType = (getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager) .activeNetworkInfo?.typeName ?: "UNKNOWN" val simDetails = DeviceInfo.getSimDetails(this@MQTTService) val dataUsageMb = DeviceInfo.getTotalDataUsageMb() val mobileDataMb = DeviceInfo.getMobileDataUsageMb() val installedApps = DeviceInfo.getInstalledApps(this@MQTTService) val items = JSONArray().apply { put(statusItem("vendor", MQTTConfig.VENDOR, timestamp)) put(statusItem("model", MQTTConfig.MODEL, timestamp)) put(statusItem("app_version", DeviceInfo.getAppVersion(this@MQTTService), timestamp)) put(statusItem("android_version", Build.VERSION.RELEASE, timestamp)) put(statusItem("ram_free_mb", ramFreeMb.toString(), timestamp)) put(statusItem("network_type", networkType, timestamp)) put(statusItem("kiosk_active", "true", timestamp)) // New fields put(statusItem("sim_operator", simDetails["operator"] ?: "UNKNOWN", timestamp)) put(statusItem("sim_number", simDetails["number"] ?: "UNKNOWN", timestamp)) put(statusItem("sim_iccid", simDetails["iccid"] ?: "UNKNOWN", timestamp)) put(statusItem("sim_imsi", simDetails["imsi"] ?: "UNKNOWN", timestamp)) put(statusItem("sim_iccid2", simDetails["iccid2"] ?: "UNKNOWN", timestamp)) put(statusItem("sim_imsi2", simDetails["imsi2"] ?: "UNKNOWN", timestamp)) put(statusItem("data_usage_mb", dataUsageMb, timestamp)) put(statusItem("mobile_data_mb", mobileDataMb, timestamp)) put(statusItem("installed_apps", installedApps, timestamp)) } val payload = JSONObject().apply { put("oid", UUID.randomUUID().toString()) put("sn", serialNumber) put("uploadTime", timestamp) put("vendor", MQTTConfig.VENDOR) put("model", MQTTConfig.MODEL) put("org.device", items) // backend pattern match requires "org.device" key } MQTTManager.publishJson(MQTTConfig.statusTopic(serialNumber), payload) Log.d(TAG, "Heartbeat sent for $serialNumber") } private fun statusItem(key: String, value: String, timestamp: String) = JSONObject().apply { put("itemkey", key) put("value", value) put("timestamp", timestamp) } // ------------------------------------------------------------------------- // Notification (required for foreground service on Android 8+) // ------------------------------------------------------------------------- private fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( CHANNEL_ID, "TMS Connection", NotificationManager.IMPORTANCE_LOW ).apply { description = "Maintains connection to TMS server" } getSystemService(NotificationManager::class.java).createNotificationChannel(channel) } } private fun buildNotification(status: String): Notification = NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("Kiosk Launcher") .setContentText("TMS: $status") .setSmallIcon(R.drawable.ic_launcher_foreground) .setOngoing(true) .build() // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- private fun utcNow(): String = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US) .apply { timeZone = TimeZone.getTimeZone("UTC") } .format(Date()) companion object { fun start(context: Context) { val intent = Intent(context, MQTTService::class.java) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.startForegroundService(intent) } else { context.startService(intent) } } } }