Set server backup banner

This commit is contained in:
Valere 2020-06-30 15:59:43 +02:00
parent f1e5129acb
commit a98b2ecce3
14 changed files with 312 additions and 168 deletions

View file

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

View file

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

View file

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

View file

@ -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<Unit> {
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<Unit> {
ssssService.storeSecret(
MASTER_KEY_SSSS_NAME,
@ -183,6 +197,7 @@ class BootstrapCrossSigningTask @Inject constructor(
isIndeterminate = true
)
)
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing USK...")
awaitCallback<Unit> {
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<Unit> {
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<KeysVersionResult?> {
session.cryptoService().keysBackupService().getCurrentVersion(it)
}
if (serverVersion == null) {
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Create megolm backup")
val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
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<Unit> {
@ -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)
}

View file

@ -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) {

View file

@ -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<UnreadCounterBadgeView>()
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)
}
}

View file

@ -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) {

View file

@ -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<BannerState> = 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<ServerBackupStatusViewState, EmptyAction, EmptyViewEvents>(initialState), KeysBackupStateListener {
@AssistedInject.Factory
interface Factory {
fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel
}
companion object : MvRxViewModelFactory<ServerBackupStatusViewModel, ServerBackupStatusViewState> {
@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<Boolean>()
val keysBackupState = MutableLiveData<KeysBackupState>()
private val keyBackupPublishSubject: PublishSubject<KeysBackupState> = 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<List<UserAccountData>, Optional<MXCrossSigningInfo>, KeysBackupState, Optional<PrivateKeysInfo>, 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) {}
}

View file

@ -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() {
}
}
}
}

View file

@ -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<Boolean>()
var keysBackupState = MutableLiveData<KeysBackupState>()
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()
}
}
}

View file

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group>
<clip-path
android:pathData="M1,2h21.5v5h-21.5zM1,17.7h21.5v5h-21.5z"/>
<path
android:pathData="M3.1663,12.4014C3.1663,7.6467 6.9953,3.8177 11.75,3.8177C13.0964,3.8177 14.4429,4.1544 15.6631,4.7435H14.9899C14.5691,4.7435 14.2325,5.0801 14.2325,5.5008C14.2325,5.9216 14.5691,6.2582 14.9899,6.2582H17.3041C17.809,6.2582 18.1877,5.8375 18.1877,5.3746V3.0604C18.1877,2.6396 17.8511,2.303 17.4303,2.303C17.0096,2.303 16.673,2.6396 16.673,3.0604V3.6074C16.6309,3.5653 16.5888,3.5653 16.5467,3.5232C15.074,2.7238 13.433,2.303 11.75,2.303C6.1958,2.303 1.6515,6.8473 1.6515,12.4014C1.6515,14.0845 2.0723,15.7676 2.8717,17.2403C2.998,17.4928 3.2504,17.619 3.545,17.619C3.6712,17.619 3.7974,17.5769 3.9236,17.5348C4.3023,17.3245 4.4286,16.8616 4.2182,16.525C3.5029,15.2627 3.1663,13.8321 3.1663,12.4014Z"
android:fillColor="#2E2F32"/>
<path
android:pathData="M20.6281,7.5626C20.4177,7.1839 19.9548,7.0577 19.6182,7.2681C19.2395,7.4785 19.1133,7.9413 19.3237,8.2779C19.9969,9.5402 20.3756,10.9288 20.3756,12.4015C20.3756,17.1562 16.5045,20.9852 11.7919,20.9852C10.4454,20.9852 9.099,20.6486 7.8787,20.0595H8.552C8.9727,20.0595 9.3094,19.7229 9.3094,19.3021C9.3094,18.8813 8.9727,18.5447 8.552,18.5447H6.2377C5.7328,18.5447 5.3541,18.9655 5.3541,19.4283V21.7426C5.3541,22.1633 5.6908,22.4999 6.1115,22.4999C6.5323,22.4999 6.8689,22.1633 6.8689,21.7426V21.1956C6.911,21.2376 6.9531,21.2376 6.9951,21.2797C8.4257,22.0792 10.0667,22.4999 11.7498,22.4999C17.304,22.4999 21.8483,17.9556 21.8483,12.4015C21.8483,10.7184 21.4275,9.0353 20.6281,7.5626Z"
android:fillColor="#2E2F32"/>
</group>
<path
android:pathData="M3,9C1.8954,9 1,9.8954 1,11V14C1,15.1046 1.8954,16 3,16H21C22.1046,16 23,15.1046 23,14V11C23,9.8954 22.1046,9 21,9H3ZM5.25,10.5C4.8358,10.5 4.5,10.8358 4.5,11.25C4.5,11.6642 4.8358,12 5.25,12H7.75C8.1642,12 8.5,11.6642 8.5,11.25C8.5,10.8358 8.1642,10.5 7.75,10.5H5.25ZM9.5,11.25C9.5,10.8358 9.8358,10.5 10.25,10.5H10.75C11.1642,10.5 11.5,10.8358 11.5,11.25C11.5,11.6642 11.1642,12 10.75,12H10.25C9.8358,12 9.5,11.6642 9.5,11.25ZM13.25,10.5C12.8358,10.5 12.5,10.8358 12.5,11.25C12.5,11.6642 12.8358,12 13.25,12H15.75C16.1642,12 16.5,11.6642 16.5,11.25C16.5,10.8358 16.1642,10.5 15.75,10.5H13.25ZM17.5,11.25C17.5,10.8358 17.8358,10.5 18.25,10.5H18.75C19.1642,10.5 19.5,10.8358 19.5,11.25C19.5,11.6642 19.1642,12 18.75,12H18.25C17.8358,12 17.5,11.6642 17.5,11.25ZM5.25,13C4.8358,13 4.5,13.3358 4.5,13.75C4.5,14.1642 4.8358,14.5 5.25,14.5H5.75C6.1642,14.5 6.5,14.1642 6.5,13.75C6.5,13.3358 6.1642,13 5.75,13H5.25ZM7.5,13.75C7.5,13.3358 7.8358,13 8.25,13H10.75C11.1642,13 11.5,13.3358 11.5,13.75C11.5,14.1642 11.1642,14.5 10.75,14.5H8.25C7.8358,14.5 7.5,14.1642 7.5,13.75ZM13.25,13C12.8358,13 12.5,13.3358 12.5,13.75C12.5,14.1642 12.8358,14.5 13.25,14.5H13.75C14.1642,14.5 14.5,14.1642 14.5,13.75C14.5,13.3358 14.1642,13 13.75,13H13.25Z"
android:fillColor="#2E2F32"
android:fillType="evenOdd"/>
</vector>

