providing progress during room key imports by switching to a flow

This commit is contained in:
Adam Brown 2022-09-04 12:43:03 +01:00
parent d05d86a02d
commit 5b2243cfa9
7 changed files with 73 additions and 24 deletions

View File

@ -6,3 +6,9 @@ sealed interface Lce<T> {
data class Content<T>(val value: T) : Lce<T>
}
sealed interface LceWithProgress<T> {
data class Loading<T>(val progress: T) : LceWithProgress<T>
data class Error<T>(val cause: Throwable) : LceWithProgress<T>
data class Content<T>(val value: T) : LceWithProgress<T>
}

View File

@ -36,6 +36,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.net.toUri
import app.dapk.st.core.Lce
import app.dapk.st.core.LceWithProgress
import app.dapk.st.core.StartObserving
import app.dapk.st.core.components.CenteredLoading
import app.dapk.st.core.components.Header
@ -136,10 +137,11 @@ internal fun SettingsScreen(viewModel: SettingsViewModel, onSignOut: () -> Unit,
}
}
is Lce.Content -> {
is LceWithProgress.Content -> {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "Import success")
Text(text = "Successfully imported ${it.importProgress.value} keys")
Spacer(modifier = Modifier.height(12.dp))
Button(onClick = { navigator.navigate.upToHome() }) {
Text(text = "Close".uppercase())
}
@ -147,7 +149,7 @@ internal fun SettingsScreen(viewModel: SettingsViewModel, onSignOut: () -> Unit,
}
}
is Lce.Error -> {
is LceWithProgress.Error -> {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "Import failed")
@ -158,7 +160,15 @@ internal fun SettingsScreen(viewModel: SettingsViewModel, onSignOut: () -> Unit,
}
}
is Lce.Loading -> CenteredLoading()
is LceWithProgress.Loading -> {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "Imported ${it.importProgress.progress} keys")
Spacer(modifier = Modifier.height(12.dp))
CircularProgressIndicator(Modifier.wrapContentSize())
}
}
}
}
}
}

View File

