Remove duplication between KeysBackupBanner.State and ServerBackupStatusViewModel.BannerState and move the some logic to the ViewModel

This commit is contained in:
Benoit Marty 2022-09-16 16:49:07 +02:00 committed by Benoit Marty
parent b4494ee8ea
commit c735ea5e3d
6 changed files with 156 additions and 116 deletions

View File

@ -20,10 +20,10 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.edit
import androidx.core.view.isVisible import androidx.core.view.isVisible
import im.vector.app.R import im.vector.app.R
import im.vector.app.databinding.ViewKeysBackupBannerBinding import im.vector.app.databinding.ViewKeysBackupBannerBinding
import im.vector.app.features.workers.signout.BannerState
import timber.log.Timber import timber.log.Timber
/** /**
@ -37,16 +37,12 @@ class KeysBackupBanner @JvmOverloads constructor(
) : ConstraintLayout(context, attrs, defStyleAttr), View.OnClickListener { ) : ConstraintLayout(context, attrs, defStyleAttr), View.OnClickListener {
var delegate: Delegate? = null var delegate: Delegate? = null
private var state: State = State.Initial private var state: BannerState = BannerState.Initial
private lateinit var views: ViewKeysBackupBannerBinding private lateinit var views: ViewKeysBackupBannerBinding
init { init {
setupView() setupView()
DefaultSharedPreferences.getInstance(context).edit {
putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false)
putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, "")
}
} }
/** /**
@ -55,7 +51,7 @@ class KeysBackupBanner @JvmOverloads constructor(
* @param newState the newState representing the view * @param newState the newState representing the view
* @param force true to force the rendering of the view * @param force true to force the rendering of the view
*/ */
fun render(newState: State, force: Boolean = false) { fun render(newState: BannerState, force: Boolean = false) {
if (newState == state && !force) { if (newState == state && !force) {
Timber.v("State unchanged") Timber.v("State unchanged")
return return
@ -66,48 +62,26 @@ class KeysBackupBanner @JvmOverloads constructor(
hideAll() hideAll()
when (newState) { when (newState) {
State.Initial -> renderInitial() BannerState.Initial -> renderInitial()
State.Hidden -> renderHidden() BannerState.Hidden -> renderHidden()
is State.Setup -> renderSetup(newState.numberOfKeys) is BannerState.Setup -> renderSetup(newState)
is State.Recover -> renderRecover(newState.version) is BannerState.Recover -> renderRecover(newState)
is State.Update -> renderUpdate(newState.version) is BannerState.Update -> renderUpdate(newState)
State.BackingUp -> renderBackingUp() BannerState.BackingUp -> renderBackingUp()
} }
} }
override fun onClick(v: View?) { override fun onClick(v: View?) {
when (state) { when (state) {
is State.Setup -> delegate?.setupKeysBackup() is BannerState.Setup -> delegate?.setupKeysBackup()
is State.Update, is BannerState.Update,
is State.Recover -> delegate?.recoverKeysBackup() is BannerState.Recover -> delegate?.recoverKeysBackup()
else -> Unit else -> Unit
} }
} }
private fun onCloseClicked() { private fun onCloseClicked() {
state.let { delegate?.onCloseClicked()
when (it) {
is State.Setup -> {
DefaultSharedPreferences.getInstance(context).edit {
putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, true)
}
}
is State.Recover -> {
DefaultSharedPreferences.getInstance(context).edit {
putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, it.version)
}
}
is State.Update -> {
DefaultSharedPreferences.getInstance(context).edit {
putString(BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION, it.version)
}
}
else -> {
// Should not happen, close button is not displayed in other cases
}
}
}
// Force refresh // Force refresh
render(state, true) render(state, true)
} }
@ -132,9 +106,8 @@ class KeysBackupBanner @JvmOverloads constructor(
isVisible = false isVisible = false
} }
private fun renderSetup(nbOfKeys: Int) { private fun renderSetup(state: BannerState.Setup) {
if (nbOfKeys == 0 || if (state.numberOfKeys == 0 || state.doNotShowAgain) {
DefaultSharedPreferences.getInstance(context).getBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false)) {
// Do not display the setup banner if there is no keys to backup, or if the user has already closed it // Do not display the setup banner if there is no keys to backup, or if the user has already closed it
isVisible = false isVisible = false
} else { } else {
@ -147,8 +120,8 @@ class KeysBackupBanner @JvmOverloads constructor(
} }
} }
private fun renderRecover(version: String) { private fun renderRecover(state: BannerState.Recover) {
if (version == DefaultSharedPreferences.getInstance(context).getString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, null)) { if (state.version == state.doNotShowForVersion) {
isVisible = false isVisible = false
} else { } else {
isVisible = true isVisible = true
@ -160,8 +133,8 @@ class KeysBackupBanner @JvmOverloads constructor(
} }
} }
private fun renderUpdate(version: String) { private fun renderUpdate(state: BannerState.Update) {
if (version == DefaultSharedPreferences.getInstance(context).getString(BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION, null)) { if (state.version == state.doNotShowForVersion) {
isVisible = false isVisible = false
} else { } else {
isVisible = true isVisible = true
@ -190,61 +163,12 @@ class KeysBackupBanner @JvmOverloads constructor(
views.viewKeysBackupBannerLoading.isVisible = false views.viewKeysBackupBannerLoading.isVisible = false
} }
/**
* The state representing the view.
* It can take one state at a time.
*/
sealed class State {
// Not yet rendered
object Initial : State()
// View will be Gone
object Hidden : State()
// Keys backup is not setup, numberOfKeys is the number of locally stored keys
data class Setup(val numberOfKeys: Int) : State()
// Keys backup can be recovered, with version from the server
data class Recover(val version: String) : State()
// Keys backup can be updated
data class Update(val version: String) : State()
// Keys are backing up
object BackingUp : State()
}
/** /**
* An interface to delegate some actions to another object. * An interface to delegate some actions to another object.
*/ */
interface Delegate { interface Delegate {
fun onCloseClicked()
fun setupKeysBackup() fun setupKeysBackup()
fun recoverKeysBackup() fun recoverKeysBackup()
} }
companion object {
/**
* Preference key for setup. Value is a boolean.
*/
private const val BANNER_SETUP_DO_NOT_SHOW_AGAIN = "BANNER_SETUP_DO_NOT_SHOW_AGAIN"
/**
* Preference key for recover. Value is a backup version (String).
*/
private const val BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION = "BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION"
/**
* Preference key for update. Value is a backup version (String).
*/
private const val BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION = "BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION"
/**
* Inform the banner that a Recover has been done for this version, so do not show the Recover banner for this version.
*/
fun onRecoverDoneForVersion(context: Context, version: String) {
DefaultSharedPreferences.getInstance(context).edit {
putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, version)
}
}
}
} }

