diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index ceb276614a..4a6aee0f6f 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -72,6 +72,7 @@ import im.vector.riotx.features.terms.ReviewTermsActivity import im.vector.riotx.features.ui.UiStateRepository import im.vector.riotx.features.widgets.WidgetActivity import im.vector.riotx.features.widgets.permissions.RoomWidgetPermissionBottomSheet +import im.vector.riotx.features.workers.signout.SignOutBottomSheetDialogFragment @Component( dependencies = [ @@ -152,6 +153,7 @@ interface ScreenComponent { fun inject(bottomSheet: RoomWidgetPermissionBottomSheet) fun inject(bottomSheet: RoomWidgetsBottomSheet) fun inject(bottomSheet: CallControlsBottomSheet) + fun inject(bottomSheet: SignOutBottomSheetDialogFragment) /* ========================================================================================== * Others diff --git a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt index badfdd96c1..2a3db0cf19 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt @@ -36,7 +36,7 @@ import im.vector.riotx.features.reactions.EmojiChooserViewModel import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel import im.vector.riotx.features.roomprofile.RoomProfileSharedActionViewModel import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel -import im.vector.riotx.features.workers.signout.SignOutViewModel +import im.vector.riotx.features.workers.signout.ServerBackupStatusViewModel @Module interface ViewModelModule { @@ -51,11 +51,6 @@ interface ViewModelModule { * Below are bindings for the androidx view models (which extend ViewModel). Will be converted to MvRx ViewModel in the future. */ - @Binds - @IntoMap - @ViewModelKey(SignOutViewModel::class) - fun bindSignOutViewModel(viewModel: SignOutViewModel): ViewModel - @Binds @IntoMap @ViewModelKey(EmojiChooserViewModel::class) diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt index 817575d91a..460c871288 100755 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt @@ -24,8 +24,10 @@ import android.view.ViewGroup import android.widget.AbsListView import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout +import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.edit import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView import androidx.transition.TransitionManager import butterknife.BindView import butterknife.ButterKnife @@ -58,22 +60,12 @@ class KeysBackupBanner @JvmOverloads constructor( var delegate: Delegate? = null private var state: State = State.Initial - private var scrollState = AbsListView.OnScrollListener.SCROLL_STATE_IDLE - set(value) { - field = value - - val pendingV = pendingVisibility - - if (pendingV != null) { - pendingVisibility = null - visibility = pendingV - } - } - - private var pendingVisibility: Int? = null - init { setupView() + PreferenceManager.getDefaultSharedPreferences(context).edit { + putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false) + putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, "") + } } /** @@ -91,7 +83,8 @@ class KeysBackupBanner @JvmOverloads constructor( state = newState hideAll() - + val parent = parent as ViewGroup + TransitionManager.beginDelayedTransition(parent) when (newState) { State.Initial -> renderInitial() State.Hidden -> renderHidden() @@ -102,22 +95,6 @@ class KeysBackupBanner @JvmOverloads constructor( } } - override fun setVisibility(visibility: Int) { - if (scrollState != AbsListView.OnScrollListener.SCROLL_STATE_IDLE) { - // Wait for scroll state to be idle - pendingVisibility = visibility - return - } - - if (visibility != getVisibility()) { - // Schedule animation - val parent = parent as ViewGroup - TransitionManager.beginDelayedTransition(parent) - } - - super.setVisibility(visibility) - } - override fun onClick(v: View?) { when (state) { is State.Setup -> { @@ -166,6 +143,8 @@ class KeysBackupBanner @JvmOverloads constructor( ButterKnife.bind(this) setOnClickListener(this) + textView1.setOnClickListener(this) + textView2.setOnClickListener(this) } private fun renderInitial() { @@ -218,10 +197,10 @@ class KeysBackupBanner @JvmOverloads constructor( } private fun renderBackingUp() { - // Do not render when backing up anymore - isVisible = false - - textView1.setText(R.string.keys_backup_banner_in_progress) + isVisible = true + textView1.setText(R.string.keys_backup_banner_setup_line1) + textView2.isVisible = true + textView2.setText(R.string.keys_backup_banner_in_progress) loading.isVisible = true } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt index 6a3fadbcb3..290a08bfad 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt @@ -32,6 +32,7 @@ import im.vector.matrix.android.api.session.securestorage.SsssKeySpec import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth import im.vector.matrix.android.internal.util.awaitCallback @@ -84,8 +85,10 @@ class BootstrapCrossSigningTask @Inject constructor( override suspend fun execute(params: Params): BootstrapResult { val crossSigningService = session.cryptoService().crossSigningService() + Timber.d("## BootstrapCrossSigningTask: initXSOnly:${params.initOnlyCrossSigning} Starting...") // Ensure cross-signing is initialized. Due to migration it is maybe not always correctly initialized if (!crossSigningService.isCrossSigningInitialized()) { + Timber.d("## BootstrapCrossSigningTask: Cross signing not enabled, so initialize") params.progressListener?.onProgress( WaitingViewData( stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing), @@ -104,8 +107,9 @@ class BootstrapCrossSigningTask @Inject constructor( return handleInitializeXSigningError(failure) } } else { - // not sure how this can happen?? + Timber.d("## BootstrapCrossSigningTask: Cross signing already setup, go to 4S setup") if (params.initOnlyCrossSigning) { + // not sure how this can happen?? return handleInitializeXSigningError(IllegalArgumentException("Cross signing already setup")) } } @@ -119,6 +123,8 @@ class BootstrapCrossSigningTask @Inject constructor( stringProvider.getString(R.string.bootstrap_crosssigning_progress_pbkdf2), isIndeterminate = true) ) + + Timber.d("## BootstrapCrossSigningTask: Creating 4S key with pass: ${params.passphrase != null}") try { keyInfo = awaitCallback { params.passphrase?.let { passphrase -> @@ -141,6 +147,7 @@ class BootstrapCrossSigningTask @Inject constructor( } } } catch (failure: Failure) { + Timber.e("## BootstrapCrossSigningTask: Creating 4S - Failed to generate key <${failure.localizedMessage}>") return BootstrapResult.FailedToCreateSSSSKey(failure) } @@ -149,19 +156,25 @@ class BootstrapCrossSigningTask @Inject constructor( stringProvider.getString(R.string.bootstrap_crosssigning_progress_default_key), isIndeterminate = true) ) + + Timber.d("## BootstrapCrossSigningTask: Creating 4S - Set default key") try { awaitCallback { ssssService.setDefaultKey(keyInfo.keyId, it) } } catch (failure: Failure) { // Maybe we could just ignore this error? + Timber.e("## BootstrapCrossSigningTask: Creating 4S - Set default key error <${failure.localizedMessage}>") return BootstrapResult.FailedToSetDefaultSSSSKey(failure) } + + Timber.d("## BootstrapCrossSigningTask: Creating 4S - gathering private keys") val xKeys = crossSigningService.getCrossSigningPrivateKeys() val mskPrivateKey = xKeys?.master ?: return BootstrapResult.MissingPrivateKey val sskPrivateKey = xKeys.selfSigned ?: return BootstrapResult.MissingPrivateKey val uskPrivateKey = xKeys.user ?: return BootstrapResult.MissingPrivateKey + Timber.d("## BootstrapCrossSigningTask: Creating 4S - gathering private keys success") try { params.progressListener?.onProgress( @@ -170,6 +183,7 @@ class BootstrapCrossSigningTask @Inject constructor( isIndeterminate = true ) ) + Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing MSK...") awaitCallback { ssssService.storeSecret( MASTER_KEY_SSSS_NAME, @@ -183,6 +197,7 @@ class BootstrapCrossSigningTask @Inject constructor( isIndeterminate = true ) ) + Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing USK...") awaitCallback { ssssService.storeSecret( USER_SIGNING_KEY_SSSS_NAME, @@ -196,6 +211,7 @@ class BootstrapCrossSigningTask @Inject constructor( stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_ssk), isIndeterminate = true ) ) + Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing SSK...") awaitCallback { ssssService.storeSecret( SELF_SIGNING_KEY_SSSS_NAME, @@ -204,6 +220,7 @@ class BootstrapCrossSigningTask @Inject constructor( ) } } catch (failure: Failure) { + Timber.e("## BootstrapCrossSigningTask: Creating 4S - Failed to store keys <${failure.localizedMessage}>") // Maybe we could just ignore this error? return BootstrapResult.FailedToStorePrivateKeyInSSSS(failure) } @@ -215,7 +232,14 @@ class BootstrapCrossSigningTask @Inject constructor( ) ) try { - if (session.cryptoService().keysBackupService().keysBackupVersion == null) { + Timber.d("## BootstrapCrossSigningTask: Creating 4S - Checking megolm backup") + + // First ensure that in sync + val serverVersion = awaitCallback { + session.cryptoService().keysBackupService().getCurrentVersion(it) + } + if (serverVersion == null) { + Timber.d("## BootstrapCrossSigningTask: Creating 4S - Create megolm backup") val creationInfo = awaitCallback { session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it) } @@ -223,6 +247,7 @@ class BootstrapCrossSigningTask @Inject constructor( session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it) } // Save it for gossiping + Timber.d("## BootstrapCrossSigningTask: Creating 4S - Save megolm backup key for gossiping") session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version) awaitCallback { @@ -239,6 +264,7 @@ class BootstrapCrossSigningTask @Inject constructor( Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup") } + Timber.d("## BootstrapCrossSigningTask: initXSOnly:${params.initOnlyCrossSigning} Finished") return BootstrapResult.Success(keyInfo) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index 8d5fc5f564..6991aaa493 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -46,7 +46,8 @@ import im.vector.riotx.features.popup.PopupAlertManager import im.vector.riotx.features.popup.VerificationVectorAlert import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler import im.vector.riotx.features.settings.VectorPreferences -import im.vector.riotx.features.workers.signout.SignOutViewModel +import im.vector.riotx.features.workers.signout.ServerBackupStatusViewModel +import im.vector.riotx.features.workers.signout.ServerBackupStatusViewState import im.vector.riotx.push.fcm.FcmHelper import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.activity_home.* @@ -60,13 +61,17 @@ data class HomeActivityArgs( val accountCreation: Boolean ) : Parcelable -class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory { +class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory, ServerBackupStatusViewModel.Factory { private lateinit var sharedActionViewModel: HomeSharedActionViewModel private val homeActivityViewModel: HomeActivityViewModel by viewModel() @Inject lateinit var viewModelFactory: HomeActivityViewModel.Factory + + private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel() + @Inject lateinit var serverBackupviewModelFactory: ServerBackupStatusViewModel.Factory + @Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler @Inject lateinit var pushManager: PushersManager @@ -92,6 +97,10 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet return unknownDeviceViewModelFactory.create(initialState) } + override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel { + return serverBackupviewModelFactory.create(initialState) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager, vectorPreferences.areNotificationEnabledForDevice()) @@ -230,7 +239,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet } // Force remote backup state update to update the banner if needed - viewModelProvider.get(SignOutViewModel::class.java).refreshRemoteStateIfNeeded() + serverBackupStatusViewModel.refreshRemoteStateIfNeeded() } override fun configure(toolbar: Toolbar) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt index c92c28079f..c736c0c1ca 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt @@ -21,13 +21,13 @@ import android.view.LayoutInflater import android.view.View import androidx.core.content.ContextCompat import androidx.core.view.forEachIndexed +import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.bottomnavigation.BottomNavigationItemView import com.google.android.material.bottomnavigation.BottomNavigationMenuView -import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo @@ -49,13 +49,10 @@ import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView import im.vector.riotx.features.popup.PopupAlertManager import im.vector.riotx.features.popup.VerificationVectorAlert import im.vector.riotx.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS -import im.vector.riotx.features.workers.signout.SignOutViewModel +import im.vector.riotx.features.workers.signout.BannerState +import im.vector.riotx.features.workers.signout.ServerBackupStatusViewModel +import im.vector.riotx.features.workers.signout.ServerBackupStatusViewState import kotlinx.android.synthetic.main.fragment_home_detail.* -import kotlinx.android.synthetic.main.fragment_home_detail.activeCallPiP -import kotlinx.android.synthetic.main.fragment_home_detail.activeCallPiPWrap -import kotlinx.android.synthetic.main.fragment_home_detail.activeCallView -import kotlinx.android.synthetic.main.fragment_home_detail.syncStateView -import kotlinx.android.synthetic.main.fragment_room_detail.* import timber.log.Timber import javax.inject.Inject @@ -65,15 +62,17 @@ private const val INDEX_ROOMS = 2 class HomeDetailFragment @Inject constructor( val homeDetailViewModelFactory: HomeDetailViewModel.Factory, + private val serverBackupStatusViewModelFactory: ServerBackupStatusViewModel.Factory, private val avatarRenderer: AvatarRenderer, private val alertManager: PopupAlertManager, private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager -) : VectorBaseFragment(), KeysBackupBanner.Delegate, ActiveCallView.Callback { +) : VectorBaseFragment(), KeysBackupBanner.Delegate, ActiveCallView.Callback, ServerBackupStatusViewModel.Factory { private val unreadCounterBadgeViews = arrayListOf() private val viewModel: HomeDetailViewModel by fragmentViewModel() private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel() + private val serverBackupStatusViewModel: ServerBackupStatusViewModel by activityViewModel() private lateinit var sharedActionViewModel: HomeSharedActionViewModel private lateinit var sharedCallActionViewModel: SharedActiveCallViewModel @@ -195,34 +194,15 @@ class HomeDetailFragment @Inject constructor( } private fun setupKeysBackupBanner() { - // Keys backup banner - // Use the SignOutViewModel, it observe the keys backup state and this is what we need here - val model = fragmentViewModelProvider.get(SignOutViewModel::class.java) - model.keysBackupState.observe(viewLifecycleOwner, Observer { keysBackupState -> - when (keysBackupState) { - null -> - homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false) - KeysBackupState.Disabled -> - homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(model.getNumberOfKeysToBackup()), false) - KeysBackupState.NotTrusted, - KeysBackupState.WrongBackUpVersion -> - // In this case, getCurrentBackupVersion() should not return "" - homeKeysBackupBanner.render(KeysBackupBanner.State.Recover(model.getCurrentBackupVersion()), false) - KeysBackupState.WillBackUp, - KeysBackupState.BackingUp -> - homeKeysBackupBanner.render(KeysBackupBanner.State.BackingUp, false) - KeysBackupState.ReadyToBackUp -> - if (model.canRestoreKeys()) { - homeKeysBackupBanner.render(KeysBackupBanner.State.Update(model.getCurrentBackupVersion()), false) - } else { - homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false) - } - else -> - homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false) + serverBackupStatusViewModel.subscribe(this) { + when (val banState = it.bannerState.invoke()) { + is BannerState.Setup -> homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(banState.numberOfKeys), false) + BannerState.BackingUp -> homeKeysBackupBanner.render(KeysBackupBanner.State.BackingUp, false) + null, + BannerState.Hidden -> homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false) } - }) - + }.disposeOnDestroyView() homeKeysBackupBanner.delegate = this } @@ -331,4 +311,8 @@ class HomeDetailFragment @Inject constructor( } } } + + override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel { + return serverBackupStatusViewModelFactory.create(initialState) + } } diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index 0b89ab8ec4..79ba5121fc 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -199,7 +199,15 @@ class DefaultNavigator @Inject constructor( } override fun openKeysBackupSetup(context: Context, showManualExport: Boolean) { - context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport)) + // if cross signing is enabled we should propose full 4S + sessionHolder.getSafeActiveSession()?.let { session -> + if (session.cryptoService().crossSigningService().canCrossSign() && context is VectorBaseActivity) { + BootstrapBottomSheet.show(context.supportFragmentManager, false) + } else { + context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport)) + } + } + } override fun openKeysBackupManager(context: Context) { diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/ServerBackupStatusViewModel.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/ServerBackupStatusViewModel.kt new file mode 100644 index 0000000000..04ece8e407 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/workers/signout/ServerBackupStatusViewModel.kt @@ -0,0 +1,176 @@ +/* + * Copyright 2019 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.riotx.features.workers.signout + +import androidx.lifecycle.MutableLiveData +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo +import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener +import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData +import im.vector.matrix.rx.rx +import im.vector.riotx.core.platform.EmptyAction +import im.vector.riotx.core.platform.EmptyViewEvents +import im.vector.riotx.core.platform.VectorViewModel +import io.reactivex.Observable +import io.reactivex.functions.Function4 +import io.reactivex.subjects.PublishSubject +import java.util.concurrent.TimeUnit + +data class ServerBackupStatusViewState( + val bannerState: Async = Uninitialized +) : MvRxState + +/** + * The state representing the view + * It can take one state at a time + */ +sealed class BannerState { + + object Hidden : BannerState() + + // Keys backup is not setup, numberOfKeys is the number of locally stored keys + data class Setup(val numberOfKeys: Int) : BannerState() + + // Keys are backing up + object BackingUp : BannerState() +} + +class ServerBackupStatusViewModel @AssistedInject constructor(@Assisted initialState: ServerBackupStatusViewState, + private val session: Session) + : VectorViewModel(initialState), KeysBackupStateListener { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: ServerBackupStatusViewState): ServerBackupStatusViewModel? { + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") + } + } + + // Keys exported manually + val keysExportedToFile = MutableLiveData() + val keysBackupState = MutableLiveData() + + private val keyBackupPublishSubject: PublishSubject = PublishSubject.create() + + init { + session.cryptoService().keysBackupService().addListener(this) + + keyBackupPublishSubject.onNext(session.cryptoService().keysBackupService().state) + keysBackupState.value = session.cryptoService().keysBackupService().state + session.rx().liveCrossSigningPrivateKeys() + Observable.combineLatest, Optional, KeysBackupState, Optional, BannerState>( + session.rx().liveAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME)), + session.rx().liveCrossSigningInfo(session.myUserId), + keyBackupPublishSubject, + session.rx().liveCrossSigningPrivateKeys(), + Function4 { _, crossSigningInfo, keyBackupState, pInfo -> + // first check if 4S is already setup + if (session.sharedSecretStorageService.isRecoverySetup()) { + // 4S is already setup sp we should not display anything + return@Function4 when (keyBackupState) { + KeysBackupState.BackingUp -> BannerState.BackingUp + else -> BannerState.Hidden + } + } + + // So recovery is not setup + // Check if cross signing is enabled and local secrets known + if (crossSigningInfo.getOrNull()?.isTrusted() == true + && pInfo.getOrNull()?.master != null + && pInfo.getOrNull()?.selfSigned != null + && pInfo.getOrNull()?.user != null + ) { + // So 4S is not setup and we have local secrets, + return@Function4 BannerState.Setup(numberOfKeys = getNumberOfKeysToBackup()) + } + + BannerState.Hidden + } + ) + .throttleLast(2000, TimeUnit.MILLISECONDS) // we don't want to flicker or catch transient states + .distinctUntilChanged() + .execute { async -> + copy( + bannerState = async + ) + } + } + + /** + * Safe way to get the current KeysBackup version + */ + fun getCurrentBackupVersion(): String { + return session.cryptoService().keysBackupService().currentBackupVersion ?: "" + } + + /** + * Safe way to get the number of keys to backup + */ + fun getNumberOfKeysToBackup(): Int { + return session.cryptoService().inboundGroupSessionsCount(false) + } + + /** + * Safe way to tell if there are more keys on the server + */ + fun canRestoreKeys(): Boolean { + return session.cryptoService().keysBackupService().canRestoreKeys() + } + + override fun onCleared() { + super.onCleared() + session.cryptoService().keysBackupService().removeListener(this) + } + + override fun onStateChange(newState: KeysBackupState) { + keyBackupPublishSubject.onNext(session.cryptoService().keysBackupService().state) + keysBackupState.value = newState + } + + fun refreshRemoteStateIfNeeded() { + if (keysBackupState.value == KeysBackupState.Disabled) { + session.cryptoService().keysBackupService().checkAndStartKeysBackup() + } + } + + override fun handle(action: EmptyAction) {} +} diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt index e1ef7bc07b..fa5c4119ed 100644 --- a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt @@ -31,16 +31,21 @@ import androidx.core.view.isVisible import androidx.lifecycle.Observer import androidx.transition.TransitionManager import butterknife.BindView +import com.airbnb.mvrx.fragmentViewModel import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.riotx.core.utils.toast +import im.vector.riotx.features.attachments.preview.AttachmentsPreviewViewModel import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity +import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsViewModel +import javax.inject.Inject -class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment() { +class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(), ServerBackupStatusViewModel.Factory { @BindView(R.id.bottom_sheet_signout_warning_text) lateinit var sheetTitle: TextView @@ -84,13 +89,23 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment() { isCancelable = true } - private lateinit var viewModel: SignOutViewModel + @Inject + lateinit var viewModelFactory: ServerBackupStatusViewModel.Factory + + override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel { + return viewModelFactory.create(initialState) + } + + private val viewModel: ServerBackupStatusViewModel by fragmentViewModel(ServerBackupStatusViewModel::class) + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - viewModel = fragmentViewModelProvider.get(SignOutViewModel::class.java) - setupClickableView.setOnClickListener { context?.let { context -> startActivityForResult(KeysBackupSetupActivity.intent(context, true), EXPORT_REQ) @@ -234,4 +249,5 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment() { } } } + } diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutViewModel.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutViewModel.kt deleted file mode 100644 index 2f26fdf377..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutViewModel.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2019 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.riotx.features.workers.signout - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import im.vector.matrix.android.api.session.Session -import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState -import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener -import javax.inject.Inject - -class SignOutViewModel @Inject constructor(private val session: Session) : ViewModel(), KeysBackupStateListener { - // Keys exported manually - var keysExportedToFile = MutableLiveData() - - var keysBackupState = MutableLiveData() - - init { - session.cryptoService().keysBackupService().addListener(this) - - keysBackupState.value = session.cryptoService().keysBackupService().state - } - - /** - * Safe way to get the current KeysBackup version - */ - fun getCurrentBackupVersion(): String { - return session.cryptoService().keysBackupService().currentBackupVersion ?: "" - } - - /** - * Safe way to get the number of keys to backup - */ - fun getNumberOfKeysToBackup(): Int { - return session.cryptoService().inboundGroupSessionsCount(false) - } - - /** - * Safe way to tell if there are more keys on the server - */ - fun canRestoreKeys(): Boolean { - return session.cryptoService().keysBackupService().canRestoreKeys() - } - - override fun onCleared() { - super.onCleared() - - session.cryptoService().keysBackupService().removeListener(this) - } - - override fun onStateChange(newState: KeysBackupState) { - keysBackupState.value = newState - } - - fun refreshRemoteStateIfNeeded() { - if (keysBackupState.value == KeysBackupState.Disabled) { - session.cryptoService().keysBackupService().checkAndStartKeysBackup() - } - } -} diff --git a/vector/src/main/res/drawable/ic_secure_backup.xml b/vector/src/main/res/drawable/ic_secure_backup.xml new file mode 100644 index 0000000000..899bb8d2ae --- /dev/null +++ b/vector/src/main/res/drawable/ic_secure_backup.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_home_detail.xml b/vector/src/main/res/layout/fragment_home_detail.xml index f90422dff9..aa7a76cf16 100644 --- a/vector/src/main/res/layout/fragment_home_detail.xml +++ b/vector/src/main/res/layout/fragment_home_detail.xml @@ -59,6 +59,8 @@ android:layout_height="wrap_content" android:background="?riotx_keys_backup_banner_accent_color" android:minHeight="67dp" + android:visibility="gone" + tools:visibility="visible" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/syncStateView" /> diff --git a/vector/src/main/res/layout/view_keys_backup_banner.xml b/vector/src/main/res/layout/view_keys_backup_banner.xml index 87c92cf8b4..4c3ec1da3f 100644 --- a/vector/src/main/res/layout/view_keys_backup_banner.xml +++ b/vector/src/main/res/layout/view_keys_backup_banner.xml @@ -10,11 +10,11 @@ New Key Backup A new secure message key backup has been detected.\n\nIf you didn’t set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings. It was me + - Never lose encrypted messages - Start using Key Backup + Secure Backup + Safeguard against losing access to encrypted messages & data Never lose encrypted messages Use Key Backup @@ -1503,7 +1504,7 @@ Why choose Riot.im? New secure message keys Manage in Key Backup - Backing up keys… + Backing up your keys. This may take several minutes… All keys backed up