11package dev.agixt.agixt
22
3+ import android.content.BroadcastReceiver
34import android.content.Context
5+ import android.content.Intent
6+ import android.content.IntentFilter
7+ import android.os.Build
48import android.util.Log
59import io.flutter.plugin.common.MethodChannel
610import io.flutter.plugin.common.BinaryMessenger
@@ -34,6 +38,10 @@ class WatchHandler(
3438 private val PATH_AUDIO_DATA = " /audio_data"
3539 private val PATH_CONNECTION_STATUS = " /connection_status"
3640 private val PATH_ERROR = " /error"
41+ // Audio streaming paths for TTS playback on watch
42+ private val PATH_AUDIO_HEADER = " /audio_header"
43+ private val PATH_AUDIO_CHUNK = " /audio_chunk"
44+ private val PATH_AUDIO_END = " /audio_end"
3745
3846 private lateinit var methodChannel: MethodChannel
3947 private var connectedNodeId: String? = null
@@ -42,9 +50,48 @@ class WatchHandler(
4250
4351 private val scope = CoroutineScope (Dispatchers .IO + SupervisorJob ())
4452
53+ // Broadcast receiver for WearableMessageService
54+ private val voiceInputReceiver = object : BroadcastReceiver () {
55+ override fun onReceive (context : Context ? , intent : Intent ? ) {
56+ if (intent?.action == WearableMessageService .ACTION_VOICE_INPUT ) {
57+ val text = intent.getStringExtra(WearableMessageService .EXTRA_TEXT )
58+ val nodeId = intent.getStringExtra(WearableMessageService .EXTRA_NODE_ID )
59+ if (! text.isNullOrEmpty()) {
60+ Log .d(TAG , " Received voice input broadcast: $text (from $nodeId )" )
61+ sendVoiceInputToFlutter(text, nodeId)
62+ }
63+ }
64+ }
65+ }
66+
67+ // / Handle voice input from MainActivity intent (when app wasn't running)
68+ fun handleVoiceInputFromIntent (text : String , nodeId : String? ) {
69+ Log .d(TAG , " Handling voice input from intent: $text (from $nodeId )" )
70+ sendVoiceInputToFlutter(text, nodeId)
71+ }
72+
73+ private fun sendVoiceInputToFlutter (text : String , nodeId : String? ) {
74+ try {
75+ methodChannel.invokeMethod(" onWatchVoiceInput" , mapOf (
76+ " text" to text,
77+ " nodeId" to nodeId
78+ ))
79+ } catch (e: Exception ) {
80+ Log .e(TAG , " Error sending voice input to Flutter" , e)
81+ }
82+ }
83+
4584 fun initialize () {
4685 methodChannel = MethodChannel (binaryMessenger, CHANNEL )
4786
87+ // Register broadcast receiver for voice input from WearableMessageService
88+ val intentFilter = IntentFilter (WearableMessageService .ACTION_VOICE_INPUT )
89+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .TIRAMISU ) {
90+ context.registerReceiver(voiceInputReceiver, intentFilter, Context .RECEIVER_NOT_EXPORTED )
91+ } else {
92+ context.registerReceiver(voiceInputReceiver, intentFilter)
93+ }
94+
4895 methodChannel.setMethodCallHandler { call, result ->
4996 when (call.method) {
5097 " initialize" -> {
@@ -144,6 +191,70 @@ class WatchHandler(
144191 }
145192 }
146193 }
194+ " sendAudioHeader" -> {
195+ // Send audio format header to watch for TTS playback
196+ val sampleRate = call.argument<Int >(" sampleRate" ) ? : 24000
197+ val bitsPerSample = call.argument<Int >(" bitsPerSample" ) ? : 16
198+ val channels = call.argument<Int >(" channels" ) ? : 1
199+ val nodeId = call.argument<String >(" nodeId" )
200+
201+ // Pack header: 4 bytes sample rate, 2 bytes bits, 2 bytes channels
202+ val header = ByteArray (8 )
203+ header[0 ] = (sampleRate and 0xFF ).toByte()
204+ header[1 ] = ((sampleRate shr 8 ) and 0xFF ).toByte()
205+ header[2 ] = ((sampleRate shr 16 ) and 0xFF ).toByte()
206+ header[3 ] = ((sampleRate shr 24 ) and 0xFF ).toByte()
207+ header[4 ] = (bitsPerSample and 0xFF ).toByte()
208+ header[5 ] = ((bitsPerSample shr 8 ) and 0xFF ).toByte()
209+ header[6 ] = (channels and 0xFF ).toByte()
210+ header[7 ] = ((channels shr 8 ) and 0xFF ).toByte()
211+
212+ scope.launch {
213+ val success = if (nodeId != null ) {
214+ sendMessageToNode(nodeId, PATH_AUDIO_HEADER , header)
215+ } else {
216+ sendMessageToWatch(PATH_AUDIO_HEADER , header)
217+ }
218+ withContext(Dispatchers .Main ) {
219+ result.success(success)
220+ }
221+ }
222+ }
223+ " sendAudioChunk" -> {
224+ // Send audio PCM data chunk to watch
225+ val audioData = call.argument<ByteArray >(" audioData" )
226+ val nodeId = call.argument<String >(" nodeId" )
227+
228+ if (audioData == null ) {
229+ result.success(false )
230+ return @setMethodCallHandler
231+ }
232+
233+ scope.launch {
234+ val success = if (nodeId != null ) {
235+ sendMessageToNode(nodeId, PATH_AUDIO_CHUNK , audioData)
236+ } else {
237+ sendMessageToWatch(PATH_AUDIO_CHUNK , audioData)
238+ }
239+ withContext(Dispatchers .Main ) {
240+ result.success(success)
241+ }
242+ }
243+ }
244+ " sendAudioEnd" -> {
245+ // Signal end of audio stream
246+ val nodeId = call.argument<String >(" nodeId" )
247+ scope.launch {
248+ val success = if (nodeId != null ) {
249+ sendMessageToNode(nodeId, PATH_AUDIO_END , byteArrayOf())
250+ } else {
251+ sendMessageToWatch(PATH_AUDIO_END , byteArrayOf())
252+ }
253+ withContext(Dispatchers .Main ) {
254+ result.success(success)
255+ }
256+ }
257+ }
147258 " checkConnection" -> {
148259 scope.launch {
149260 checkWatchConnection()
@@ -341,6 +452,7 @@ class WatchHandler(
341452
342453 fun destroy () {
343454 try {
455+ context.unregisterReceiver(voiceInputReceiver)
344456 Wearable .getDataClient(context).removeListener(this )
345457 Wearable .getMessageClient(context).removeListener(this )
346458 Wearable .getCapabilityClient(context).removeListener(this )
0 commit comments