diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index 8566577aeb..054b371ce3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -43,6 +43,7 @@ import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -354,6 +355,21 @@ class WireActivity : BaseActivity() { val isUserUiBlocked = viewModel.globalAppState.blockUserUI != null val isAuthenticationRoute = currentBaseRoute in authenticationGraphRoutes val isSessionTransitionInProgress = viewModel.globalAppState.isSessionTransitionInProgress + var noSessionAuthenticationStartedWithoutSession by remember { mutableStateOf(false) } + LaunchedEffect(currentBaseRoute, currentUserId) { + when { + currentUserId == null && currentBaseRoute in noSessionLoginAuthenticationRoutes -> + noSessionAuthenticationStartedWithoutSession = true + currentBaseRoute != null && currentBaseRoute !in authenticationGraphRoutes -> + noSessionAuthenticationStartedWithoutSession = false + } + } + val navHostStartDestination = resolveNavHostStartDestination( + initialStartDestination = startDestination, + currentUserId = currentUserId, + currentBaseRoute = currentBaseRoute, + noSessionAuthenticationStartedWithoutSession = noSessionAuthenticationStartedWithoutSession, + ) LaunchedEffect(currentUserId) { currentUserId?.let(lastKnownCurrentAccount::update) } @@ -364,7 +380,7 @@ class WireActivity : BaseActivity() { currentUserId = currentUserId, sessionBackedAuthenticationUserId = sessionBackedAuthenticationUserId, currentBaseRoute = currentBaseRoute, - startDestinationBaseRoute = startDestination.baseRoute, + startDestinationBaseRoute = navHostStartDestination.baseRoute, isUserUiBlocked = isUserUiBlocked, isSessionTransitionInProgress = isSessionTransitionInProgress, ) @@ -372,13 +388,18 @@ class WireActivity : BaseActivity() { if (graphContext?.sessionGraph != null) { lastSessionGraphContext.value = graphContext } + val graphContextWithRetainedImageLoader = graphContext?.copy( + imageLoaderSessionGraph = resolveWireActivityImageLoaderSessionGraph( + activeSessionGraph = graphContext.sessionGraph, + retainedSessionGraph = lastSessionGraphContext.value?.sessionGraph, + ) + ) val canRenderSessionBackedRoute = currentUserId != null || sessionBackedAuthenticationUserId != null val canRetainSessionGraphForContent = !isAuthenticationRoute && canRenderSessionBackedRoute - val contentGraphContext = graphContext ?: when { + val contentGraphContext = graphContextWithRetainedImageLoader ?: when { currentBaseRoute != null && canRetainSessionGraphForContent -> lastSessionGraphContext.value else -> null } - val navHostStartDestination = resolveNavHostStartDestination(startDestination, currentUserId, currentBaseRoute) val backgroundType by remember { derivedStateOf { currentBackStackEntryState.value?.safeDestination()?.style.let { @@ -410,25 +431,14 @@ class WireActivity : BaseActivity() { initialStartDestination: Direction, currentUserId: UserId?, currentBaseRoute: String?, - ): Direction { - if (currentUserId != null || initialStartDestination.baseRoute in authenticationGraphRoutes) { - return initialStartDestination - } - return when (currentBaseRoute) { - NewLoginScreenDestination.baseRoute -> NewLoginScreenDestination() - LoginScreenDestination.baseRoute -> WelcomeScreenDestination() - WelcomeScreenDestination.baseRoute -> WelcomeScreenDestination() - NewWelcomeEmptyStartScreenDestination.baseRoute, - null -> loggedOutStartDestination() - else -> initialStartDestination - } - } - - private fun loggedOutStartDestination(): Direction = - when (loginTypeSelector.canUseNewLogin()) { - true -> NewWelcomeEmptyStartScreenDestination() - false -> WelcomeScreenDestination() - } + noSessionAuthenticationStartedWithoutSession: Boolean, + ): Direction = resolveWireActivityNavHostStartDestination( + initialStartDestination = initialStartDestination, + currentUserId = currentUserId, + currentBaseRoute = currentBaseRoute, + canUseNewLogin = loginTypeSelector.canUseNewLogin(), + noSessionAuthenticationStartedWithoutSession = noSessionAuthenticationStartedWithoutSession, + ) private fun isNavigationAllowed(navigationCommand: NavigationCommand): Boolean { if (navigationCommand.destination.baseRoute != NewLoginScreenDestination.baseRoute) return true @@ -591,6 +601,7 @@ class WireActivity : BaseActivity() { graph = it, viewModelFactory = (it as? ViewModelGraph)?.metroViewModelFactory ?: appGraph.metroViewModelFactory, sessionGraph = sessionGraph, + imageLoaderSessionGraph = sessionGraph, activityViewModels = activityViewModels, ) } @@ -673,7 +684,7 @@ class WireActivity : BaseActivity() { logoutAction: (wipeData: Boolean) -> Unit, content: @Composable () -> Unit, ) { - val imageLoader = sessionGraph?.wireSessionImageLoader + val imageLoader = imageLoaderSessionGraph?.wireSessionImageLoader CompositionLocalProvider( LocalMetroViewModelFactory provides viewModelFactory, LocalWireViewModelScopeKey provides graph.viewModelScopeKey, @@ -1210,6 +1221,7 @@ private data class WireActivityGraphContext( val graph: MetroViewModelGraph, val viewModelFactory: MetroViewModelFactory, val sessionGraph: AppSessionViewModelGraph?, + val imageLoaderSessionGraph: AppSessionViewModelGraph?, val activityViewModels: WireActivityScopedViewModels?, ) @@ -1265,6 +1277,11 @@ private val accountCreationAuthenticationRoutes = setOf( private val noSessionAuthenticationGraphRoutes = loginContinuationAuthenticationRoutes + accountCreationAuthenticationRoutes +private val noSessionLoginAuthenticationRoutes = setOf( + LoginScreenDestination.baseRoute, + NewLoginScreenDestination.baseRoute, +) + loginContinuationAuthenticationRoutes + private val sessionBackedAuthenticationGraphRoutes = setOf( RegisterDeviceScreenDestination.baseRoute, RemoveDeviceScreenDestination.baseRoute, @@ -1285,10 +1302,51 @@ internal fun resolveWireActivityActiveGraph(request: WireActivityActiveGraphRequ request.effectiveBaseRoute in authenticationGraphRoutes && request.effectiveBaseRoute !in sessionBackedAuthenticationGraphRoutes -> request.authenticationViewModelGraph request.sessionGraph != null -> request.sessionGraph - request.currentBaseRoute == null -> request.authenticationViewModelGraph + request.currentBaseRoute == null && request.effectiveBaseRoute in authenticationGraphRoutes -> request.authenticationViewModelGraph else -> null } +internal fun resolveWireActivityNavHostStartDestination( + initialStartDestination: Direction, + currentUserId: UserId?, + currentBaseRoute: String?, + canUseNewLogin: Boolean, + noSessionAuthenticationStartedWithoutSession: Boolean = false, +): Direction = when { + currentUserId != null && + noSessionAuthenticationStartedWithoutSession && + currentBaseRoute in noSessionLoginAuthenticationRoutes -> + resolveWireActivityLoggedOutStartDestination(canUseNewLogin) + + currentUserId != null || initialStartDestination.baseRoute in authenticationGraphRoutes -> + initialStartDestination + + currentBaseRoute == NewLoginScreenDestination.baseRoute || + currentBaseRoute == NewWelcomeEmptyStartScreenDestination.baseRoute || + currentBaseRoute in noSessionAuthenticationGraphRoutes -> + resolveWireActivityLoggedOutStartDestination(canUseNewLogin) + + currentBaseRoute == null -> + resolveWireActivityLoggedOutStartDestination(canUseNewLogin) + + currentBaseRoute == LoginScreenDestination.baseRoute || + currentBaseRoute == WelcomeScreenDestination.baseRoute -> + WelcomeScreenDestination() + + else -> initialStartDestination +} + +internal fun resolveWireActivityLoggedOutStartDestination(canUseNewLogin: Boolean): Direction = + when (canUseNewLogin) { + true -> NewWelcomeEmptyStartScreenDestination() + false -> WelcomeScreenDestination() + } + +internal fun resolveWireActivityImageLoaderSessionGraph( + activeSessionGraph: AppSessionViewModelGraph?, + retainedSessionGraph: AppSessionViewModelGraph?, +): AppSessionViewModelGraph? = activeSessionGraph ?: retainedSessionGraph + private fun Bundle.sessionBackedAuthenticationUserId(): UserId? { val value = getString(SessionBackedAuthenticationNavArgs.USER_ID_VALUE_KEY) val domain = getString(SessionBackedAuthenticationNavArgs.USER_ID_DOMAIN_KEY) diff --git a/app/src/test/kotlin/com/wire/android/ui/WireActivityActiveGraphResolverTest.kt b/app/src/test/kotlin/com/wire/android/ui/WireActivityActiveGraphResolverTest.kt index ae26c2a879..8f5f482b36 100644 --- a/app/src/test/kotlin/com/wire/android/ui/WireActivityActiveGraphResolverTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/WireActivityActiveGraphResolverTest.kt @@ -18,11 +18,20 @@ package com.wire.android.ui +import com.ramcosta.composedestinations.generated.app.destinations.HomeScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.LoginScreenDestination +import com.ramcosta.composedestinations.generated.app.destinations.NewLoginScreenDestination +import com.ramcosta.composedestinations.generated.app.destinations.NewLoginPasswordScreenDestination +import com.ramcosta.composedestinations.generated.app.destinations.NewWelcomeEmptyStartScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.RegisterDeviceScreenDestination +import com.ramcosta.composedestinations.generated.app.destinations.WelcomeScreenDestination import com.wire.android.di.metro.AppAuthenticationViewModelGraph import com.wire.android.di.metro.AppSessionViewModelGraph +import com.wire.android.navigation.baseRoute +import com.wire.kalium.logic.data.user.UserId import io.mockk.mockk +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertSame import org.junit.jupiter.api.Test @@ -66,4 +75,327 @@ class WireActivityActiveGraphResolverTest { assertSame(sessionGraph, graph) } + + @Test + fun givenCurrentRouteIsUnknownAndLoggedOutStartIsAuth_whenResolvingActiveGraph_thenAuthenticationGraphIsUsed() { + val graph = resolveWireActivityActiveGraph( + WireActivityActiveGraphRequest( + authenticationViewModelGraph = authenticationViewModelGraph, + sessionGraph = null, + effectiveBaseRoute = NewWelcomeEmptyStartScreenDestination.baseRoute, + currentBaseRoute = null, + usesAuthenticationGraph = true, + usesNoSessionAuthenticationGraph = false, + usesInvalidSessionBackedAuthenticationGraph = false, + isSessionTransitionInProgress = false, + ) + ) + + assertSame(authenticationViewModelGraph, graph) + } + + @Test + fun givenCurrentRouteIsUnknownAndLoggedOutStartIsSessionRoute_whenResolvingActiveGraph_thenNoGraphIsUsed() { + val graph = resolveWireActivityActiveGraph( + WireActivityActiveGraphRequest( + authenticationViewModelGraph = authenticationViewModelGraph, + sessionGraph = null, + effectiveBaseRoute = HomeScreenDestination.baseRoute, + currentBaseRoute = null, + usesAuthenticationGraph = false, + usesNoSessionAuthenticationGraph = false, + usesInvalidSessionBackedAuthenticationGraph = false, + isSessionTransitionInProgress = false, + ) + ) + + assertNull(graph) + } + + @Test + fun givenCurrentRouteIsUnknownAndSessionGraphExists_whenResolvingActiveGraph_thenSessionGraphIsUsed() { + val graph = resolveWireActivityActiveGraph( + WireActivityActiveGraphRequest( + authenticationViewModelGraph = authenticationViewModelGraph, + sessionGraph = sessionGraph, + effectiveBaseRoute = HomeScreenDestination.baseRoute, + currentBaseRoute = null, + usesAuthenticationGraph = false, + usesNoSessionAuthenticationGraph = false, + usesInvalidSessionBackedAuthenticationGraph = false, + isSessionTransitionInProgress = false, + ) + ) + + assertSame(sessionGraph, graph) + } + + @Test + fun givenSessionTransitionIsInProgressOnSessionRoute_whenResolvingActiveGraph_thenNoGraphIsUsed() { + val graph = resolveWireActivityActiveGraph( + WireActivityActiveGraphRequest( + authenticationViewModelGraph = authenticationViewModelGraph, + sessionGraph = sessionGraph, + effectiveBaseRoute = HomeScreenDestination.baseRoute, + currentBaseRoute = HomeScreenDestination.baseRoute, + usesAuthenticationGraph = false, + usesNoSessionAuthenticationGraph = false, + usesInvalidSessionBackedAuthenticationGraph = false, + isSessionTransitionInProgress = true, + ) + ) + + assertNull(graph) + } + + @Test + fun givenUserLogsOutFromSessionRoute_whenResolvingTransientStates_thenSessionGraphIsDroppedAndAuthRootIsUsed() { + val loggedInGraph = resolveActiveGraph( + sessionGraph = sessionGraph, + effectiveBaseRoute = HomeScreenDestination.baseRoute, + currentBaseRoute = HomeScreenDestination.baseRoute, + ) + val transitioningGraph = resolveActiveGraph( + sessionGraph = null, + effectiveBaseRoute = HomeScreenDestination.baseRoute, + currentBaseRoute = HomeScreenDestination.baseRoute, + isSessionTransitionInProgress = true, + ) + val loggedOutStartDestination = resolveWireActivityNavHostStartDestination( + initialStartDestination = HomeScreenDestination, + currentUserId = null, + currentBaseRoute = null, + canUseNewLogin = true, + ) + val loggedOutGraph = resolveActiveGraph( + sessionGraph = null, + effectiveBaseRoute = loggedOutStartDestination.baseRoute, + currentBaseRoute = null, + usesAuthenticationGraph = true, + ) + + assertSame(sessionGraph, loggedInGraph) + assertNull(transitioningGraph) + assertEquals(NewWelcomeEmptyStartScreenDestination.baseRoute, loggedOutStartDestination.baseRoute) + assertSame(authenticationViewModelGraph, loggedOutGraph) + } + + @Test + fun givenUserSwitchesAccountsFromSessionRoute_whenResolvingTransientStates_thenGraphIsDroppedUntilNewSessionGraphExists() { + val newSessionGraph = mockk() + val currentUserId = UserId("current-user", "domain") + val switchedUserId = UserId("switched-user", "domain") + + val currentAccountGraph = resolveActiveGraph( + sessionGraph = sessionGraph, + effectiveBaseRoute = HomeScreenDestination.baseRoute, + currentBaseRoute = HomeScreenDestination.baseRoute, + ) + val switchingGraph = resolveActiveGraph( + sessionGraph = null, + effectiveBaseRoute = HomeScreenDestination.baseRoute, + currentBaseRoute = HomeScreenDestination.baseRoute, + isSessionTransitionInProgress = true, + ) + val switchedStartDestination = resolveWireActivityNavHostStartDestination( + initialStartDestination = HomeScreenDestination, + currentUserId = switchedUserId, + currentBaseRoute = HomeScreenDestination.baseRoute, + canUseNewLogin = true, + ) + val switchedAccountGraph = resolveActiveGraph( + sessionGraph = newSessionGraph, + effectiveBaseRoute = switchedStartDestination.baseRoute, + currentBaseRoute = HomeScreenDestination.baseRoute, + ) + + assertEquals( + HomeScreenDestination.baseRoute, + resolveWireActivityNavHostStartDestination( + initialStartDestination = HomeScreenDestination, + currentUserId = currentUserId, + currentBaseRoute = HomeScreenDestination.baseRoute, + canUseNewLogin = true, + ).baseRoute + ) + assertSame(sessionGraph, currentAccountGraph) + assertNull(switchingGraph) + assertEquals(HomeScreenDestination.baseRoute, switchedStartDestination.baseRoute) + assertSame(newSessionGraph, switchedAccountGraph) + } + + @Test + fun givenLoggedOutUserHasUnknownRouteAndNewLoginEnabled_whenResolvingNavHostStartDestination_thenEmptyWelcomeRootIsUsed() { + val destination = resolveWireActivityNavHostStartDestination( + initialStartDestination = HomeScreenDestination, + currentUserId = null, + currentBaseRoute = null, + canUseNewLogin = true, + ) + + assertEquals(NewWelcomeEmptyStartScreenDestination.baseRoute, destination.baseRoute) + } + + @Test + fun givenLoggedOutUserHasUnknownRouteAndNewLoginDisabled_whenResolvingNavHostStartDestination_thenWelcomeRootIsUsed() { + val destination = resolveWireActivityNavHostStartDestination( + initialStartDestination = HomeScreenDestination, + currentUserId = null, + currentBaseRoute = null, + canUseNewLogin = false, + ) + + assertEquals(WelcomeScreenDestination.baseRoute, destination.baseRoute) + } + + @Test + fun givenLoggedOutUserIsOnNewLoginRoute_whenResolvingNavHostStartDestination_thenEmptyWelcomeRootIsUsed() { + val destination = resolveWireActivityNavHostStartDestination( + initialStartDestination = HomeScreenDestination, + currentUserId = null, + currentBaseRoute = NewLoginScreenDestination.baseRoute, + canUseNewLogin = true, + ) + + assertEquals(NewWelcomeEmptyStartScreenDestination.baseRoute, destination.baseRoute) + } + + @Test + fun givenLoggedOutUserIsOnOldLoginRoute_whenResolvingNavHostStartDestination_thenWelcomeRootIsUsed() { + val destination = resolveWireActivityNavHostStartDestination( + initialStartDestination = HomeScreenDestination, + currentUserId = null, + currentBaseRoute = LoginScreenDestination.baseRoute, + canUseNewLogin = false, + ) + + assertEquals(WelcomeScreenDestination.baseRoute, destination.baseRoute) + } + + @Test + fun givenLoggedInUserHasUnknownRoute_whenResolvingNavHostStartDestination_thenInitialStartDestinationIsUsed() { + val destination = resolveWireActivityNavHostStartDestination( + initialStartDestination = HomeScreenDestination, + currentUserId = UserId("user", "domain"), + currentBaseRoute = null, + canUseNewLogin = true, + ) + + assertEquals(HomeScreenDestination.baseRoute, destination.baseRoute) + } + + @Test + fun givenLoggedInUserIsOnNestedNoSessionAuthRoute_whenResolvingNavHostStartDestination_thenInitialStartDestinationIsKept() { + val destination = resolveWireActivityNavHostStartDestination( + initialStartDestination = HomeScreenDestination, + currentUserId = UserId("user", "domain"), + currentBaseRoute = NewLoginPasswordScreenDestination.baseRoute, + canUseNewLogin = true, + ) + + assertEquals(HomeScreenDestination.baseRoute, destination.baseRoute) + } + + @Test + fun givenNoSessionLoginFlowReceivesCurrentUserOnPasswordRoute_whenResolvingNavHostStartDestination_thenAuthRootIsKept() { + val destination = resolveWireActivityNavHostStartDestination( + initialStartDestination = HomeScreenDestination, + currentUserId = UserId("user", "domain"), + currentBaseRoute = NewLoginPasswordScreenDestination.baseRoute, + canUseNewLogin = true, + noSessionAuthenticationStartedWithoutSession = true, + ) + + assertEquals(NewWelcomeEmptyStartScreenDestination.baseRoute, destination.baseRoute) + } + + @Test + fun givenAddAccountFlowReceivesCurrentUserOnPasswordRoute_whenResolvingNavHostStartDestination_thenSessionRootIsKept() { + val destination = resolveWireActivityNavHostStartDestination( + initialStartDestination = HomeScreenDestination, + currentUserId = UserId("user", "domain"), + currentBaseRoute = NewLoginPasswordScreenDestination.baseRoute, + canUseNewLogin = true, + noSessionAuthenticationStartedWithoutSession = false, + ) + + assertEquals(HomeScreenDestination.baseRoute, destination.baseRoute) + } + + @Test + fun givenLoggedInUserIsOnNewLoginRoute_whenResolvingNavHostStartDestination_thenInitialStartDestinationIsKept() { + val destination = resolveWireActivityNavHostStartDestination( + initialStartDestination = HomeScreenDestination, + currentUserId = UserId("user", "domain"), + currentBaseRoute = NewLoginScreenDestination.baseRoute, + canUseNewLogin = true, + ) + + assertEquals(HomeScreenDestination.baseRoute, destination.baseRoute) + } + + @Test + fun givenInitialStartDestinationIsAuthenticationRoot_whenResolvingNavHostStartDestination_thenInitialStartDestinationIsUsed() { + val destination = resolveWireActivityNavHostStartDestination( + initialStartDestination = NewWelcomeEmptyStartScreenDestination, + currentUserId = null, + currentBaseRoute = null, + canUseNewLogin = false, + ) + + assertEquals(NewWelcomeEmptyStartScreenDestination.baseRoute, destination.baseRoute) + } + + @Test + fun givenLoggedOutUserIsOnNestedNoSessionAuthRoute_whenResolvingNavHostStartDestination_thenEmptyWelcomeRootIsUsed() { + val destination = resolveWireActivityNavHostStartDestination( + initialStartDestination = HomeScreenDestination, + currentUserId = null, + currentBaseRoute = NewLoginPasswordScreenDestination.baseRoute, + canUseNewLogin = true, + ) + + assertEquals(NewWelcomeEmptyStartScreenDestination.baseRoute, destination.baseRoute) + } + + @Test + fun givenActiveSessionGraphExists_whenResolvingImageLoaderSessionGraph_thenActiveSessionGraphIsUsed() { + val retainedSessionGraph = mockk() + + val imageLoaderSessionGraph = resolveWireActivityImageLoaderSessionGraph( + activeSessionGraph = sessionGraph, + retainedSessionGraph = retainedSessionGraph, + ) + + assertSame(sessionGraph, imageLoaderSessionGraph) + } + + @Test + fun givenAuthGraphIsActiveAndSessionContentIsRetained_whenResolvingImageLoaderSessionGraph_thenRetainedSessionGraphIsUsed() { + val imageLoaderSessionGraph = resolveWireActivityImageLoaderSessionGraph( + activeSessionGraph = null, + retainedSessionGraph = sessionGraph, + ) + + assertSame(sessionGraph, imageLoaderSessionGraph) + } + + private fun resolveActiveGraph( + sessionGraph: AppSessionViewModelGraph?, + effectiveBaseRoute: String, + currentBaseRoute: String?, + usesAuthenticationGraph: Boolean = false, + isSessionTransitionInProgress: Boolean = false, + ) = resolveWireActivityActiveGraph( + WireActivityActiveGraphRequest( + authenticationViewModelGraph = authenticationViewModelGraph, + sessionGraph = sessionGraph, + effectiveBaseRoute = effectiveBaseRoute, + currentBaseRoute = currentBaseRoute, + usesAuthenticationGraph = usesAuthenticationGraph, + usesNoSessionAuthenticationGraph = false, + usesInvalidSessionBackedAuthenticationGraph = false, + isSessionTransitionInProgress = isSessionTransitionInProgress, + ) + ) }