Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: refactor backup related code #1038

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 { [email protected](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
Loading