View file

@ -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" />

View file

@ -10,11 +10,11 @@
<ImageView
android:id="@+id/view_keys_backup_banner_picto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="19dp"
android:layout_marginLeft="19dp"
android:src="@drawable/key_small"
android:src="@drawable/ic_secure_backup"
android:tint="?riotx_text_primary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
@ -36,7 +36,7 @@
android:layout_marginLeft="27dp"
android:text="@string/keys_backup_banner_setup_line1"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
android:textSize="18sp"
app:layout_constraintBottom_toTopOf="@id/view_keys_backup_banner_text_2"
app:layout_constraintEnd_toStartOf="@id/view_keys_backup_banner_barrier"
app:layout_constraintStart_toEndOf="@id/view_keys_backup_banner_picto"
@ -49,8 +49,8 @@
android:layout_marginStart="27dp"
android:layout_marginLeft="27dp"
android:text="@string/keys_backup_banner_setup_line2"
android:textColor="?colorAccent"
android:textSize="15sp"
android:textColor="?riotx_text_secondary"
android:textSize="14sp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/view_keys_backup_banner_space2"
app:layout_constraintEnd_toStartOf="@id/view_keys_backup_banner_barrier"

View file

@ -1493,9 +1493,10 @@ Why choose Riot.im?
<string name="new_recovery_method_popup_title">New Key Backup</string>
<string name="new_recovery_method_popup_description">A new secure message key backup has been detected.\n\nIf you didnt 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.</string>
<string name="new_recovery_method_popup_was_me">It was me</string>
<!-- Keys backup banner -->
<string name="keys_backup_banner_setup_line1">Never lose encrypted messages</string>
<string name="keys_backup_banner_setup_line2">Start using Key Backup</string>
<string name="keys_backup_banner_setup_line1">Secure Backup</string>
<string name="keys_backup_banner_setup_line2">Safeguard against losing access to encrypted messages &amp; data </string>
<string name="keys_backup_banner_recover_line1">Never lose encrypted messages</string>
<string name="keys_backup_banner_recover_line2">Use Key Backup</string>
@ -1503,7 +1504,7 @@ Why choose Riot.im?
<string name="keys_backup_banner_update_line1">New secure message keys</string>
<string name="keys_backup_banner_update_line2">Manage in Key Backup</string>
<string name="keys_backup_banner_in_progress">Backing up keys…</string>
<string name="keys_backup_banner_in_progress">Backing up your keys. This may take several minutes…</string>
<!-- Keys backup info -->
<string name="keys_backup_info_keys_all_backup_up">All keys backed up</string>