Update the current user's last active time when navigating (#606)

This commit is contained in:
Brian Yencho 2024-01-14 10:26:48 -06:00 committed by Álison Fernandes
parent 3def25366b
commit 056b6eb30c
9 changed files with 119 additions and 7 deletions

View file

@ -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.
*/

View file

@ -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<UserState.SpecialCircumstance?>(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,

View file

@ -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,

View file

@ -19,7 +19,7 @@ private const val KEY_NAV_DESTINATION = "nav_state"
*/
@HiltViewModel
class RootNavViewModel @Inject constructor(
authRepository: AuthRepository,
private val authRepository: AuthRepository,
) : BaseViewModel<RootNavState, Unit, RootNavAction>(
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.
*/

View file

@ -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,

View file

@ -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<Unit, VaultUnlockedNavBarEvent, VaultUnlockedNavBarAction>(
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()
}
/**

View file

@ -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"

View file

@ -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<UserState?>(null)
private val authRepository = mockk<AuthRepository>() {
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,

View file

@ -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,
)
}