diff --git a/app/src/main/java/com/example/ava/esphome/voiceassistant/VoiceAssistant.kt b/app/src/main/java/com/example/ava/esphome/voiceassistant/VoiceAssistant.kt index 6457a39..f712803 100644 --- a/app/src/main/java/com/example/ava/esphome/voiceassistant/VoiceAssistant.kt +++ b/app/src/main/java/com/example/ava/esphome/voiceassistant/VoiceAssistant.kt @@ -6,8 +6,6 @@ import com.example.ava.esphome.Connected import com.example.ava.esphome.Disconnected import com.example.ava.esphome.EspHomeState import com.example.ava.esphome.voiceassistant.VoiceTimer.Companion.timerFromEvent -import com.example.ava.tasker.StopRingingRunner -import com.example.ava.tasker.WakeSatelliteRunner import com.example.esphomeproto.api.VoiceAssistantAnnounceRequest import com.example.esphomeproto.api.VoiceAssistantConfigurationRequest import com.example.esphomeproto.api.VoiceAssistantEventResponse @@ -72,14 +70,22 @@ class VoiceAssistant( @RequiresPermission(Manifest.permission.RECORD_AUDIO) fun start() { startVoiceInput() - - // Wire up tasker actions - WakeSatelliteRunner.register { scope.launch { wakeAssistant() } } - StopRingingRunner.register { scope.launch { stopTimer() } } } fun subscribe() = subscription.asSharedFlow() + fun wakeAssistant() { + scope.launch { doWakeAssistant() } + } + + fun stopAssistant() { + scope.launch { doStopAssistant() } + } + + fun stopTimer() { + doStopTimer() + } + @RequiresPermission(Manifest.permission.RECORD_AUDIO) private fun startVoiceInput() = isConnected .flatMapLatest { isConnected -> @@ -207,21 +213,21 @@ class VoiceAssistant( // Allow using the wake word to stop the timer. // TODO: Should the assistant also wake? if (isRinging) { - stopTimer() + doStopTimer() } else { - wakeAssistant(wakeWordPhrase) + doWakeAssistant(wakeWordPhrase) } } private suspend fun onStopDetected() { if (isRinging) { - stopTimer() + doStopTimer() } else { - stopAssistant() + doStopAssistant() } } - private suspend fun wakeAssistant( + private suspend fun doWakeAssistant( wakeWordPhrase: String = "", isContinueConversation: Boolean = false ) { @@ -257,7 +263,7 @@ class VoiceAssistant( ended = { onTtsFinished(it) } ) - private suspend fun stopAssistant() { + private suspend fun doStopAssistant() { // Ignore the stop request if the assistant is idle or currently streaming // microphone audio as there's either nothing to stop or the stop word was // used incidentally as part of the voice command. @@ -268,7 +274,7 @@ class VoiceAssistant( voiceOutput.unDuck() } - private fun stopTimer() { + private fun doStopTimer() { Timber.d("Stop timer") if (isRinging) { _ringingTimer.update { null } @@ -281,7 +287,7 @@ class VoiceAssistant( Timber.d("TTS finished") if (continueConversation) { Timber.d("Continuing conversation") - wakeAssistant(isContinueConversation = true) + doWakeAssistant(isContinueConversation = true) } else { voiceOutput.unDuck() } @@ -295,7 +301,7 @@ class VoiceAssistant( scope.launch { onTimerSoundFinished(it) } } } else { - stopTimer() + doStopTimer() } } else { voiceOutput.unDuck() @@ -316,7 +322,5 @@ class VoiceAssistant( override fun close() { scope.cancel() voiceOutput.close() - WakeSatelliteRunner.unregister() - StopRingingRunner.unregister() } } \ No newline at end of file diff --git a/app/src/main/java/com/example/ava/services/VoiceSatelliteService.kt b/app/src/main/java/com/example/ava/services/VoiceSatelliteService.kt index 9dc4de6..4ab5353 100644 --- a/app/src/main/java/com/example/ava/services/VoiceSatelliteService.kt +++ b/app/src/main/java/com/example/ava/services/VoiceSatelliteService.kt @@ -15,14 +15,12 @@ import com.example.ava.nsd.NsdRegistration import com.example.ava.nsd.registerVoiceSatelliteNsd import com.example.ava.settings.VoiceSatelliteSettings import com.example.ava.settings.VoiceSatelliteSettingsStore -import com.example.ava.tasker.ActivityConfigAvaActivity -import com.example.ava.tasker.AvaActivityRunner +import com.example.ava.tasker.observeTaskerState import com.example.ava.utils.translate import com.example.ava.wakelocks.WifiWakeLock -import com.joaomgcd.taskerpluginlibrary.extensions.requestQuery import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf @@ -112,11 +110,8 @@ class VoiceSatelliteService() : LifecycleService() { return super.onStartCommand(intent, flags, startId) } - private fun startTaskerStateObserver() { - combine(voiceSatelliteState, voiceTimers) { state, timers -> - AvaActivityRunner.updateState(state, timers) - ActivityConfigAvaActivity::class.java.requestQuery(this) - }.launchIn(lifecycleScope) + private fun startTaskerStateObserver() = lifecycleScope.launch { + _voiceSatellite.collectLatest { it?.observeTaskerState(this@VoiceSatelliteService) } } private fun updateNotificationOnStateChanges() = _voiceSatellite @@ -127,7 +122,7 @@ class VoiceSatelliteService() : LifecycleService() { (getSystemService(NOTIFICATION_SERVICE) as NotificationManager).notify( 2, createVoiceSatelliteServiceNotification( - this@VoiceSatelliteService, + this, it.translate(resources) ) ) @@ -136,7 +131,7 @@ class VoiceSatelliteService() : LifecycleService() { private fun registerVoiceSatelliteNsd(settings: VoiceSatelliteSettings) = registerVoiceSatelliteNsd( - context = this@VoiceSatelliteService, + context = this, name = settings.name, port = settings.serverPort, macAddress = settings.macAddress diff --git a/app/src/main/java/com/example/ava/tasker/TaskerRegistration.kt b/app/src/main/java/com/example/ava/tasker/TaskerRegistration.kt new file mode 100644 index 0000000..52f230b --- /dev/null +++ b/app/src/main/java/com/example/ava/tasker/TaskerRegistration.kt @@ -0,0 +1,37 @@ +package com.example.ava.tasker + +import android.content.Context +import com.example.ava.esphome.EspHomeDevice +import com.joaomgcd.taskerpluginlibrary.extensions.requestQuery +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onStart + +/** + * Registers Tasker actions and passes device state to Tasker conditions until cancelled. + */ +suspend fun EspHomeDevice.observeTaskerState(context: Context) = combine( + voiceAssistant.state, + voiceAssistant.allTimers +) { state, timers -> + AvaActivityRunner.updateState(state, timers) + ActivityConfigAvaActivity::class.java.requestQuery(context) +}.onStart { + registerTaskerActions() +}.onCompletion { + unregisterTaskerActions() +} + // Although implemented as a flow, conceptually this method is just a long running + // background task as nothing is ever emitted, collect hides the implementation + // detail and surfaces a cancellable suspend function instead + .collect { } + +private fun EspHomeDevice.registerTaskerActions() { + WakeSatelliteRunner.register { voiceAssistant.wakeAssistant() } + StopRingingRunner.register { voiceAssistant.stopTimer() } +} + +private fun unregisterTaskerActions() { + WakeSatelliteRunner.unregister() + StopRingingRunner.unregister() +} \ No newline at end of file diff --git a/app/src/test/java/com/example/ava/TaskerPluginsTest.kt b/app/src/test/java/com/example/ava/TaskerPluginsTest.kt index cf69ba0..3ed1296 100644 --- a/app/src/test/java/com/example/ava/TaskerPluginsTest.kt +++ b/app/src/test/java/com/example/ava/TaskerPluginsTest.kt @@ -44,6 +44,7 @@ class TaskerPluginsTest { val voiceAssistant = createVoiceAssistant(voiceInput = voiceInput) val sentMessages = mutableListOf() val messageJob = voiceAssistant.subscribe().onEach { sentMessages.add(it) }.launchIn(this) + WakeSatelliteRunner.register { voiceAssistant.wakeAssistant() } advanceUntilIdle() val result = WakeSatelliteRunner().run(dummyContext, TaskerInput(Unit)) @@ -75,6 +76,7 @@ class TaskerPluginsTest { } } val voiceAssistant = createVoiceAssistant(voiceOutput = voiceOutput) + StopRingingRunner.register { voiceAssistant.stopTimer() } // Make it ring by sending a timer finished event voiceAssistant.handleMessage(voiceAssistantTimerEventResponse {