View File

@ -18,6 +18,7 @@ package im.vector.app.features.crypto.keysbackup.restore
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.airbnb.mvrx.viewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R import im.vector.app.R
@ -27,8 +28,9 @@ import im.vector.app.core.extensions.observeEvent
import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.extensions.replaceFragment
import im.vector.app.core.platform.SimpleFragmentActivity import im.vector.app.core.platform.SimpleFragmentActivity
import im.vector.app.core.ui.views.KeysBackupBanner
import im.vector.app.features.crypto.quads.SharedSecureStorageActivity import im.vector.app.features.crypto.quads.SharedSecureStorageActivity
import im.vector.app.features.workers.signout.ServerBackupStatusAction
import im.vector.app.features.workers.signout.ServerBackupStatusViewModel
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import javax.inject.Inject import javax.inject.Inject
@ -46,6 +48,7 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() {
override fun getTitleRes() = R.string.title_activity_keys_backup_restore override fun getTitleRes() = R.string.title_activity_keys_backup_restore
private lateinit var viewModel: KeysBackupRestoreSharedViewModel private lateinit var viewModel: KeysBackupRestoreSharedViewModel
private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel()
override fun onBackPressed() { override fun onBackPressed() {
hideWaitingView() hideWaitingView()
@ -95,7 +98,8 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() {
} }
KeysBackupRestoreSharedViewModel.NAVIGATE_TO_SUCCESS -> { KeysBackupRestoreSharedViewModel.NAVIGATE_TO_SUCCESS -> {
viewModel.keyVersionResult.value?.version?.let { viewModel.keyVersionResult.value?.version?.let {
KeysBackupBanner.onRecoverDoneForVersion(this, it) // Inform the banner that a Recover has been done for this version, so do not show the Recover banner for this version.
serverBackupStatusViewModel.handle(ServerBackupStatusAction.OnRecoverDoneForVersion(it))
} }
replaceFragment(views.container, KeysBackupRestoreSuccessFragment::class.java, allowStateLoss = true) replaceFragment(views.container, KeysBackupRestoreSuccessFragment::class.java, allowStateLoss = true)
} }

View File

@ -56,6 +56,7 @@ import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS import im.vector.app.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
import im.vector.app.features.workers.signout.BannerState import im.vector.app.features.workers.signout.BannerState
import im.vector.app.features.workers.signout.ServerBackupStatusAction
import im.vector.app.features.workers.signout.ServerBackupStatusViewModel import im.vector.app.features.workers.signout.ServerBackupStatusViewModel
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
@ -289,13 +290,15 @@ class HomeDetailFragment :
} }
private fun setupKeysBackupBanner() { private fun setupKeysBackupBanner() {
serverBackupStatusViewModel.handle(ServerBackupStatusAction.OnBannerDisplayed)
serverBackupStatusViewModel serverBackupStatusViewModel
.onEach { .onEach {
when (val banState = it.bannerState.invoke()) { when (val banState = it.bannerState.invoke()) {
is BannerState.Setup -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(banState.numberOfKeys), false) is BannerState.Setup,
BannerState.BackingUp -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.BackingUp, false) BannerState.BackingUp,
null, BannerState.Hidden -> views.homeKeysBackupBanner.render(banState, false)
BannerState.Hidden -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false) null -> views.homeKeysBackupBanner.render(BannerState.Hidden, false)
else -> Unit /* No op? */
} }
} }
views.homeKeysBackupBanner.delegate = this views.homeKeysBackupBanner.delegate = this
@ -402,6 +405,10 @@ class HomeDetailFragment :
* KeysBackupBanner Listener * KeysBackupBanner Listener
* ========================================================================================== */ * ========================================================================================== */
override fun onCloseClicked() {
serverBackupStatusViewModel.handle(ServerBackupStatusAction.OnBannerClosed)
}
override fun setupKeysBackup() { override fun setupKeysBackup() {
navigator.openKeysBackupSetup(requireActivity(), false) navigator.openKeysBackupSetup(requireActivity(), false)
} }

