mirror of
https://github.com/bitwarden/android.git
synced 2025-02-17 12:30:00 +03:00
Ensure VaultDiskSource emits when replace operation does not actually change any data (#412)
This commit is contained in:
parent
27140bf02c
commit
bf9845d7a0
6 changed files with 100 additions and 52 deletions
|
@ -0,0 +1,9 @@
|
|||
package com.x8bit.bitwarden.data.platform.repository.util
|
||||
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
||||
/**
|
||||
* Creates a [MutableSharedFlow] with a buffer of [Int.MAX_VALUE].
|
||||
*/
|
||||
fun <T> bufferedMutableSharedFlow(): MutableSharedFlow<T> =
|
||||
MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE)
|
|
@ -25,6 +25,9 @@ interface VaultDiskSource {
|
|||
|
||||
/**
|
||||
* Replaces all [vault] data for a given [userId] with the new `vault`.
|
||||
*
|
||||
* This will always cause the [getCiphers], [getCollections], and [getFolders] functions to
|
||||
* re-emit even if the underlying data has not changed.
|
||||
*/
|
||||
suspend fun replaceVaultData(userId: String, vault: SyncResponseJson)
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.x8bit.bitwarden.data.vault.datasource.disk
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.CiphersDao
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.CollectionsDao
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.FoldersDao
|
||||
|
@ -12,6 +13,7 @@ import kotlinx.coroutines.awaitAll
|
|||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
|
@ -25,49 +27,63 @@ class VaultDiskSourceImpl(
|
|||
private val json: Json,
|
||||
) : VaultDiskSource {
|
||||
|
||||
private val forceCiphersFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Cipher>>()
|
||||
private val forceCollectionsFlow =
|
||||
bufferedMutableSharedFlow<List<SyncResponseJson.Collection>>()
|
||||
private val forceFolderFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Folder>>()
|
||||
|
||||
override fun getCiphers(
|
||||
userId: String,
|
||||
): Flow<List<SyncResponseJson.Cipher>> =
|
||||
ciphersDao
|
||||
.getAllCiphers(userId = userId)
|
||||
.map { entities ->
|
||||
entities.map { entity ->
|
||||
json.decodeFromString<SyncResponseJson.Cipher>(entity.cipherJson)
|
||||
}
|
||||
}
|
||||
merge(
|
||||
forceCiphersFlow,
|
||||
ciphersDao
|
||||
.getAllCiphers(userId = userId)
|
||||
.map { entities ->
|
||||
entities.map { entity ->
|
||||
json.decodeFromString<SyncResponseJson.Cipher>(entity.cipherJson)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
override fun getCollections(
|
||||
userId: String,
|
||||
): Flow<List<SyncResponseJson.Collection>> =
|
||||
collectionsDao
|
||||
.getAllCollections(userId = userId)
|
||||
.map { entities ->
|
||||
entities.map { entity ->
|
||||
SyncResponseJson.Collection(
|
||||
id = entity.id,
|
||||
name = entity.name,
|
||||
organizationId = entity.organizationId,
|
||||
shouldHidePasswords = entity.shouldHidePasswords,
|
||||
externalId = entity.externalId,
|
||||
isReadOnly = entity.isReadOnly,
|
||||
)
|
||||
}
|
||||
}
|
||||
merge(
|
||||
forceCollectionsFlow,
|
||||
collectionsDao
|
||||
.getAllCollections(userId = userId)
|
||||
.map { entities ->
|
||||
entities.map { entity ->
|
||||
SyncResponseJson.Collection(
|
||||
id = entity.id,
|
||||
name = entity.name,
|
||||
organizationId = entity.organizationId,
|
||||
shouldHidePasswords = entity.shouldHidePasswords,
|
||||
externalId = entity.externalId,
|
||||
isReadOnly = entity.isReadOnly,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
override fun getFolders(
|
||||
userId: String,
|
||||
): Flow<List<SyncResponseJson.Folder>> =
|
||||
foldersDao
|
||||
.getAllFolders(userId = userId)
|
||||
.map { entities ->
|
||||
entities.map { entity ->
|
||||
SyncResponseJson.Folder(
|
||||
id = entity.id,
|
||||
name = entity.name,
|
||||
revisionDate = entity.revisionDate,
|
||||
)
|
||||
}
|
||||
}
|
||||
merge(
|
||||
forceFolderFlow,
|
||||
foldersDao
|
||||
.getAllFolders(userId = userId)
|
||||
.map { entities ->
|
||||
entities.map { entity ->
|
||||
SyncResponseJson.Folder(
|
||||
id = entity.id,
|
||||
name = entity.name,
|
||||
revisionDate = entity.revisionDate,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
override suspend fun replaceVaultData(
|
||||
userId: String,
|
||||
|
@ -116,11 +132,17 @@ class VaultDiskSourceImpl(
|
|||
},
|
||||
)
|
||||
}
|
||||
awaitAll(
|
||||
deferredCiphers,
|
||||
deferredCollections,
|
||||
deferredFolders,
|
||||
)
|
||||
// When going from 0 items to 0 items, the respective dao flow will not re-emit
|
||||
// So we use this to give it a little push.
|
||||
if (!deferredCiphers.await()) {
|
||||
forceCiphersFlow.tryEmit(emptyList())
|
||||
}
|
||||
if (!deferredCollections.await()) {
|
||||
forceCollectionsFlow.tryEmit(emptyList())
|
||||
}
|
||||
if (!deferredFolders.await()) {
|
||||
forceFolderFlow.tryEmit(emptyList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,18 +30,21 @@ interface CiphersDao {
|
|||
): Flow<List<CipherEntity>>
|
||||
|
||||
/**
|
||||
* Deletes all the stored ciphers associated with the given [userId].
|
||||
* Deletes all the stored ciphers associated with the given [userId]. This will return the
|
||||
* number of rows deleted by this query.
|
||||
*/
|
||||
@Query("DELETE FROM ciphers WHERE user_id = :userId")
|
||||
suspend fun deleteAllCiphers(userId: String)
|
||||
suspend fun deleteAllCiphers(userId: String): Int
|
||||
|
||||
/**
|
||||
* Deletes all the stored ciphers associated with the given [userId] and then add all new
|
||||
* [ciphers] to the database.
|
||||
* [ciphers] to the database. This will return `true` if any changes were made to the database
|
||||
* and `false` otherwise.
|
||||
*/
|
||||
@Transaction
|
||||
suspend fun replaceAllCiphers(userId: String, ciphers: List<CipherEntity>) {
|
||||
deleteAllCiphers(userId)
|
||||
suspend fun replaceAllCiphers(userId: String, ciphers: List<CipherEntity>): Boolean {
|
||||
val deletedCiphersCount = deleteAllCiphers(userId)
|
||||
insertCiphers(ciphers)
|
||||
return deletedCiphersCount > 0 || ciphers.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,10 +34,11 @@ interface CollectionsDao {
|
|||
fun getAllCollections(userId: String): Flow<List<CollectionEntity>>
|
||||
|
||||
/**
|
||||
* Deletes all the stored collections associated with the given [userId].
|
||||
* Deletes all the stored collections associated with the given [userId]. This will return the
|
||||
* number of rows deleted by this query.
|
||||
*/
|
||||
@Query("DELETE FROM collections WHERE user_id = :userId")
|
||||
suspend fun deleteAllCollections(userId: String)
|
||||
suspend fun deleteAllCollections(userId: String): Int
|
||||
|
||||
/**
|
||||
* Deletes the stored collection associated with the given [userId] that matches the
|
||||
|
@ -48,11 +49,18 @@ interface CollectionsDao {
|
|||
|
||||
/**
|
||||
* Deletes all the stored [collections] associated with the given [userId] and then add all new
|
||||
* `collections` to the database.
|
||||
* `collections` to the database. This will return `true` if any changes were made to the
|
||||
* database and `false` otherwise.
|
||||
*
|
||||
* @return `true` if any changes were made to the database.
|
||||
*/
|
||||
@Transaction
|
||||
suspend fun replaceAllCollections(userId: String, collections: List<CollectionEntity>) {
|
||||
deleteAllCollections(userId)
|
||||
suspend fun replaceAllCollections(
|
||||
userId: String,
|
||||
collections: List<CollectionEntity>,
|
||||
): Boolean {
|
||||
val deletedCollectionsCount = deleteAllCollections(userId)
|
||||
insertCollections(collections)
|
||||
return deletedCollectionsCount > 0 || collections.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,10 +36,11 @@ interface FoldersDao {
|
|||
): Flow<List<FolderEntity>>
|
||||
|
||||
/**
|
||||
* Deletes all the stored folders associated with the given [userId].
|
||||
* Deletes all the stored folders associated with the given [userId]. This will return the
|
||||
* number of rows deleted by this query.
|
||||
*/
|
||||
@Query("DELETE FROM folders WHERE user_id = :userId")
|
||||
suspend fun deleteAllFolders(userId: String)
|
||||
suspend fun deleteAllFolders(userId: String): Int
|
||||
|
||||
/**
|
||||
* Deletes the stored folder associated with the given [userId] that matches the [folderId].
|
||||
|
@ -49,11 +50,13 @@ interface FoldersDao {
|
|||
|
||||
/**
|
||||
* Deletes all the stored [folders] associated with the given [userId] and then add all new
|
||||
* `folders` to the database.
|
||||
* `folders` to the database. This will return `true` if any changes were made to the database
|
||||
* and `false` otherwise.
|
||||
*/
|
||||
@Transaction
|
||||
suspend fun replaceAllFolders(userId: String, folders: List<FolderEntity>) {
|
||||
deleteAllFolders(userId)
|
||||
suspend fun replaceAllFolders(userId: String, folders: List<FolderEntity>): Boolean {
|
||||
val deletedFoldersCount = deleteAllFolders(userId)
|
||||
insertFolders(folders)
|
||||
return deletedFoldersCount > 0 || folders.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue