package com.example.tvcontroller.services import android.content.Context import android.content.Context.MODE_PRIVATE import android.util.Log import com.example.tvcontroller.data.Integration import com.example.tvcontroller.webrtc.RtcPeerConnection import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.engine.cio.CIO import io.ktor.client.plugins.websocket.WebSockets import io.ktor.client.plugins.websocket.webSocket import io.ktor.client.request.cookie import io.ktor.client.request.headers import io.ktor.client.request.request import io.ktor.client.request.setBody import io.ktor.client.statement.HttpResponse import io.ktor.http.HttpMethod import io.ktor.websocket.DefaultWebSocketSession import io.ktor.websocket.Frame import io.ktor.websocket.readText import kotlinx.coroutines.runBlocking import org.json.JSONObject import org.webrtc.IceCandidate private const val SHARED_PREFERENCES_NAME = "devices"; private const val TAG = "DeviceService" class DeviceService(private val context: Context) { private var client = HttpClient(CIO) { install(WebSockets) } private var websocket: DefaultWebSocketSession? = null private var serverAddress: String = "" private var token: String = "" private var deviceId: String = "" var rtcPeerConnection: RtcPeerConnection? = null suspend fun initialize() { loadPreferences() if (token.isEmpty()) return getIntegration()?.let { connect() } } suspend fun registerIntegration(name: String, code: String) { Log.i(TAG, "Creating integration for $name with code $code at $serverAddress") val requestJson = JSONObject() requestJson.put("name", name) requestJson.put("code", code) token = "" deviceId = "" try { val response: HttpResponse = client.request("http://$serverAddress/api/integrations/register") { method = HttpMethod.Post setBody(requestJson.toString()) headers { append("Content-Type", "application/json") } cookie(name = "token", value = token) } val body: String = response.body() val responseJson = JSONObject(body) if (response.status.value != 200) { val error = responseJson.getString("error") Log.e(TAG, "Error getting integration: ${response.status.value} $error") return } token = responseJson.getString("token") deviceId = responseJson.getString("id") savePreferences() Log.i(TAG, "Response: ${response.status.value} $body") } catch (e: Exception) { Log.e(TAG, "Error registering integration", e) } } suspend fun getIntegration(): Integration? { Log.i(TAG, "Getting integration $deviceId at $serverAddress") try { val response: HttpResponse = client.request("http://$serverAddress/api/integrations/$deviceId") { method = HttpMethod.Get headers { append("Authorization", "Bearer $token") } cookie(name = "token", value = token) } val body: String = response.body() val responseJson = JSONObject(body) if (response.status.value != 200) { val error = responseJson.getString("error") Log.e(TAG, "Error getting integration: ${response.status.value} $error") return null } val integration = Integration( responseJson.getString("id"), responseJson.getString("name") ) return integration } catch (e: Exception) { Log.e(TAG, "Error getting integration", e) } return null } private fun loadPreferences() { val sharedPreferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, MODE_PRIVATE) serverAddress = sharedPreferences.getString("server_address", "")!! token = sharedPreferences.getString("token", "")!! deviceId = sharedPreferences.getString("device_id", "")!! Log.i(TAG, "Loaded preferences: $serverAddress $token") } private fun savePreferences() { val sharedPreferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, MODE_PRIVATE) val editor = sharedPreferences.edit() editor.apply { putString("server_address", serverAddress) putString("token", token) putString("device_id", deviceId) apply() } } suspend fun connect() { Log.i(TAG, "Connecting to websocket at $serverAddress") val (host, port) = serverAddress.split(":") val portInt = if (port.isEmpty()) 80 else port.toInt() client.webSocket( method = HttpMethod.Get, host = host, port = portInt, path = "/ws", request = { cookie(name = "token", value = token) } ) { Log.i(TAG, "Listening for incoming websocket messages") websocket = this while (true) { val frame = incoming.receive() if (frame is Frame.Text) { val dataString = frame.readText() val dataJson = JSONObject(dataString) val senderId = dataJson.getString("sender") val message = dataJson.getJSONObject("message") val type = message.getString("type") when (type) { RtcPeerConnection.TYPE_OFFER -> { Log.i(TAG, "Received offer from $senderId") val sdp = message.getString("sdp") rtcPeerConnection?.connect(senderId, sdp) } RtcPeerConnection.TYPE_ICE_CANDIDATE -> { Log.i(TAG, "Received ice candidate") val candidateString = message.getString("candidate") val candidateJson = JSONObject(candidateString) val sdpMid = candidateJson.getString("sdpMid") val sdpMLineIndex = candidateJson.getInt("sdpMLineIndex") val sdp = candidateJson.getString("candidate") val candidate = IceCandidate(sdpMid, sdpMLineIndex, sdp) Log.i(TAG, "Candidate: $candidate") rtcPeerConnection?.addIceCandidate(candidate) } } } } } } fun sendWebRtcAnswer(targetId: String, sdp: String) { val messageJson = JSONObject() messageJson.put("type", TYPE_SIGNALING) messageJson.put("target", targetId) messageJson.put("message", JSONObject().apply { put("sdp", sdp) put("type", RtcPeerConnection.TYPE_ANSWER) }) runBlocking { Log.i(TAG, "Sending answer") val frame = Frame.Text(messageJson.toString()) websocket?.send(frame) } } fun sendWebRtcOffer(targetId: String, sdp: String) { val messageJson = JSONObject() messageJson.put("type", TYPE_SIGNALING) messageJson.put("target", targetId) messageJson.put("message", JSONObject().apply { put("sdp", sdp) put("type", RtcPeerConnection.TYPE_OFFER) }) runBlocking { Log.i(TAG, "Sending offer") val frame = Frame.Text(messageJson.toString()) websocket?.send(frame) } } fun sendWebRtcIceCandidate(targetId: String, candidate: IceCandidate) { val messageJson = JSONObject() messageJson.put("type", TYPE_SIGNALING) messageJson.put("target", targetId) messageJson.put("message", JSONObject().apply { put("candidate", JSONObject().apply { put("sdpMid", candidate.sdpMid) put("sdpMLineIndex", candidate.sdpMLineIndex) put("candidate", candidate.sdp) }) put("type", RtcPeerConnection.TYPE_ICE_CANDIDATE) }) runBlocking { Log.i(TAG, "Sending ice candidate") val frame = Frame.Text(messageJson.toString()) websocket?.send(frame) } } fun setServerAddress(url: String) { serverAddress = url } fun getServerAddress(): String { return serverAddress } fun getToken(): String { return token } companion object { const val TYPE_SIGNALING = "signaling" } }