From c5d35bfc935822eceb08cb82f55716380b49141c Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 4 Sep 2026 09:07:08 +0600 Subject: [PATCH] fix(remote-control): update connection handling and improve logging for remote control events --- .../spotube/core/remote/RemoteControlClient.kt | 4 ++++ .../spotube/core/remote/RemoteControlHandler.kt | 17 ++++++++++++----- .../spotube/modules/devices/DevicesScreen.kt | 3 ++- .../modules/devices/RemoteControlScreen.kt | 13 +++++++++---- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/RemoteControlClient.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/RemoteControlClient.kt index e6d268f9..b88fca90 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/RemoteControlClient.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/RemoteControlClient.kt @@ -101,13 +101,16 @@ class RemoteControlClient { private suspend fun receiveLoop(host: String, port: Int) { val currentSession = session ?: return + logger.d { "Starting receive loop for $host:$port" } try { for (frame in currentSession.incoming) { when (frame) { is Frame.Text -> { val text = frame.readText() + logger.d { "Received frame: $text" } try { val event = json.decodeFromString(RemoteControlEvent.serializer(), text) + logger.d { "Parsed event: $event" } when (event) { is RemoteControlEvent.Connected -> { logger.i { "Connection authorized by server" } @@ -136,6 +139,7 @@ class RemoteControlClient { else -> {} } } + logger.d { "Receive loop exited normally" } } catch (e: Exception) { logger.e(e) { "Error in receive loop" } _connectionState.value = ConnectionState.Error(e.message ?: "Connection lost") diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/RemoteControlHandler.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/RemoteControlHandler.kt index f192855a..3c709a95 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/RemoteControlHandler.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/RemoteControlHandler.kt @@ -74,7 +74,7 @@ class RemoteControlHandler( val waitingMessage = RemoteControlEvent.WaitingForPermission( "Waiting for permission from $deviceName..." ) - session.send(Frame.Text(json.encodeToString(RemoteControlEvent.WaitingForPermission.serializer(), waitingMessage))) + session.send(Frame.Text(json.encodeToString(RemoteControlEvent.serializer(), waitingMessage))) val request = ConnectionRequest( deviceId = deviceId ?: "unknown", @@ -99,14 +99,18 @@ class RemoteControlHandler( } // Send connected message - session.send(Frame.Text(json.encodeToString(RemoteControlEvent.Connected.serializer(), RemoteControlEvent.Connected))) + session.send(Frame.Text(json.encodeToString(RemoteControlEvent.serializer(), RemoteControlEvent.Connected))) logger.i { "Remote control connection established from $deviceName ($deviceId)" } + // Broadcast initial player state so the controller shows current track info + broadcastState(session) + try { handleControlLoop(session) } catch (e: Exception) { logger.w(e) { "Error in remote control session" } } finally { + logger.d { "Closing session in finally block" } session.close() } } @@ -129,9 +133,11 @@ class RemoteControlHandler( } private suspend fun handleControlLoop(session: WebSocketServerSession) { + logger.d { "Starting control loop for session" } for (frame in session.incoming) { if (frame is Frame.Text) { val text = frame.readText() + logger.d { "Received command: $text" } try { val envelope = json.decodeFromString(CommandEnvelope.serializer(), text) handleCommand(session, envelope) @@ -141,6 +147,7 @@ class RemoteControlHandler( } } } + logger.d { "Control loop exited normally" } } private suspend fun handleCommand(session: WebSocketServerSession, envelope: CommandEnvelope) { @@ -197,12 +204,12 @@ class RemoteControlHandler( } private suspend fun sendAck(session: WebSocketServerSession, commandId: String) { - val text = json.encodeToString(RemoteControlEvent.Ack.serializer(), RemoteControlEvent.Ack(commandId)) + val text = json.encodeToString(RemoteControlEvent.serializer(), RemoteControlEvent.Ack(commandId)) session.send(Frame.Text(text)) } private suspend fun sendError(session: WebSocketServerSession, message: String) { - val text = json.encodeToString(RemoteControlEvent.Error.serializer(), RemoteControlEvent.Error(message)) + val text = json.encodeToString(RemoteControlEvent.serializer(), RemoteControlEvent.Error(message)) session.send(Frame.Text(text)) } @@ -221,7 +228,7 @@ class RemoteControlHandler( currentTrackAlbum = current?.albumOrNull(), currentTrackCoverUrl = current?.coverUrlOrNull(), ) - val text = json.encodeToString(RemoteControlEvent.PlayerState.serializer(), state) + val text = json.encodeToString(RemoteControlEvent.serializer(), state) session.send(Frame.Text(text)) } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/DevicesScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/DevicesScreen.kt index b4b4ce9c..95366a4a 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/DevicesScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/DevicesScreen.kt @@ -72,7 +72,8 @@ fun DevicesScreen( viewModel.startDiscovery() onDispose { viewModel.stopDiscovery() - viewModel.disconnect() + // Don't disconnect here - the connection should persist when navigating to RemoteControlScreen + // The RemoteControlViewModel will manage the connection lifecycle } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/RemoteControlScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/RemoteControlScreen.kt index e5f1e7f8..5880ee1f 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/RemoteControlScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/RemoteControlScreen.kt @@ -57,6 +57,7 @@ import dev.krtirtho.spotube.core.ui.base.GhostIconButton import dev.krtirtho.spotube.core.ui.base.IconButton import dev.krtirtho.spotube.core.ui.base.Slider import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar +import dev.krtirtho.spotube.modules.shell.LocalAppShellBottomInset import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.IconsaxCloseSquare import dev.krtirtho.spotube.resources.iconsax.IconsaxNext @@ -77,6 +78,7 @@ fun RemoteControlScreen( val viewModel = koinViewModel() val playerState by viewModel.playerState.collectAsStateWithLifecycle() val connectionState by viewModel.connectionState.collectAsStateWithLifecycle() + val shellBottomInset = LocalAppShellBottomInset.current Scaffold( topBar = { @@ -110,14 +112,15 @@ fun RemoteControlScreen( onSetVolume = viewModel::setVolume, onToggleShuffle = viewModel::toggleShuffle, onCycleLoopMode = viewModel::cycleLoopMode, - modifier = Modifier.padding(padding) + modifier = Modifier.padding(padding).padding(bottom = shellBottomInset) ) } is ConnectionState.Connecting -> { Box( modifier = Modifier .fillMaxSize() - .padding(padding), + .padding(padding) + .padding(bottom = shellBottomInset), contentAlignment = Alignment.Center ) { Text("Connecting...") @@ -127,7 +130,8 @@ fun RemoteControlScreen( Box( modifier = Modifier .fillMaxSize() - .padding(padding), + .padding(padding) + .padding(bottom = shellBottomInset), contentAlignment = Alignment.Center ) { Text("Disconnected") @@ -137,7 +141,8 @@ fun RemoteControlScreen( Box( modifier = Modifier .fillMaxSize() - .padding(padding), + .padding(padding) + .padding(bottom = shellBottomInset), contentAlignment = Alignment.Center ) { Text("Connection error: ${(connectionState as ConnectionState.Error).message}")