Skip to content

Commit 6819afe

Browse files
authored
Fix opening of native screen with new onboarding (#6220)
1 parent 9bf5b17 commit 6819afe

File tree

2 files changed

+177
-5
lines changed

2 files changed

+177
-5
lines changed

app/src/main/kotlin/io/homeassistant/companion/android/launch/LaunchViewModel.kt

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package io.homeassistant.companion.android.launch
22

3+
import androidx.annotation.VisibleForTesting
34
import androidx.lifecycle.ViewModel
45
import androidx.lifecycle.viewModelScope
56
import androidx.work.WorkManager
67
import dagger.assisted.Assisted
78
import dagger.assisted.AssistedFactory
89
import dagger.assisted.AssistedInject
910
import dagger.hilt.android.lifecycle.HiltViewModel
11+
import io.homeassistant.companion.android.BuildConfig
1012
import io.homeassistant.companion.android.automotive.navigation.AutomotiveRoute
1113
import io.homeassistant.companion.android.common.data.authentication.SessionState
1214
import io.homeassistant.companion.android.common.data.network.NetworkState
@@ -60,15 +62,43 @@ internal sealed interface LaunchUiState {
6062
* asynchronously and navigates to the frontend.
6163
*/
6264
@HiltViewModel(assistedFactory = LaunchViewModelFactory::class)
63-
internal class LaunchViewModel @AssistedInject constructor(
64-
@Assisted initialDeepLink: LaunchActivity.DeepLink?,
65+
internal class LaunchViewModel @VisibleForTesting constructor(
66+
initialDeepLink: LaunchActivity.DeepLink?,
6567
private val workManager: WorkManager,
6668
private val serverManager: ServerManager,
6769
private val networkStatusMonitor: NetworkStatusMonitor,
68-
@param:LocationTrackingSupport private val hasLocationTrackingSupport: Boolean,
69-
@param:IsAutomotive private val isAutomotive: Boolean,
70+
private val hasLocationTrackingSupport: Boolean,
71+
isAutomotive: Boolean,
72+
isFullFlavor: Boolean,
7073
) : ViewModel() {
7174

75+
@AssistedInject
76+
constructor(
77+
@Assisted initialDeepLink: LaunchActivity.DeepLink?,
78+
workManager: WorkManager,
79+
serverManager: ServerManager,
80+
networkStatusMonitor: NetworkStatusMonitor,
81+
@LocationTrackingSupport hasLocationTrackingSupport: Boolean,
82+
@IsAutomotive isAutomotive: Boolean,
83+
) : this(
84+
initialDeepLink,
85+
workManager,
86+
serverManager,
87+
networkStatusMonitor,
88+
hasLocationTrackingSupport,
89+
isAutomotive = isAutomotive,
90+
isFullFlavor = BuildConfig.FLAVOR == "full",
91+
)
92+
93+
/**
94+
* Indicates whether the app should navigate to the automotive-specific screen.
95+
*
96+
* This is only true when running on Android Automotive with the full flavor.
97+
* The Play Store requires automotive apps to use a dedicated UI instead of a WebView,
98+
* otherwise the app gets rejected.
99+
*/
100+
private val shouldNavigateToAutomotive: Boolean = isAutomotive && isFullFlavor
101+
72102
private val _uiState = MutableStateFlow<LaunchUiState>(LaunchUiState.Loading)
73103
val uiState = _uiState.asStateFlow()
74104

@@ -173,7 +203,7 @@ internal class LaunchViewModel @AssistedInject constructor(
173203
NetworkState.READY_LOCAL, NetworkState.READY_REMOTE -> {
174204
workManager.enqueueResyncRegistration()
175205
_uiState.value = LaunchUiState.Ready(
176-
if (isAutomotive) {
206+
if (shouldNavigateToAutomotive) {
177207
AutomotiveRoute
178208
} else {
179209
FrontendRoute(path, serverId)

app/src/test/kotlin/io/homeassistant/companion/android/launch/LaunchViewModelTest.kt

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class LaunchViewModelTest {
4848
initialDeepLink: LaunchActivity.DeepLink? = null,
4949
hasLocationTrackingSupport: Boolean = false,
5050
isAutomotive: Boolean = false,
51+
isFullFlavor: Boolean = true,
5152
) {
5253
viewModel = LaunchViewModel(
5354
initialDeepLink,
@@ -56,6 +57,7 @@ class LaunchViewModelTest {
5657
networkStatusMonitor,
5758
hasLocationTrackingSupport,
5859
isAutomotive,
60+
isFullFlavor,
5961
)
6062
}
6163

@@ -402,4 +404,144 @@ class LaunchViewModelTest {
402404
viewModel.uiState.value,
403405
)
404406
}
407+
408+
@Test
409+
fun `Given isAutomotive is true but isFullFlavor is false, when network is READY, then navigate to frontend route`() = runTest {
410+
val server = mockk<Server>(relaxed = true)
411+
412+
every { workManager.enqueue(any<OneTimeWorkRequest>()) } returns mockk()
413+
414+
coEvery { serverManager.getServer(ServerManager.SERVER_ID_ACTIVE) } returns server
415+
coEvery { serverManager.isRegistered() } returns true
416+
coEvery { serverManager.authenticationRepository().getSessionState() } returns SessionState.CONNECTED
417+
val networkStateFlow = MutableStateFlow(NetworkState.READY_REMOTE)
418+
coEvery { networkStatusMonitor.observeNetworkStatus(any()) } returns networkStateFlow
419+
420+
createViewModel(isAutomotive = true, isFullFlavor = false)
421+
advanceUntilIdle()
422+
423+
assertEquals(
424+
LaunchUiState.Ready(FrontendRoute(null, ServerManager.SERVER_ID_ACTIVE)),
425+
viewModel.uiState.value,
426+
)
427+
}
428+
429+
@Test
430+
fun `Given network is UNAVAILABLE then READY, when observing network, then navigate to frontend after recovery`() = runTest {
431+
val server = mockk<Server>(relaxed = true)
432+
433+
every { workManager.enqueue(any<OneTimeWorkRequest>()) } returns mockk()
434+
435+
coEvery { serverManager.getServer(ServerManager.SERVER_ID_ACTIVE) } returns server
436+
coEvery { serverManager.isRegistered() } returns true
437+
coEvery { serverManager.authenticationRepository().getSessionState() } returns SessionState.CONNECTED
438+
val networkStateFlow = MutableStateFlow(NetworkState.UNAVAILABLE)
439+
coEvery { networkStatusMonitor.observeNetworkStatus(any()) } returns networkStateFlow
440+
441+
createViewModel()
442+
advanceUntilIdle()
443+
444+
assertEquals(LaunchUiState.NetworkUnavailable, viewModel.uiState.value)
445+
assertEquals(1, networkStateFlow.subscriptionCount.value)
446+
447+
networkStateFlow.emit(NetworkState.READY_LOCAL)
448+
advanceUntilIdle()
449+
450+
assertEquals(
451+
LaunchUiState.Ready(FrontendRoute(null, ServerManager.SERVER_ID_ACTIVE)),
452+
viewModel.uiState.value,
453+
)
454+
assertEquals(0, networkStateFlow.subscriptionCount.value)
455+
456+
verify(exactly = 1) {
457+
workManager.enqueue(any<OneTimeWorkRequest>())
458+
}
459+
}
460+
461+
@Test
462+
fun `Given initial deep link is OpenOnboarding with null url, when creating viewModel, then navigate to onboarding without url`() = runTest {
463+
createViewModel(
464+
initialDeepLink = LaunchActivity.DeepLink.OpenOnboarding(
465+
urlToOnboard = null,
466+
hideExistingServers = false,
467+
skipWelcome = false,
468+
),
469+
hasLocationTrackingSupport = true,
470+
)
471+
advanceUntilIdle()
472+
473+
assertEquals(
474+
LaunchUiState.Ready(
475+
OnboardingRoute(
476+
hasLocationTracking = true,
477+
urlToOnboard = null,
478+
hideExistingServers = false,
479+
skipWelcome = false,
480+
),
481+
),
482+
viewModel.uiState.value,
483+
)
484+
}
485+
486+
@Test
487+
fun `Given valid connected servers, when cleaning up servers, then do not remove connected servers`() = runTest {
488+
val connectedServer1 = Server(
489+
id = 1,
490+
_name = "Connected Server 1",
491+
connection = ServerConnectionInfo(externalUrl = "http://server1.com"),
492+
session = ServerSessionInfo(),
493+
user = ServerUserInfo(id = null, name = null, isOwner = false, isAdmin = false),
494+
)
495+
val connectedServer2 = Server(
496+
id = 2,
497+
_name = "Connected Server 2",
498+
connection = ServerConnectionInfo(externalUrl = "http://server2.com"),
499+
session = ServerSessionInfo(),
500+
user = ServerUserInfo(id = null, name = null, isOwner = false, isAdmin = false),
501+
)
502+
503+
coEvery { serverManager.defaultServers } returns listOf(connectedServer1, connectedServer2)
504+
coEvery { serverManager.authenticationRepository(connectedServer1.id).getSessionState() } returns SessionState.CONNECTED
505+
coEvery { serverManager.authenticationRepository(connectedServer2.id).getSessionState() } returns SessionState.CONNECTED
506+
507+
createViewModel()
508+
advanceUntilIdle()
509+
510+
coVerify(exactly = 0) { serverManager.removeServer(any()) }
511+
}
512+
513+
@Test
514+
fun `Given initial deep link is NavigateTo with null path, when server is connected, then navigate to frontend without path`() = runTest {
515+
val serverId = 5
516+
val server = mockk<Server>(relaxed = true)
517+
every { workManager.enqueue(any<OneTimeWorkRequest>()) } returns mockk()
518+
519+
coEvery { serverManager.getServer(serverId) } returns server
520+
coEvery { serverManager.isRegistered() } returns true
521+
coEvery { serverManager.authenticationRepository().getSessionState() } returns SessionState.CONNECTED
522+
val networkStateFlow = MutableStateFlow(NetworkState.READY_REMOTE)
523+
coEvery { networkStatusMonitor.observeNetworkStatus(any()) } returns networkStateFlow
524+
525+
createViewModel(LaunchActivity.DeepLink.NavigateTo(path = null, serverId = serverId))
526+
advanceUntilIdle()
527+
528+
assertEquals(
529+
LaunchUiState.Ready(FrontendRoute(null, serverId)),
530+
viewModel.uiState.value,
531+
)
532+
}
533+
534+
@Test
535+
fun `Given initial deep link is OpenWearOnboarding with null url, when full flavor, then navigate to wear onboarding without url`() = runTest {
536+
createViewModel(
537+
initialDeepLink = LaunchActivity.DeepLink.OpenWearOnboarding(wearName = "Pixel Watch", urlToOnboard = null),
538+
hasLocationTrackingSupport = true,
539+
)
540+
advanceUntilIdle()
541+
542+
assertEquals(
543+
LaunchUiState.Ready(WearOnboardingRoute(wearName = "Pixel Watch", urlToOnboard = null)),
544+
viewModel.uiState.value,
545+
)
546+
}
405547
}

0 commit comments

Comments
 (0)