View File

@ -57,6 +57,7 @@ import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS import im.vector.app.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS
import im.vector.app.features.spaces.SpaceListBottomSheet import im.vector.app.features.spaces.SpaceListBottomSheet
import im.vector.app.features.workers.signout.BannerState import im.vector.app.features.workers.signout.BannerState
import im.vector.app.features.workers.signout.ServerBackupStatusAction
import im.vector.app.features.workers.signout.ServerBackupStatusViewModel import im.vector.app.features.workers.signout.ServerBackupStatusViewModel
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -300,13 +301,15 @@ class NewHomeDetailFragment :
} }
private fun setupKeysBackupBanner() { private fun setupKeysBackupBanner() {
serverBackupStatusViewModel.handle(ServerBackupStatusAction.OnBannerDisplayed)
serverBackupStatusViewModel serverBackupStatusViewModel
.onEach { .onEach {
when (val banState = it.bannerState.invoke()) { when (val banState = it.bannerState.invoke()) {
is BannerState.Setup -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(banState.numberOfKeys), false) is BannerState.Setup,
BannerState.BackingUp -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.BackingUp, false) BannerState.BackingUp,
null, BannerState.Hidden -> views.homeKeysBackupBanner.render(banState, false)
BannerState.Hidden -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false) null -> views.homeKeysBackupBanner.render(BannerState.Hidden, false)
else -> Unit /* No op? */
} }
} }
views.homeKeysBackupBanner.delegate = this views.homeKeysBackupBanner.delegate = this
@ -348,6 +351,10 @@ class NewHomeDetailFragment :
* KeysBackupBanner Listener * KeysBackupBanner Listener
* ========================================================================================== */ * ========================================================================================== */
override fun onCloseClicked() {
serverBackupStatusViewModel.handle(ServerBackupStatusAction.OnBannerClosed)
}
override fun setupKeysBackup() { override fun setupKeysBackup() {
navigator.openKeysBackupSetup(requireActivity(), false) navigator.openKeysBackupSetup(requireActivity(), false)
} }

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.workers.signout
import im.vector.app.core.platform.VectorViewModelAction
sealed interface ServerBackupStatusAction : VectorViewModelAction {
data class OnRecoverDoneForVersion(val version: String) : ServerBackupStatusAction
object OnBannerDisplayed : ServerBackupStatusAction
object OnBannerClosed : ServerBackupStatusAction
}

View File

