mirror of
https://github.com/bitwarden/android.git
synced 2026-08-01 18:53:39 -05:00
BIT-205: Populate vault with login items (#246)
This commit is contained in:
committed by
Álison Fernandes
parent
3e6ce662d8
commit
fd10de9456
+5
-12
@@ -8,31 +8,24 @@ import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.DateTimeParseException
|
||||
|
||||
/**
|
||||
* Used to serialize and deserialize [LocalDateTime].
|
||||
*/
|
||||
class LocalDateTimeSerializer : KSerializer<LocalDateTime> {
|
||||
private val localDateTimeFormatter =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SS'Z'")
|
||||
private val localDateTimeFormatterNanoSeconds =
|
||||
private val dateTimeFormatterDeserialization = DateTimeFormatter
|
||||
.ofPattern("yyyy-MM-dd'T'HH:mm:ss.[SSSSSSS][SSSSSS][SSSSS][SSSS][SSS][SS][S]'Z'")
|
||||
private val dateTimeFormatterSerialization =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSS'Z'")
|
||||
override val descriptor: SerialDescriptor
|
||||
get() = PrimitiveSerialDescriptor(serialName = "LocalDateTime", kind = PrimitiveKind.STRING)
|
||||
|
||||
override fun deserialize(decoder: Decoder): LocalDateTime =
|
||||
decoder.decodeString().let { dateString ->
|
||||
try {
|
||||
LocalDateTime
|
||||
.parse(dateString, localDateTimeFormatter)
|
||||
} catch (exception: DateTimeParseException) {
|
||||
LocalDateTime
|
||||
.parse(dateString, localDateTimeFormatterNanoSeconds)
|
||||
}
|
||||
LocalDateTime.parse(dateString, dateTimeFormatterDeserialization)
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: LocalDateTime) {
|
||||
encoder.encodeString(localDateTimeFormatter.format(value))
|
||||
encoder.encodeString(dateTimeFormatterSerialization.format(value))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,12 @@ interface VaultSdkSource {
|
||||
/**
|
||||
* Decrypts a list of [Cipher]s returning a list of [CipherListView] wrapped in a [Result].
|
||||
*/
|
||||
suspend fun decryptCipherList(cipherList: List<Cipher>): Result<List<CipherListView>>
|
||||
suspend fun decryptCipherListCollection(cipherList: List<Cipher>): Result<List<CipherListView>>
|
||||
|
||||
/**
|
||||
* Decrypts a list of [Cipher]s returning a list of [CipherView] wrapped in a [Result].
|
||||
*/
|
||||
suspend fun decryptCipherList(cipherList: List<Cipher>): Result<List<CipherView>>
|
||||
|
||||
/**
|
||||
* Decrypts a [Folder] returning a [FolderView] wrapped in a [Result].
|
||||
|
||||
+6
-1
@@ -35,9 +35,14 @@ class VaultSdkSourceImpl(
|
||||
override suspend fun decryptCipher(cipher: Cipher): Result<CipherView> =
|
||||
runCatching { clientVault.ciphers().decrypt(cipher) }
|
||||
|
||||
override suspend fun decryptCipherList(cipherList: List<Cipher>): Result<List<CipherListView>> =
|
||||
override suspend fun decryptCipherListCollection(
|
||||
cipherList: List<Cipher>,
|
||||
): Result<List<CipherListView>> =
|
||||
runCatching { clientVault.ciphers().decryptList(cipherList) }
|
||||
|
||||
override suspend fun decryptCipherList(cipherList: List<Cipher>): Result<List<CipherView>> =
|
||||
runCatching { cipherList.map { clientVault.ciphers().decrypt(it) } }
|
||||
|
||||
override suspend fun decryptFolder(folder: Folder): Result<FolderView> =
|
||||
runCatching { clientVault.folders().decrypt(folder) }
|
||||
|
||||
|
||||
@@ -20,6 +20,10 @@ import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -37,6 +41,8 @@ class VaultRepositoryImpl constructor(
|
||||
|
||||
private var syncJob: Job = Job().apply { complete() }
|
||||
|
||||
private var willSyncAfterUnlock = false
|
||||
|
||||
private val vaultDataMutableStateFlow =
|
||||
MutableStateFlow<DataState<VaultData>>(DataState.Loading)
|
||||
|
||||
@@ -48,7 +54,7 @@ class VaultRepositoryImpl constructor(
|
||||
}
|
||||
|
||||
override fun sync() {
|
||||
if (!syncJob.isCompleted) return
|
||||
if (!syncJob.isCompleted || willSyncAfterUnlock) return
|
||||
vaultDataMutableStateFlow.value.data?.let { data ->
|
||||
vaultDataMutableStateFlow.update {
|
||||
DataState.Pending(data = data)
|
||||
@@ -86,10 +92,16 @@ class VaultRepositoryImpl constructor(
|
||||
}
|
||||
|
||||
override suspend fun unlockVaultAndSync(masterPassword: String): VaultUnlockResult {
|
||||
return initializeCrypto(masterPassword = masterPassword)
|
||||
.also { vaultUnlockedResult ->
|
||||
if (vaultUnlockedResult is VaultUnlockResult.Success) sync()
|
||||
return flow {
|
||||
willSyncAfterUnlock = true
|
||||
emit(initializeCrypto(masterPassword = masterPassword))
|
||||
}
|
||||
.onEach {
|
||||
willSyncAfterUnlock = false
|
||||
if (it is VaultUnlockResult.Success) sync()
|
||||
}
|
||||
.onCompletion { willSyncAfterUnlock = false }
|
||||
.first()
|
||||
}
|
||||
|
||||
private fun storeUserKeyAndPrivateKey(
|
||||
@@ -156,7 +168,7 @@ class VaultRepositoryImpl constructor(
|
||||
onSuccess = { (decryptedCipherList, decryptedFolderList) ->
|
||||
DataState.Loaded(
|
||||
data = VaultData(
|
||||
cipherListViewList = decryptedCipherList,
|
||||
cipherViewList = decryptedCipherList,
|
||||
folderViewList = decryptedFolderList,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
package com.x8bit.bitwarden.data.vault.repository.model
|
||||
|
||||
import com.bitwarden.core.CipherListView
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.bitwarden.core.FolderView
|
||||
|
||||
/**
|
||||
* Represents decrypted vault data.
|
||||
*
|
||||
* @param cipherListViewList List of decrypted ciphers.
|
||||
* @param cipherViewList List of decrypted ciphers.
|
||||
* @param folderViewList List of decrypted folders.
|
||||
*/
|
||||
data class VaultData(
|
||||
val cipherListViewList: List<CipherListView>,
|
||||
val cipherViewList: List<CipherView>,
|
||||
val folderViewList: List<FolderView>,
|
||||
)
|
||||
|
||||
@@ -7,12 +7,16 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AccountSummary
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.concat
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.hexToColor
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.initials
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toViewState
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -32,8 +36,8 @@ private const val KEY_STATE = "state"
|
||||
@HiltViewModel
|
||||
class VaultViewModel @Inject constructor(
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
vaultRepository: VaultRepository,
|
||||
) : BaseViewModel<VaultState, VaultEvent, VaultAction>(
|
||||
// TODO retrieve this from the data layer BIT-205
|
||||
initialState = savedStateHandle[KEY_STATE] ?: VaultState(
|
||||
initials = activeAccountSummary.initials,
|
||||
avatarColorString = activeAccountSummary.avatarColorHex,
|
||||
@@ -46,30 +50,18 @@ class VaultViewModel @Inject constructor(
|
||||
stateFlow
|
||||
.onEach { savedStateHandle[KEY_STATE] = it }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
vaultRepository
|
||||
.vaultDataStateFlow
|
||||
.onEach { sendAction(VaultAction.Internal.VaultDataReceive(vaultData = it)) }
|
||||
.launchIn(viewModelScope)
|
||||
// TODO remove this block once vault unlocked is implemented in BIT-1082
|
||||
viewModelScope.launch {
|
||||
// TODO will need to load actual vault items BIT-205
|
||||
@Suppress("MagicNumber")
|
||||
delay(2000)
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = VaultState.ViewState.Content(
|
||||
loginItemsCount = 0,
|
||||
cardItemsCount = 0,
|
||||
identityItemsCount = 0,
|
||||
secureNoteItemsCount = 0,
|
||||
favoriteItems = emptyList(),
|
||||
folderItems = listOf(
|
||||
VaultState.ViewState.FolderItem(
|
||||
id = null,
|
||||
name = R.string.folder_none.asText(),
|
||||
itemCount = 0,
|
||||
),
|
||||
),
|
||||
// TODO Take into account the max threshold of no folder as well as the
|
||||
// case where it is empty, in which case, no folder is a folder. BIT-205
|
||||
noFolderItems = emptyList(),
|
||||
trashItemsCount = 0,
|
||||
delay(5000)
|
||||
if (vaultRepository.vaultDataStateFlow.value == DataState.Loading) {
|
||||
sendAction(
|
||||
VaultAction.Internal.VaultDataReceive(
|
||||
DataState.Error(error = IllegalStateException()),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -78,17 +70,18 @@ class VaultViewModel @Inject constructor(
|
||||
|
||||
override fun handleAction(action: VaultAction) {
|
||||
when (action) {
|
||||
VaultAction.AddItemClick -> handleAddItemClick()
|
||||
VaultAction.CardGroupClick -> handleCardClick()
|
||||
is VaultAction.AddItemClick -> handleAddItemClick()
|
||||
is VaultAction.CardGroupClick -> handleCardClick()
|
||||
is VaultAction.FolderClick -> handleFolderItemClick(action)
|
||||
VaultAction.IdentityGroupClick -> handleIdentityClick()
|
||||
VaultAction.LoginGroupClick -> handleLoginClick()
|
||||
VaultAction.SearchIconClick -> handleSearchIconClick()
|
||||
is VaultAction.IdentityGroupClick -> handleIdentityClick()
|
||||
is VaultAction.LoginGroupClick -> handleLoginClick()
|
||||
is VaultAction.SearchIconClick -> handleSearchIconClick()
|
||||
is VaultAction.AccountSwitchClick -> handleAccountSwitchClick(action)
|
||||
VaultAction.AddAccountClick -> handleAddAccountClick()
|
||||
VaultAction.SecureNoteGroupClick -> handleSecureNoteClick()
|
||||
VaultAction.TrashClick -> handleTrashClick()
|
||||
is VaultAction.AddAccountClick -> handleAddAccountClick()
|
||||
is VaultAction.SecureNoteGroupClick -> handleSecureNoteClick()
|
||||
is VaultAction.TrashClick -> handleTrashClick()
|
||||
is VaultAction.VaultItemClick -> handleVaultItemClick(action)
|
||||
is VaultAction.Internal.VaultDataReceive -> handleVaultDataReceive(action)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +143,41 @@ class VaultViewModel @Inject constructor(
|
||||
private fun handleVaultItemClick(action: VaultAction.VaultItemClick) {
|
||||
sendEvent(VaultEvent.NavigateToVaultItem(action.vaultItem.id))
|
||||
}
|
||||
|
||||
private fun handleVaultDataReceive(action: VaultAction.Internal.VaultDataReceive) {
|
||||
when (val vaultData = action.vaultData) {
|
||||
is DataState.Error -> vaultErrorReceive(vaultData = vaultData)
|
||||
is DataState.Loaded -> vaultLoadedReceive(vaultData = vaultData)
|
||||
is DataState.Loading -> vaultLoadingReceive()
|
||||
is DataState.NoNetwork -> vaultNoNetworkReceive(vaultData = vaultData)
|
||||
is DataState.Pending -> vaultPendingReceive(vaultData = vaultData)
|
||||
}
|
||||
}
|
||||
private fun vaultErrorReceive(vaultData: DataState.Error<VaultData>) {
|
||||
// TODO update state to error state BIT-1157
|
||||
mutableStateFlow.update { it.copy(viewState = VaultState.ViewState.NoItems) }
|
||||
sendEvent(VaultEvent.ShowToast(message = "Vault error state not yet implemented"))
|
||||
}
|
||||
|
||||
private fun vaultLoadedReceive(vaultData: DataState.Loaded<VaultData>) {
|
||||
mutableStateFlow.update { it.copy(viewState = vaultData.data.toViewState()) }
|
||||
}
|
||||
|
||||
private fun vaultLoadingReceive() {
|
||||
mutableStateFlow.update { it.copy(viewState = VaultState.ViewState.Loading) }
|
||||
}
|
||||
|
||||
private fun vaultNoNetworkReceive(vaultData: DataState.NoNetwork<VaultData>) {
|
||||
// TODO update state to no network state BIT-1158
|
||||
mutableStateFlow.update { it.copy(viewState = VaultState.ViewState.NoItems) }
|
||||
sendEvent(VaultEvent.ShowToast(message = "Vault no network state not yet implemented"))
|
||||
}
|
||||
|
||||
private fun vaultPendingReceive(vaultData: DataState.Pending<VaultData>) {
|
||||
// TODO update state to refresh state BIT-505
|
||||
mutableStateFlow.update { it.copy(viewState = vaultData.data.toViewState()) }
|
||||
sendEvent(VaultEvent.ShowToast(message = "Refreshing"))
|
||||
}
|
||||
//endregion VaultAction Handlers
|
||||
}
|
||||
|
||||
@@ -499,4 +527,17 @@ sealed class VaultAction {
|
||||
* User clicked the trash button.
|
||||
*/
|
||||
data object TrashClick : VaultAction()
|
||||
|
||||
/**
|
||||
* Models actions that the [VaultViewModel] itself might send.
|
||||
*/
|
||||
sealed class Internal : VaultAction() {
|
||||
|
||||
/**
|
||||
* Indicates a vault data was received.
|
||||
*/
|
||||
data class VaultDataReceive(
|
||||
val vaultData: DataState<VaultData>,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.vault.util
|
||||
|
||||
import com.bitwarden.core.CipherType
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.VaultState
|
||||
|
||||
/**
|
||||
* Transforms a [CipherView] into a [VaultState.ViewState.VaultItem].
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
private fun CipherView.toVaultItem(): VaultState.ViewState.VaultItem =
|
||||
when (type) {
|
||||
CipherType.LOGIN -> VaultState.ViewState.VaultItem.Login(
|
||||
id = id.toString(),
|
||||
name = name.asText(),
|
||||
username = login?.username?.asText(),
|
||||
)
|
||||
|
||||
CipherType.SECURE_NOTE -> VaultState.ViewState.VaultItem.SecureNote(
|
||||
id = id.toString(),
|
||||
name = name.asText(),
|
||||
)
|
||||
|
||||
CipherType.CARD -> VaultState.ViewState.VaultItem.Card(
|
||||
id = id.toString(),
|
||||
name = name.asText(),
|
||||
brand = card?.brand?.asText(),
|
||||
lastFourDigits = card?.number
|
||||
?.takeLast(4)
|
||||
?.asText(),
|
||||
)
|
||||
|
||||
CipherType.IDENTITY -> VaultState.ViewState.VaultItem.Identity(
|
||||
id = id.toString(),
|
||||
name = name.asText(),
|
||||
firstName = identity?.firstName?.asText(),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms [VaultData] into [VaultState.ViewState].
|
||||
*/
|
||||
fun VaultData.toViewState(): VaultState.ViewState =
|
||||
if (cipherViewList.isEmpty() && folderViewList.isEmpty()) {
|
||||
VaultState.ViewState.NoItems
|
||||
} else {
|
||||
VaultState.ViewState.Content(
|
||||
loginItemsCount = cipherViewList.count { it.type == CipherType.LOGIN },
|
||||
cardItemsCount = cipherViewList.count { it.type == CipherType.CARD },
|
||||
identityItemsCount = cipherViewList.count { it.type == CipherType.IDENTITY },
|
||||
secureNoteItemsCount = cipherViewList.count { it.type == CipherType.SECURE_NOTE },
|
||||
favoriteItems = cipherViewList
|
||||
.filter { it.favorite }
|
||||
.map { it.toVaultItem() },
|
||||
folderItems = folderViewList.map { folderView ->
|
||||
VaultState.ViewState.FolderItem(
|
||||
id = folderView.id,
|
||||
name = folderView.name.asText(),
|
||||
itemCount = cipherViewList.count { folderView.id == it.folderId },
|
||||
)
|
||||
},
|
||||
noFolderItems = cipherViewList
|
||||
.filter { it.folderId.isNullOrBlank() }
|
||||
.map { it.toVaultItem() },
|
||||
// TODO need to populate trash item count in BIT-969
|
||||
trashItemsCount = 0,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user