Skip to content

Commit 41e74e5

Browse files
authored
Check AssistVoiceInteractionService before opening assist (#6521)
1 parent 17fd3ff commit 41e74e5

File tree

2 files changed

+64
-1
lines changed

2 files changed

+64
-1
lines changed

app/src/main/kotlin/io/homeassistant/companion/android/assist/service/AssistVoiceInteractionService.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,11 @@ class AssistVoiceInteractionService : VoiceInteractionService() {
7171
)
7272
}
7373
private var lastTriggerTime: Instant? = null
74+
private var isServiceReady = false
7475

7576
override fun onReady() {
7677
super.onReady()
78+
isServiceReady = true
7779
Timber.d("VoiceInteractionService is ready")
7880
serviceScope.launch {
7981
if (assistConfigManager.isWakeWordEnabled()) {
@@ -87,6 +89,7 @@ class AssistVoiceInteractionService : VoiceInteractionService() {
8789

8890
override fun onShutdown() {
8991
super.onShutdown()
92+
isServiceReady = false
9093
Timber.d("VoiceInteractionService is shutting down")
9194
// Don't use stopListening() as it launches a coroutine that may not complete before cancel
9295
serviceScope.cancel()
@@ -239,6 +242,10 @@ class AssistVoiceInteractionService : VoiceInteractionService() {
239242
}
240243

241244
private fun launchAssist(wakeWord: String? = null) {
245+
if (!isServiceReady) {
246+
Timber.w("Cannot launch Assist: VoiceInteractionService is not ready yet")
247+
return
248+
}
242249
val args = Bundle().apply {
243250
wakeWord?.let { putString(EXTRA_WAKE_WORD, it) }
244251
}

app/src/test/kotlin/io/homeassistant/companion/android/assist/service/AssistVoiceInteractionServiceTest.kt

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ import org.junit.Test
3333
import org.junit.jupiter.api.assertNull
3434
import org.junit.runner.RunWith
3535
import org.robolectric.Robolectric
36-
import org.robolectric.annotation.Config
3736
import org.robolectric.RobolectricTestRunner
3837
import org.robolectric.Shadows
3938
import org.robolectric.android.controller.ServiceController
39+
import org.robolectric.annotation.Config
4040
import org.robolectric.shadows.ShadowVoiceInteractionService
4141

4242
@OptIn(ExperimentalCoroutinesApi::class)
@@ -356,4 +356,60 @@ class AssistVoiceInteractionServiceTest {
356356
// Both detections should have sent a broadcast, even though the second was debounced
357357
assertEquals(2, wakeWordBroadcasts.size)
358358
}
359+
360+
@Test
361+
fun `Given service not ready when wake word detected then do not show session`() = runTest {
362+
val shadow = Shadows.shadowOf(service) as ShadowVoiceInteractionService
363+
coEvery { assistConfigManager.getSelectedWakeWordModel() } returns microWakeWordModelConfigs[0]
364+
365+
// Do NOT call onReady() - service is not ready
366+
367+
val intent = Intent().apply {
368+
action = "io.homeassistant.companion.android.START_LISTENING"
369+
}
370+
service.onStartCommand(intent, 0, 1)
371+
advanceUntilIdle()
372+
373+
// Simulate wake word detection while service is not ready
374+
onWakeWordDetectedSlot.captured.invoke(microWakeWordModelConfigs[0])
375+
advanceUntilIdle()
376+
377+
// showSession should NOT have been called
378+
assertNull(shadow.lastSessionBundle)
379+
// But the listener should still have been stopped to release the microphone
380+
coVerify { wakeWordListener.stop() }
381+
}
382+
383+
@Test
384+
fun `Given service shut down after ready when wake word detected then do not show session`() = runTest {
385+
val shadow = Shadows.shadowOf(service) as ShadowVoiceInteractionService
386+
coEvery { assistConfigManager.getSelectedWakeWordModel() } returns microWakeWordModelConfigs[0]
387+
388+
// Make service ready and start listening so the wake word callback is captured
389+
service.onReady()
390+
advanceUntilIdle()
391+
392+
val intent = Intent().apply {
393+
action = "io.homeassistant.companion.android.START_LISTENING"
394+
}
395+
service.onStartCommand(intent, 0, 1)
396+
advanceUntilIdle()
397+
398+
// Simulate a shutdown that races with the wake-word coroutine: when
399+
// stop() is called inside the launched coroutine, the service shuts down
400+
// concurrently (setting isServiceReady = false). Because launchAssist()
401+
// runs synchronously after stop() returns, only the isServiceReady guard
402+
// prevents showSession() from being called.
403+
coEvery { wakeWordListener.stop() } coAnswers {
404+
service.onShutdown()
405+
}
406+
407+
// Invoke the wake word callback to queue the coroutine, then advance
408+
onWakeWordDetectedSlot.captured.invoke(microWakeWordModelConfigs[0])
409+
advanceUntilIdle()
410+
411+
// showSession should NOT have been called because the service is no longer ready
412+
assertNull(shadow.lastSessionBundle)
413+
coVerify { wakeWordListener.stop() }
414+
}
359415
}

0 commit comments

Comments
 (0)