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.graphics.Bitmap import android.graphics.PixelFormat import android.hardware.display.DisplayManager import android.hardware.display.VirtualDisplay import android.media.ImageReader import android.media.projection.MediaProjection import android.media.projection.MediaProjectionManager import android.os.Handler import android.os.HandlerThread import android.os.IBinder import android.util.DisplayMetrics import android.util.Log import android.view.WindowManager import com.shukria.kiosklauncher.util.MQTTConfig import com.shukria.kiosklauncher.util.MQTTManager import java.io.ByteArrayOutputStream class ScreenShareService : Service() { companion object { private const val TAG = "ScreenShareService" private const val CHANNEL_ID = "screen_share_channel" private const val NOTIF_ID = 2002 private const val FRAME_INTERVAL_MS = 1000L private const val JPEG_QUALITY = 50 const val EXTRA_SERIAL_NUMBER = "serial_number" private var instance: ScreenShareService? = null fun prepare(context: Context, serialNumber: String) { val intent = Intent(context, ScreenShareService::class.java).apply { putExtra(EXTRA_SERIAL_NUMBER, serialNumber) } context.startForegroundService(intent) } fun deliverAndCapture(resultCode: Int, data: Intent) { val svc = instance if (svc != null) { svc.beginCapture(resultCode, data) } else { Log.e(TAG, "deliverAndCapture: service instance is null — was prepare() called?") } } fun stop(context: Context) { context.stopService(Intent(context, ScreenShareService::class.java)) } fun isRunning() = instance != null } private var mediaProjection: MediaProjection? = null private var virtualDisplay: VirtualDisplay? = null private var imageReader: ImageReader? = null private var handlerThread: HandlerThread? = null private var handler: Handler? = null private var serialNumber = "" @Volatile private var active = false override fun onCreate() { super.onCreate() instance = this createNotificationChannel() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { startForeground(NOTIF_ID, buildNotification()) serialNumber = intent?.getStringExtra(EXTRA_SERIAL_NUMBER) ?: serialNumber Log.i(TAG, "FGS prepared, waiting for consent — sn=$serialNumber") return START_NOT_STICKY } fun beginCapture(resultCode: Int, data: Intent) { Log.i(TAG, "beginCapture called, resultCode=$resultCode") val mgr = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager mediaProjection = mgr.getMediaProjection(resultCode, data) if (mediaProjection == null) { Log.e(TAG, "getMediaProjection() returned null, stopping") stopSelf() return } startCapture() } private fun startCapture() { val wm = getSystemService(WINDOW_SERVICE) as WindowManager val metrics = DisplayMetrics().also { wm.defaultDisplay.getRealMetrics(it) } val width = metrics.widthPixels / 2 val height = metrics.heightPixels / 2 val density = metrics.densityDpi handlerThread = HandlerThread("ScreenShare").also { it.start() } handler = Handler(handlerThread!!.looper) imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 2) virtualDisplay = mediaProjection?.createVirtualDisplay( "TMS-ScreenShare", width, height, density, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, imageReader?.surface, null, handler ) active = true handler?.post(object : Runnable { override fun run() { if (!active) return captureAndPublish(width, height) handler?.postDelayed(this, FRAME_INTERVAL_MS) } }) Log.i(TAG, "Screen capture started ${width}x${height} @ 1fps → sn=$serialNumber") } private fun captureAndPublish(width: Int, height: Int) { val image = imageReader?.acquireLatestImage() ?: return try { val plane = image.planes[0] val buffer = plane.buffer val pixelStride = plane.pixelStride val rowStride = plane.rowStride val rowPadding = rowStride - pixelStride * width val bitmap = Bitmap.createBitmap(width + rowPadding / pixelStride, height, Bitmap.Config.ARGB_8888) bitmap.copyPixelsFromBuffer(buffer) val cropped = Bitmap.createBitmap(bitmap, 0, 0, width, height) bitmap.recycle() val out = ByteArrayOutputStream() cropped.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, out) cropped.recycle() val bytes = out.toByteArray() MQTTManager.publishBinary(MQTTConfig.screenPushTopic(serialNumber), bytes, qos = 0) Log.v(TAG, "Published frame ${bytes.size} bytes") } catch (e: Exception) { Log.w(TAG, "Frame capture error: ${e.message}") } finally { image.close() } } override fun onDestroy() { active = false virtualDisplay?.release() mediaProjection?.stop() imageReader?.close() handlerThread?.quitSafely() instance = null Log.i(TAG, "ScreenShareService destroyed") super.onDestroy() } override fun onBind(intent: Intent?): IBinder? = null private fun createNotificationChannel() { val channel = NotificationChannel( CHANNEL_ID, "Screen Share", NotificationManager.IMPORTANCE_LOW ) getSystemService(NotificationManager::class.java).createNotificationChannel(channel) } private fun buildNotification(): Notification = Notification.Builder(this, CHANNEL_ID) .setContentTitle("Screen Share Active") .setContentText("Streaming to TMS") .setSmallIcon(android.R.drawable.ic_menu_camera) .build() }