From 60f146afbc9162c87788c95b160a04d2129ba7f1 Mon Sep 17 00:00:00 2001 From: Fritz Heiden Date: Tue, 1 Apr 2025 08:06:09 +0200 Subject: [PATCH] feat: listen to websocket --- app/build.gradle.kts | 2 +- .../services/ControllerService.kt | 17 -------- .../tvcontroller/services/DeviceService.kt | 43 +++++++++++++++---- .../tvcontroller/ui/views/SettingsView.kt | 15 +------ .../ui/views/SettingsViewModel.kt | 16 ++----- gradle/libs.versions.toml | 3 +- 6 files changed, 43 insertions(+), 53 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 682240c..c4f5c46 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -52,6 +52,7 @@ dependencies { implementation(libs.androidx.navigation.compose) implementation(libs.ktor.client.core) implementation(libs.ktor.client.cio) + implementation(libs.ktor.client.websockets) implementation(libs.androidx.camera.core) implementation(libs.androidx.camera.camera2) implementation(libs.androidx.camera.lifecycle) @@ -60,7 +61,6 @@ dependencies { implementation(libs.androidx.camera.mlkit.vision) implementation(libs.androidx.camera.extensions) implementation(libs.material3) - implementation(libs.opencsv) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/app/src/main/java/com/example/tvcontroller/services/ControllerService.kt b/app/src/main/java/com/example/tvcontroller/services/ControllerService.kt index 27dc16f..b198457 100644 --- a/app/src/main/java/com/example/tvcontroller/services/ControllerService.kt +++ b/app/src/main/java/com/example/tvcontroller/services/ControllerService.kt @@ -2,9 +2,7 @@ package com.example.tvcontroller.services import android.content.Context import android.util.Log -import com.example.tvcontroller.R import com.example.tvcontroller.data.RemoteCommand -import com.opencsv.CSVReader import org.json.JSONObject @@ -38,21 +36,6 @@ class ControllerService( } fun loadCommands() { - Log.i("ControllerService", "Loading commands"); - var inputStream = context.resources.openRawResource(R.raw.samsung) - var csvReader = CSVReader(inputStream.reader()) - csvReader.forEach { nextLine -> - if (nextLine.size < 5) return@forEach - if (nextLine[0] == "functionname") return@forEach - var remoteCommand = RemoteCommand() - remoteCommand.functionName = nextLine[0] - remoteCommand.protocol = "samsung" - remoteCommand.device = nextLine[2] - remoteCommand.subdevice = nextLine[3] - remoteCommand.function = nextLine[4] - samsungCommands[remoteCommand.functionName!!] = remoteCommand - } - Log.i("ControllerService", "Commands loaded: ${samsungCommands.size}") } companion object { diff --git a/app/src/main/java/com/example/tvcontroller/services/DeviceService.kt b/app/src/main/java/com/example/tvcontroller/services/DeviceService.kt index 38c51ff..6563183 100644 --- a/app/src/main/java/com/example/tvcontroller/services/DeviceService.kt +++ b/app/src/main/java/com/example/tvcontroller/services/DeviceService.kt @@ -8,18 +8,25 @@ import io.ktor.client.engine.cio.* import io.ktor.client.* import io.ktor.client.call.body import io.ktor.client.plugins.cookies.HttpCookies +import io.ktor.client.plugins.websocket.WebSockets +import io.ktor.client.plugins.websocket.webSocket 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.Frame +import io.ktor.websocket.readText +import kotlinx.coroutines.runBlocking import org.json.JSONObject private const val SHARED_PREFERENCES_NAME = "devices"; +private const val TAG = "DeviceService" class DeviceService(private val context: Context) { private var client = HttpClient(CIO) { install(HttpCookies) + install(WebSockets) } private var serverAddress: String = "" private var token: String = "" @@ -30,7 +37,7 @@ class DeviceService(private val context: Context) { } suspend fun registerIntegration(name: String, code: String) { - Log.i("DeviceService", "Creating integration for $name with code $code at $serverAddress") + Log.i(TAG, "Creating integration for $name with code $code at $serverAddress") val requestJson = JSONObject() requestJson.put("name", name) requestJson.put("code", code) @@ -49,21 +56,21 @@ class DeviceService(private val context: Context) { val responseJson = JSONObject(body) if (response.status.value != 200) { val error = responseJson.getString("error") - Log.e("DeviceService", "Error getting integration: ${response.status.value} $error") + Log.e(TAG, "Error getting integration: ${response.status.value} $error") return } token = responseJson.getString("token") deviceId = responseJson.getString("id") savePreferences() - Log.i("DeviceService", "Response: ${response.status.value} $body") + Log.i(TAG, "Response: ${response.status.value} $body") } catch (e: Exception) { - Log.e("DeviceService", "Error registering integration", e) + Log.e(TAG, "Error registering integration", e) } } suspend fun getIntegration(): Integration? { - Log.i("DeviceService", "Getting integration $deviceId at $serverAddress") + Log.i(TAG, "Getting integration $deviceId at $serverAddress") try { val response: HttpResponse = client.request("http://$serverAddress/api/integrations/$deviceId") { @@ -77,7 +84,7 @@ class DeviceService(private val context: Context) { val responseJson = JSONObject(body) if (response.status.value != 200) { val error = responseJson.getString("error") - Log.e("DeviceService", "Error getting integration: ${response.status.value} $error") + Log.e(TAG, "Error getting integration: ${response.status.value} $error") return null } val integration = Integration( @@ -86,7 +93,7 @@ class DeviceService(private val context: Context) { ) return integration } catch (e: Exception) { - Log.e("DeviceService", "Error getting integration", e) + Log.e(TAG, "Error getting integration", e) } return null } @@ -96,7 +103,7 @@ class DeviceService(private val context: Context) { serverAddress = sharedPreferences.getString("server_address", "")!! token = sharedPreferences.getString("token", "")!! deviceId = sharedPreferences.getString("device_id", "")!! - Log.i("DeviceService", "Loaded preferences: $serverAddress $token") + Log.i(TAG, "Loaded preferences: $serverAddress $token") } private fun savePreferences() { @@ -110,6 +117,26 @@ class DeviceService(private val context: Context) { } } + fun connect() { + Log.i(TAG, "Connecting to websocket at $serverAddress") + runBlocking { + // split server address into host and port + val (host, port) = serverAddress.split(":") + // if no port is specified, assume 80 + val portInt = if (port.isEmpty()) 80 else port.toInt() + client.webSocket(method = HttpMethod.Get, host = host, port = portInt, path = "/ws") { + Log.i(TAG, "Listening for incoming websocket messages") + while (true) { + val frame = incoming.receive() + if (frame is Frame.Text) { + val message = frame.readText() + Log.i(TAG, "Received message: $message") + } + } + } + } + } + fun setServerAddress(url: String) { serverAddress = url } diff --git a/app/src/main/java/com/example/tvcontroller/ui/views/SettingsView.kt b/app/src/main/java/com/example/tvcontroller/ui/views/SettingsView.kt index bdd4bf1..6aebdbc 100644 --- a/app/src/main/java/com/example/tvcontroller/ui/views/SettingsView.kt +++ b/app/src/main/java/com/example/tvcontroller/ui/views/SettingsView.kt @@ -32,24 +32,20 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.example.tvcontroller.R import com.example.tvcontroller.Settings -import com.example.tvcontroller.ui.views.SettingsViewModel import com.example.tvcontroller.ui.views.SettingsViewModel.Companion.CONNECT_CONTROLLER_VIEW import com.example.tvcontroller.ui.views.SettingsViewModel.Companion.MAIN_SETTINGS_VIEW import com.example.tvcontroller.services.BluetoothService -import com.example.tvcontroller.services.ControllerService import com.example.tvcontroller.services.DeviceService -import com.example.tvcontroller.webrtc.RtcPeerConnection @OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsView( deviceService: DeviceService, - bluetoothService: BluetoothService, - rtcPeerConnection: RtcPeerConnection + bluetoothService: BluetoothService ) { val viewModel = viewModel( factory = SettingsViewModel.provideFactory( - deviceService, bluetoothService, rtcPeerConnection + deviceService, bluetoothService ) ) val navController = rememberNavController() @@ -121,13 +117,6 @@ fun SettingsView( stringResource(id = R.string.connect_button_label) ) } - - Button( - onClick = { viewModel.connectRtcPeerConnection() }, - modifier = Modifier.fillMaxWidth() - ) { - Text(text = "Connect RTC Peer") - } } } diff --git a/app/src/main/java/com/example/tvcontroller/ui/views/SettingsViewModel.kt b/app/src/main/java/com/example/tvcontroller/ui/views/SettingsViewModel.kt index 8e98c72..cf62376 100644 --- a/app/src/main/java/com/example/tvcontroller/ui/views/SettingsViewModel.kt +++ b/app/src/main/java/com/example/tvcontroller/ui/views/SettingsViewModel.kt @@ -12,14 +12,12 @@ import com.example.tvcontroller.Settings import com.example.tvcontroller.data.BluetoothDevice import com.example.tvcontroller.services.BluetoothService import com.example.tvcontroller.services.DeviceService -import com.example.tvcontroller.webrtc.RtcPeerConnection import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch class SettingsViewModel( private val deviceService: DeviceService, - private val bluetoothService: BluetoothService, - private val rtcPeerConnection: RtcPeerConnection + private val bluetoothService: BluetoothService ) : ViewModel() { var serverAddress by mutableStateOf(deviceService.getServerAddress()) private set @@ -54,12 +52,7 @@ class SettingsViewModel( deviceService.registerIntegration(deviceName, registrationCode) updateConnectionState() updateDeviceInfo() - } - } - - fun connectRtcPeerConnection() { - viewModelScope.launch(Dispatchers.IO) { - rtcPeerConnection.connect() + deviceService.connect() } } @@ -112,12 +105,11 @@ class SettingsViewModel( fun provideFactory( deviceService: DeviceService, - bluetoothService: BluetoothService, - rtcPeerConnection: RtcPeerConnection + bluetoothService: BluetoothService ) = object : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T { - return SettingsViewModel(deviceService, bluetoothService, rtcPeerConnection) as T + return SettingsViewModel(deviceService, bluetoothService) as T } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 05418c1..a1ea627 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,6 @@ activityCompose = "1.8.0" composeBom = "2024.04.01" material3 = "1.4.0-alpha10" navigationCompose = "2.8.4" -opencsv = "4.6" [libraries] androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "cameraCore" } @@ -39,8 +38,8 @@ androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit androidx-material3 = { group = "androidx.compose.material3", name = "material3" } ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" } material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" } -opencsv = { module = "com.opencsv:opencsv", version.ref = "opencsv" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }