diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt index 1b7a5243e2..4f6382bcc5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt @@ -84,7 +84,7 @@ interface CryptoService { fun importRoomKeys(roomKeysAsArray: ByteArray, password: String, progressListener: ProgressListener?, callback: MatrixCallback) - fun exportRoomKeys(password: String, callback: MatrixCallback) + suspend fun exportRoomKeys(password: String): ByteArray fun setRoomBlacklistUnverifiedDevices(roomId: String) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index cbd1ee00a9..4e6ba514f8 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -928,14 +928,10 @@ internal class DefaultCryptoService @Inject constructor( * Export the crypto keys * * @param password the password - * @param callback the exported keys + * @return the exported keys */ - override fun exportRoomKeys(password: String, callback: MatrixCallback) { - cryptoCoroutineScope.launch(coroutineDispatchers.main) { - runCatching { - exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT) - }.foldToCallback(callback) - } + override suspend fun exportRoomKeys(password: String): ByteArray { + return exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT) } /** diff --git a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt index 9dbfa8fe30..3c11bfcd13 100644 --- a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt @@ -32,6 +32,7 @@ import im.vector.app.features.call.conference.VectorJitsiActivity import im.vector.app.features.call.transfer.CallTransferActivity import im.vector.app.features.createdirect.CreateDirectRoomActivity import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity +import im.vector.app.features.crypto.keysbackup.setup.KeysBackupSetupActivity import im.vector.app.features.crypto.quads.SharedSecureStorageActivity import im.vector.app.features.crypto.recover.BootstrapBottomSheet import im.vector.app.features.crypto.verification.VerificationBottomSheet @@ -138,6 +139,7 @@ interface ScreenComponent { fun inject(activity: LinkHandlerActivity) fun inject(activity: MainActivity) fun inject(activity: RoomDirectoryActivity) + fun inject(activity: KeysBackupSetupActivity) fun inject(activity: BugReportActivity) fun inject(activity: FilteredRoomsActivity) fun inject(activity: CreateRoomActivity) diff --git a/vector/src/main/java/im/vector/app/features/crypto/keys/KeysExporter.kt b/vector/src/main/java/im/vector/app/features/crypto/keys/KeysExporter.kt index c7e4c26385..2c66a14cb4 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keys/KeysExporter.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keys/KeysExporter.kt @@ -18,35 +18,24 @@ package im.vector.app.features.crypto.keys import android.content.Context import android.net.Uri -import im.vector.app.features.session.coroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.internal.extensions.foldToCallback -import org.matrix.android.sdk.internal.util.awaitCallback - -class KeysExporter(private val session: Session) { +import javax.inject.Inject +class KeysExporter @Inject constructor( + private val session: Session, + private val context: Context +) { /** - * Export keys and return the file path with the callback + * Export keys and write them to the provided uri */ - fun export(context: Context, password: String, uri: Uri, callback: MatrixCallback) { - session.coroutineScope.launch(Dispatchers.Main) { - runCatching { - withContext(Dispatchers.IO) { - val data = awaitCallback { session.cryptoService().exportRoomKeys(password, it) } - val os = context.contentResolver?.openOutputStream(uri) - if (os == null) { - false - } else { - os.write(data) - os.flush() - true - } - } - }.foldToCallback(callback) + suspend fun export(password: String, uri: Uri) { + return withContext(Dispatchers.IO) { + val data = session.cryptoService().exportRoomKeys(password) + context.contentResolver.openOutputStream(uri) + ?.use { it.write(data) } + ?: throw IllegalStateException("Unable to open file for writting") } } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt index 586c461fac..7cc46ef62c 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt @@ -18,10 +18,13 @@ package im.vector.app.features.crypto.keysbackup.setup import android.app.Activity import android.content.Context import android.content.Intent +import android.net.Uri import androidx.core.view.isVisible import androidx.fragment.app.FragmentManager +import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R +import im.vector.app.core.di.ScreenComponent import im.vector.app.core.dialogs.ExportKeysDialog import im.vector.app.core.extensions.observeEvent import im.vector.app.core.extensions.queryExportKeys @@ -30,7 +33,8 @@ import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.SimpleFragmentActivity import im.vector.app.core.utils.toast import im.vector.app.features.crypto.keys.KeysExporter -import org.matrix.android.sdk.api.MatrixCallback +import kotlinx.coroutines.launch +import javax.inject.Inject class KeysBackupSetupActivity : SimpleFragmentActivity() { @@ -38,6 +42,13 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() { private lateinit var viewModel: KeysBackupSetupSharedViewModel + @Inject lateinit var keysExporter: KeysExporter + + override fun injectWith(injector: ScreenComponent) { + super.injectWith(injector) + injector.inject(this) + } + override fun initUiAndData() { super.initUiAndData() if (isFirstCreation()) { @@ -132,30 +143,7 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() { ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener { override fun onPassphrase(passphrase: String) { showWaitingView() - - KeysExporter(session) - .export(this@KeysBackupSetupActivity, - passphrase, - uri, - object : MatrixCallback { - override fun onSuccess(data: Boolean) { - if (data) { - toast(getString(R.string.encryption_exported_successfully)) - Intent().apply { - putExtra(MANUAL_EXPORT, true) - }.let { - setResult(Activity.RESULT_OK, it) - finish() - } - } - hideWaitingView() - } - - override fun onFailure(failure: Throwable) { - toast(failure.localizedMessage ?: getString(R.string.unexpected_error)) - hideWaitingView() - } - }) + export(passphrase, uri) } }) } else { @@ -165,6 +153,20 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() { } } + private fun export(passphrase: String, uri: Uri) { + lifecycleScope.launch { + try { + keysExporter.export(passphrase, uri) + toast(getString(R.string.encryption_exported_successfully)) + setResult(Activity.RESULT_OK, Intent().apply { putExtra(MANUAL_EXPORT, true) }) + finish() + } catch (failure: Throwable) { + toast(failure.localizedMessage ?: getString(R.string.unexpected_error)) + } + hideWaitingView() + } + } + override fun onBackPressed() { if (viewModel.shouldPromptOnBack) { if (waitingView?.isVisible == true) { diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt index 4fd2c72b33..1ae434f2f5 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -20,6 +20,7 @@ package im.vector.app.features.settings import android.annotation.SuppressLint import android.app.Activity import android.content.Intent +import android.net.Uri import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup @@ -60,6 +61,7 @@ import im.vector.app.features.raw.wellknown.isE2EByDefault import im.vector.app.features.themes.ThemeUtils import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable +import kotlinx.coroutines.launch import me.gujun.android.span.span import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.extensions.getFingerprintHumanReadable @@ -75,6 +77,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( private val vectorPreferences: VectorPreferences, private val activeSessionHolder: ActiveSessionHolder, private val pinCodeStore: PinCodeStore, + private val keysExporter: KeysExporter, private val navigator: Navigator ) : VectorSettingsBaseFragment() { @@ -320,29 +323,24 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( override fun onPassphrase(passphrase: String) { displayLoadingView() - KeysExporter(session) - .export(requireContext(), - passphrase, - uri, - object : MatrixCallback { - override fun onSuccess(data: Boolean) { - if (data) { - requireActivity().toast(getString(R.string.encryption_exported_successfully)) - } else { - requireActivity().toast(getString(R.string.unexpected_error)) - } - hideLoadingView() - } - - override fun onFailure(failure: Throwable) { - onCommonDone(failure.localizedMessage) - } - }) + export(passphrase, uri) } }) } } + private fun export(passphrase: String, uri: Uri) { + lifecycleScope.launch { + try { + keysExporter.export(passphrase, uri) + requireActivity().toast(getString(R.string.encryption_exported_successfully)) + } catch (failure: Throwable) { + requireActivity().toast(errorFormatter.toHumanReadable(failure)) + } + hideLoadingView() + } + } + private val pinActivityResultLauncher = registerStartForActivityResult { if (it.resultCode == Activity.RESULT_OK) { doOpenPinCodePreferenceScreen() diff --git a/vector/src/main/java/im/vector/app/features/workers/signout/SignOutBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/app/features/workers/signout/SignOutBottomSheetDialogFragment.kt index 389d0f0cfc..48459cdd9e 100644 --- a/vector/src/main/java/im/vector/app/features/workers/signout/SignOutBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/app/features/workers/signout/SignOutBottomSheetDialogFragment.kt @@ -42,9 +42,7 @@ import im.vector.app.features.crypto.keysbackup.setup.KeysBackupSetupActivity import im.vector.app.features.crypto.recover.BootstrapBottomSheet import im.vector.app.features.crypto.recover.SetupMode -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState -import timber.log.Timber import javax.inject.Inject // TODO this needs to be refactored to current standard and remove legacy @@ -113,31 +111,6 @@ class SignOutBottomSheetDialogFragment : views.setupMegolmBackupButton.action = { setupBackupActivityResultLauncher.launch(KeysBackupSetupActivity.intent(requireContext(), true)) } - - viewModel.observeViewEvents { - when (it) { - is SignoutCheckViewModel.ViewEvents.ExportKeys -> { - it.exporter - .export(requireContext(), - it.passphrase, - it.uri, - object : MatrixCallback { - override fun onSuccess(data: Boolean) { - if (data) { - viewModel.handle(SignoutCheckViewModel.Actions.KeySuccessfullyManuallyExported) - } else { - viewModel.handle(SignoutCheckViewModel.Actions.KeyExportFailed) - } - } - - override fun onFailure(failure: Throwable) { - Timber.e("## Failed to export manually keys ${failure.localizedMessage}") - viewModel.handle(SignoutCheckViewModel.Actions.KeyExportFailed) - } - }) - } - } - } } override fun invalidate() = withState(viewModel) { state -> diff --git a/vector/src/main/java/im/vector/app/features/workers/signout/SignoutCheckViewModel.kt b/vector/src/main/java/im/vector/app/features/workers/signout/SignoutCheckViewModel.kt index 21c0c7481a..df7a826b48 100644 --- a/vector/src/main/java/im/vector/app/features/workers/signout/SignoutCheckViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/workers/signout/SignoutCheckViewModel.kt @@ -17,6 +17,7 @@ package im.vector.app.features.workers.signout import android.net.Uri +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Async import com.airbnb.mvrx.FragmentViewModelContext @@ -27,13 +28,14 @@ import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.core.extensions.exhaustive -import im.vector.app.core.platform.VectorViewEvents +import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.features.crypto.keys.KeysExporter +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME @@ -41,6 +43,7 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_S import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener import org.matrix.android.sdk.rx.rx +import timber.log.Timber data class SignoutCheckViewState( val userId: String = "", @@ -50,18 +53,15 @@ data class SignoutCheckViewState( val hasBeenExportedToFile: Async = Uninitialized ) : MvRxState -class SignoutCheckViewModel @AssistedInject constructor(@Assisted initialState: SignoutCheckViewState, - private val session: Session) - : VectorViewModel(initialState), KeysBackupStateListener { +class SignoutCheckViewModel @AssistedInject constructor( + @Assisted initialState: SignoutCheckViewState, + private val session: Session, + private val keysExporter: KeysExporter +) : VectorViewModel(initialState), KeysBackupStateListener { sealed class Actions : VectorViewModelAction { data class ExportKeys(val passphrase: String, val uri: Uri) : Actions() object KeySuccessfullyManuallyExported : Actions() - object KeyExportFailed : Actions() - } - - sealed class ViewEvents : VectorViewEvents { - data class ExportKeys(val exporter: KeysExporter, val passphrase: String, val uri: Uri) : ViewEvents() } @AssistedFactory @@ -128,22 +128,32 @@ class SignoutCheckViewModel @AssistedInject constructor(@Assisted initialState: override fun handle(action: Actions) { when (action) { - is Actions.ExportKeys -> { - setState { - copy(hasBeenExportedToFile = Loading()) - } - _viewEvents.post(ViewEvents.ExportKeys(KeysExporter(session), action.passphrase, action.uri)) - } + is Actions.ExportKeys -> handleExportKeys(action) Actions.KeySuccessfullyManuallyExported -> { setState { copy(hasBeenExportedToFile = Success(true)) } } - Actions.KeyExportFailed -> { - setState { - copy(hasBeenExportedToFile = Uninitialized) - } - } }.exhaustive } + + private fun handleExportKeys(action: Actions.ExportKeys) { + setState { + copy(hasBeenExportedToFile = Loading()) + } + + viewModelScope.launch { + val newState = try { + keysExporter.export(action.passphrase, action.uri) + Success(true) + } catch (failure: Throwable) { + Timber.e("## Failed to export manually keys ${failure.localizedMessage}") + Uninitialized + } + + setState { + copy(hasBeenExportedToFile = newState) + } + } + } }