@ -16,6 +16,8 @@
package im.vector.app.features.workers.signout package im.vector.app.features.workers.signout
import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksState
@ -24,9 +26,9 @@ import com.airbnb.mvrx.Uninitialized
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.core.di.DefaultPreferences
import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.EmptyAction
import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -51,29 +53,55 @@ data class ServerBackupStatusViewState(
* The state representing the view. * The state representing the view.
* It can take one state at a time. * It can take one state at a time.
*/ */
sealed class BannerState { sealed interface BannerState {
// Not yet rendered
object Initial : BannerState
object Hidden : BannerState() // View will be Gone
object Hidden : BannerState
// Keys backup is not setup, numberOfKeys is the number of locally stored keys // Keys backup is not setup, numberOfKeys is the number of locally stored keys
data class Setup(val numberOfKeys: Int) : BannerState() data class Setup(val numberOfKeys: Int, val doNotShowAgain: Boolean) : BannerState
// Keys backup can be recovered, with version from the server
data class Recover(val version: String, val doNotShowForVersion: String) : BannerState
// Keys backup can be updated
data class Update(val version: String, val doNotShowForVersion: String) : BannerState
// Keys are backing up // Keys are backing up
object BackingUp : BannerState() object BackingUp : BannerState
} }
class ServerBackupStatusViewModel @AssistedInject constructor( class ServerBackupStatusViewModel @AssistedInject constructor(
@Assisted initialState: ServerBackupStatusViewState, @Assisted initialState: ServerBackupStatusViewState,
private val session: Session private val session: Session,
@DefaultPreferences
private val sharedPreferences: SharedPreferences,
) : ) :
VectorViewModel<ServerBackupStatusViewState, EmptyAction, EmptyViewEvents>(initialState), KeysBackupStateListener { VectorViewModel<ServerBackupStatusViewState, ServerBackupStatusAction, EmptyViewEvents>(initialState), KeysBackupStateListener {
@AssistedFactory @AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<ServerBackupStatusViewModel, ServerBackupStatusViewState> { interface Factory : MavericksAssistedViewModelFactory<ServerBackupStatusViewModel, ServerBackupStatusViewState> {
override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel
} }
companion object : MavericksViewModelFactory<ServerBackupStatusViewModel, ServerBackupStatusViewState> by hiltMavericksViewModelFactory() companion object : MavericksViewModelFactory<ServerBackupStatusViewModel, ServerBackupStatusViewState> by hiltMavericksViewModelFactory() {
/**
* Preference key for setup. Value is a boolean.
*/
private const val BANNER_SETUP_DO_NOT_SHOW_AGAIN = "BANNER_SETUP_DO_NOT_SHOW_AGAIN"
/**
* Preference key for recover. Value is a backup version (String).
*/
private const val BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION = "BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION"
/**
* Preference key for update. Value is a backup version (String).
*/
private const val BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION = "BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION"
}
// Keys exported manually // Keys exported manually
val keysExportedToFile = MutableLiveData<Boolean>() val keysExportedToFile = MutableLiveData<Boolean>()
@ -105,7 +133,10 @@ class ServerBackupStatusViewModel @AssistedInject constructor(
pInfo.getOrNull()?.allKnown().orFalse()) pInfo.getOrNull()?.allKnown().orFalse())
) { ) {
// So 4S is not setup and we have local secrets, // So 4S is not setup and we have local secrets,
return@combine BannerState.Setup(numberOfKeys = getNumberOfKeysToBackup()) return@combine BannerState.Setup(
numberOfKeys = getNumberOfKeysToBackup(),
doNotShowAgain = sharedPreferences.getBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false)
)
} }
BannerState.Hidden BannerState.Hidden
} }
@ -161,5 +192,47 @@ class ServerBackupStatusViewModel @AssistedInject constructor(
} }
} }
override fun handle(action: EmptyAction) {} override fun handle(action: ServerBackupStatusAction) {
when (action) {
is ServerBackupStatusAction.OnRecoverDoneForVersion -> handleOnRecoverDoneForVersion(action)
ServerBackupStatusAction.OnBannerDisplayed -> handleOnBannerDisplayed()
ServerBackupStatusAction.OnBannerClosed -> handleOnBannerClosed()
}
}
private fun handleOnRecoverDoneForVersion(action: ServerBackupStatusAction.OnRecoverDoneForVersion) {
sharedPreferences.edit {
putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, action.version)
}
}
private fun handleOnBannerDisplayed() {
sharedPreferences.edit {
putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false)
putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, "")
}
}
private fun handleOnBannerClosed() = withState { state ->
when (val bannerState = state.bannerState()) {
is BannerState.Setup -> {
sharedPreferences.edit {
putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, true)
}
}
is BannerState.Recover -> {
sharedPreferences.edit {
putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, bannerState.version)
}
}
is BannerState.Update -> {
sharedPreferences.edit {
putString(BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION, bannerState.version)
}
}
else -> {
// Should not happen, close button is not displayed in other cases
}
}
}
} }