Skip to content

Commit ba3169b

Browse files
authored
Open Assist external message in FrontendScreen (#6477)
1 parent 41e74e5 commit ba3169b

File tree

10 files changed

+181
-5
lines changed

10 files changed

+181
-5
lines changed

app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModel.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,16 @@ internal class FrontendViewModel @VisibleForTesting constructor(
254254
_navigationEvents.tryEmit(FrontendNavigationEvent.NavigateToSettings)
255255
}
256256

257+
is FrontendHandlerEvent.ShowAssist -> {
258+
_navigationEvents.tryEmit(
259+
FrontendNavigationEvent.NavigateToAssist(
260+
serverId = _viewState.value.serverId,
261+
pipelineId = result.pipelineId,
262+
startListening = result.startListening,
263+
),
264+
)
265+
}
266+
257267
is FrontendHandlerEvent.AuthError -> {
258268
onError(result.error)
259269
}

app/src/main/kotlin/io/homeassistant/companion/android/frontend/externalbus/incoming/IncomingExternalBusMessage.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,22 @@ data class OpenSettingsMessage(override val id: Int? = null) : IncomingExternalB
106106
@Serializable
107107
@SerialName("theme-update")
108108
data class ThemeUpdateMessage(override val id: Int? = null) : IncomingExternalBusMessage
109+
110+
/**
111+
* Message requesting the app to open the voice assistant (Assist).
112+
*
113+
* Sent when the user triggers the voice assistant from the frontend UI.
114+
* No response is expected for this message.
115+
*/
116+
@Serializable
117+
@SerialName("assist/show")
118+
data class OpenAssistMessage(
119+
override val id: Int? = null,
120+
val payload: OpenAssistPayload = OpenAssistPayload(),
121+
) : IncomingExternalBusMessage
122+
123+
@Serializable
124+
data class OpenAssistPayload(
125+
@SerialName("pipeline_id") val pipelineId: String? = null,
126+
@SerialName("start_listening") val startListening: Boolean = true,
127+
)

app/src/main/kotlin/io/homeassistant/companion/android/frontend/handler/FrontendHandlerEvent.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ sealed interface FrontendHandlerEvent {
3030
*/
3131
data object OpenSettings : FrontendHandlerEvent
3232

33+
/**
34+
* User triggered the voice assistant from the frontend.
35+
*/
36+
data class ShowAssist(val pipelineId: String?, val startListening: Boolean) : FrontendHandlerEvent
37+
3338
/**
3439
* Frontend theme changed (colors, dark mode, etc.).
3540
*/

app/src/main/kotlin/io/homeassistant/companion/android/frontend/handler/FrontendMessageHandler.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import io.homeassistant.companion.android.frontend.externalbus.WebViewScript
1212
import io.homeassistant.companion.android.frontend.externalbus.incoming.ConfigGetMessage
1313
import io.homeassistant.companion.android.frontend.externalbus.incoming.ConnectionStatusMessage
1414
import io.homeassistant.companion.android.frontend.externalbus.incoming.IncomingExternalBusMessage
15+
import io.homeassistant.companion.android.frontend.externalbus.incoming.OpenAssistMessage
1516
import io.homeassistant.companion.android.frontend.externalbus.incoming.OpenSettingsMessage
1617
import io.homeassistant.companion.android.frontend.externalbus.incoming.ThemeUpdateMessage
1718
import io.homeassistant.companion.android.frontend.externalbus.incoming.UnknownIncomingMessage
@@ -163,6 +164,14 @@ class FrontendMessageHandler @Inject constructor(
163164
FrontendHandlerEvent.OpenSettings
164165
}
165166

167+
is OpenAssistMessage -> {
168+
Timber.d("Open assist request received with id: ${message.id}")
169+
FrontendHandlerEvent.ShowAssist(
170+
pipelineId = message.payload.pipelineId,
171+
startListening = message.payload.startListening,
172+
)
173+
}
174+
166175
is ThemeUpdateMessage -> {
167176
Timber.d("Theme update received")
168177
FrontendHandlerEvent.ThemeUpdated

app/src/main/kotlin/io/homeassistant/companion/android/frontend/navigation/FrontendNavigation.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import androidx.navigation.activity
1212
import androidx.navigation.compose.composable
1313
import androidx.navigation.toRoute
1414
import io.homeassistant.companion.android.WIPFeature
15+
import io.homeassistant.companion.android.assist.AssistActivity
1516
import io.homeassistant.companion.android.common.data.servers.ServerManager.Companion.SERVER_ID_ACTIVE
1617
import io.homeassistant.companion.android.frontend.FrontendScreen
1718
import io.homeassistant.companion.android.frontend.FrontendViewModel
@@ -72,6 +73,16 @@ internal fun NavGraphBuilder.frontendScreen(
7273
FrontendNavigationHandler(
7374
navigationEvents = viewModel.navigationEvents,
7475
onNavigateToSettings = onNavigateToSettings,
76+
onNavigateToAssist = { serverId, pipelineId, startListening ->
77+
navController.context.startActivity(
78+
AssistActivity.newInstance(
79+
context = navController.context,
80+
serverId = serverId,
81+
pipelineId = pipelineId,
82+
startListening = startListening,
83+
),
84+
)
85+
},
7586
)
7687

7788
FrontendScreen(
@@ -107,13 +118,18 @@ internal fun NavGraphBuilder.frontendScreen(
107118
internal fun FrontendNavigationHandler(
108119
navigationEvents: SharedFlow<FrontendNavigationEvent>,
109120
onNavigateToSettings: () -> Unit,
121+
onNavigateToAssist: (serverId: Int, pipelineId: String?, startListening: Boolean) -> Unit,
110122
) {
111123
LaunchedEffect(Unit) {
112124
navigationEvents.collect { event ->
113125
when (event) {
114126
is FrontendNavigationEvent.NavigateToSettings -> {
115127
onNavigateToSettings()
116128
}
129+
130+
is FrontendNavigationEvent.NavigateToAssist -> {
131+
onNavigateToAssist(event.serverId, event.pipelineId, event.startListening)
132+
}
117133
}
118134
}
119135
}

app/src/main/kotlin/io/homeassistant/companion/android/frontend/navigation/FrontendNavigationEvent.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,11 @@ package io.homeassistant.companion.android.frontend.navigation
1313
sealed interface FrontendNavigationEvent {
1414
/** Navigate to the app settings screen */
1515
data object NavigateToSettings : FrontendNavigationEvent
16+
17+
/** Navigate to the voice assistant (Assist) screen */
18+
data class NavigateToAssist(
19+
val serverId: Int,
20+
val pipelineId: String?,
21+
val startListening: Boolean,
22+
) : FrontendNavigationEvent
1623
}

app/src/test/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModelTest.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,34 @@ class FrontendViewModelTest {
372372
assertEquals(authError, (state as FrontendViewState.Error).error)
373373
}
374374

375+
@Test
376+
fun `Given show assist message result when collected then NavigateToAssist event is emitted`() = runTest {
377+
val messageFlow = MutableSharedFlow<FrontendHandlerEvent>()
378+
every { externalBusHandler.messageResults() } returns messageFlow
379+
every { urlManager.serverUrlFlow(any(), any()) } returns flowOf(
380+
UrlLoadResult.Success(url = testUrlWithAuth, serverId = serverId),
381+
)
382+
383+
val viewModel = createViewModel()
384+
385+
// Collect navigation events
386+
val navigationEvents = mutableListOf<FrontendNavigationEvent>()
387+
val job = backgroundScope.launch { viewModel.navigationEvents.collect { navigationEvents.add(it) } }
388+
389+
advanceTimeBy(CONNECTION_TIMEOUT - 1.seconds)
390+
391+
// Emit show assist message
392+
messageFlow.emit(FrontendHandlerEvent.ShowAssist(pipelineId = "abc", startListening = false))
393+
advanceUntilIdle()
394+
395+
val event = navigationEvents.filterIsInstance<FrontendNavigationEvent.NavigateToAssist>().firstOrNull()
396+
assertTrue(event != null, "Expected NavigateToAssist event")
397+
assertEquals(serverId, event!!.serverId)
398+
assertEquals("abc", event.pipelineId)
399+
assertEquals(false, event.startListening)
400+
job.cancel()
401+
}
402+
375403
@Test
376404
fun `Given open settings message result when collected then navigation event is emitted`() = runTest {
377405
val messageFlow = MutableSharedFlow<FrontendHandlerEvent>()

app/src/test/kotlin/io/homeassistant/companion/android/frontend/externalbus/incoming/IncomingExternalBusMessageTest.kt

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package io.homeassistant.companion.android.frontend.externalbus.incoming
22

33
import io.homeassistant.companion.android.frontend.externalbus.frontendExternalBusJson
4-
import org.junit.jupiter.api.Assertions
54
import org.junit.jupiter.api.Assertions.assertEquals
5+
import org.junit.jupiter.api.Assertions.assertFalse
66
import org.junit.jupiter.api.Assertions.assertInstanceOf
7+
import org.junit.jupiter.api.Assertions.assertNull
78
import org.junit.jupiter.api.Assertions.assertTrue
89
import org.junit.jupiter.api.Test
910

@@ -17,7 +18,7 @@ class IncomingExternalBusMessageTest {
1718

1819
assertInstanceOf(ConnectionStatusMessage::class.java, message)
1920
val statusMessage = message as ConnectionStatusMessage
20-
Assertions.assertEquals(1, statusMessage.id)
21+
assertEquals(1, statusMessage.id)
2122
assertEquals("connected", statusMessage.payload.event)
2223
assertTrue(statusMessage.payload.isConnected)
2324
}
@@ -29,7 +30,7 @@ class IncomingExternalBusMessageTest {
2930
val message = frontendExternalBusJson.decodeFromString<IncomingExternalBusMessage>(json)
3031

3132
assertInstanceOf(ConfigGetMessage::class.java, message)
32-
Assertions.assertEquals(42, (message as ConfigGetMessage).id)
33+
assertEquals(42, (message as ConfigGetMessage).id)
3334
}
3435

3536
@Test
@@ -39,7 +40,7 @@ class IncomingExternalBusMessageTest {
3940
val message = frontendExternalBusJson.decodeFromString<IncomingExternalBusMessage>(json)
4041

4142
assertInstanceOf(ThemeUpdateMessage::class.java, message)
42-
Assertions.assertEquals(5, (message as ThemeUpdateMessage).id)
43+
assertEquals(5, (message as ThemeUpdateMessage).id)
4344
}
4445

4546
@Test
@@ -49,7 +50,33 @@ class IncomingExternalBusMessageTest {
4950
val message = frontendExternalBusJson.decodeFromString<IncomingExternalBusMessage>(json)
5051

5152
assertInstanceOf(OpenSettingsMessage::class.java, message)
52-
Assertions.assertEquals(5, (message as OpenSettingsMessage).id)
53+
assertEquals(5, (message as OpenSettingsMessage).id)
54+
}
55+
56+
@Test
57+
fun `Given assist-show JSON then parses to OpenAssistMessage with payload`() {
58+
val json = """{"type":"assist/show","id":7,"payload":{"pipeline_id":"abc","start_listening":false}}"""
59+
60+
val message = frontendExternalBusJson.decodeFromString<IncomingExternalBusMessage>(json)
61+
62+
assertInstanceOf(OpenAssistMessage::class.java, message)
63+
val assistMessage = message as OpenAssistMessage
64+
assertEquals(7, assistMessage.id)
65+
assertEquals("abc", assistMessage.payload.pipelineId)
66+
assertFalse(assistMessage.payload.startListening)
67+
}
68+
69+
@Test
70+
fun `Given assist-show JSON without payload then parses to OpenAssistMessage with defaults`() {
71+
val json = """{"type":"assist/show","id":8}"""
72+
73+
val message = frontendExternalBusJson.decodeFromString<IncomingExternalBusMessage>(json)
74+
75+
assertInstanceOf(OpenAssistMessage::class.java, message)
76+
val assistMessage = message as OpenAssistMessage
77+
assertEquals(8, assistMessage.id)
78+
assertNull(assistMessage.payload.pipelineId)
79+
assertTrue(assistMessage.payload.startListening)
5380
}
5481

5582
@Test

app/src/test/kotlin/io/homeassistant/companion/android/frontend/handler/FrontendMessageHandlerTest.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import io.homeassistant.companion.android.frontend.externalbus.WebViewScript
1111
import io.homeassistant.companion.android.frontend.externalbus.incoming.ConfigGetMessage
1212
import io.homeassistant.companion.android.frontend.externalbus.incoming.ConnectionStatusMessage
1313
import io.homeassistant.companion.android.frontend.externalbus.incoming.ConnectionStatusPayload
14+
import io.homeassistant.companion.android.frontend.externalbus.incoming.OpenAssistMessage
15+
import io.homeassistant.companion.android.frontend.externalbus.incoming.OpenAssistPayload
1416
import io.homeassistant.companion.android.frontend.externalbus.incoming.OpenSettingsMessage
1517
import io.homeassistant.companion.android.frontend.externalbus.incoming.ThemeUpdateMessage
1618
import io.homeassistant.companion.android.frontend.externalbus.incoming.UnknownIncomingMessage
@@ -199,6 +201,24 @@ class FrontendMessageHandlerTest {
199201
assertEquals(0, configResult["hasBarCodeScanner"]?.jsonPrimitive?.int)
200202
}
201203

204+
@Test
205+
fun `Given open assist message when messageResults then emits ShowAssist with payload`() = runTest {
206+
val message = OpenAssistMessage(
207+
id = 7,
208+
payload = OpenAssistPayload(pipelineId = "abc", startListening = false),
209+
)
210+
every { externalBusRepository.incomingMessages() } returns flowOf(message)
211+
212+
handler.messageResults().test {
213+
val result = awaitItem()
214+
assertTrue(result is FrontendHandlerEvent.ShowAssist)
215+
val showAssist = result as FrontendHandlerEvent.ShowAssist
216+
assertEquals("abc", showAssist.pipelineId)
217+
assertEquals(false, showAssist.startListening)
218+
expectNoEvents()
219+
}
220+
}
221+
202222
@Test
203223
fun `Given open settings message when messageResults then emits OpenSettings`() = runTest {
204224
val message = OpenSettingsMessage(id = 1)

app/src/test/kotlin/io/homeassistant/companion/android/frontend/navigation/FrontendNavigationTest.kt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class FrontendNavigationTest {
3838
FrontendNavigationHandler(
3939
navigationEvents = navigationEvents,
4040
onNavigateToSettings = { settingsNavigated = true },
41+
onNavigateToAssist = { _, _, _ -> },
4142
)
4243
}
4344

@@ -47,4 +48,38 @@ class FrontendNavigationTest {
4748

4849
assertEquals(true, settingsNavigated)
4950
}
51+
52+
@Test
53+
fun `Given NavigateToAssist event then onNavigateToAssist is called with correct params`() = runTest {
54+
var capturedServerId: Int? = null
55+
var capturedPipelineId: String? = null
56+
var capturedStartListening: Boolean? = null
57+
val navigationEvents = MutableSharedFlow<FrontendNavigationEvent>()
58+
59+
composeTestRule.setContent {
60+
FrontendNavigationHandler(
61+
navigationEvents = navigationEvents,
62+
onNavigateToSettings = { },
63+
onNavigateToAssist = { serverId, pipelineId, startListening ->
64+
capturedServerId = serverId
65+
capturedPipelineId = pipelineId
66+
capturedStartListening = startListening
67+
},
68+
)
69+
}
70+
71+
composeTestRule.waitForIdle()
72+
navigationEvents.emit(
73+
FrontendNavigationEvent.NavigateToAssist(
74+
serverId = 1,
75+
pipelineId = "abc",
76+
startListening = false,
77+
),
78+
)
79+
composeTestRule.waitForIdle()
80+
81+
assertEquals(1, capturedServerId)
82+
assertEquals("abc", capturedPipelineId)
83+
assertEquals(false, capturedStartListening)
84+
}
5085
}

0 commit comments

Comments
 (0)