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

@ -20,10 +20,10 @@ import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.edit
import androidx.core.view.isVisible
import im.vector.app.R
import im.vector.app.databinding.ViewKeysBackupBannerBinding
import im.vector.app.features.workers.signout.BannerState
import timber.log.Timber
/**
@ -37,16 +37,12 @@ class KeysBackupBanner @JvmOverloads constructor(
) : ConstraintLayout(context, attrs, defStyleAttr), View.OnClickListener {
var delegate: Delegate? = null
private var state: State = State.Initial
private var state: BannerState = BannerState.Initial
private lateinit var views: ViewKeysBackupBannerBinding
init {
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 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) {
Timber.v("State unchanged")
return
@ -66,48 +62,26 @@ class KeysBackupBanner @JvmOverloads constructor(
hideAll()
when (newState) {
State.Initial -> renderInitial()
State.Hidden -> renderHidden()
is State.Setup -> renderSetup(newState.numberOfKeys)
is State.Recover -> renderRecover(newState.version)
is State.Update -> renderUpdate(newState.version)
State.BackingUp -> renderBackingUp()
BannerState.Initial -> renderInitial()
BannerState.Hidden -> renderHidden()
is BannerState.Setup -> renderSetup(newState)
is BannerState.Recover -> renderRecover(newState)
is BannerState.Update -> renderUpdate(newState)
BannerState.BackingUp -> renderBackingUp()
}
}
override fun onClick(v: View?) {
when (state) {
is State.Setup -> delegate?.setupKeysBackup()
is State.Update,
is State.Recover -> delegate?.recoverKeysBackup()
is BannerState.Setup -> delegate?.setupKeysBackup()
is BannerState.Update,
is BannerState.Recover -> delegate?.recoverKeysBackup()
else -> Unit
}
}
private fun onCloseClicked() {
state.let {
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
}
}
}
delegate?.onCloseClicked()
// Force refresh
render(state, true)
}
@ -132,9 +106,8 @@ class KeysBackupBanner @JvmOverloads constructor(
isVisible = false
}
private fun renderSetup(nbOfKeys: Int) {
if (nbOfKeys == 0 ||
DefaultSharedPreferences.getInstance(context).getBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false)) {
private fun renderSetup(state: BannerState.Setup) {
if (state.numberOfKeys == 0 || state.doNotShowAgain) {
// Do not display the setup banner if there is no keys to backup, or if the user has already closed it
isVisible = false
} else {
@ -147,8 +120,8 @@ class KeysBackupBanner @JvmOverloads constructor(
}
}
private fun renderRecover(version: String) {
if (version == DefaultSharedPreferences.getInstance(context).getString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, null)) {
private fun renderRecover(state: BannerState.Recover) {
if (state.version == state.doNotShowForVersion) {
isVisible = false
} else {
isVisible = true
@ -160,8 +133,8 @@ class KeysBackupBanner @JvmOverloads constructor(
}
}
private fun renderUpdate(version: String) {
if (version == DefaultSharedPreferences.getInstance(context).getString(BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION, null)) {
private fun renderUpdate(state: BannerState.Update) {
if (state.version == state.doNotShowForVersion) {
isVisible = false
} else {
isVisible = true
@ -190,61 +163,12 @@ class KeysBackupBanner @JvmOverloads constructor(
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.
*/
interface Delegate {
fun onCloseClicked()
fun setupKeysBackup()
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)
}
}
}
}

@ -18,6 +18,7 @@ package im.vector.app.features.crypto.keysbackup.restore
import android.app.Activity
import android.content.Context
import android.content.Intent
import com.airbnb.mvrx.viewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
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.replaceFragment
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.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 javax.inject.Inject
@ -46,6 +48,7 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() {
override fun getTitleRes() = R.string.title_activity_keys_backup_restore
private lateinit var viewModel: KeysBackupRestoreSharedViewModel
private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel()
override fun onBackPressed() {
hideWaitingView()
@ -95,7 +98,8 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() {
}
KeysBackupRestoreSharedViewModel.NAVIGATE_TO_SUCCESS -> {
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)
}

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

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

@ -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
}

@ -16,6 +16,8 @@
package im.vector.app.features.workers.signout
import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MavericksState
@ -24,9 +26,9 @@ import com.airbnb.mvrx.Uninitialized
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.di.DefaultPreferences
import im.vector.app.core.di.MavericksAssistedViewModelFactory
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.VectorViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
@ -51,29 +53,55 @@ data class ServerBackupStatusViewState(
* The state representing the view.
* 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
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
object BackingUp : BannerState()
object BackingUp : BannerState
}
class ServerBackupStatusViewModel @AssistedInject constructor(
@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
interface Factory : MavericksAssistedViewModelFactory<ServerBackupStatusViewModel, ServerBackupStatusViewState> {
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
val keysExportedToFile = MutableLiveData<Boolean>()
@ -105,7 +133,10 @@ class ServerBackupStatusViewModel @AssistedInject constructor(
pInfo.getOrNull()?.allKnown().orFalse())
) {
// 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
}
@ -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
}
}
}
}