From c632b4feb45d23cdf2e0b1ab2cf29101bbd9d095 Mon Sep 17 00:00:00 2001 From: "ingvar.skogen" Date: Wed, 31 Jan 2024 15:20:28 +0300 Subject: [PATCH] Refactor backup related code --- app/build.gradle | 1 + .../StagedWalletSecurityManager.kt | 8 +- .../android/wallet/event/EffectChannelFlow.kt | 14 ++ .../wallet/extension/FlowExtensions.kt | 13 ++ .../infrastructure/backup/BackupManager.kt | 69 +++++---- .../infrastructure/backup/BackupsState.kt | 4 +- .../wallet/ui/extension/FragmentExtensions.kt | 8 +- .../ui/fragment/home/navigation/Navigation.kt | 3 +- .../fragment/home/navigation/TariNavigator.kt | 6 +- .../ChooseRestoreOptionFragment.kt | 58 ++++---- .../ChooseRestoreOptionModel.kt | 19 +++ .../ChooseRestoreOptionState.kt | 9 -- .../ChooseRestoreOptionViewModel.kt | 136 ++++++++++-------- .../option/RecoveryOptionView.kt | 10 +- .../option/RecoveryOptionViewModel.kt | 4 +- .../EnterRestorationPasswordFragment.kt | 11 +- .../EnterRestorationPasswordModel.kt | 12 ++ .../EnterRestorationPasswordViewModel.kt | 17 ++- .../allSettings/AllSettingsViewModel.kt | 4 - .../backupSettings/BackupSettingsFragment.kt | 2 +- .../backupSettings/BackupSettingsViewModel.kt | 32 ++++- .../option/BackupOptionModel.kt | 30 ++++ .../backupSettings/option/BackupOptionView.kt | 38 +++-- .../option/BackupOptionViewModel.kt | 128 +++++++---------- .../settings/backup/data/BackupOptionDto.kt | 4 +- .../{BackupOptions.kt => BackupOptionType.kt} | 2 +- .../backup/data/BackupSettingsRepository.kt | 113 +++++++++------ 27 files changed, 459 insertions(+), 296 deletions(-) create mode 100644 app/src/main/java/com/tari/android/wallet/event/EffectChannelFlow.kt create mode 100644 app/src/main/java/com/tari/android/wallet/extension/FlowExtensions.kt create mode 100644 app/src/main/java/com/tari/android/wallet/ui/fragment/restore/chooseRestoreOption/ChooseRestoreOptionModel.kt delete mode 100644 app/src/main/java/com/tari/android/wallet/ui/fragment/restore/chooseRestoreOption/ChooseRestoreOptionState.kt create mode 100644 app/src/main/java/com/tari/android/wallet/ui/fragment/restore/enterRestorationPassword/EnterRestorationPasswordModel.kt create mode 100644 app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/backupSettings/option/BackupOptionModel.kt rename app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/data/{BackupOptions.kt => BackupOptionType.kt} (77%) diff --git a/app/build.gradle b/app/build.gradle index 6f9170611..4d730282a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,6 +6,7 @@ apply plugin: "kotlin-android" apply plugin: "kotlin-kapt" apply from: "../download-libwallet.gradle" apply plugin: "io.sentry.android.gradle" +apply plugin: 'kotlin-parcelize' android { namespace "com.tari.android.wallet" diff --git a/app/src/main/java/com/tari/android/wallet/application/securityStage/StagedWalletSecurityManager.kt b/app/src/main/java/com/tari/android/wallet/application/securityStage/StagedWalletSecurityManager.kt index 32597528a..97afecbd0 100644 --- a/app/src/main/java/com/tari/android/wallet/application/securityStage/StagedWalletSecurityManager.kt +++ b/app/src/main/java/com/tari/android/wallet/application/securityStage/StagedWalletSecurityManager.kt @@ -44,7 +44,7 @@ class StagedWalletSecurityManager : CommonViewModel() { get() = tariSettingsSharedRepository.hasVerifiedSeedWords val isBackupOn - get() = backupPrefsRepository.getOptionList.any { it.isEnable } + get() = backupPrefsRepository.optionList.any { it.isEnabled } val isBackupPasswordSet get() = !backupPrefsRepository.backupPassword.isNullOrEmpty() @@ -117,7 +117,7 @@ class StagedWalletSecurityManager : CommonViewModel() { private fun openStage1() { dismissDialog.postValue(Unit) - tariNavigator?.let { + tariNavigator.let { it.toAllSettings() it.toBackupSettings(false) it.toWalletBackupWithRecoveryPhrase() @@ -136,7 +136,7 @@ class StagedWalletSecurityManager : CommonViewModel() { private fun openStage1B() { dismissDialog.postValue(Unit) - tariNavigator?.let { + tariNavigator.let { it.toAllSettings() it.toBackupSettings(true) } @@ -154,7 +154,7 @@ class StagedWalletSecurityManager : CommonViewModel() { private fun openStage2() { dismissDialog.postValue(Unit) - tariNavigator?.let { + tariNavigator.let { it.toAllSettings() it.toBackupSettings(false) it.toChangePassword() diff --git a/app/src/main/java/com/tari/android/wallet/event/EffectChannelFlow.kt b/app/src/main/java/com/tari/android/wallet/event/EffectChannelFlow.kt new file mode 100644 index 000000000..7f5801b2b --- /dev/null +++ b/app/src/main/java/com/tari/android/wallet/event/EffectChannelFlow.kt @@ -0,0 +1,14 @@ +package com.tari.android.wallet.event + +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.receiveAsFlow + +class EffectChannelFlow { + private val channel: Channel = Channel(Channel.CONFLATED) + val flow: Flow = channel.receiveAsFlow() + + suspend fun send(effect: Effect) { + channel.send(effect) + } +} diff --git a/app/src/main/java/com/tari/android/wallet/extension/FlowExtensions.kt b/app/src/main/java/com/tari/android/wallet/extension/FlowExtensions.kt new file mode 100644 index 000000000..ea6e9478c --- /dev/null +++ b/app/src/main/java/com/tari/android/wallet/extension/FlowExtensions.kt @@ -0,0 +1,13 @@ +package com.tari.android.wallet.extension + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +inline fun LifecycleOwner.launchAndRepeatOnLifecycle( + state: Lifecycle.State, + crossinline block: suspend CoroutineScope.() -> Unit, +) = lifecycleScope.launch { repeatOnLifecycle(state) { block() } } diff --git a/app/src/main/java/com/tari/android/wallet/infrastructure/backup/BackupManager.kt b/app/src/main/java/com/tari/android/wallet/infrastructure/backup/BackupManager.kt index 2700df741..6b022ba9f 100644 --- a/app/src/main/java/com/tari/android/wallet/infrastructure/backup/BackupManager.kt +++ b/app/src/main/java/com/tari/android/wallet/infrastructure/backup/BackupManager.kt @@ -45,7 +45,7 @@ import com.tari.android.wallet.infrastructure.backup.googleDrive.GoogleDriveBack import com.tari.android.wallet.infrastructure.backup.local.LocalBackupStorage import com.tari.android.wallet.notification.NotificationHelper import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupOptionDto -import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupOptions +import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupOptionType import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupSettingsRepository import io.reactivex.subjects.BehaviorSubject import kotlinx.coroutines.CoroutineScope @@ -72,8 +72,6 @@ class BackupManager @Inject constructor( private val logger get() = Logger.t(BackupManager::class.simpleName) - var currentOption: BackupOptions? = BackupOptions.Dropbox - private val coroutineContext = Job() private var localScope = CoroutineScope(coroutineContext) @@ -85,35 +83,34 @@ class BackupManager @Inject constructor( .subscribe() init { - val backupsState = BackupsState(backupSettingsRepository.getOptionList.associate { Pair(it.type, getBackupStateByOption(it)) }) + val backupsState = BackupsState(backupSettingsRepository.optionList.associate { dto -> Pair(dto.type, dto.toBackupState()) }) EventBus.backupState.post(backupsState) EventBus.subscribe(this) { trigger.onNext(Unit) } EventBus.subscribe(this) { trigger.onNext(Unit) } } - fun setupStorage(option: BackupOptions, hostFragment: Fragment) { - currentOption = option - getStorageByOption(option).setup(hostFragment) + fun setupStorage(optionType: BackupOptionType, hostFragment: Fragment) { + optionType.getStorage().setup(hostFragment) } - suspend fun onSetupActivityResult(requestCode: Int, resultCode: Int, intent: Intent?): Boolean = - currentOption?.let { getStorageByOption(it).onSetupActivityResult(requestCode, resultCode, intent) } ?: false + suspend fun onSetupActivityResult(optionType: BackupOptionType, requestCode: Int, resultCode: Int, intent: Intent?): Boolean = + optionType.getStorage().onSetupActivityResult(requestCode, resultCode, intent) fun backupNow() = trigger.onNext(Unit) - private suspend fun backupAll() = backupSettingsRepository.getOptionList.forEach { backup(it.type) } + private suspend fun backupAll() = backupSettingsRepository.optionList.forEach { backup(it.type) } - private suspend fun backup(optionType: BackupOptions) = backupMutex.withLock { - val currentDto = backupSettingsRepository.getOptionList.firstOrNull { it.type == optionType } ?: return - if (!currentDto.isEnable) { - logger.d("Backup is disabled. Exit.") + private suspend fun backup(optionType: BackupOptionType) = backupMutex.withLock { + val currentDto = backupSettingsRepository.findOption(optionType) + if (!currentDto.isEnabled) { + logger.i("Backup is disabled for $optionType. Exit.") return } val backupsState = EventBus.backupState.publishSubject.value!!.copy() if (backupsState.backupsStates[optionType] is BackupState.BackupInProgress) { - logger.d("Backup is in progress. Exit.") + logger.i("Backup is in progress for $optionType. Exit.") return } @@ -122,65 +119,63 @@ class BackupManager @Inject constructor( EventBus.backupState.post(newState) } - logger.i("Backup started") + logger.i("Backup started for $optionType") updateState(BackupState.BackupInProgress) try { - val backupDate = getStorageByOption(optionType).backup() + val backupDate = optionType.getStorage().backup() backupSettingsRepository.updateOption( currentDto.copy( - isEnable = true, + isEnabled = true, lastSuccessDate = SerializableTime(backupDate), lastFailureDate = null ) ) - logger.i("Backup successful") + logger.i("Backup successful for $optionType") updateState(BackupState.BackupUpToDate) } catch (exception: Throwable) { - logger.i("Backup failed $exception") + logger.e("Backup failed for $optionType: ${exception.message}") if (exception is BackupStorageAuthRevokedException) { - logger.i("Error happened on backup BackupStorageAuthRevokedException") + logger.e("BackupStorageAuthRevokedException happened during backup: ${exception.message}") turnOff(optionType) postBackupFailedNotification(exception) } - logger.i("Error happened while backing up") updateState(BackupState.BackupFailed(exception)) backupSettingsRepository.updateOption(currentDto.copy(lastSuccessDate = null, lastFailureDate = SerializableTime(DateTime.now()))) } } fun turnOffAll() = localScope.launch { - backupSettingsRepository.getOptionList.forEach { turnOff(it.type) } + backupSettingsRepository.optionList.forEach { turnOff(it.type) } } - fun turnOff(optionType: BackupOptions) = with(backupMutex) { + fun turnOff(optionType: BackupOptionType) = with(backupMutex) { val backupsState = EventBus.backupState.publishSubject.value!!.copy() backupSettingsRepository.updateOption(BackupOptionDto(optionType)) backupSettingsRepository.backupPassword = null val newState = backupsState.copy(backupsStates = backupsState.backupsStates.toMutableMap().also { it[optionType] = BackupState.BackupDisabled }) EventBus.backupState.post(newState) - val backupStorage = getStorageByOption(optionType) - localScope.launch { backupStorage.signOut() } + localScope.launch { optionType.getStorage().signOut() } } - suspend fun signOut() { - getStorageByOption(currentOption!!).signOut() + suspend fun signOut(optionType: BackupOptionType) { + optionType.getStorage().signOut() } - suspend fun restoreLatestBackup(password: String? = null) = backupMutex.withLock { - getStorageByOption(currentOption!!).restoreLatestBackup(password) + suspend fun restoreLatestBackup(optionType: BackupOptionType, password: String? = null) = backupMutex.withLock { + optionType.getStorage().restoreLatestBackup(password) } - private fun getBackupStateByOption(optionDto: BackupOptionDto): BackupState = when { - !optionDto.isEnable -> BackupState.BackupDisabled - optionDto.lastFailureDate != null -> BackupState.BackupFailed() + private fun BackupOptionDto.toBackupState(): BackupState = when { + !this.isEnabled -> BackupState.BackupDisabled + this.lastFailureDate != null -> BackupState.BackupFailed() else -> BackupState.BackupUpToDate } - private fun getStorageByOption(optionType: BackupOptions): BackupStorage = when (optionType) { - BackupOptions.Google -> googleDriveBackupStorage - BackupOptions.Local -> localFileBackupStorage - BackupOptions.Dropbox -> dropboxBackupStorage + private fun BackupOptionType.getStorage(): BackupStorage = when (this) { + BackupOptionType.Google -> googleDriveBackupStorage + BackupOptionType.Local -> localFileBackupStorage + BackupOptionType.Dropbox -> dropboxBackupStorage } private fun postBackupFailedNotification(exception: Exception) { diff --git a/app/src/main/java/com/tari/android/wallet/infrastructure/backup/BackupsState.kt b/app/src/main/java/com/tari/android/wallet/infrastructure/backup/BackupsState.kt index 96e4d6886..e2eb19337 100644 --- a/app/src/main/java/com/tari/android/wallet/infrastructure/backup/BackupsState.kt +++ b/app/src/main/java/com/tari/android/wallet/infrastructure/backup/BackupsState.kt @@ -1,8 +1,8 @@ package com.tari.android.wallet.infrastructure.backup -import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupOptions +import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupOptionType -data class BackupsState(val backupsStates: Map) { +data class BackupsState(val backupsStates: Map) { val backupsState: BackupState get() { diff --git a/app/src/main/java/com/tari/android/wallet/ui/extension/FragmentExtensions.kt b/app/src/main/java/com/tari/android/wallet/ui/extension/FragmentExtensions.kt index 3dbdf0773..6d337113a 100644 --- a/app/src/main/java/com/tari/android/wallet/ui/extension/FragmentExtensions.kt +++ b/app/src/main/java/com/tari/android/wallet/ui/extension/FragmentExtensions.kt @@ -38,6 +38,7 @@ import androidx.annotation.ColorRes import androidx.annotation.DimenRes import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import androidx.core.os.bundleOf import androidx.fragment.app.Fragment fun Fragment.string(@StringRes id: Int): String = requireContext().string(id) @@ -53,4 +54,9 @@ fun Fragment.dimenPx(@DimenRes id: Int): Int = requireContext().dimenPx(id) fun Fragment.dimen(@DimenRes id: Int): Float = requireContext().dimen(id) -fun Fragment.drawable(@DrawableRes id: Int): Drawable? = requireContext().drawable(id) \ No newline at end of file +fun Fragment.drawable(@DrawableRes id: Int): Drawable? = requireContext().drawable(id) + +inline fun T.withArgs(vararg pairs: Pair): T { + arguments = bundleOf(*pairs) + return this +} \ No newline at end of file diff --git a/app/src/main/java/com/tari/android/wallet/ui/fragment/home/navigation/Navigation.kt b/app/src/main/java/com/tari/android/wallet/ui/fragment/home/navigation/Navigation.kt index 2b428a343..4836e8e1c 100644 --- a/app/src/main/java/com/tari/android/wallet/ui/fragment/home/navigation/Navigation.kt +++ b/app/src/main/java/com/tari/android/wallet/ui/fragment/home/navigation/Navigation.kt @@ -8,6 +8,7 @@ import com.tari.android.wallet.ui.fragment.contact_book.data.contacts.ContactDto import com.tari.android.wallet.ui.fragment.contact_book.data.contacts.YatDto import com.tari.android.wallet.ui.fragment.pinCode.PinCodeScreenBehavior import com.tari.android.wallet.ui.fragment.send.common.TransactionData +import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupOptionType sealed class Navigation { @@ -111,7 +112,7 @@ sealed class Navigation { } sealed class ChooseRestoreOptionNavigation : Navigation() { - object ToEnterRestorePassword : ChooseRestoreOptionNavigation() + data class ToEnterRestorePassword(val optionType: BackupOptionType) : ChooseRestoreOptionNavigation() object ToRestoreWithRecoveryPhrase : ChooseRestoreOptionNavigation() diff --git a/app/src/main/java/com/tari/android/wallet/ui/fragment/home/navigation/TariNavigator.kt b/app/src/main/java/com/tari/android/wallet/ui/fragment/home/navigation/TariNavigator.kt index 6f07a5f3d..8adb088c0 100644 --- a/app/src/main/java/com/tari/android/wallet/ui/fragment/home/navigation/TariNavigator.kt +++ b/app/src/main/java/com/tari/android/wallet/ui/fragment/home/navigation/TariNavigator.kt @@ -68,6 +68,7 @@ import com.tari.android.wallet.ui.fragment.settings.backgroundService.Background import com.tari.android.wallet.ui.fragment.settings.backup.backupOnboarding.BackupOnboardingFlowFragment import com.tari.android.wallet.ui.fragment.settings.backup.backupSettings.BackupSettingsFragment import com.tari.android.wallet.ui.fragment.settings.backup.changeSecurePassword.ChangeSecurePasswordFragment +import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupOptionType import com.tari.android.wallet.ui.fragment.settings.backup.enterCurrentPassword.EnterCurrentPasswordFragment import com.tari.android.wallet.ui.fragment.settings.backup.verifySeedPhrase.VerifySeedPhraseFragment import com.tari.android.wallet.ui.fragment.settings.backup.writeDownSeedWords.WriteDownSeedPhraseFragment @@ -110,7 +111,7 @@ class TariNavigator @Inject constructor(val prefs: SharedPrefsRepository, val ta is ContactBookNavigation.ToContactTransactionHistory -> toContactTransactionHistory(navigation.contact) is ContactBookNavigation.ToAddPhoneContact -> toAddPhoneContact() is ContactBookNavigation.ToSelectTariUser -> addFragment(SelectUserContactFragment.newInstance()) - Navigation.ChooseRestoreOptionNavigation.ToEnterRestorePassword -> toEnterRestorePassword() + is Navigation.ChooseRestoreOptionNavigation.ToEnterRestorePassword -> toEnterRestorePassword(navigation.optionType) Navigation.ChooseRestoreOptionNavigation.OnRestoreCompleted -> onRestoreCompleted() Navigation.ChooseRestoreOptionNavigation.ToRestoreWithRecoveryPhrase -> toRestoreWithRecoveryPhrase() AllSettingsNavigation.ToBugReporting -> DebugActivity.launch(activity, DebugNavigation.BugReport) @@ -165,7 +166,6 @@ class TariNavigator @Inject constructor(val prefs: SharedPrefsRepository, val ta } Navigation.ChatNavigation.ToAddChat -> addFragment(AddChatFragment()) - else -> Unit } } @@ -181,7 +181,7 @@ class TariNavigator @Inject constructor(val prefs: SharedPrefsRepository, val ta activity.startActivity(intent) } - fun toEnterRestorePassword() = addFragment(EnterRestorationPasswordFragment.newInstance()) + fun toEnterRestorePassword(optionType: BackupOptionType) = addFragment(EnterRestorationPasswordFragment.newInstance(optionType)) fun toRestoreWithRecoveryPhrase() = addFragment(InputSeedWordsFragment.newInstance()) diff --git a/app/src/main/java/com/tari/android/wallet/ui/fragment/restore/chooseRestoreOption/ChooseRestoreOptionFragment.kt b/app/src/main/java/com/tari/android/wallet/ui/fragment/restore/chooseRestoreOption/ChooseRestoreOptionFragment.kt index 8a9d94353..dde6fa218 100644 --- a/app/src/main/java/com/tari/android/wallet/ui/fragment/restore/chooseRestoreOption/ChooseRestoreOptionFragment.kt +++ b/app/src/main/java/com/tari/android/wallet/ui/fragment/restore/chooseRestoreOption/ChooseRestoreOptionFragment.kt @@ -39,13 +39,16 @@ import android.view.View import android.view.ViewGroup import androidx.core.view.children import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle import com.tari.android.wallet.databinding.FragmentChooseRestoreOptionBinding -import com.tari.android.wallet.extension.observe +import com.tari.android.wallet.extension.launchAndRepeatOnLifecycle import com.tari.android.wallet.ui.common.CommonFragment -import com.tari.android.wallet.ui.fragment.home.navigation.Navigation +import com.tari.android.wallet.ui.fragment.restore.chooseRestoreOption.ChooseRestoreOptionModel.Effect import com.tari.android.wallet.ui.fragment.restore.chooseRestoreOption.option.RecoveryOptionView import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupOptionDto -import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupOptions +import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupOptionType +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.launch class ChooseRestoreOptionFragment : CommonFragment() { @@ -75,45 +78,50 @@ class ChooseRestoreOptionFragment : CommonFragment + when (effect) { + is Effect.BeginProgress -> updateProgress(effect.optionType, isStarted = true) + is Effect.EndProgress -> updateProgress(effect.optionType, isStarted = false) + is Effect.SetupStorage -> effect.backupManager.setupStorage( + optionType = effect.optionType, + hostFragment = this@ChooseRestoreOptionFragment, + ) + } + } + } - observe(options) { initOptions(it) } + launch { + viewModel.uiState.filterNotNull().collect { state -> + initOptions(state.options) + } + } + } } private fun initOptions(options: List) { + ui.optionsContainer.removeAllViews() for (option in options) { val view = RecoveryOptionView(requireContext()).apply { viewLifecycle = viewLifecycleOwner - ui.restoreWalletCtaView.setOnClickListener { startRecovery(option.type) } + ui.restoreWalletCtaView.setOnClickListener { this@ChooseRestoreOptionFragment.viewModel.selectBackupOption(option.type) } init(option.type) } ui.optionsContainer.addView(view) } } - private fun processState(state: ChooseRestoreOptionState) { - when (state) { - is ChooseRestoreOptionState.BeginProgress -> updateProgress(state.backupOptions, true) - is ChooseRestoreOptionState.EndProgress -> updateProgress(state.backupOptions, false) - } - } - - - private fun updateProgress(backupOptions: BackupOptions, isStarted: Boolean) { + private fun updateProgress(optionType: BackupOptionType, isStarted: Boolean) { blockingBackPressDispatcher.isEnabled = isStarted - getBackupOptionView(backupOptions)?.updateLoading(isStarted) + getBackupOptionView(optionType)?.updateLoading(isStarted) } - private fun getBackupOptionView(backupOptions: BackupOptions): RecoveryOptionView? = - ui.optionsContainer.children.mapNotNull { it as? RecoveryOptionView }.firstOrNull { it.viewModel.option == backupOptions } + private fun getBackupOptionView(optionType: BackupOptionType): RecoveryOptionView? = + ui.optionsContainer.children.mapNotNull { it as? RecoveryOptionView }.firstOrNull { it.viewModel.option == optionType } } diff --git a/app/src/main/java/com/tari/android/wallet/ui/fragment/restore/chooseRestoreOption/ChooseRestoreOptionModel.kt b/app/src/main/java/com/tari/android/wallet/ui/fragment/restore/chooseRestoreOption/ChooseRestoreOptionModel.kt new file mode 100644 index 000000000..eccc28b4d --- /dev/null +++ b/app/src/main/java/com/tari/android/wallet/ui/fragment/restore/chooseRestoreOption/ChooseRestoreOptionModel.kt @@ -0,0 +1,19 @@ +package com.tari.android.wallet.ui.fragment.restore.chooseRestoreOption + +import com.tari.android.wallet.infrastructure.backup.BackupManager +import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupOptionDto +import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupOptionType + +object ChooseRestoreOptionModel { + + data class UiState( + val selectedOption: BackupOptionType? = null, + val options: List = emptyList(), + ) + + sealed interface Effect { + data class BeginProgress(val optionType: BackupOptionType) : Effect + data class EndProgress(val optionType: BackupOptionType) : Effect + data class SetupStorage(val backupManager: BackupManager, val optionType: BackupOptionType) : Effect + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tari/android/wallet/ui/fragment/restore/chooseRestoreOption/ChooseRestoreOptionState.kt b/app/src/main/java/com/tari/android/wallet/ui/fragment/restore/chooseRestoreOption/ChooseRestoreOptionState.kt deleted file mode 100644 index 781b9597a..000000000 --- a/app/src/main/java/com/tari/android/wallet/ui/fragment/restore/chooseRestoreOption/ChooseRestoreOptionState.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tari.android.wallet.ui.fragment.restore.chooseRestoreOption - -import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupOptions - -sealed class ChooseRestoreOptionState(val backupOptions: BackupOptions) { - class BeginProgress(backupOptions: BackupOptions) : ChooseRestoreOptionState(backupOptions) - - class EndProgress(backupOptions: BackupOptions) : ChooseRestoreOptionState(backupOptions) -} \ No newline at end of file diff --git a/app/src/main/java/com/tari/android/wallet/ui/fragment/restore/chooseRestoreOption/ChooseRestoreOptionViewModel.kt b/app/src/main/java/com/tari/android/wallet/ui/fragment/restore/chooseRestoreOption/ChooseRestoreOptionViewModel.kt index b78d1bc55..01ec4520d 100644 --- a/app/src/main/java/com/tari/android/wallet/ui/fragment/restore/chooseRestoreOption/ChooseRestoreOptionViewModel.kt +++ b/app/src/main/java/com/tari/android/wallet/ui/fragment/restore/chooseRestoreOption/ChooseRestoreOptionViewModel.kt @@ -1,13 +1,11 @@ package com.tari.android.wallet.ui.fragment.restore.chooseRestoreOption import android.content.Intent -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.tari.android.wallet.R -import com.tari.android.wallet.application.WalletManager import com.tari.android.wallet.application.WalletState import com.tari.android.wallet.data.WalletConfig +import com.tari.android.wallet.event.EffectChannelFlow import com.tari.android.wallet.event.EventBus import com.tari.android.wallet.extension.addTo import com.tari.android.wallet.ffi.FFITariWalletAddress @@ -22,15 +20,19 @@ import com.tari.android.wallet.model.WalletError import com.tari.android.wallet.model.throwIf import com.tari.android.wallet.service.service.WalletServiceLauncher import com.tari.android.wallet.ui.common.CommonViewModel -import com.tari.android.wallet.ui.common.SingleLiveEvent import com.tari.android.wallet.ui.dialog.error.ErrorDialogArgs import com.tari.android.wallet.ui.dialog.error.WalletErrorArgs import com.tari.android.wallet.ui.fragment.home.navigation.Navigation -import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupOptionDto -import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupOptions +import com.tari.android.wallet.ui.fragment.restore.chooseRestoreOption.ChooseRestoreOptionModel.Effect +import com.tari.android.wallet.ui.fragment.restore.chooseRestoreOption.ChooseRestoreOptionModel.UiState +import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupOptionType import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupSettingsRepository import com.tari.android.wallet.util.WalletUtil import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.io.IOException import java.util.concurrent.TimeUnit @@ -44,106 +46,120 @@ class ChooseRestoreOptionViewModel : CommonViewModel() { @Inject lateinit var backupSettingsRepository: BackupSettingsRepository - @Inject - lateinit var walletManager: WalletManager - @Inject lateinit var walletServiceLauncher: WalletServiceLauncher @Inject lateinit var walletConfig: WalletConfig - private val _state = SingleLiveEvent() - val state: LiveData = _state + private val _effect = EffectChannelFlow() + val effect: Flow = _effect.flow - val options = MutableLiveData>() + private val _uiState = MutableStateFlow(UiState()) + val uiState = _uiState.asStateFlow() init { component.inject(this) - options.postValue(backupSettingsRepository.getOptionList) + _uiState.update { it.copy(options = backupSettingsRepository.optionList) } EventBus.walletState.publishSubject.filter { it is WalletState.Running }.subscribe { - if (WalletUtil.walletExists(walletConfig) && state.value != null) { - backupSettingsRepository.restoredTxs?.let { - if (it.utxos.orEmpty().isEmpty()) return@let - - val sourceAddress = FFITariWalletAddress(HexString(it.source)) - val tariWalletAddress = TariWalletAddress(it.source, sourceAddress.getEmojiId()) - val message = resourceManager.getString(R.string.backup_restored_tx) - val error = WalletError() - walletService.restoreWithUnbindedOutputs(it.utxos, tariWalletAddress, message, error) - throwIf(error) + _uiState.value.selectedOption + ?.takeIf { WalletUtil.walletExists(walletConfig) } + ?.let { selectedOption -> + backupSettingsRepository.restoredTxs + ?.takeIf { it.utxos.orEmpty().isEmpty() } + ?.let { restoredTxs -> + val sourceAddress = FFITariWalletAddress(HexString(restoredTxs.source)) + val tariWalletAddress = TariWalletAddress(restoredTxs.source, sourceAddress.getEmojiId()) + val message = resourceManager.getString(R.string.backup_restored_tx) + val error = WalletError() + walletService.restoreWithUnbindedOutputs(restoredTxs.utxos, tariWalletAddress, message, error) + throwIf(error) + } + + val dto = backupSettingsRepository.getOptionDto(selectedOption).copy(isEnabled = true) + backupSettingsRepository.updateOption(dto) + backupManager.backupNow() + + navigation.postValue(Navigation.ChooseRestoreOptionNavigation.OnRestoreCompleted) } - - val dto = backupSettingsRepository.getOptionDto(state.value!!.backupOptions)!!.copy(isEnable = true) - backupSettingsRepository.updateOption(dto) - backupManager.backupNow() - - navigation.postValue(Navigation.ChooseRestoreOptionNavigation.OnRestoreCompleted) - } }.addTo(compositeDisposable) EventBus.walletState.publishSubject.filter { it is WalletState.Failed } .map { it as WalletState.Failed } - .debounce(300L, TimeUnit.MILLISECONDS).subscribe { - viewModelScope.launch(Dispatchers.IO) { - handleException(WalletStartFailedException(it.exception)) + .debounce(300L, TimeUnit.MILLISECONDS) + .map { it.exception } + .subscribe { exception -> + _uiState.value.selectedOption?.let { selectedOption -> + viewModelScope.launch(Dispatchers.IO) { + handleException(selectedOption, WalletStartFailedException(exception)) + } } }.addTo(compositeDisposable) } - fun startRestore(options: BackupOptions) { - _state.postValue(ChooseRestoreOptionState.BeginProgress(options)) + fun selectBackupOption(optionType: BackupOptionType) { + viewModelScope.launch { + _uiState.update { it.copy(selectedOption = optionType) } + _effect.send(Effect.SetupStorage(backupManager, optionType)) + _effect.send(Effect.BeginProgress(optionType)) + } + } + + fun navigateToRecoveryPhrase() { + navigation.postValue(Navigation.ChooseRestoreOptionNavigation.ToRestoreWithRecoveryPhrase) } fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - viewModelScope.launch(Dispatchers.IO) { - try { - if (backupManager.onSetupActivityResult(requestCode, resultCode, data)) { - restoreFromBackup() + _uiState.value.selectedOption?.let { selectedOption -> + viewModelScope.launch(Dispatchers.IO) { + try { + if (backupManager.onSetupActivityResult(selectedOption, requestCode, resultCode, data)) { + restoreFromBackup(selectedOption) + } + } catch (exception: Exception) { + logger.e("Backup storage setup for $selectedOption failed: ${exception.message}") + backupManager.signOut(selectedOption) + _effect.send(Effect.EndProgress(selectedOption)) + showAuthFailedDialog() } - } catch (exception: Exception) { - logger.i(exception.message + "Backup storage setup failed") - backupManager.signOut() - _state.postValue(ChooseRestoreOptionState.EndProgress(backupManager.currentOption!!)) - showAuthFailedDialog() } - } + } ?: logger.e("SelectedOption is null") } - private suspend fun restoreFromBackup() { + private suspend fun restoreFromBackup(currentOption: BackupOptionType) { try { // try to restore with no password - backupManager.restoreLatestBackup() + backupManager.restoreLatestBackup(currentOption) viewModelScope.launch(Dispatchers.Main) { walletServiceLauncher.start() } } catch (exception: Throwable) { - handleException(exception) + handleException(currentOption, exception) } } - private suspend fun handleException(exception: Throwable) { + private suspend fun handleException(currentOption: BackupOptionType, exception: Throwable) { when (exception) { is BackupStorageAuthRevokedException -> { - logger.i("Auth revoked") - backupManager.signOut() + logger.e("Auth revoked for $currentOption") + backupManager.signOut(currentOption) showAuthFailedDialog() } is BackupStorageTamperedException -> { // backup file not found - logger.i("Backup file not found") - backupManager.signOut() + logger.e("Backup file not found for $currentOption") + backupManager.signOut(currentOption) showBackupFileNotFoundDialog() } is BackupFileIsEncryptedException -> { - navigation.postValue(Navigation.ChooseRestoreOptionNavigation.ToEnterRestorePassword) + navigation.postValue(Navigation.ChooseRestoreOptionNavigation.ToEnterRestorePassword(currentOption)) } is WalletStartFailedException -> { - logger.i("Restore failed: wallet start failed") + logger.e("Restore failed for $currentOption: wallet start failed") viewModelScope.launch(Dispatchers.Main) { walletServiceLauncher.stopAndDelete() } @@ -158,19 +174,19 @@ class ChooseRestoreOptionViewModel : CommonViewModel() { } is IOException -> { - logger.i("Restore failed: network connection") - backupManager.signOut() + logger.e("Restore failed for $currentOption: network connection") + backupManager.signOut(currentOption) showRestoreFailedDialog(resourceManager.getString(R.string.error_no_connection_title)) } else -> { - logger.i("Restore failed") - backupManager.signOut() + logger.e("Restore failed for $currentOption") + backupManager.signOut(currentOption) showRestoreFailedDialog(exception.message ?: exception.toString()) } } - _state.postValue(ChooseRestoreOptionState.EndProgress(backupManager.currentOption!!)) + _effect.send(Effect.EndProgress(currentOption)) } private fun showBackupFileNotFoundDialog() { diff --git a/app/src/main/java/com/tari/android/wallet/ui/fragment/restore/chooseRestoreOption/option/RecoveryOptionView.kt b/app/src/main/java/com/tari/android/wallet/ui/fragment/restore/chooseRestoreOption/option/RecoveryOptionView.kt index 3f824f80d..f52ca2f4d 100644 --- a/app/src/main/java/com/tari/android/wallet/ui/fragment/restore/chooseRestoreOption/option/RecoveryOptionView.kt +++ b/app/src/main/java/com/tari/android/wallet/ui/fragment/restore/chooseRestoreOption/option/RecoveryOptionView.kt @@ -9,7 +9,7 @@ import com.tari.android.wallet.databinding.ViewRestoreOptionBinding import com.tari.android.wallet.ui.component.common.CommonView import com.tari.android.wallet.ui.extension.gone import com.tari.android.wallet.ui.extension.setVisible -import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupOptions +import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupOptionType class RecoveryOptionView : CommonView { @@ -26,11 +26,11 @@ class RecoveryOptionView : CommonView R.string.back_up_wallet_restore_with_google_drive - BackupOptions.Local -> R.string.back_up_wallet_restore_with_local_files - BackupOptions.Dropbox -> R.string.back_up_wallet_restore_with_dropbox + BackupOptionType.Google -> R.string.back_up_wallet_restore_with_google_drive + BackupOptionType.Local -> R.string.back_up_wallet_restore_with_local_files + BackupOptionType.Dropbox -> R.string.back_up_wallet_restore_with_dropbox } ui.title.text = context.getString(text) bindViewModel(RecoveryOptionViewModel().apply { this.option = option }) diff --git a/app/src/main/java/com/tari/android/wallet/ui/fragment/restore/chooseRestoreOption/option/RecoveryOptionViewModel.kt b/app/src/main/java/com/tari/android/wallet/ui/fragment/restore/chooseRestoreOption/option/RecoveryOptionViewModel.kt index 05d4517d0..54cc79caf 100644 --- a/app/src/main/java/com/tari/android/wallet/ui/fragment/restore/chooseRestoreOption/option/RecoveryOptionViewModel.kt +++ b/app/src/main/java/com/tari/android/wallet/ui/fragment/restore/chooseRestoreOption/option/RecoveryOptionViewModel.kt @@ -1,9 +1,9 @@ package com.tari.android.wallet.ui.fragment.restore.chooseRestoreOption.option import com.tari.android.wallet.ui.common.CommonViewModel -import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupOptions +import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupOptionType class RecoveryOptionViewModel : CommonViewModel() { - var option: BackupOptions? = null + var option: BackupOptionType? = null } \ No newline at end of file diff --git a/app/src/main/java/com/tari/android/wallet/ui/fragment/restore/enterRestorationPassword/EnterRestorationPasswordFragment.kt b/app/src/main/java/com/tari/android/wallet/ui/fragment/restore/enterRestorationPassword/EnterRestorationPasswordFragment.kt index fb4606d42..ee24863b6 100644 --- a/app/src/main/java/com/tari/android/wallet/ui/fragment/restore/enterRestorationPassword/EnterRestorationPasswordFragment.kt +++ b/app/src/main/java/com/tari/android/wallet/ui/fragment/restore/enterRestorationPassword/EnterRestorationPasswordFragment.kt @@ -52,10 +52,14 @@ import com.tari.android.wallet.ui.common.CommonFragment import com.tari.android.wallet.ui.extension.colorFromAttribute import com.tari.android.wallet.ui.extension.gone import com.tari.android.wallet.ui.extension.hideKeyboard +import com.tari.android.wallet.ui.extension.parcelable import com.tari.android.wallet.ui.extension.setOnThrottledClickListener import com.tari.android.wallet.ui.extension.showKeyboard import com.tari.android.wallet.ui.extension.string import com.tari.android.wallet.ui.extension.visible +import com.tari.android.wallet.ui.extension.withArgs +import com.tari.android.wallet.ui.fragment.restore.enterRestorationPassword.EnterRestorationPasswordModel.Parameters +import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupOptionType class EnterRestorationPasswordFragment : CommonFragment() { @@ -67,6 +71,7 @@ class EnterRestorationPasswordFragment : CommonFragment() + val state: LiveData = _state + init { component.inject(this) EventBus.walletState.publishSubject.filter { it is WalletState.Running }.subscribe { if (WalletUtil.walletExists(walletConfig)) { - val dto = backupSettingsRepository.getOptionDto(backupManager.currentOption!!)!!.copy(isEnable = true) + val dto = backupSettingsRepository.getOptionDto(parameters.selectedOptionType)!!.copy(isEnabled = true) backupSettingsRepository.updateOption(dto) backupManager.backupNow() @@ -58,13 +64,14 @@ class EnterRestorationPasswordViewModel : CommonViewModel() { }.addTo(compositeDisposable) } - private val _state = SingleLiveEvent() - val state: LiveData = _state + fun assignParameters(parameters: Parameters) { + this.parameters = parameters + } fun onBack() { backPressed.postValue(Unit) viewModelScope.launch(Dispatchers.IO) { - backupManager.signOut() + backupManager.signOut(parameters.selectedOptionType) } } @@ -76,7 +83,7 @@ class EnterRestorationPasswordViewModel : CommonViewModel() { private fun performRestoration(password: String) { viewModelScope.launch(Dispatchers.IO) { try { - backupManager.restoreLatestBackup(password) + backupManager.restoreLatestBackup(parameters.selectedOptionType, password) backupSettingsRepository.backupPassword = password viewModelScope.launch(Dispatchers.Main) { walletServiceLauncher.start() diff --git a/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/allSettings/AllSettingsViewModel.kt b/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/allSettings/AllSettingsViewModel.kt index f0ea57b2d..eb0dc40b0 100644 --- a/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/allSettings/AllSettingsViewModel.kt +++ b/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/allSettings/AllSettingsViewModel.kt @@ -82,7 +82,6 @@ import com.tari.android.wallet.ui.fragment.settings.allSettings.button.ButtonVie import com.tari.android.wallet.ui.fragment.settings.allSettings.myProfile.MyProfileViewHolderItem import com.tari.android.wallet.ui.fragment.settings.allSettings.title.SettingsTitleViewHolderItem import com.tari.android.wallet.ui.fragment.settings.allSettings.version.SettingsVersionViewHolderItem -import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupSettingsRepository import com.tari.android.wallet.ui.fragment.settings.userAutorization.BiometricAuthenticationViewModel import com.tari.android.wallet.yat.YatAdapter import com.tari.android.wallet.yat.YatSharedRepository @@ -99,9 +98,6 @@ class AllSettingsViewModel : CommonViewModel() { @Inject lateinit var yatAdapter: YatAdapter - @Inject - lateinit var backupSettingsRepository: BackupSettingsRepository - @Inject lateinit var backupManager: BackupManager diff --git a/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/backupSettings/BackupSettingsFragment.kt b/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/backupSettings/BackupSettingsFragment.kt index 570affaf1..aa49ebe6f 100644 --- a/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/backupSettings/BackupSettingsFragment.kt +++ b/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/backupSettings/BackupSettingsFragment.kt @@ -98,7 +98,7 @@ class BackupSettingsFragment : CommonFragment) { diff --git a/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/backupSettings/BackupSettingsViewModel.kt b/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/backupSettings/BackupSettingsViewModel.kt index 66508eef4..fb800b340 100644 --- a/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/backupSettings/BackupSettingsViewModel.kt +++ b/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/backupSettings/BackupSettingsViewModel.kt @@ -15,6 +15,7 @@ import com.tari.android.wallet.ui.common.CommonViewModel import com.tari.android.wallet.ui.dialog.error.ErrorDialogArgs import com.tari.android.wallet.ui.fragment.home.navigation.Navigation import com.tari.android.wallet.ui.fragment.settings.backup.backupSettings.option.BackupOptionViewModel +import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupOptionType import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupSettingsRepository import com.tari.android.wallet.ui.fragment.settings.userAutorization.BiometricAuthenticationViewModel import kotlinx.coroutines.Dispatchers @@ -32,7 +33,7 @@ class BackupSettingsViewModel : CommonViewModel() { lateinit var biometricAuthenticationViewModel: BiometricAuthenticationViewModel - val options = MutableLiveData>() + val optionViewModels = MutableLiveData>() private val _isBackupNowAvailable = MutableLiveData() val isBackupNowAvailable: LiveData = _isBackupNowAvailable @@ -42,6 +43,8 @@ class BackupSettingsViewModel : CommonViewModel() { private val _updatePasswordEnabled = MutableLiveData() val setPasswordVisible: LiveData = _updatePasswordEnabled + private var currentOption: BackupOptionType? = null // todo could be irrelevant if multiple backups started simultaneously + init { component.inject(this) @@ -49,7 +52,16 @@ class BackupSettingsViewModel : CommonViewModel() { backupStateChanged.postValue(Unit) - options.postValue(backupSettingsRepository.getOptionList.map { option -> BackupOptionViewModel().apply { setup(option.type) } }) + optionViewModels.postValue( + backupSettingsRepository.optionList.map { option -> + BackupOptionViewModel( + optionType = option.type, + backupSettingsViewModel = this@BackupSettingsViewModel, + backupManager = backupManager, + backupSettingsRepository = backupSettingsRepository, + ).apply { setup() } + } + ) loadOptionData() } @@ -57,7 +69,7 @@ class BackupSettingsViewModel : CommonViewModel() { fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { viewModelScope.launch(Dispatchers.IO) { kotlin.runCatching { - options.value.orEmpty().firstOrNull { it.option.value!!.type == backupManager.currentOption } + optionViewModels.value.orEmpty().firstOrNull { it.optionType == currentOption } ?.onActivityResult(requestCode, resultCode, data) } } @@ -85,9 +97,13 @@ class BackupSettingsViewModel : CommonViewModel() { fun onBackupToCloud() = backupManager.backupNow() + fun onOptionSelected(currentOption: BackupOptionType) { + this.currentOption = currentOption + } + private fun onBackupStateChanged(backupState: BackupsState) { backupState.backupsStates.forEach { state -> - options.value.orEmpty().firstOrNull { it.option.value!!.type == state.key }?.onBackupStateChanged(state.value) + optionViewModels.value.orEmpty().firstOrNull { it.optionType == state.key }?.onBackupStateChanged(state.value) } loadOptionData() @@ -97,9 +113,9 @@ class BackupSettingsViewModel : CommonViewModel() { private fun loadOptionData() { val backupState = EventBus.backupState.publishSubject.value - val optionsDto = backupSettingsRepository.getOptionList - _updatePasswordEnabled.postValue(optionsDto.any { it.isEnable }) - _isBackupNowAvailable.postValue(optionsDto.any { it.isEnable } && + val optionsDto = backupSettingsRepository.optionList + _updatePasswordEnabled.postValue(optionsDto.any { it.isEnabled }) + _isBackupNowAvailable.postValue(optionsDto.any { it.isEnabled } && backupState != null && backupState.backupsStates.all { it.value !is BackupState.BackupInProgress }) } @@ -113,9 +129,11 @@ class BackupSettingsViewModel : CommonViewModel() { exception is BackupStorageFullException -> resourceManager.getString( R.string.backup_wallet_storage_full_desc ) + exception is BackupStorageAuthRevokedException -> resourceManager.getString( R.string.check_backup_storage_status_auth_revoked_error_description ) + exception is UnknownHostException -> resourceManager.getString(R.string.error_no_connection_title) exception?.message == null -> resourceManager.getString(R.string.back_up_wallet_backing_up_unknown_error) else -> resourceManager.getString(R.string.back_up_wallet_backing_up_error_desc, exception.message!!) diff --git a/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/backupSettings/option/BackupOptionModel.kt b/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/backupSettings/option/BackupOptionModel.kt new file mode 100644 index 000000000..17d2affb3 --- /dev/null +++ b/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/backupSettings/option/BackupOptionModel.kt @@ -0,0 +1,30 @@ +package com.tari.android.wallet.ui.fragment.settings.backup.backupSettings.option + +import com.tari.android.wallet.infrastructure.backup.BackupManager +import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupOptionDto +import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupOptionType + +object BackupOptionModel { + data class UiState( + val option: BackupOptionDto, + val switchChecked: Boolean, + val loading: Boolean = false, + val lastSuccessDate: String? = null, + ) { + constructor(option: BackupOptionDto) : this( + option = option, + switchChecked = option.isEnabled, + ) + + fun startLoading() = this.copy(loading = true) + fun stopLoading() = this.copy(loading = false) + fun switchOn() = this.copy(switchChecked = true) + fun switchOff() = this.copy(switchChecked = false) + fun switch(value: Boolean) = this.copy(switchChecked = value) + } + + + sealed interface Effect { + data class SetupStorage(val backupManager: BackupManager, val optionType: BackupOptionType) : Effect + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/backupSettings/option/BackupOptionView.kt b/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/backupSettings/option/BackupOptionView.kt index dcee4d523..e8234041a 100644 --- a/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/backupSettings/option/BackupOptionView.kt +++ b/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/backupSettings/option/BackupOptionView.kt @@ -6,14 +6,20 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.lifecycle.LifecycleCoroutineScope +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.coroutineScope import com.tari.android.wallet.databinding.ViewBackupOptionBinding -import com.tari.android.wallet.extension.observe import com.tari.android.wallet.ui.component.common.CommonView import com.tari.android.wallet.ui.extension.setVisible +import com.tari.android.wallet.ui.fragment.settings.backup.backupSettings.option.BackupOptionModel.Effect +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.launch -class BackupOptionView : CommonView { +class BackupOptionView : CommonView, LifecycleObserver { private lateinit var fragment: Fragment + private lateinit var lifecycleScope: LifecycleCoroutineScope override fun bindingInflate(layoutInflater: LayoutInflater, parent: ViewGroup?, attachToRoot: Boolean): ViewBackupOptionBinding = ViewBackupOptionBinding.inflate(layoutInflater, parent, attachToRoot) @@ -30,20 +36,28 @@ class BackupOptionView : CommonView + when (effect) { + is Effect.SetupStorage -> effect.backupManager.setupStorage(effect.optionType, fragment) + } + } + + viewModel.uiState.filterNotNull().collect { state -> + setSwitchCheck(state.switchChecked) + onChangeInProgress(state.loading) + updateLastSuccessDate(state.lastSuccessDate) + } + } } private fun setSwitchCheck(isChecked: Boolean) = with(ui) { @@ -61,8 +75,8 @@ class BackupOptionView : CommonView viewModel.onBackupSwitchChecked(isChecked) } } - private fun updateLastSuccessDate(date: String) { - ui.lastBackupTimeTextView.setVisible(date.isNotBlank(), View.GONE) + private fun updateLastSuccessDate(date: String?) { + ui.lastBackupTimeTextView.setVisible(!date.isNullOrBlank(), View.GONE) ui.lastBackupTimeTextView.text = date } diff --git a/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/backupSettings/option/BackupOptionViewModel.kt b/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/backupSettings/option/BackupOptionViewModel.kt index ddda64789..4a509ea1c 100644 --- a/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/backupSettings/option/BackupOptionViewModel.kt +++ b/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/backupSettings/option/BackupOptionViewModel.kt @@ -1,10 +1,9 @@ package com.tari.android.wallet.ui.fragment.settings.backup.backupSettings.option import android.content.Intent -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.tari.android.wallet.R +import com.tari.android.wallet.event.EffectChannelFlow import com.tari.android.wallet.event.EventBus import com.tari.android.wallet.extension.addTo import com.tari.android.wallet.infrastructure.backup.BackupException @@ -12,7 +11,6 @@ import com.tari.android.wallet.infrastructure.backup.BackupManager import com.tari.android.wallet.infrastructure.backup.BackupState import com.tari.android.wallet.infrastructure.backup.BackupStorageFullException import com.tari.android.wallet.ui.common.CommonViewModel -import com.tari.android.wallet.ui.common.SingleLiveEvent import com.tari.android.wallet.ui.dialog.error.ErrorDialogArgs import com.tari.android.wallet.ui.dialog.modular.DialogArgs import com.tari.android.wallet.ui.dialog.modular.ModularDialogArgs @@ -20,90 +18,82 @@ import com.tari.android.wallet.ui.dialog.modular.modules.body.BodyModule import com.tari.android.wallet.ui.dialog.modular.modules.button.ButtonModule import com.tari.android.wallet.ui.dialog.modular.modules.button.ButtonStyle import com.tari.android.wallet.ui.dialog.modular.modules.head.HeadModule -import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupOptionDto -import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupOptions +import com.tari.android.wallet.ui.fragment.settings.backup.backupSettings.BackupSettingsViewModel +import com.tari.android.wallet.ui.fragment.settings.backup.backupSettings.option.BackupOptionModel.Effect +import com.tari.android.wallet.ui.fragment.settings.backup.backupSettings.option.BackupOptionModel.UiState +import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupOptionType import com.tari.android.wallet.ui.fragment.settings.backup.data.BackupSettingsRepository import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.joda.time.DateTime import org.joda.time.format.DateTimeFormat import java.util.Locale -import javax.inject.Inject -class BackupOptionViewModel : CommonViewModel() { +class BackupOptionViewModel( + val optionType: BackupOptionType, + private val backupSettingsViewModel: BackupSettingsViewModel, + private val backupSettingsRepository: BackupSettingsRepository, + private val backupManager: BackupManager, +) : CommonViewModel() { - @Inject - lateinit var backupSettingsRepository: BackupSettingsRepository + private val _uiState = MutableStateFlow(UiState(option = backupSettingsRepository.findOption(optionType))) + val uiState = _uiState.asStateFlow() - @Inject - lateinit var backupManager: BackupManager + private val _effect = EffectChannelFlow() + val effect: Flow = _effect.flow - private val _option = MutableLiveData() - val option: LiveData = _option - - private val _switchChecked = MutableLiveData() - val switchChecked: LiveData = _switchChecked - - private val _inProgress = MutableLiveData(false) - val inProgress: LiveData = _inProgress - - private val _openFolderSelection = SingleLiveEvent() - val openFolderSelection: LiveData = _openFolderSelection - - val lastSuccessDate = MutableLiveData() - - init { - component.inject(this) - } val title: Int - get() = when (option.value!!.type) { - BackupOptions.Google -> R.string.back_up_wallet_google_title - BackupOptions.Local -> R.string.back_up_wallet_local_file_title - BackupOptions.Dropbox -> R.string.back_up_wallet_dropbox_backup_title + get() = when (optionType) { + BackupOptionType.Google -> R.string.back_up_wallet_google_title + BackupOptionType.Local -> R.string.back_up_wallet_local_file_title + BackupOptionType.Dropbox -> R.string.back_up_wallet_dropbox_backup_title } - fun setup(option: BackupOptions) { - _option.value = backupSettingsRepository.getOptionList.first { it.type == option } - _switchChecked.value = _option.value!!.isEnable - onBackupStateChanged(EventBus.backupState.publishSubject.value?.backupsStates?.get(option)) + fun setup() { + onBackupStateChanged(EventBus.backupState.publishSubject.value?.backupsStates?.get(optionType)) // todo why update? } fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { viewModelScope.launch(Dispatchers.IO) { - val currentOption = _option.value!!.type try { - if (backupManager.onSetupActivityResult(requestCode, resultCode, data)) { - backupSettingsRepository.getOptionDto(currentOption)?.copy(isEnable = true)?.let { backupSettingsRepository.updateOption(it) } + if (backupManager.onSetupActivityResult(optionType, requestCode, resultCode, data)) { // todo move to backupManager + backupSettingsRepository.getOptionDto(optionType).copy(isEnabled = true)?.let { backupSettingsRepository.updateOption(it) } EventBus.backupState.publishSubject .filter { - it.backupsStates[currentOption] is BackupState.BackupUpToDate || it.backupsStates[currentOption] is BackupState.BackupFailed - }.take(1) + it.backupsStates[optionType] is BackupState.BackupUpToDate || it.backupsStates[optionType] is BackupState.BackupFailed + } + .take(1) .subscribe { - (it.backupsStates[currentOption] as? BackupState.BackupFailed)?.let { turnOff(currentOption, it.backupException) } + (it.backupsStates[optionType] as? BackupState.BackupFailed)?.let { turnOff(optionType, it.backupException) } }.addTo(compositeDisposable) backupManager.backupNow() } } catch (e: Throwable) { - turnOff(currentOption, e) + turnOff(optionType, e) } } } - private fun turnOff(backupOption: BackupOptions, throwable: Throwable?) { - logger.i("Backup storage setup failed: $throwable") + private fun turnOff(backupOption: BackupOptionType, throwable: Throwable?) { + logger.e("Backup storage setup failed: $throwable") backupManager.turnOff(backupOption) - _inProgress.postValue(false) - _switchChecked.postValue(false) + _uiState.update { it.stopLoading().switchOff() } showBackupStorageSetupFailedDialog(throwable) } fun onBackupSwitchChecked(isChecked: Boolean) { - _inProgress.postValue(true) - _switchChecked.postValue(isChecked) + _uiState.update { it.startLoading().switch(isChecked) } + backupSettingsViewModel.onOptionSelected(optionType) if (isChecked) { - _openFolderSelection.postValue(Unit) + viewModelScope.launch { + _effect.send(Effect.SetupStorage(backupManager, optionType)) + } } else { tryToTurnOffBackup() } @@ -113,7 +103,7 @@ class BackupOptionViewModel : CommonViewModel() { val onAcceptAction = { viewModelScope.launch(Dispatchers.IO) { try { - backupManager.turnOff(_option.value!!.type) + backupManager.turnOff(optionType) dismissDialog.postValue(Unit) } catch (exception: Exception) { logger.i(exception.toString()) @@ -122,8 +112,7 @@ class BackupOptionViewModel : CommonViewModel() { } val onDismissAction = { - _inProgress.postValue(false) - _switchChecked.postValue(true) + _uiState.update { it.stopLoading().switchOn() } } val args = ModularDialogArgs( @@ -153,47 +142,39 @@ class BackupOptionViewModel : CommonViewModel() { else -> resourceManager.getString(R.string.back_up_wallet_storage_setup_error_desc) } modularDialog.postValue(ErrorDialogArgs(errorTitle, errorDescription) { - _switchChecked.postValue(false) - _inProgress.postValue(false) + _uiState.update { it.stopLoading().switchOff() } }.getModular(resourceManager)) } fun onBackupStateChanged(backupState: BackupState?) { updateLastSuccessfulBackupDate(null) when (backupState) { - BackupState.BackupDisabled -> handleDisabledState() - BackupState.BackupInProgress -> handleInProgressState() - BackupState.BackupUpToDate -> handleUpToDateState() + is BackupState.BackupDisabled -> handleDisabledState() + is BackupState.BackupInProgress -> handleInProgressState() + is BackupState.BackupUpToDate -> handleUpToDateState() is BackupState.BackupFailed -> handleFailedState() else -> Unit } } private fun handleFailedState() { - if (_inProgress.value!!) { - _switchChecked.postValue(false) - } else { - _switchChecked.postValue(true) - } - _inProgress.postValue(false) + val shouldCheck = uiState.value.loading // todo why it depends on the loading state? + _uiState.update { it.stopLoading().switch(shouldCheck) } showBackupStorageSetupFailedDialog() } private fun handleUpToDateState() { - val currentState = backupSettingsRepository.getOptionDto(_option.value!!.type) - _inProgress.postValue(false) - _switchChecked.postValue(true) - updateLastSuccessfulBackupDate(currentState?.lastSuccessDate?.date) + val currentState = backupSettingsRepository.getOptionDto(optionType) + _uiState.update { it.stopLoading().switchOn() } + updateLastSuccessfulBackupDate(currentState.lastSuccessDate?.date) } private fun handleInProgressState() { - _inProgress.postValue(true) - _switchChecked.postValue(true) + _uiState.update { it.startLoading().switchOn() } } private fun handleDisabledState() { - _inProgress.postValue(false) - _switchChecked.postValue(false) + _uiState.update { it.stopLoading().switchOff() } } private fun updateLastSuccessfulBackupDate(lastSuccessfulBackupDate: DateTime?) { @@ -205,7 +186,8 @@ class BackupOptionViewModel : CommonViewModel() { BACKUP_TIME_FORMATTER.print(date) ) } ?: "" - lastSuccessDate.postValue(date) + + _uiState.update { it.copy(lastSuccessDate = date) } } companion object { diff --git a/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/data/BackupOptionDto.kt b/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/data/BackupOptionDto.kt index ddeefe2c6..829a27998 100644 --- a/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/data/BackupOptionDto.kt +++ b/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/data/BackupOptionDto.kt @@ -4,8 +4,8 @@ import com.tari.android.wallet.data.sharedPrefs.delegates.SerializableTime import java.io.Serializable data class BackupOptionDto( - val type: BackupOptions, - val isEnable: Boolean = false, + val type: BackupOptionType, + val isEnabled: Boolean = false, val lastSuccessDate: SerializableTime? = null, val lastFailureDate: SerializableTime? = null ) : Serializable \ No newline at end of file diff --git a/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/data/BackupOptions.kt b/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/data/BackupOptionType.kt similarity index 77% rename from app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/data/BackupOptions.kt rename to app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/data/BackupOptionType.kt index 5bde32dc9..dc4a67ea1 100644 --- a/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/data/BackupOptions.kt +++ b/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/data/BackupOptionType.kt @@ -1,6 +1,6 @@ package com.tari.android.wallet.ui.fragment.settings.backup.data -enum class BackupOptions { +enum class BackupOptionType { Google, Dropbox, Local diff --git a/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/data/BackupSettingsRepository.kt b/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/data/BackupSettingsRepository.kt index 91c5ae971..2f84b9092 100644 --- a/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/data/BackupSettingsRepository.kt +++ b/app/src/main/java/com/tari/android/wallet/ui/fragment/settings/backup/data/BackupSettingsRepository.kt @@ -17,59 +17,95 @@ import javax.inject.Singleton @Singleton class BackupSettingsRepository @Inject constructor( - private val context: Context, - private val sharedPrefs: SharedPreferences, + context: Context, + sharedPrefs: SharedPreferences, networkRepository: NetworkRepository -) : - CommonRepository(networkRepository) { - - var localFileOption: BackupOptionDto? by SharedPrefGsonDelegate(sharedPrefs, this, formatKey(Keys.localFileOptionsKey), BackupOptionDto::class.java) - - var googleDriveOption: BackupOptionDto? by SharedPrefGsonDelegate(sharedPrefs, this, formatKey(Keys.googleDriveOptionKey), BackupOptionDto::class.java) - - var dropboxOption: BackupOptionDto? by SharedPrefGsonDelegate(sharedPrefs, this, formatKey(Keys.dropboxOptionsKey), BackupOptionDto::class.java) - - var dropboxCredential: DbxCredential? by SharedPrefGsonDelegate(sharedPrefs, this, formatKey(Keys.dropboxCredentialKey), DbxCredential::class.java) - - var backupPassword: String? by SharedPrefStringSecuredDelegate(context, sharedPrefs, this, formatKey(Keys.backupPassword)) - - var localBackupFolderURI: Uri? by SharedPrefGsonDelegate(sharedPrefs, this, formatKey(Keys.localBackupFolderURI), Uri::class.java) - - var restoredTxs: BackupUtxos? by SharedPrefGsonDelegate(sharedPrefs, this, formatKey(Keys.lastRestoredTxs), BackupUtxos::class.java, null) - - init { - localFileOption = localFileOption ?: BackupOptionDto(BackupOptions.Local) - googleDriveOption = googleDriveOption ?: BackupOptionDto(BackupOptions.Google) - } - - val getOptionList: List +) : CommonRepository(networkRepository) { + + var localFileOption: BackupOptionDto? by SharedPrefGsonDelegate( + prefs = sharedPrefs, + commonRepository = this, + name = formatKey(Keys.localFileOptionsKey), + type = BackupOptionDto::class.java, + defValue = BackupOptionDto(BackupOptionType.Local), + ) + + var googleDriveOption: BackupOptionDto? by SharedPrefGsonDelegate( + prefs = sharedPrefs, + commonRepository = this, + name = formatKey(Keys.googleDriveOptionKey), + type = BackupOptionDto::class.java, + defValue = BackupOptionDto(BackupOptionType.Google), + ) + + var dropboxOption: BackupOptionDto? by SharedPrefGsonDelegate( + prefs = sharedPrefs, + commonRepository = this, + name = formatKey(Keys.dropboxOptionsKey), + type = BackupOptionDto::class.java, + defValue = BackupOptionDto(BackupOptionType.Dropbox), + ) + + var dropboxCredential: DbxCredential? by SharedPrefGsonDelegate( + prefs = sharedPrefs, + commonRepository = this, + name = formatKey(Keys.dropboxCredentialKey), + type = DbxCredential::class.java, + ) + + var backupPassword: String? by SharedPrefStringSecuredDelegate( + context = context, + prefs = sharedPrefs, + commonRepository = this, + name = formatKey(Keys.backupPassword), + ) + + var localBackupFolderURI: Uri? by SharedPrefGsonDelegate( + prefs = sharedPrefs, + commonRepository = this, + name = formatKey(Keys.localBackupFolderURI), + type = Uri::class.java, + ) + + var restoredTxs: BackupUtxos? by SharedPrefGsonDelegate( + prefs = sharedPrefs, + commonRepository = this, + name = formatKey(Keys.lastRestoredTxs), + type = BackupUtxos::class.java, + defValue = null, + ) + + val optionList: List get() = if (BuildConfig.FLAVOR == Constants.Build.privacyFlavor) { listOfNotNull(localFileOption).toList() } else { listOfNotNull(googleDriveOption, dropboxOption).toList() } + fun findOption(optionType: BackupOptionType): BackupOptionDto = optionList.find { it.type == optionType } + ?: error("Impossible backup option $optionType") + fun clear() { backupPassword = null localBackupFolderURI = null - localFileOption = BackupOptionDto(BackupOptions.Local) - googleDriveOption = BackupOptionDto(BackupOptions.Google) - dropboxOption = BackupOptionDto(BackupOptions.Dropbox) + localFileOption = BackupOptionDto(BackupOptionType.Local) + googleDriveOption = BackupOptionDto(BackupOptionType.Google) + dropboxOption = BackupOptionDto(BackupOptionType.Dropbox) } fun updateOption(option: BackupOptionDto) { when (option.type) { - BackupOptions.Google -> googleDriveOption = option - BackupOptions.Local -> localFileOption = option - BackupOptions.Dropbox -> dropboxOption = option + BackupOptionType.Google -> googleDriveOption = option + BackupOptionType.Local -> localFileOption = option + BackupOptionType.Dropbox -> dropboxOption = option } } - fun getOptionDto(type: BackupOptions): BackupOptionDto? = when (type) { - BackupOptions.Google -> googleDriveOption - BackupOptions.Local -> localFileOption - BackupOptions.Dropbox -> dropboxOption - } + fun getOptionDto(type: BackupOptionType): BackupOptionDto = when (type) { + BackupOptionType.Google -> googleDriveOption + BackupOptionType.Local -> localFileOption + BackupOptionType.Dropbox -> dropboxOption + } ?: error("The $type option") object Keys { const val googleDriveOptionKey = "tari_wallet_google_drive_backup_options" @@ -78,11 +114,6 @@ class BackupSettingsRepository @Inject constructor( const val dropboxCredentialKey = "tari_wallet_dropbox_credential_key" const val backupPassword = "tari_wallet_last_next_alarm_time" const val localBackupFolderURI = "tari_wallet_local_backup_folder_uri" - const val lastBackupDialogShownTime = "last_shown_time_key" const val lastRestoredTxs = "tari_wallet_restored_txs" } - - companion object { - const val delayTimeInMinutes = 5 - } } \ No newline at end of file