@ -2,6 +2,7 @@ package app.dapk.st.settings
import android.net.Uri
import app.dapk.st.core.Lce
import app.dapk.st.core.LceWithProgress
import app.dapk.st.design.components.Route
import app.dapk.st.design.components.SpiderPage
import app.dapk.st.push.Registrar
@ -15,7 +16,7 @@ internal sealed interface Page {
object Security : Page
data class ImportRoomKey(
val selectedFile: NamedUri? = null,
val importProgress: Lce<Unit>? = null,
val importProgress: LceWithProgress<Long>? = null,
) : Page
data class PushProviders(

View File

@ -2,11 +2,14 @@ package app.dapk.st.settings
import android.content.ContentResolver
import android.net.Uri
import android.util.Log
import androidx.lifecycle.viewModelScope
import app.dapk.st.core.Lce
import app.dapk.st.core.LceWithProgress
import app.dapk.st.design.components.SpiderPage
import app.dapk.st.domain.StoreCleaner
import app.dapk.st.matrix.crypto.CryptoService
import app.dapk.st.matrix.crypto.ImportResult
import app.dapk.st.matrix.sync.SyncService
import app.dapk.st.push.PushTokenRegistrars
import app.dapk.st.push.Registrar
@ -15,6 +18,8 @@ import app.dapk.st.settings.SettingsEvent.*
import app.dapk.st.viewmodel.DapkViewModel
import app.dapk.st.viewmodel.MutableStateFactory
import app.dapk.st.viewmodel.defaultStateFactory
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
private const val PRIVACY_POLICY_URL = "https://ouchadam.github.io/small-talk/privacy/"
@ -120,17 +125,26 @@ internal class SettingsViewModel(
}
fun importFromFileKeys(file: Uri, passphrase: String) {
updatePageState<Page.ImportRoomKey> { copy(importProgress = Lce.Loading()) }
updatePageState<Page.ImportRoomKey> { copy(importProgress = LceWithProgress.Loading(0L)) }
viewModelScope.launch {
kotlin.runCatching {
with(cryptoService) {
val roomsToRefresh = contentResolver.openInputStream(file)?.importRoomKeys(passphrase)
roomsToRefresh?.let { syncService.forceManualRefresh(roomsToRefresh) }
}
}.fold(
onSuccess = { updatePageState<Page.ImportRoomKey> { copy(importProgress = Lce.Content(Unit)) } },
onFailure = { updatePageState<Page.ImportRoomKey> { copy(importProgress = Lce.Error(it)) } }
)
with(cryptoService) {
contentResolver.openInputStream(file)?.importRoomKeys(passphrase)
?.onEach {
when (it) {
is ImportResult.Error -> {
updatePageState<Page.ImportRoomKey> { copy(importProgress = LceWithProgress.Error(it.cause)) }
}
is ImportResult.Update -> {
updatePageState<Page.ImportRoomKey> { copy(importProgress = LceWithProgress.Loading(it.importedKeysCount)) }
}
is ImportResult.Success -> {
syncService.forceManualRefresh(it.roomIds.toList())
updatePageState<Page.ImportRoomKey> { copy(importProgress = LceWithProgress.Content(it.totalImportedKeysCount)) }
}
}
}
?.launchIn(viewModelScope)
}
}
}

View File

@ -18,7 +18,7 @@ interface CryptoService : MatrixService {
suspend fun encrypt(roomId: RoomId, credentials: DeviceCredentials, messageJson: JsonString): Crypto.EncryptionResult
suspend fun decrypt(encryptedPayload: EncryptedMessageContent): DecryptionResult
suspend fun importRoomKeys(keys: List<SharedRoomKey>)
suspend fun InputStream.importRoomKeys(password: String): List<RoomId>
suspend fun InputStream.importRoomKeys(password: String): Flow<ImportResult>
suspend fun maybeCreateMoreKeys(serverKeyCount: ServerKeyCount)
suspend fun updateOlmSession(userIds: List<UserId>, syncToken: SyncToken?)
@ -159,4 +159,10 @@ fun MatrixServiceProvider.cryptoService(): CryptoService = this.getService(key =
fun interface RoomMembersProvider {
suspend fun userIdsForRoom(roomId: RoomId): List<UserId>
}
}
sealed interface ImportResult {
data class Success(val roomIds: Set<RoomId>, val totalImportedKeysCount: Long) : ImportResult
data class Error(val cause: Throwable) : ImportResult
data class Update(val importedKeysCount: Long) : ImportResult
}

View File

@ -4,6 +4,7 @@ import app.dapk.st.core.logP
import app.dapk.st.matrix.common.*
import app.dapk.st.matrix.crypto.Crypto
import app.dapk.st.matrix.crypto.CryptoService
import app.dapk.st.matrix.crypto.ImportResult
import app.dapk.st.matrix.crypto.Verification
import kotlinx.coroutines.flow.Flow
import java.io.InputStream
@ -48,7 +49,7 @@ internal class DefaultCryptoService(
verificationHandler.onUserVerificationAction(verificationAction)
}
override suspend fun InputStream.importRoomKeys(password: String): List<RoomId> {
override suspend fun InputStream.importRoomKeys(password: String): Flow<ImportResult> {
return logP("import room keys") {
with(roomKeyImporter) {
importRoomKeys(password) {

View File

@ -7,6 +7,8 @@ import app.dapk.st.matrix.common.AlgorithmName
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.common.SessionId
import app.dapk.st.matrix.common.SharedRoomKey
import app.dapk.st.matrix.crypto.ImportResult
import kotlinx.coroutines.flow.*
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@ -28,8 +30,10 @@ class RoomKeyImporter(
private val dispatchers: CoroutineDispatchers,
) {
suspend fun InputStream.importRoomKeys(password: String, onChunk: suspend (List<SharedRoomKey>) -> Unit): List<RoomId> {
return dispatchers.withIoContext {
suspend fun InputStream.importRoomKeys(password: String, onChunk: suspend (List<SharedRoomKey>) -> Unit): Flow<ImportResult> {
return flow {
var importedKeysCount = 0L
val decryptCipher = Cipher.getInstance("AES/CTR/NoPadding")
var jsonSegment = ""
@ -87,13 +91,20 @@ class RoomKeyImporter(
)
}
.chunked(500)
.forEach { onChunk(it) }
.forEach {
onChunk(it)
importedKeysCount += it.size
emit(ImportResult.Update(importedKeysCount))
}
}
roomIds.toList().ifEmpty {
throw IOException("Found no rooms to import in the file")
if (roomIds.isEmpty()) {
emit(ImportResult.Error(IOException("Found no rooms to import in the file")))
} else {
emit(ImportResult.Success(roomIds, importedKeysCount))
}
}
}
}.flowOn(dispatchers.io)
}
private fun Cipher.initialize(payload: ByteArray, passphrase: String) {