mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 10:48:47 +03:00
Add support for the pull-to-refresh in settings (#615)
This commit is contained in:
parent
82b174168e
commit
561aabc528
10 changed files with 246 additions and 17 deletions
|
@ -10,6 +10,37 @@ import androidx.core.content.edit
|
|||
abstract class BaseDiskSource(
|
||||
private val sharedPreferences: SharedPreferences,
|
||||
) {
|
||||
/**
|
||||
* Gets the [Boolean] for the given [key] from [SharedPreferences], or return the [default]
|
||||
* value if that key is not present.
|
||||
*/
|
||||
protected fun getBoolean(
|
||||
key: String,
|
||||
default: Boolean? = null,
|
||||
): Boolean? =
|
||||
if (sharedPreferences.contains(key)) {
|
||||
sharedPreferences.getBoolean(key, false)
|
||||
} else {
|
||||
// Make sure we can return a null value as a default if necessary
|
||||
default
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts the [value] in [SharedPreferences] for the given [key] (or removes the key when the
|
||||
* value is `null`).
|
||||
*/
|
||||
protected fun putBoolean(
|
||||
key: String,
|
||||
value: Boolean?,
|
||||
): Unit =
|
||||
sharedPreferences.edit {
|
||||
if (value != null) {
|
||||
putBoolean(key, value)
|
||||
} else {
|
||||
remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the [Int] for the given [key] from [SharedPreferences], or return the [default] value
|
||||
* if that key is not present.
|
||||
|
|
|
@ -53,4 +53,20 @@ interface SettingsDiskSource {
|
|||
userId: String,
|
||||
vaultTimeoutAction: VaultTimeoutAction?,
|
||||
)
|
||||
|
||||
/**
|
||||
* Gets the current state of the pull to refresh feature for the given [userId].
|
||||
*/
|
||||
fun getPullToRefreshEnabled(userId: String): Boolean?
|
||||
|
||||
/**
|
||||
* Emits updates that track [getPullToRefreshEnabled] for the given [userId]. This will replay
|
||||
* the last known value, if any.
|
||||
*/
|
||||
fun getPullToRefreshEnabledFlow(userId: String): Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* Stores the given [isPullToRefreshEnabled] for the given [userId].
|
||||
*/
|
||||
fun storePullToRefreshEnabled(userId: String, isPullToRefreshEnabled: Boolean?)
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
|||
import kotlinx.coroutines.flow.onSubscription
|
||||
|
||||
private const val APP_LANGUAGE_KEY = "$BASE_KEY:appLocale"
|
||||
private const val PULL_TO_REFRESH_KEY = "$BASE_KEY:syncOnRefresh"
|
||||
private const val VAULT_TIMEOUT_ACTION_KEY = "$BASE_KEY:vaultTimeoutAction"
|
||||
private const val VAULT_TIME_IN_MINUTES_KEY = "$BASE_KEY:vaultTimeout"
|
||||
|
||||
|
@ -27,6 +28,9 @@ class SettingsDiskSourceImpl(
|
|||
private val mutableVaultTimeoutInMinutesFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Int?>>()
|
||||
|
||||
private val mutablePullToRefreshEnabledFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
override var appLanguage: AppLanguage?
|
||||
get() = getString(key = APP_LANGUAGE_KEY)
|
||||
?.let { storedValue ->
|
||||
|
@ -82,6 +86,18 @@ class SettingsDiskSourceImpl(
|
|||
getMutableVaultTimeoutActionFlow(userId = userId).tryEmit(vaultTimeoutAction)
|
||||
}
|
||||
|
||||
override fun getPullToRefreshEnabled(userId: String): Boolean? =
|
||||
getBoolean(key = "${PULL_TO_REFRESH_KEY}_$userId")
|
||||
|
||||
override fun getPullToRefreshEnabledFlow(userId: String): Flow<Boolean?> =
|
||||
getMutablePullToRefreshEnabledFlowMap(userId = userId)
|
||||
.onSubscription { emit(getPullToRefreshEnabled(userId = userId)) }
|
||||
|
||||
override fun storePullToRefreshEnabled(userId: String, isPullToRefreshEnabled: Boolean?) {
|
||||
putBoolean(key = "${PULL_TO_REFRESH_KEY}_$userId", value = isPullToRefreshEnabled)
|
||||
getMutablePullToRefreshEnabledFlowMap(userId = userId).tryEmit(isPullToRefreshEnabled)
|
||||
}
|
||||
|
||||
private fun getMutableVaultTimeoutActionFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<VaultTimeoutAction?> =
|
||||
|
@ -95,4 +111,11 @@ class SettingsDiskSourceImpl(
|
|||
mutableVaultTimeoutInMinutesFlowMap.getOrPut(userId) {
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun getMutablePullToRefreshEnabledFlowMap(
|
||||
userId: String,
|
||||
): MutableSharedFlow<Boolean?> =
|
||||
mutablePullToRefreshEnabledFlowMap.getOrPut(userId) {
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,4 +63,14 @@ interface SettingsRepository {
|
|||
* Stores the given [VaultTimeoutAction] for the given [userId].
|
||||
*/
|
||||
fun storeVaultTimeoutAction(userId: String, vaultTimeoutAction: VaultTimeoutAction?)
|
||||
|
||||
/**
|
||||
* Gets updates for the pull to refresh enabled.
|
||||
*/
|
||||
fun getPullToRefreshEnabledFlow(): StateFlow<Boolean>
|
||||
|
||||
/**
|
||||
* Stores the given [isPullToRefreshEnabled] for the active user.
|
||||
*/
|
||||
fun storePullToRefreshEnabled(isPullToRefreshEnabled: Boolean)
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
|||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
@ -116,6 +117,29 @@ class SettingsRepositoryImpl(
|
|||
vaultTimeoutAction = vaultTimeoutAction,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getPullToRefreshEnabledFlow(): StateFlow<Boolean> {
|
||||
val userId = activeUserId ?: return MutableStateFlow(false)
|
||||
return settingsDiskSource
|
||||
.getPullToRefreshEnabledFlow(userId = userId)
|
||||
.map { it ?: false }
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = settingsDiskSource
|
||||
.getPullToRefreshEnabled(userId = userId)
|
||||
?: false,
|
||||
)
|
||||
}
|
||||
|
||||
override fun storePullToRefreshEnabled(isPullToRefreshEnabled: Boolean) {
|
||||
activeUserId?.let {
|
||||
settingsDiskSource.storePullToRefreshEnabled(
|
||||
userId = it,
|
||||
isPullToRefreshEnabled = isPullToRefreshEnabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.other
|
|||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
|
@ -19,13 +20,14 @@ private const val KEY_STATE = "state"
|
|||
*/
|
||||
@HiltViewModel
|
||||
class OtherViewModel @Inject constructor(
|
||||
private val settingsRepo: SettingsRepository,
|
||||
private val vaultRepo: VaultRepository,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<OtherState, OtherEvent, OtherAction>(
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
?: OtherState(
|
||||
allowScreenCapture = false,
|
||||
allowSyncOnRefresh = false,
|
||||
allowSyncOnRefresh = settingsRepo.getPullToRefreshEnabledFlow().value,
|
||||
clearClipboardFrequency = OtherState.ClearClipboardFrequency.DEFAULT,
|
||||
lastSyncTime = "5/14/2023 4:52 PM",
|
||||
),
|
||||
|
@ -44,7 +46,7 @@ class OtherViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleAllowSyncToggled(action: OtherAction.AllowSyncToggle) {
|
||||
// TODO BIT-461 hook up to pull-to-refresh feature
|
||||
settingsRepo.storePullToRefreshEnabled(action.isSyncEnabled)
|
||||
mutableStateFlow.update { it.copy(allowSyncOnRefresh = action.isSyncEnabled) }
|
||||
}
|
||||
|
||||
|
|
|
@ -232,4 +232,65 @@ class SettingsDiskSourceTest {
|
|||
)
|
||||
assertNull(fakeSharedPreferences.getString(vaultTimeoutActionKey, null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `storePullToRefreshEnabled when values are present should pull from SharedPreferences`() {
|
||||
val pullToRefreshBaseKey = "bwPreferencesStorage:syncOnRefresh"
|
||||
val mockUserId = "mockUserId"
|
||||
val pullToRefreshKey = "${pullToRefreshBaseKey}_$mockUserId"
|
||||
fakeSharedPreferences
|
||||
.edit()
|
||||
.putBoolean(pullToRefreshKey, true)
|
||||
.apply()
|
||||
assertEquals(true, settingsDiskSource.getPullToRefreshEnabled(userId = mockUserId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `storePullToRefreshEnabled when values are absent should return null`() {
|
||||
val mockUserId = "mockUserId"
|
||||
assertNull(settingsDiskSource.getPullToRefreshEnabled(userId = mockUserId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPullToRefreshEnabledFlow should react to changes in getPullToRefreshEnabled`() =
|
||||
runTest {
|
||||
val mockUserId = "mockUserId"
|
||||
settingsDiskSource.getPullToRefreshEnabledFlow(userId = mockUserId).test {
|
||||
// The initial values of the Flow and the property are in sync
|
||||
assertNull(settingsDiskSource.getPullToRefreshEnabled(userId = mockUserId))
|
||||
assertNull(awaitItem())
|
||||
|
||||
// Updating the disk source updates shared preferences
|
||||
settingsDiskSource.storePullToRefreshEnabled(
|
||||
userId = mockUserId,
|
||||
isPullToRefreshEnabled = true,
|
||||
)
|
||||
assertEquals(true, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `storePullToRefreshEnabled for non-null values should update SharedPreferences`() {
|
||||
val pullToRefreshBaseKey = "bwPreferencesStorage:syncOnRefresh"
|
||||
val mockUserId = "mockUserId"
|
||||
val pullToRefreshKey = "${pullToRefreshBaseKey}_$mockUserId"
|
||||
settingsDiskSource.storePullToRefreshEnabled(
|
||||
userId = mockUserId,
|
||||
isPullToRefreshEnabled = true,
|
||||
)
|
||||
assertTrue(fakeSharedPreferences.getBoolean(pullToRefreshKey, false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `storePullToRefreshEnabled for null values should clear SharedPreferences`() {
|
||||
val pullToRefreshBaseKey = "bwPreferencesStorage:syncOnRefresh"
|
||||
val mockUserId = "mockUserId"
|
||||
val pullToRefreshKey = "${pullToRefreshBaseKey}_$mockUserId"
|
||||
fakeSharedPreferences.edit { putBoolean(pullToRefreshKey, false) }
|
||||
settingsDiskSource.storePullToRefreshEnabled(
|
||||
userId = mockUserId,
|
||||
isPullToRefreshEnabled = null,
|
||||
)
|
||||
assertFalse(fakeSharedPreferences.contains(pullToRefreshKey))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,9 +19,14 @@ class FakeSettingsDiskSource : SettingsDiskSource {
|
|||
private val mutableVaultTimeoutInMinutesFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Int?>>()
|
||||
|
||||
private val mutablePullToRefreshEnabledFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
private val storedVaultTimeoutActions = mutableMapOf<String, VaultTimeoutAction?>()
|
||||
private val storedVaultTimeoutInMinutes = mutableMapOf<String, Int?>()
|
||||
|
||||
private val storedPullToRefreshEnabled = mutableMapOf<String, Boolean?>()
|
||||
|
||||
override var appLanguage: AppLanguage? = null
|
||||
|
||||
override fun clearData(userId: String) {
|
||||
|
@ -62,6 +67,18 @@ class FakeSettingsDiskSource : SettingsDiskSource {
|
|||
getMutableVaultTimeoutActionsFlow(userId = userId).tryEmit(vaultTimeoutAction)
|
||||
}
|
||||
|
||||
override fun getPullToRefreshEnabled(userId: String): Boolean? =
|
||||
storedPullToRefreshEnabled[userId]
|
||||
|
||||
override fun getPullToRefreshEnabledFlow(userId: String): Flow<Boolean?> =
|
||||
getMutablePullToRefreshEnabledFlow(userId = userId)
|
||||
.onSubscription { emit(getPullToRefreshEnabled(userId = userId)) }
|
||||
|
||||
override fun storePullToRefreshEnabled(userId: String, isPullToRefreshEnabled: Boolean?) {
|
||||
storedPullToRefreshEnabled[userId] = isPullToRefreshEnabled
|
||||
getMutablePullToRefreshEnabledFlow(userId = userId).tryEmit(isPullToRefreshEnabled)
|
||||
}
|
||||
|
||||
//region Private helper functions
|
||||
|
||||
private fun getMutableVaultTimeoutActionsFlow(
|
||||
|
@ -78,5 +95,12 @@ class FakeSettingsDiskSource : SettingsDiskSource {
|
|||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun getMutablePullToRefreshEnabledFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<Boolean?> =
|
||||
mutablePullToRefreshEnabledFlowMap.getOrPut(userId) {
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
//endregion Private helper functions
|
||||
}
|
||||
|
|
|
@ -2,11 +2,13 @@ package com.x8bit.bitwarden.data.platform.repository
|
|||
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.util.FakeSettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
@ -271,6 +273,38 @@ class SettingsRepositoryTest {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPullToRefreshEnabledFlow should react to changes in SettingsDiskSource`() = runTest {
|
||||
val userId = "userId"
|
||||
val userState = mockk<UserStateJson> {
|
||||
every { activeUserId } returns userId
|
||||
}
|
||||
coEvery { authDiskSource.userState } returns userState
|
||||
settingsRepository
|
||||
.getPullToRefreshEnabledFlow()
|
||||
.test {
|
||||
assertFalse(awaitItem())
|
||||
fakeSettingsDiskSource.storePullToRefreshEnabled(
|
||||
userId = userId,
|
||||
isPullToRefreshEnabled = true,
|
||||
)
|
||||
assertTrue(awaitItem())
|
||||
fakeSettingsDiskSource.storePullToRefreshEnabled(
|
||||
userId = userId,
|
||||
isPullToRefreshEnabled = false,
|
||||
)
|
||||
assertFalse(awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `storePullToRefreshEnabled should properly update SettingsDiskSource`() {
|
||||
val userId = "userId"
|
||||
every { authDiskSource.userState?.activeUserId } returns userId
|
||||
settingsRepository.storePullToRefreshEnabled(true)
|
||||
assertEquals(true, fakeSettingsDiskSource.getPullToRefreshEnabled(userId = userId))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.other
|
|||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import io.mockk.every
|
||||
|
@ -9,12 +10,17 @@ import io.mockk.just
|
|||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class OtherViewModelTest : BaseViewModelTest() {
|
||||
val vaultRepository = mockk<VaultRepository>()
|
||||
private val mutablePullToRefreshStateFlow = MutableStateFlow(false)
|
||||
private val settingsRepository = mockk<SettingsRepository> {
|
||||
every { getPullToRefreshEnabledFlow() } returns mutablePullToRefreshStateFlow
|
||||
}
|
||||
private val vaultRepository = mockk<VaultRepository>()
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct when not set`() {
|
||||
|
@ -51,21 +57,18 @@ class OtherViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `on AllowSyncToggled should update value in state`() = runTest {
|
||||
fun `on AllowSyncToggled should update value in state`() {
|
||||
every {
|
||||
settingsRepository.storePullToRefreshEnabled(isPullToRefreshEnabled = true)
|
||||
} just runs
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
expectNoEvents()
|
||||
}
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
DEFAULT_STATE,
|
||||
awaitItem(),
|
||||
)
|
||||
viewModel.trySendAction(OtherAction.AllowSyncToggle(true))
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(allowSyncOnRefresh = true),
|
||||
awaitItem(),
|
||||
)
|
||||
viewModel.trySendAction(OtherAction.AllowSyncToggle(true))
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(allowSyncOnRefresh = true),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
verify(exactly = 1) {
|
||||
settingsRepository.storePullToRefreshEnabled(isPullToRefreshEnabled = true)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -114,6 +117,7 @@ class OtherViewModelTest : BaseViewModelTest() {
|
|||
private fun createViewModel(
|
||||
state: OtherState? = null,
|
||||
) = OtherViewModel(
|
||||
settingsRepo = settingsRepository,
|
||||
vaultRepo = vaultRepository,
|
||||
savedStateHandle = SavedStateHandle().apply {
|
||||
set("state", state)
|
||||
|
|
Loading…
Add table
Reference in a new issue