BIT-1547: Hook up remaining push notification sync handling (#848)

This commit is contained in:
Sean Weiser
2024-01-29 22:15:59 -06:00
committed by Álison Fernandes
parent d2ffd7bf01
commit 8489bd1476
9 changed files with 1163 additions and 95 deletions

View File

@@ -54,6 +54,7 @@ import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsList
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsListFlow
import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS
import com.x8bit.bitwarden.data.auth.util.toSdkParams
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
@@ -71,7 +72,9 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import javax.inject.Singleton
@@ -95,6 +98,7 @@ class AuthRepositoryImpl(
private val settingsRepository: SettingsRepository,
private val vaultRepository: VaultRepository,
private val userLogoutManager: UserLogoutManager,
private val pushManager: PushManager,
dispatcherManager: DispatcherManager,
private val elapsedRealtimeMillisProvider: () -> Long = { SystemClock.elapsedRealtime() },
) : AuthRepository {
@@ -117,7 +121,9 @@ class AuthRepositoryImpl(
* use of [Dispatchers.Unconfined] allows for this to happen synchronously whenever any of
* these flows changes.
*/
private val collectionScope = CoroutineScope(dispatcherManager.unconfined)
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
private val ioScope = CoroutineScope(dispatcherManager.io)
override var twoFactorResponse: TwoFactorRequired? = null
@@ -136,7 +142,7 @@ class AuthRepositoryImpl(
?: AuthState.Unauthenticated
}
.stateIn(
scope = collectionScope,
scope = unconfinedScope,
started = SharingStarted.Eagerly,
initialValue = AuthState.Uninitialized,
)
@@ -169,7 +175,7 @@ class AuthRepositoryImpl(
!mutableHasPendingAccountDeletionStateFlow.value
}
.stateIn(
scope = collectionScope,
scope = unconfinedScope,
started = SharingStarted.Eagerly,
initialValue = authDiskSource
.userState
@@ -199,6 +205,22 @@ class AuthRepositoryImpl(
override var hasPendingAccountAddition: Boolean
by mutableHasPendingAccountAdditionStateFlow::value
init {
pushManager
.syncOrgKeysFlow
.onEach {
val userId = activeUserId ?: return@onEach
refreshAccessTokenSynchronously(userId)
vaultRepository.sync()
}
.launchIn(ioScope)
pushManager
.logoutFlow
.onEach { logout() }
.launchIn(unconfinedScope)
}
override fun clearPendingAccountDeletion() {
mutableHasPendingAccountDeletionStateFlow.value = false
}

View File

@@ -12,6 +12,7 @@ import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.AuthRepositoryImpl
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
@@ -49,6 +50,7 @@ object AuthRepositoryModule {
settingsRepository: SettingsRepository,
vaultRepository: VaultRepository,
userLogoutManager: UserLogoutManager,
pushManager: PushManager,
): AuthRepository = AuthRepositoryImpl(
accountsService = accountsService,
authRequestsService = authRequestsService,
@@ -65,5 +67,6 @@ object AuthRepositoryModule {
settingsRepository = settingsRepository,
vaultRepository = vaultRepository,
userLogoutManager = userLogoutManager,
pushManager = pushManager,
)
}

View File

@@ -149,6 +149,8 @@ class PushManagerImpl @Inject constructor(
SyncCipherUpsertData(
cipherId = payload.id,
revisionDate = payload.revisionDate,
organizationId = payload.organizationId,
collectionIds = payload.collectionIds,
isUpdate = type == NotificationType.SYNC_CIPHER_UPDATE,
),
)

View File

@@ -23,7 +23,7 @@ sealed class NotificationPayload {
@SerialName("id") val id: String,
@SerialName("userId") override val userId: String?,
@SerialName("organizationId") val organizationId: String?,
@SerialName("collectionIds") val collectionIds: List<String>,
@SerialName("collectionIds") val collectionIds: List<String>?,
@Contextual
@SerialName("revisionDate") val revisionDate: ZonedDateTime,
) : NotificationPayload()

View File

@@ -13,5 +13,7 @@ import java.time.ZonedDateTime
data class SyncCipherUpsertData(
val cipherId: String,
val revisionDate: ZonedDateTime,
val organizationId: String?,
val collectionIds: List<String>?,
val isUpdate: Boolean,
)

View File

@@ -19,8 +19,11 @@ import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.datasource.network.util.isNoConnectionError
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherDeleteData
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherUpsertData
import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderDeleteData
import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderUpsertData
import com.x8bit.bitwarden.data.platform.manager.model.SyncSendDeleteData
import com.x8bit.bitwarden.data.platform.manager.model.SyncSendUpsertData
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
@@ -91,11 +94,13 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
@@ -236,16 +241,36 @@ class VaultRepositoryImpl(
}
.launchIn(unconfinedScope)
pushManager
.fullSyncFlow
.onEach { syncIfNecessary() }
.launchIn(unconfinedScope)
pushManager
.syncCipherDeleteFlow
.onEach(::deleteCipher)
.launchIn(unconfinedScope)
pushManager
.syncCipherUpsertFlow
.onEach(::syncCipherIfNecessary)
.launchIn(ioScope)
pushManager
.syncSendDeleteFlow
.onEach(::deleteSend)
.launchIn(unconfinedScope)
pushManager
.syncSendUpsertFlow
.onEach(::syncSendIfNecessary)
.launchIn(ioScope)
pushManager
.syncFolderDeleteFlow
.onEach(::deleteFolder)
.launchIn(unconfinedScope)
pushManager
.syncFolderUpsertFlow
.onEach(::syncFolderIfNecessary)
@@ -1231,19 +1256,76 @@ class VaultRepositoryImpl(
.onEach { mutableSendDataStateFlow.value = it }
//region Push notification helpers
/**
* Deletes the cipher specified by [syncCipherDeleteData] from disk.
*/
private suspend fun deleteCipher(syncCipherDeleteData: SyncCipherDeleteData) {
val userId = activeUserId ?: return
val cipherId = syncCipherDeleteData.cipherId
vaultDiskSource.deleteCipher(
userId = userId,
cipherId = cipherId,
)
}
/**
* Syncs an individual cipher contained in [syncCipherUpsertData] to disk if certain criteria
* are met. If the resource cannot be found cloud-side, and it was updated, delete it from disk
* for now.
*/
@Suppress("ReturnCount")
private suspend fun syncCipherIfNecessary(syncCipherUpsertData: SyncCipherUpsertData) {
val userId = activeUserId ?: return
// TODO Handle other filtering logic including revision date comparison. This will still be
// handled as part of BIT-1547.
val cipherId = syncCipherUpsertData.cipherId
val organizationId = syncCipherUpsertData.organizationId
val collectionIds = syncCipherUpsertData.collectionIds
val revisionDate = syncCipherUpsertData.revisionDate
val isUpdate = syncCipherUpsertData.isUpdate
val localCipher = ciphersStateFlow
.mapNotNull { it.data }
.first()
.find { it.id == cipherId }
// Return if local cipher is more recent
if (localCipher != null &&
localCipher.revisionDate.epochSecond > revisionDate.toEpochSecond()
) {
return
}
var shouldUpdate: Boolean
val shouldCheckCollections: Boolean
when {
isUpdate -> {
shouldUpdate = localCipher != null
shouldCheckCollections = true
}
collectionIds == null || organizationId == null -> {
shouldUpdate = localCipher == null
shouldCheckCollections = false
}
else -> {
shouldUpdate = false
shouldCheckCollections = true
}
}
if (!shouldUpdate && shouldCheckCollections && organizationId != null) {
// Check if there are any collections in common
shouldUpdate = collectionsStateFlow
.mapNotNull { it.data }
.first()
.mapNotNull { it.id }
.any { collectionIds?.contains(it) == true } == true
}
if (!shouldUpdate) return
ciphersService
.getCipher(cipherId)
.fold(
@@ -1259,6 +1341,19 @@ class VaultRepositoryImpl(
)
}
/**
* Deletes the send specified by [syncSendDeleteData] from disk.
*/
private suspend fun deleteSend(syncSendDeleteData: SyncSendDeleteData) {
val userId = activeUserId ?: return
val sendId = syncSendDeleteData.sendId
vaultDiskSource.deleteSend(
userId = userId,
sendId = sendId,
)
}
/**
* Syncs an individual send contained in [syncSendUpsertData] to disk if certain criteria are
* met. If the resource cannot be found cloud-side, and it was updated, delete it from disk for
@@ -1266,12 +1361,22 @@ class VaultRepositoryImpl(
*/
private suspend fun syncSendIfNecessary(syncSendUpsertData: SyncSendUpsertData) {
val userId = activeUserId ?: return
// TODO Handle other filtering logic including revision date comparison. This will still be
// handled as part of BIT-1547.
val sendId = syncSendUpsertData.sendId
val isUpdate = syncSendUpsertData.isUpdate
val revisionDate = syncSendUpsertData.revisionDate
val localSend = sendDataStateFlow
.mapNotNull { it.data }
.first()
.sendViewList
.find { it.id == sendId }
val isValidCreate = !isUpdate && localSend == null
val isValidUpdate = isUpdate &&
localSend != null &&
localSend.revisionDate.epochSecond < revisionDate.toEpochSecond()
if (!isValidCreate && !isValidUpdate) return
sendsService
.getSend(sendId)
.fold(
@@ -1287,17 +1392,44 @@ class VaultRepositoryImpl(
)
}
/**
* Deletes the folder specified by [syncFolderDeleteData] from disk.
*/
private suspend fun deleteFolder(syncFolderDeleteData: SyncFolderDeleteData) {
val userId = activeUserId ?: return
val folderId = syncFolderDeleteData.folderId
clearFolderIdFromCiphers(
folderId = folderId,
userId = userId,
)
vaultDiskSource.deleteFolder(
folderId = folderId,
userId = userId,
)
}
/**
* Syncs an individual folder contained in [syncFolderUpsertData] to disk if certain criteria
* are met.
*/
private suspend fun syncFolderIfNecessary(syncFolderUpsertData: SyncFolderUpsertData) {
val userId = activeUserId ?: return
// TODO Handle other filtering logic including revision date comparison. This will still be
// handled as part of BIT-1547.
val folderId = syncFolderUpsertData.folderId
val isUpdate = syncFolderUpsertData.isUpdate
val revisionDate = syncFolderUpsertData.revisionDate
val localFolder = foldersStateFlow
.mapNotNull { it.data }
.first()
.find { it.id == folderId }
val isValidCreate = !isUpdate && localFolder == null
val isValidUpdate = isUpdate &&
localFolder != null &&
localFolder.revisionDate.epochSecond < revisionDate.toEpochSecond()
if (!isValidCreate && !isValidUpdate) return
folderService
.getFolder(folderId)
.onSuccess { vaultDiskSource.saveFolder(userId, it) }