diff --git a/core/src/main/kotlin/app/dapk/st/core/Lce.kt b/core/src/main/kotlin/app/dapk/st/core/Lce.kt index bce0ff4..ebb3e14 100644 --- a/core/src/main/kotlin/app/dapk/st/core/Lce.kt +++ b/core/src/main/kotlin/app/dapk/st/core/Lce.kt @@ -6,3 +6,9 @@ sealed interface Lce { data class Content(val value: T) : Lce } +sealed interface LceWithProgress { + data class Loading(val progress: T) : LceWithProgress + data class Error(val cause: Throwable) : LceWithProgress + data class Content(val value: T) : LceWithProgress +} + diff --git a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsScreen.kt b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsScreen.kt index 7b1862d..c88c682 100644 --- a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsScreen.kt +++ b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsScreen.kt @@ -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()) + } + } + } } } } diff --git a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsState.kt b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsState.kt index fa78752..6dc49ca 100644 --- a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsState.kt +++ b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsState.kt @@ -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? = null, + val importProgress: LceWithProgress? = null, ) : Page data class PushProviders( diff --git a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsViewModel.kt b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsViewModel.kt index 6644542..0114bac 100644 --- a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsViewModel.kt +++ b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsViewModel.kt @@ -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 { copy(importProgress = Lce.Loading()) } + updatePageState { 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 { copy(importProgress = Lce.Content(Unit)) } }, - onFailure = { updatePageState { copy(importProgress = Lce.Error(it)) } } - ) + with(cryptoService) { + contentResolver.openInputStream(file)?.importRoomKeys(passphrase) + ?.onEach { + when (it) { + is ImportResult.Error -> { + updatePageState { copy(importProgress = LceWithProgress.Error(it.cause)) } + } + is ImportResult.Update -> { + updatePageState { copy(importProgress = LceWithProgress.Loading(it.importedKeysCount)) } + } + is ImportResult.Success -> { + syncService.forceManualRefresh(it.roomIds.toList()) + updatePageState { copy(importProgress = LceWithProgress.Content(it.totalImportedKeysCount)) } + } + } + } + ?.launchIn(viewModelScope) + } } } diff --git a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/CryptoService.kt b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/CryptoService.kt index 47bc545..bd79f71 100644 --- a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/CryptoService.kt +++ b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/CryptoService.kt @@ -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) - suspend fun InputStream.importRoomKeys(password: String): List + suspend fun InputStream.importRoomKeys(password: String): Flow suspend fun maybeCreateMoreKeys(serverKeyCount: ServerKeyCount) suspend fun updateOlmSession(userIds: List, syncToken: SyncToken?) @@ -159,4 +159,10 @@ fun MatrixServiceProvider.cryptoService(): CryptoService = this.getService(key = fun interface RoomMembersProvider { suspend fun userIdsForRoom(roomId: RoomId): List -} \ No newline at end of file +} + +sealed interface ImportResult { + data class Success(val roomIds: Set, val totalImportedKeysCount: Long) : ImportResult + data class Error(val cause: Throwable) : ImportResult + data class Update(val importedKeysCount: Long) : ImportResult +} diff --git a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/DefaultCryptoService.kt b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/DefaultCryptoService.kt index 4264d3f..6293bb9 100644 --- a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/DefaultCryptoService.kt +++ b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/DefaultCryptoService.kt @@ -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 { + override suspend fun InputStream.importRoomKeys(password: String): Flow { return logP("import room keys") { with(roomKeyImporter) { importRoomKeys(password) { diff --git a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/RoomKeyImporter.kt b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/RoomKeyImporter.kt index 9845899..e14f486 100644 --- a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/RoomKeyImporter.kt +++ b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/RoomKeyImporter.kt @@ -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) -> Unit): List { - return dispatchers.withIoContext { + suspend fun InputStream.importRoomKeys(password: String, onChunk: suspend (List) -> Unit): Flow { + 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) {