Skip to content

Commit

Permalink
Refactor backup related code
Browse files Browse the repository at this point in the history
  • Loading branch information
igordanilcenko committed Jan 31, 2024
1 parent 13b867a commit c632b4f
Show file tree
Hide file tree
Showing 27 changed files with 459 additions and 296 deletions.
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -117,7 +117,7 @@ class StagedWalletSecurityManager : CommonViewModel() {

private fun openStage1() {
dismissDialog.postValue(Unit)
tariNavigator?.let {
tariNavigator.let {
it.toAllSettings()
it.toBackupSettings(false)
it.toWalletBackupWithRecoveryPhrase()
Expand All @@ -136,7 +136,7 @@ class StagedWalletSecurityManager : CommonViewModel() {

private fun openStage1B() {
dismissDialog.postValue(Unit)
tariNavigator?.let {
tariNavigator.let {
it.toAllSettings()
it.toBackupSettings(true)
}
Expand All @@ -154,7 +154,7 @@ class StagedWalletSecurityManager : CommonViewModel() {

private fun openStage2() {
dismissDialog.postValue(Unit)
tariNavigator?.let {
tariNavigator.let {
it.toAllSettings()
it.toBackupSettings(false)
it.toChangePassword()
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Effect : Any> {
private val channel: Channel<Effect> = Channel(Channel.CONFLATED)
val flow: Flow<Effect> = channel.receiveAsFlow()

suspend fun send(effect: Effect) {
channel.send(effect)
}
}
Original file line number Diff line number Diff line change
@@ -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() } }
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand All @@ -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<Event.App.AppBackgrounded>(this) { trigger.onNext(Unit) }
EventBus.subscribe<Event.App.AppForegrounded>(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
}

Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<BackupOptions, BackupState>) {
data class BackupsState(val backupsStates: Map<BackupOptionType, BackupState>) {

val backupsState: BackupState
get() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
fun Fragment.drawable(@DrawableRes id: Int): Drawable? = requireContext().drawable(id)

inline fun <reified T : Fragment> T.withArgs(vararg pairs: Pair<String, Any?>): T {
arguments = bundleOf(*pairs)
return this
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -111,7 +112,7 @@ sealed class Navigation {
}

sealed class ChooseRestoreOptionNavigation : Navigation() {
object ToEnterRestorePassword : ChooseRestoreOptionNavigation()
data class ToEnterRestorePassword(val optionType: BackupOptionType) : ChooseRestoreOptionNavigation()

object ToRestoreWithRecoveryPhrase : ChooseRestoreOptionNavigation()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -165,7 +166,6 @@ class TariNavigator @Inject constructor(val prefs: SharedPrefsRepository, val ta
}

Navigation.ChatNavigation.ToAddChat -> addFragment(AddChatFragment())
else -> Unit
}
}

Expand All @@ -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())

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<FragmentChooseRestoreOptionBinding, ChooseRestoreOptionViewModel>() {

Expand Down Expand Up @@ -75,45 +78,50 @@ class ChooseRestoreOptionFragment : CommonFragment<FragmentChooseRestoreOptionBi
}

private fun setupUI() = with(ui) {
restoreWithRecoveryPhraseCtaView.setOnClickListener { viewModel.navigation.postValue(Navigation.ChooseRestoreOptionNavigation.ToRestoreWithRecoveryPhrase) }
restoreWithRecoveryPhraseCtaView.setOnClickListener { viewModel.navigateToRecoveryPhrase() }
}

private fun startRecovery(options: BackupOptions) {
viewModel.startRestore(options)
viewModel.backupManager.setupStorage(options, this)
}

private fun observeUI() = with(viewModel) {
observe(state) { processState(it) }
private fun observeUI() {
viewLifecycleOwner.launchAndRepeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.effect.collect { effect ->
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<BackupOptionDto>) {
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 }
}

Loading

0 comments on commit c632b4f

Please sign in to comment.