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 3f6c281..9a25014 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 @@ -158,6 +158,7 @@ internal fun SettingsScreen(viewModel: SettingsViewModel, onSignOut: () -> Unit, ImportResult.Error.Type.NoKeysFound -> "No keys found in the file" ImportResult.Error.Type.UnexpectedDecryptionOutput -> "Unable to decrypt file, double check your passphrase" is ImportResult.Error.Type.Unknown -> "${type.cause::class.java.simpleName}: ${type.cause.message}" + ImportResult.Error.Type.UnableToOpenFile -> "Unable to open file" } Text(text = "Import failed\n$message", textAlign = TextAlign.Center) 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 4b68513..56dd839 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 @@ -4,7 +4,6 @@ import android.content.ContentResolver import android.net.Uri 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 @@ -127,22 +126,30 @@ internal class SettingsViewModel( updatePageState { copy(importProgress = ImportResult.Update(0)) } viewModelScope.launch { with(cryptoService) { - contentResolver.openInputStream(file)?.importRoomKeys(passphrase) - ?.onEach { - updatePageState { copy(importProgress = it) } - when (it) { - is ImportResult.Error -> { - // do nothing - } - is ImportResult.Update -> { - // do nothing - } - is ImportResult.Success -> { - syncService.forceManualRefresh(it.roomIds.toList()) - } + runCatching { contentResolver.openInputStream(file)!! } + .fold( + onSuccess = { fileStream -> + fileStream.importRoomKeys(passphrase) + .onEach { + updatePageState { copy(importProgress = it) } + when (it) { + is ImportResult.Error -> { + // do nothing + } + is ImportResult.Update -> { + // do nothing + } + is ImportResult.Success -> { + syncService.forceManualRefresh(it.roomIds.toList()) + } + } + } + .launchIn(viewModelScope) + }, + onFailure = { + updatePageState { copy(importProgress = ImportResult.Error(ImportResult.Error.Type.UnableToOpenFile)) } } - } - ?.launchIn(viewModelScope) + ) } } } diff --git a/features/settings/src/test/kotlin/app/dapk/st/settings/SettingsViewModelTest.kt b/features/settings/src/test/kotlin/app/dapk/st/settings/SettingsViewModelTest.kt index 5fd5279..0a2067c 100644 --- a/features/settings/src/test/kotlin/app/dapk/st/settings/SettingsViewModelTest.kt +++ b/features/settings/src/test/kotlin/app/dapk/st/settings/SettingsViewModelTest.kt @@ -3,6 +3,7 @@ package app.dapk.st.settings import ViewModelTest import app.dapk.st.core.Lce import app.dapk.st.design.components.SpiderPage +import app.dapk.st.matrix.crypto.ImportResult import fake.* import fixture.FakeStoreCleaner import fixture.aRoomId @@ -10,6 +11,7 @@ import internalfake.FakeSettingsItemFactory import internalfake.FakeUriFilenameResolver import internalfixture.aImportRoomKeysPage import internalfixture.aSettingTextItem +import kotlinx.coroutines.flow.flowOf import org.junit.Test private const val APP_PRIVACY_POLICY_URL = "https://ouchadam.github.io/small-talk/privacy/" @@ -21,6 +23,8 @@ private val A_IMPORT_ROOM_KEYS_PAGE_WITH_SELECTION = aImportRoomKeysPage( state = Page.ImportRoomKey(selectedFile = NamedUri(A_FILENAME, A_URI.instance)) ) private val A_LIST_OF_ROOM_IDS = listOf(aRoomId()) +private val AN_IMPORT_SUCCESS = ImportResult.Success(A_LIST_OF_ROOM_IDS.toSet(), totalImportedKeysCount = 5) +private val AN_IMPORT_FILE_ERROR = ImportResult.Error(ImportResult.Error.Type.UnableToOpenFile) private val AN_INPUT_STREAM = FakeInputStream() private const val A_PASSPHRASE = "passphrase" private val AN_ERROR = RuntimeException() @@ -166,15 +170,15 @@ internal class SettingsViewModelTest { fun `given success when importing room keys, then emits progress`() = runViewModelTest { fakeSyncService.expectUnit { it.forceManualRefresh(A_LIST_OF_ROOM_IDS) } fakeContentResolver.givenFile(A_URI.instance).returns(AN_INPUT_STREAM.instance) - fakeCryptoService.givenImportKeys(AN_INPUT_STREAM.instance, A_PASSPHRASE).returns(A_LIST_OF_ROOM_IDS) + fakeCryptoService.givenImportKeys(AN_INPUT_STREAM.instance, A_PASSPHRASE).returns(flowOf(AN_IMPORT_SUCCESS)) viewModel .test(initialState = SettingsScreenState(A_IMPORT_ROOM_KEYS_PAGE_WITH_SELECTION)) .importFromFileKeys(A_URI.instance, A_PASSPHRASE) assertStates( - { copy(page = page.updateState { copy(importProgress = Lce.Loading()) }) }, - { copy(page = page.updateState { copy(importProgress = Lce.Content(Unit)) }) }, + { copy(page = page.updateState { copy(importProgress = ImportResult.Update(0L)) }) }, + { copy(page = page.updateState { copy(importProgress = AN_IMPORT_SUCCESS) }) }, ) assertNoEvents() verifyExpects() @@ -189,8 +193,8 @@ internal class SettingsViewModelTest { .importFromFileKeys(A_URI.instance, A_PASSPHRASE) assertStates( - { copy(page = page.updateState { copy(importProgress = Lce.Loading()) }) }, - { copy(page = page.updateState { copy(importProgress = Lce.Error(AN_ERROR)) }) }, + { copy(page = page.updateState { copy(importProgress = ImportResult.Update(0L)) }) }, + { copy(page = page.updateState { copy(importProgress = AN_IMPORT_FILE_ERROR) }) }, ) assertNoEvents() } 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 39f3c98..adb85c9 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 @@ -169,6 +169,7 @@ sealed interface ImportResult { data class Unknown(val cause: Throwable): Type object NoKeysFound: Type object UnexpectedDecryptionOutput: Type + object UnableToOpenFile: Type } }