From 708c942edc415aba86923d984125fb979dcc37f5 Mon Sep 17 00:00:00 2001 From: Brian Yencho Date: Sun, 14 Jan 2024 10:26:48 -0600 Subject: [PATCH] Update the current user's last active time when navigating (#606) --- .../data/auth/repository/AuthRepository.kt | 5 ++++ .../auth/repository/AuthRepositoryImpl.kt | 12 +++++++- .../platform/feature/rootnav/RootNavScreen.kt | 11 +++++++ .../feature/rootnav/RootNavViewModel.kt | 12 +++++++- .../VaultUnlockedNavBarScreen.kt | 12 ++++++++ .../VaultUnlockedNavBarViewModel.kt | 15 +++++++++- .../auth/repository/AuthRepositoryTest.kt | 18 +++++++++++ .../feature/rootnav/RootNavViewModelTest.kt | 11 +++++++ .../VaultUnlockedNavBarViewModelTest.kt | 30 ++++++++++++++++--- 9 files changed, 119 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt index 3304bf7119..d27ebeb88d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt @@ -72,6 +72,11 @@ interface AuthRepository : AuthenticatorProvider { */ fun switchAccount(userId: String): SwitchAccountResult + /** + * Updates the "last active time" for the current user. + */ + fun updateLastActiveTime() + /** * Attempt to register a new account with the given parameters. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt index 0bb2bf5ad2..f004bfb9e5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.auth.repository +import android.os.SystemClock import com.bitwarden.core.HashPurpose import com.bitwarden.core.Kdf import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource @@ -55,7 +56,7 @@ import javax.inject.Singleton /** * Default implementation of [AuthRepository]. */ -@Suppress("LongParameterList") +@Suppress("LongParameterList", "TooManyFunctions") @Singleton class AuthRepositoryImpl constructor( private val accountsService: AccountsService, @@ -68,6 +69,7 @@ class AuthRepositoryImpl constructor( private val vaultRepository: VaultRepository, private val userLogoutManager: UserLogoutManager, dispatcherManager: DispatcherManager, + private val elapsedRealtimeMillisProvider: () -> Long = { SystemClock.elapsedRealtime() }, ) : AuthRepository { private val mutableSpecialCircumstanceStateFlow = MutableStateFlow(null) @@ -294,6 +296,14 @@ class AuthRepositoryImpl constructor( return SwitchAccountResult.AccountSwitched } + override fun updateLastActiveTime() { + val userId = activeUserId ?: return + authDiskSource.storeLastActiveTimeMillis( + userId = userId, + lastActiveTimeMillis = elapsedRealtimeMillisProvider(), + ) + } + @Suppress("ReturnCount", "LongMethod") override suspend fun register( email: String, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt index 2b02cef623..5e8ccea84b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt @@ -24,6 +24,8 @@ import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.VAULT_UNLOCKED_GRAP import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.navigateToVaultUnlockedGraph import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.vaultUnlockedGraph import com.x8bit.bitwarden.ui.platform.theme.RootTransitionProviders +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import java.util.concurrent.atomic.AtomicReference /** @@ -43,6 +45,15 @@ fun RootNavScreen( if (isNotSplashScreen) onSplashScreenRemoved() } + LaunchedEffect(Unit) { + navController + .currentBackStackEntryFlow + .onEach { + viewModel.trySendAction(RootNavAction.BackStackUpdate) + } + .launchIn(this) + } + NavHost( navController = navController, startDestination = SPLASH_ROUTE, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt index 798dfb7134..12d3412486 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt @@ -19,7 +19,7 @@ private const val KEY_NAV_DESTINATION = "nav_state" */ @HiltViewModel class RootNavViewModel @Inject constructor( - authRepository: AuthRepository, + private val authRepository: AuthRepository, ) : BaseViewModel( initialState = RootNavState.Splash, ) { @@ -32,10 +32,15 @@ class RootNavViewModel @Inject constructor( override fun handleAction(action: RootNavAction) { when (action) { + is RootNavAction.BackStackUpdate -> handleBackStackUpdate() is RootNavAction.Internal.UserStateUpdateReceive -> handleUserStateUpdateReceive(action) } } + private fun handleBackStackUpdate() { + authRepository.updateLastActiveTime() + } + private fun handleUserStateUpdateReceive( action: RootNavAction.Internal.UserStateUpdateReceive, ) { @@ -93,6 +98,11 @@ sealed class RootNavState : Parcelable { */ sealed class RootNavAction { + /** + * Indicates the backstack has changed. + */ + data object BackStackUpdate : RootNavAction() + /** * Internal ViewModel actions. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt index 98f05fe247..78d011fa25 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.material3.NavigationBarItemDefaults import androidx.compose.material3.ScaffoldDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -57,6 +58,8 @@ import com.x8bit.bitwarden.ui.tools.feature.send.sendGraph import com.x8bit.bitwarden.ui.vault.feature.vault.VAULT_GRAPH_ROUTE import com.x8bit.bitwarden.ui.vault.feature.vault.navigateToVaultGraph import com.x8bit.bitwarden.ui.vault.feature.vault.vaultGraph +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize /** @@ -98,6 +101,15 @@ fun VaultUnlockedNavBarScreen( } } } + LaunchedEffect(Unit) { + navController + .currentBackStackEntryFlow + .onEach { + viewModel.trySendAction(VaultUnlockedNavBarAction.BackStackUpdate) + } + .launchIn(this) + } + VaultUnlockedNavBarScaffold( navController = navController, onNavigateToVaultItem = onNavigateToVaultItem, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModel.kt index 8276520d24..30af28e34c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModel.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar +import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.ui.platform.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @@ -8,7 +9,9 @@ import javax.inject.Inject * Manages bottom tab navigation of the application. */ @HiltViewModel -class VaultUnlockedNavBarViewModel @Inject constructor() : +class VaultUnlockedNavBarViewModel @Inject constructor( + private val authRepository: AuthRepository, +) : BaseViewModel( initialState = Unit, ) { @@ -19,6 +22,7 @@ class VaultUnlockedNavBarViewModel @Inject constructor() : VaultUnlockedNavBarAction.SendTabClick -> handleSendTabClicked() VaultUnlockedNavBarAction.SettingsTabClick -> handleSettingsTabClicked() VaultUnlockedNavBarAction.VaultTabClick -> handleVaultTabClicked() + VaultUnlockedNavBarAction.BackStackUpdate -> handleBackStackUpdate() } } // #region BottomTabViewModel Action Handlers @@ -49,6 +53,10 @@ class VaultUnlockedNavBarViewModel @Inject constructor() : private fun handleSettingsTabClicked() { sendEvent(VaultUnlockedNavBarEvent.NavigateToSettingsScreen) } + + private fun handleBackStackUpdate() { + authRepository.updateLastActiveTime() + } // #endregion BottomTabViewModel Action Handlers } @@ -75,6 +83,11 @@ sealed class VaultUnlockedNavBarAction { * click Settings tab. */ data object SettingsTabClick : VaultUnlockedNavBarAction() + + /** + * Indicates the backstack has changed. + */ + data object BackStackUpdate : VaultUnlockedNavBarAction() } /** diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt index 224b735c83..2558b9eab1 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt @@ -125,6 +125,8 @@ class AuthRepositoryTest { every { logout(any()) } just runs } + private var elapsedRealtimeMillis = 123456789L + private val repository = AuthRepositoryImpl( accountsService = accountsService, identityService = identityService, @@ -136,6 +138,7 @@ class AuthRepositoryTest { vaultRepository = vaultRepository, userLogoutManager = userLogoutManager, dispatcherManager = dispatcherManager, + elapsedRealtimeMillisProvider = { elapsedRealtimeMillis }, ) @BeforeEach @@ -1220,6 +1223,21 @@ class AuthRepositoryTest { verify { vaultRepository.clearUnlockedData() } } + @Test + fun `updateLastActiveTime should update the last active time for the current user`() { + val userId = USER_ID_1 + fakeAuthDiskSource.userState = SINGLE_USER_STATE_1 + + assertNull(fakeAuthDiskSource.getLastActiveTimeMillis(userId = userId)) + + repository.updateLastActiveTime() + + assertEquals( + elapsedRealtimeMillis, + fakeAuthDiskSource.getLastActiveTimeMillis(userId = userId), + ) + } + @Test fun `getPasswordBreachCount should return failure when service returns failure`() = runTest { val password = "password" diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt index bd92492ecf..8cb50299b8 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt @@ -5,7 +5,10 @@ import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.platform.repository.model.Environment import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import io.mockk.every +import io.mockk.just import io.mockk.mockk +import io.mockk.runs +import io.mockk.verify import kotlinx.coroutines.flow.MutableStateFlow import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test @@ -14,6 +17,7 @@ class RootNavViewModelTest : BaseViewModelTest() { private val mutableUserStateFlow = MutableStateFlow(null) private val authRepository = mockk() { every { userStateFlow } returns mutableUserStateFlow + every { updateLastActiveTime() } just runs } @Test @@ -74,6 +78,13 @@ class RootNavViewModelTest : BaseViewModelTest() { assertEquals(RootNavState.VaultLocked, viewModel.stateFlow.value) } + @Test + fun `BackStackUpdate should call updateLastActiveTime`() { + val viewModel = createViewModel() + viewModel.trySendAction(RootNavAction.BackStackUpdate) + verify { authRepository.updateLastActiveTime() } + } + private fun createViewModel(): RootNavViewModel = RootNavViewModel( authRepository = authRepository, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModelTest.kt index 02a53a060e..f5fe8d26b7 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModelTest.kt @@ -1,15 +1,25 @@ package com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar import app.cash.turbine.test +import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.verify import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test class VaultUnlockedNavBarViewModelTest : BaseViewModelTest() { + private val authRepository: AuthRepository = mockk { + every { updateLastActiveTime() } just runs + } + @Test fun `VaultTabClick should navigate to the vault screen`() = runTest { - val viewModel = VaultUnlockedNavBarViewModel() + val viewModel = createViewModel() viewModel.eventFlow.test { viewModel.trySendAction(VaultUnlockedNavBarAction.VaultTabClick) assertEquals(VaultUnlockedNavBarEvent.NavigateToVaultScreen, awaitItem()) @@ -18,7 +28,7 @@ class VaultUnlockedNavBarViewModelTest : BaseViewModelTest() { @Test fun `SendTabClick should navigate to the send screen`() = runTest { - val viewModel = VaultUnlockedNavBarViewModel() + val viewModel = createViewModel() viewModel.eventFlow.test { viewModel.trySendAction(VaultUnlockedNavBarAction.SendTabClick) assertEquals(VaultUnlockedNavBarEvent.NavigateToSendScreen, awaitItem()) @@ -27,7 +37,7 @@ class VaultUnlockedNavBarViewModelTest : BaseViewModelTest() { @Test fun `GeneratorTabClick should navigate to the generator screen`() = runTest { - val viewModel = VaultUnlockedNavBarViewModel() + val viewModel = createViewModel() viewModel.eventFlow.test { viewModel.actionChannel.trySend(VaultUnlockedNavBarAction.GeneratorTabClick) assertEquals(VaultUnlockedNavBarEvent.NavigateToGeneratorScreen, awaitItem()) @@ -36,10 +46,22 @@ class VaultUnlockedNavBarViewModelTest : BaseViewModelTest() { @Test fun `SettingsTabClick should navigate to the settings screen`() = runTest { - val viewModel = VaultUnlockedNavBarViewModel() + val viewModel = createViewModel() viewModel.eventFlow.test { viewModel.actionChannel.trySend(VaultUnlockedNavBarAction.SettingsTabClick) assertEquals(VaultUnlockedNavBarEvent.NavigateToSettingsScreen, awaitItem()) } } + + @Test + fun `BackStackUpdate should call updateLastActiveTime`() { + val viewModel = createViewModel() + viewModel.trySendAction(VaultUnlockedNavBarAction.BackStackUpdate) + verify { authRepository.updateLastActiveTime() } + } + + private fun createViewModel() = + VaultUnlockedNavBarViewModel( + authRepository = authRepository, + ) }