package com.shukria.kiosklauncher.service import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.Service import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.net.ConnectivityManager import android.net.wifi.WifiManager 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 com.shukria.kiosklauncher.util.WifiController 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 var wifiStateReceiver: BroadcastReceiver? = null private lateinit var serialNumber: String private val heartbeatHandler = Handler(Looper.getMainLooper()) private val heartbeatRunnable = object : Runnable { override fun run() { sendHeartbeat() heartbeatHandler.postDelayed(this, MQTTConfig.getHeartbeatIntervalMs(applicationContext)) } } private fun resetHeartbeat() { heartbeatHandler.removeCallbacks(heartbeatRunnable) heartbeatHandler.post(heartbeatRunnable) } // ------------------------------------------------------------------------- // Service lifecycle // ------------------------------------------------------------------------- override fun onCreate() { super.onCreate() createNotificationChannel() startForeground(NOTIFICATION_ID, buildNotification("Connecting…")) RemoteControlService.autoEnable(this) // 1. If Wi-Fi check setting is enabled, turn off Wi-Fi immediately via DPM val prefs = getSharedPreferences("settings_prefs", Context.MODE_PRIVATE) if (prefs.getBoolean("wifi_check_enabled", false)) { Log.i(TAG, "Wi-Fi check enabled on startup – disabling Wi-Fi via WifiController") WifiController.disableWifi(this) } // 2. Register dynamic receiver to prevent Wi-Fi from being turned on in background val filter = IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION) wifiStateReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val wifiState = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_UNKNOWN) if (wifiState == WifiManager.WIFI_STATE_ENABLED || wifiState == WifiManager.WIFI_STATE_ENABLING) { val currentPrefs = context.getSharedPreferences("settings_prefs", Context.MODE_PRIVATE) if (currentPrefs.getBoolean("wifi_check_enabled", false)) { Log.i(TAG, "Wi-Fi turned on but 'wifi check' is enabled – forcing disable via WifiController") WifiController.disableWifi(context) } } } } registerReceiver(wifiStateReceiver, filter) // Bind YSDK and wait for connection before initializing MQTT YSDKManager.bind(this) YSDKManager.runWhenReady { serialNumber = DeviceInfo.getSerialNumber(this) initMQTT() } } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (intent?.action == ACTION_UPDATE_HEARTBEAT) { resetHeartbeat() } // Restart automatically if the OS kills the service return START_STICKY } override fun onDestroy() { // Unregister wifiStateReceiver to avoid context leaks wifiStateReceiver?.let { try { unregisterReceiver(it) } catch (e: Exception) { Log.e(TAG, "Error unregistering wifiStateReceiver", e) } } 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 } val payloadString = payload.toString() Log.d(TAG, "Payload Size: ${payloadString.toByteArray().size} bytes") 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 { const val ACTION_UPDATE_HEARTBEAT = "com.shukria.kiosklauncher.action.UPDATE_HEARTBEAT" 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) } } fun updateHeartbeatInterval(context: Context) { val intent = Intent(context, MQTTService::class.java).apply { action = ACTION_UPDATE_HEARTBEAT } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.startForegroundService(intent) } else { context.startService(intent) } } } }