From 16f581f479fa42f1ae7196116cf93ddd2b5ff380 Mon Sep 17 00:00:00 2001 From: Sanju Sivalingam Date: Fri, 20 Feb 2026 02:11:41 +0530 Subject: [PATCH] feat(android): wire voice recording and transcript into ConnectionService --- .../droidclaw/connection/CommandRouter.kt | 11 ++++++++ .../droidclaw/connection/ConnectionService.kt | 26 ++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/thisux/droidclaw/connection/CommandRouter.kt b/android/app/src/main/java/com/thisux/droidclaw/connection/CommandRouter.kt index 6558e68..f51e7d8 100644 --- a/android/app/src/main/java/com/thisux/droidclaw/connection/CommandRouter.kt +++ b/android/app/src/main/java/com/thisux/droidclaw/connection/CommandRouter.kt @@ -62,8 +62,19 @@ class CommandRouter( currentSteps.value = currentSteps.value + step Log.d(TAG, "Step ${step.step}: ${step.reasoning}") } + "transcript_partial" -> { + ConnectionService.overlayTranscript.value = msg.text ?: "" + ConnectionService.instance?.overlay?.updateTranscript(msg.text ?: "") + Log.d(TAG, "Transcript partial: ${msg.text}") + } + "transcript_final" -> { + ConnectionService.overlayTranscript.value = msg.text ?: "" + ConnectionService.instance?.overlay?.updateTranscript(msg.text ?: "") + Log.d(TAG, "Transcript final: ${msg.text}") + } "goal_completed" -> { currentGoalStatus.value = if (msg.success == true) GoalStatus.Completed else GoalStatus.Failed + ConnectionService.instance?.overlay?.returnToIdle() Log.i(TAG, "Goal completed: success=${msg.success}, steps=${msg.stepsUsed}") } "goal_failed" -> { diff --git a/android/app/src/main/java/com/thisux/droidclaw/connection/ConnectionService.kt b/android/app/src/main/java/com/thisux/droidclaw/connection/ConnectionService.kt index a267066..e1d9fa6 100644 --- a/android/app/src/main/java/com/thisux/droidclaw/connection/ConnectionService.kt +++ b/android/app/src/main/java/com/thisux/droidclaw/connection/ConnectionService.kt @@ -30,6 +30,11 @@ import android.net.Uri import android.provider.Settings import com.thisux.droidclaw.model.StopGoalMessage import com.thisux.droidclaw.overlay.AgentOverlay +import com.thisux.droidclaw.model.VoiceStartMessage +import com.thisux.droidclaw.model.VoiceChunkMessage +import com.thisux.droidclaw.model.VoiceStopMessage +import com.thisux.droidclaw.model.OverlayMode +import androidx.compose.runtime.snapshotFlow import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first @@ -47,6 +52,7 @@ class ConnectionService : LifecycleService() { val currentGoalStatus = MutableStateFlow(GoalStatus.Idle) val currentGoal = MutableStateFlow("") val errorMessage = MutableStateFlow(null) + val overlayTranscript = MutableStateFlow("") var instance: ConnectionService? = null const val ACTION_CONNECT = "com.thisux.droidclaw.CONNECT" @@ -59,13 +65,31 @@ class ConnectionService : LifecycleService() { private var commandRouter: CommandRouter? = null private var captureManager: ScreenCaptureManager? = null private var wakeLock: PowerManager.WakeLock? = null - private var overlay: AgentOverlay? = null + internal var overlay: AgentOverlay? = null override fun onCreate() { super.onCreate() instance = this createNotificationChannel() overlay = AgentOverlay(this) + overlay?.onAudioChunk = { base64 -> + webSocket?.sendTyped(VoiceChunkMessage(data = base64)) + } + overlay?.onVoiceSend = { _ -> + webSocket?.sendTyped(VoiceStopMessage(action = "send")) + } + overlay?.onVoiceCancel = { + webSocket?.sendTyped(VoiceStopMessage(action = "cancel")) + } + overlay?.let { ov -> + lifecycleScope.launch { + snapshotFlow { ov.mode.value }.collect { mode -> + if (mode == OverlayMode.Listening) { + webSocket?.sendTyped(VoiceStartMessage()) + } + } + } + } } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {