package com.ultron.jarvistv import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.Service import android.content.Intent import android.os.Build import android.os.IBinder import fi.iki.elonen.NanoHTTPD import org.json.JSONObject /** * Always-on foreground service that hosts a tiny local HTTP command server on port 8080. * Jarvis (me, over your network via the Pi) sends commands here; this routes them to Actions * or the accessibility engine. Runs as a foreground service so Android TV won't kill it. * * Example: curl -s -X POST 10.0.0.42:8080/cmd -d '{"action":"youtube","query":"lofi beats"}' */ class ControlService : Service() { private lateinit var server: CommandServer override fun onCreate() { super.onCreate() startForegroundNotice() server = CommandServer(this).also { it.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false) } UiBus.status("Jarvis online — listening on :$PORT") } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int) = START_STICKY override fun onDestroy() { if (this::server.isInitialized) server.stop() super.onDestroy() } override fun onBind(intent: Intent?): IBinder? = null private fun startForegroundNotice() { val chId = "jarvis" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val nm = getSystemService(NotificationManager::class.java) nm.createNotificationChannel( NotificationChannel(chId, "Jarvis", NotificationManager.IMPORTANCE_LOW)) } val n: Notification = Notification.Builder(this, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) chId else "") .setContentTitle("Jarvis") .setContentText("Listening for commands") .setSmallIcon(R.drawable.ic_jarvis) .build() startForeground(1, n) } /** The command router. Keep the surface small and explicit. */ private class CommandServer(val svc: ControlService) : NanoHTTPD(PORT) { override fun serve(session: IHTTPSession): Response { return try { val params = readBody(session) val action = params.optString("action") val ctx = svc.applicationContext val result = when (action) { "launch" -> Actions.launchApp(ctx, params.getString("package")) "url" -> Actions.openUrl(ctx, params.getString("url")) "google" -> Actions.googleSearch(ctx, params.getString("query")) "youtube" -> Actions.searchAndPlayYouTube(ctx, params.getString("query")) "yt_id" -> Actions.playYouTubeId(ctx, params.getString("id")) "image" -> Actions.showImage(ctx, params.getString("url")) "stream" -> Actions.streamVideo(ctx, params.getString("url")) "back" -> { a11y()?.back(); "back" } "home" -> { a11y()?.home(); "home" } "click" -> { a11y()?.clickText(params.getString("text")); "clicked" } "type" -> { a11y()?.typeText(params.getString("text")); "typed" } "scroll" -> { a11y()?.scrollDown(); "scrolled" } "ping" -> "pong" else -> "Unknown action: $action" } UiBus.status(result) json(mapOf("ok" to true, "result" to result)) } catch (e: Exception) { json(mapOf("ok" to false, "error" to (e.message ?: "error"))) } } private fun a11y() = JarvisAccessibilityService.instance private fun readBody(session: IHTTPSession): JSONObject { val files = HashMap() session.parseBody(files) val raw = files["postData"] if (!raw.isNullOrBlank()) return JSONObject(raw) // also accept query-string params for quick manual testing val o = JSONObject() session.parameters.forEach { (k, v) -> if (v.isNotEmpty()) o.put(k, v[0]) } return o } private fun json(map: Map): Response = newFixedLengthResponse(Response.Status.OK, "application/json", JSONObject(map).toString()) } companion object { const val PORT = 8080 } }