Signout to setup 4S

This commit is contained in:
Valere 2020-07-08 09:59:13 +02:00
parent a98b2ecce3
commit 332f227bc1
24 changed files with 684 additions and 342 deletions

View File

@ -61,6 +61,8 @@ interface CrossSigningService {
fun canCrossSign(): Boolean
fun allPrivateKeysKnown(): Boolean
fun trustUser(otherUserId: String,
callback: MatrixCallback<Unit>)

View File

@ -507,6 +507,13 @@ internal class DefaultCrossSigningService @Inject constructor(
&& cryptoStore.getCrossSigningPrivateKeys()?.user != null
}
override fun allPrivateKeysKnown(): Boolean {
return checkSelfTrust().isVerified()
&& cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null
&& cryptoStore.getCrossSigningPrivateKeys()?.user != null
&& cryptoStore.getCrossSigningPrivateKeys()?.master != null
}
override fun trustUser(otherUserId: String, callback: MatrixCallback<Unit>) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
Timber.d("## CrossSigning - Mark user $userId as trusted ")

View File

@ -36,7 +36,6 @@ 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.ServerBackupStatusViewModel
@Module
interface ViewModelModule {

View File

@ -16,9 +16,16 @@
package im.vector.riotx.core.extensions
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Parcelable
import androidx.fragment.app.Fragment
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.toast
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
fun VectorBaseFragment.addFragment(frameId: Int, fragment: Fragment) {
parentFragmentManager.commitTransactionNow { add(frameId, fragment) }
@ -89,3 +96,29 @@ fun Fragment.getAllChildFragments(): List<Fragment> {
// Define a missing constant
const val POP_BACK_STACK_EXCLUSIVE = 0
fun Fragment.queryExportKeys(userId: String, requestCode: Int) {
// We need WRITE_EXTERNAL permission
// if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES,
// this,
// PERMISSION_REQUEST_CODE_EXPORT_KEYS,
// R.string.permissions_rationale_msg_keys_backup_export)) {
// WRITE permissions are not needed
val timestamp = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).let {
it.format(Date())
}
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "text/plain"
intent.putExtra(
Intent.EXTRA_TITLE,
"riot-megolm-export-$userId-$timestamp.txt"
)
try {
startActivityForResult(Intent.createChooser(intent, getString(R.string.keys_backup_setup_step1_manual_export)), requestCode)
} catch (activityNotFoundException: ActivityNotFoundException) {
activity?.toast(R.string.error_no_external_application_found)
}
// }
}

View File

@ -65,3 +65,12 @@ fun Session.hasUnsavedKeys(): Boolean {
return cryptoService().inboundGroupSessionsCount(false) > 0
&& cryptoService().keysBackupService().state != KeysBackupState.ReadyToBackUp
}
fun Session.cannotLogoutSafely(): Boolean {
// has some encrypted chat
return hasUnsavedKeys()
// has local cross signing keys
|| (cryptoService().crossSigningService().allPrivateKeysKnown()
// That are not backed up
&& !sharedSecretStorageService.isRecoverySetup())
}

View File

@ -21,13 +21,10 @@ import androidx.preference.PreferenceManager
import android.util.AttributeSet
import android.view.View
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

View File

@ -17,37 +17,34 @@
package im.vector.riotx.features.crypto.keys
import android.content.Context
import android.os.Environment
import android.net.Uri
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.util.awaitCallback
import im.vector.riotx.core.files.addEntryToDownloadManager
import im.vector.riotx.core.files.writeToFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class KeysExporter(private val session: Session) {
/**
* Export keys and return the file path with the callback
*/
fun export(context: Context, password: String, callback: MatrixCallback<String>) {
fun export(context: Context, password: String, uri: Uri, callback: MatrixCallback<Boolean>) {
GlobalScope.launch(Dispatchers.Main) {
runCatching {
val data = awaitCallback<ByteArray> { session.cryptoService().exportRoomKeys(password, it) }
withContext(Dispatchers.IO) {
val parentDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
val file = File(parentDir, "riotx-keys-" + System.currentTimeMillis() + ".txt")
writeToFile(data, file)
addEntryToDownloadManager(context, file, "text/plain")
file.absolutePath
val data = awaitCallback<ByteArray> { session.cryptoService().exportRoomKeys(password, it) }
val os = context.contentResolver?.openOutputStream(uri)
if (os == null) {
false
} else {
os.write(data)
os.flush()
true
}
}
}.foldToCallback(callback)
}

View File

@ -15,6 +15,8 @@
*/
package im.vector.riotx.features.crypto.keysbackup.setup
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import androidx.appcompat.app.AlertDialog
@ -132,36 +134,16 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
this,
PERMISSION_REQUEST_CODE_EXPORT_KEYS,
R.string.permissions_rationale_msg_keys_backup_export)) {
ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
override fun onPassphrase(passphrase: String) {
showWaitingView()
try {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_TITLE, "riot-megolm-export-${session.myUserId}-${System.currentTimeMillis()}.txt")
KeysExporter(session)
.export(this@KeysBackupSetupActivity,
passphrase,
object : MatrixCallback<String> {
override fun onSuccess(data: String) {
hideWaitingView()
AlertDialog.Builder(this@KeysBackupSetupActivity)
.setMessage(getString(R.string.encryption_export_saved_as, data))
.setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ ->
val resultIntent = Intent()
resultIntent.putExtra(MANUAL_EXPORT, true)
setResult(RESULT_OK, resultIntent)
finish()
}
.show()
}
override fun onFailure(failure: Throwable) {
toast(failure.localizedMessage ?: getString(R.string.unexpected_error))
hideWaitingView()
}
})
}
})
startActivityForResult(Intent.createChooser(intent, getString(R.string.keys_backup_setup_step1_manual_export)), REQUEST_CODE_SAVE_MEGOLM_EXPORT)
} catch (activityNotFoundException: ActivityNotFoundException) {
toast(R.string.error_no_external_application_found)
}
}
}
@ -173,6 +155,47 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_SAVE_MEGOLM_EXPORT) {
val uri = data?.data
if (resultCode == Activity.RESULT_OK && uri != null) {
ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
override fun onPassphrase(passphrase: String) {
showWaitingView()
KeysExporter(session)
.export(this@KeysBackupSetupActivity,
passphrase,
uri,
object : MatrixCallback<Boolean> {
override fun onSuccess(data: Boolean) {
if (data) {
toast(getString(R.string.encryption_exported_successfully))
Intent().apply {
putExtra(MANUAL_EXPORT, true)
}.let {
setResult(Activity.RESULT_OK, it)
finish()
}
}
hideWaitingView()
}
override fun onFailure(failure: Throwable) {
toast(failure.localizedMessage ?: getString(R.string.unexpected_error))
hideWaitingView()
}
})
}
})
} else {
toast(getString(R.string.unexpected_error))
hideWaitingView()
}
}
super.onActivityResult(requestCode, resultCode, data)
}
override fun onBackPressed() {
if (viewModel.shouldPromptOnBack) {
if (waitingView?.isVisible == true) {
@ -205,6 +228,7 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
const val KEYS_VERSION = "KEYS_VERSION"
const val MANUAL_EXPORT = "MANUAL_EXPORT"
const val EXTRA_SHOW_MANUAL_EXPORT = "SHOW_MANUAL_EXPORT"
const val REQUEST_CODE_SAVE_MEGOLM_EXPORT = 101
fun intent(context: Context, showManualExport: Boolean): Intent {
val intent = Intent(context, KeysBackupSetupActivity::class.java)

View File

@ -15,13 +15,13 @@
*/
package im.vector.riotx.features.crypto.keysbackup.setup
import android.os.AsyncTask
import android.os.Bundle
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.ImageView
import androidx.lifecycle.Observer
import androidx.lifecycle.viewModelScope
import androidx.transition.TransitionManager
import butterknife.BindView
import butterknife.OnClick
@ -33,6 +33,8 @@ import im.vector.riotx.core.extensions.showPassword
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.ui.views.PasswordStrengthBar
import im.vector.riotx.features.settings.VectorLocale
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject
class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment() {
@ -117,9 +119,9 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment()
if (newValue.isEmpty()) {
viewModel.passwordStrength.value = null
} else {
AsyncTask.execute {
viewModel.viewModelScope.launch(Dispatchers.IO) {
val strength = zxcvbn.measure(newValue)
activity?.runOnUiThread {
launch(Dispatchers.Main) {
viewModel.passwordStrength.value = strength
}
}

View File

@ -168,7 +168,6 @@ class BootstrapCrossSigningTask @Inject constructor(
return BootstrapResult.FailedToSetDefaultSSSSKey(failure)
}
Timber.d("## BootstrapCrossSigningTask: Creating 4S - gathering private keys")
val xKeys = crossSigningService.getCrossSigningPrivateKeys()
val mskPrivateKey = xKeys?.master ?: return BootstrapResult.MissingPrivateKey

View File

@ -406,7 +406,10 @@ class BootstrapSharedViewModel @AssistedInject constructor(
setState {
copy(
recoveryKeyCreationInfo = bootstrapResult.keyInfo,
step = BootstrapStep.SaveRecoveryKey(false)
step = BootstrapStep.SaveRecoveryKey(
// If a passphrase was used, saving key is optional
state.passphrase != null
)
)
}
}

View File

@ -68,7 +68,6 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
private val homeActivityViewModel: HomeActivityViewModel by viewModel()
@Inject lateinit var viewModelFactory: HomeActivityViewModel.Factory
private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel()
@Inject lateinit var serverBackupviewModelFactory: ServerBackupStatusViewModel.Factory

View File

@ -21,7 +21,6 @@ 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
@ -194,7 +193,6 @@ class HomeDetailFragment @Inject constructor(
}
private fun setupKeysBackupBanner() {
serverBackupStatusViewModel.subscribe(this) {
when (val banState = it.bannerState.invoke()) {
is BannerState.Setup -> homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(banState.numberOfKeys), false)

View File

@ -207,7 +207,6 @@ class DefaultNavigator @Inject constructor(
context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport))
}
}
}
override fun openKeysBackupManager(context: Context) {

View File

@ -34,16 +34,16 @@ import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.dialogs.ExportKeysDialog
import im.vector.riotx.core.extensions.queryExportKeys
import im.vector.riotx.core.intent.ExternalIntentData
import im.vector.riotx.core.intent.analyseIntent
import im.vector.riotx.core.intent.getFilenameFromUri
import im.vector.riotx.core.platform.SimpleTextWatcher
import im.vector.riotx.core.preference.VectorPreference
import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_EXPORT_KEYS
import im.vector.riotx.core.utils.allGranted
import im.vector.riotx.core.utils.checkPermissions
import im.vector.riotx.core.utils.openFileSelection
import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.crypto.keys.KeysExporter
@ -52,7 +52,8 @@ import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActiv
import javax.inject.Inject
class VectorSettingsSecurityPrivacyFragment @Inject constructor(
private val vectorPreferences: VectorPreferences
private val vectorPreferences: VectorPreferences,
private val activeSessionHolder: ActiveSessionHolder
) : VectorSettingsBaseFragment() {
override var titleRes = R.string.settings_security_and_privacy
@ -119,38 +120,69 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
}
private fun refreshXSigningStatus() {
val xSigningIsEnableInAccount = session.cryptoService().crossSigningService().isCrossSigningInitialized()
val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified()
val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign()
val crossSigningKeys = session.cryptoService().crossSigningService().getMyCrossSigningKeys()
val xSigningIsEnableInAccount = crossSigningKeys != null
val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified()
val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign()
if (xSigningKeyCanSign) {
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_trusted)
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_complete)
} else if (xSigningKeysAreTrusted) {
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_custom)
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_trusted)
} else if (xSigningIsEnableInAccount) {
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_black)
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_not_trusted)
} else {
mCrossSigningStatePreference.setIcon(android.R.color.transparent)
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_disabled)
}
if (xSigningKeyCanSign) {
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_trusted)
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_complete)
} else if (xSigningKeysAreTrusted) {
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_custom)
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_trusted)
} else if (xSigningIsEnableInAccount) {
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_black)
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_not_trusted)
} else {
mCrossSigningStatePreference.setIcon(android.R.color.transparent)
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_disabled)
}
mCrossSigningStatePreference.isVisible = true
mCrossSigningStatePreference.isVisible = true
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
if (allGranted(grantResults)) {
if (requestCode == PERMISSION_REQUEST_CODE_EXPORT_KEYS) {
exportKeys()
queryExportKeys(activeSessionHolder.getSafeActiveSession()?.myUserId ?: "", REQUEST_CODE_SAVE_MEGOLM_EXPORT)
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE_SAVE_MEGOLM_EXPORT) {
val uri = data?.data
if (resultCode == Activity.RESULT_OK && uri != null) {
activity?.let { activity ->
ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener {
override fun onPassphrase(passphrase: String) {
displayLoadingView()
KeysExporter(session)
.export(requireContext(),
passphrase,
uri,
object : MatrixCallback<Boolean> {
override fun onSuccess(data: Boolean) {
if (data) {
requireActivity().toast(getString(R.string.encryption_exported_successfully))
} else {
requireActivity().toast(getString(R.string.unexpected_error))
}
hideLoadingView()
}
override fun onFailure(failure: Throwable) {
onCommonDone(failure.localizedMessage)
}
})
}
})
}
}
}
if (resultCode == Activity.RESULT_OK) {
when (requestCode) {
REQUEST_E2E_FILE_REQUEST_CODE -> importKeys(data)
@ -169,7 +201,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
}
exportPref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
exportKeys()
queryExportKeys(activeSessionHolder.getSafeActiveSession()?.myUserId ?: "", REQUEST_CODE_SAVE_MEGOLM_EXPORT)
true
}
@ -179,46 +211,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
}
}
/**
* Manage the e2e keys export.
*/
private fun exportKeys() {
// We need WRITE_EXTERNAL permission
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES,
this,
PERMISSION_REQUEST_CODE_EXPORT_KEYS,
R.string.permissions_rationale_msg_keys_backup_export)) {
activity?.let { activity ->
ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener {
override fun onPassphrase(passphrase: String) {
displayLoadingView()
KeysExporter(session)
.export(requireContext(),
passphrase,
object : MatrixCallback<String> {
override fun onSuccess(data: String) {
if (isAdded) {
hideLoadingView()
AlertDialog.Builder(activity)
.setMessage(getString(R.string.encryption_export_saved_as, data))
.setCancelable(false)
.setPositiveButton(R.string.ok, null)
.show()
}
}
override fun onFailure(failure: Throwable) {
onCommonDone(failure.localizedMessage)
}
})
}
})
}
}
}
/**
* Manage the e2e keys import.
*/
@ -515,6 +507,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
companion object {
private const val REQUEST_E2E_FILE_REQUEST_CODE = 123
private const val REQUEST_CODE_SAVE_MEGOLM_EXPORT = 124
private const val PUSHER_PREFERENCE_KEY_BASE = "PUSHER_PREFERENCE_KEY_BASE"
private const val DEVICES_PREFERENCE_KEY_BASE = "DEVICES_PREFERENCE_KEY_BASE"

View File

@ -94,9 +94,8 @@ class ServerBackupStatusViewModel @AssistedInject constructor(@Assisted initialS
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),
@ -126,13 +125,15 @@ class ServerBackupStatusViewModel @AssistedInject constructor(@Assisted initialS
BannerState.Hidden
}
)
.throttleLast(2000, TimeUnit.MILLISECONDS) // we don't want to flicker or catch transient states
.throttleLast(1000, TimeUnit.MILLISECONDS) // we don't want to flicker or catch transient states
.distinctUntilChanged()
.execute { async ->
copy(
bannerState = async
)
}
keyBackupPublishSubject.onNext(session.cryptoService().keysBackupService().state)
}
/**

View File

@ -28,24 +28,27 @@ import android.widget.ProgressBar
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.transition.TransitionManager
import butterknife.BindView
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import im.vector.matrix.android.api.MatrixCallback
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.dialogs.ExportKeysDialog
import im.vector.riotx.core.extensions.queryExportKeys
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 im.vector.riotx.features.crypto.recover.BootstrapBottomSheet
import timber.log.Timber
import javax.inject.Inject
class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(), ServerBackupStatusViewModel.Factory {
// TODO this needs to be refactored to current standard and remove legacy
class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(), SignoutCheckViewModel.Factory {
@BindView(R.id.bottom_sheet_signout_warning_text)
lateinit var sheetTitle: TextView
@ -53,14 +56,20 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(),
@BindView(R.id.bottom_sheet_signout_backingup_status_group)
lateinit var backingUpStatusGroup: ViewGroup
@BindView(R.id.keys_backup_setup)
lateinit var setupClickableView: View
@BindView(R.id.setupRecoveryButton)
lateinit var setupRecoveryButton: SignoutBottomSheetActionButton
@BindView(R.id.keys_backup_activate)
lateinit var activateClickableView: View
@BindView(R.id.setupMegolmBackupButton)
lateinit var setupMegolmBackupButton: SignoutBottomSheetActionButton
@BindView(R.id.keys_backup_dont_want)
lateinit var dontWantClickableView: View
@BindView(R.id.exportManuallyButton)
lateinit var exportManuallyButton: SignoutBottomSheetActionButton
@BindView(R.id.exitAnywayButton)
lateinit var exitAnywayButton: SignoutBottomSheetActionButton
@BindView(R.id.signOutButton)
lateinit var signOutButton: SignoutBottomSheetActionButton
@BindView(R.id.bottom_sheet_signout_icon_progress_bar)
lateinit var backupProgress: ProgressBar
@ -71,8 +80,8 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(),
@BindView(R.id.bottom_sheet_backup_status_text)
lateinit var backupStatusTex: TextView
@BindView(R.id.bottom_sheet_signout_button)
lateinit var signoutClickableView: View
@BindView(R.id.signoutExportingLoading)
lateinit var signoutExportingLoading: View
@BindView(R.id.root_layout)
lateinit var rootLayout: ViewGroup
@ -83,6 +92,7 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(),
fun newInstance() = SignOutBottomSheetDialogFragment()
private const val EXPORT_REQ = 0
private const val QUERY_EXPORT_KEYS = 1
}
init {
@ -90,65 +100,36 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(),
}
@Inject
lateinit var viewModelFactory: ServerBackupStatusViewModel.Factory
lateinit var viewModelFactory: SignoutCheckViewModel.Factory
override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel {
override fun create(initialState: SignoutCheckViewState): SignoutCheckViewModel {
return viewModelFactory.create(initialState)
}
private val viewModel: ServerBackupStatusViewModel by fragmentViewModel(ServerBackupStatusViewModel::class)
private val viewModel: SignoutCheckViewModel by fragmentViewModel(SignoutCheckViewModel::class)
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
override fun onResume() {
super.onResume()
viewModel.refreshRemoteStateIfNeeded()
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setupClickableView.setOnClickListener {
context?.let { context ->
startActivityForResult(KeysBackupSetupActivity.intent(context, true), EXPORT_REQ)
}
setupRecoveryButton.action = {
BootstrapBottomSheet.show(parentFragmentManager, false)
}
activateClickableView.setOnClickListener {
context?.let { context ->
startActivity(KeysBackupManageActivity.intent(context))
}
}
signoutClickableView.setOnClickListener {
this.onSignOut?.run()
}
dontWantClickableView.setOnClickListener { _ ->
exitAnywayButton.action = {
context?.let {
AlertDialog.Builder(it)
.setTitle(R.string.are_you_sure)
.setMessage(R.string.sign_out_bottom_sheet_will_lose_secure_messages)
.setPositiveButton(R.string.backup) { _, _ ->
when (viewModel.keysBackupState.value) {
KeysBackupState.NotTrusted -> {
context?.let { context ->
startActivity(KeysBackupManageActivity.intent(context))
}
}
KeysBackupState.Disabled -> {
context?.let { context ->
startActivityForResult(KeysBackupSetupActivity.intent(context, true), EXPORT_REQ)
}
}
KeysBackupState.BackingUp,
KeysBackupState.WillBackUp -> {
// keys are already backing up please wait
context?.toast(R.string.keys_backup_is_not_finished_please_wait)
}
else -> {
// nop
}
}
}
.setPositiveButton(R.string.backup, null)
.setNegativeButton(R.string.action_sign_out) { _, _ ->
onSignOut?.run()
}
@ -156,71 +137,143 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(),
}
}
viewModel.keysExportedToFile.observe(viewLifecycleOwner, Observer {
val hasExportedToFile = it ?: false
if (hasExportedToFile) {
// We can allow to sign out
sheetTitle.text = getString(R.string.action_sign_out_confirmation_simple)
signoutClickableView.isVisible = true
dontWantClickableView.isVisible = false
setupClickableView.isVisible = false
activateClickableView.isVisible = false
backingUpStatusGroup.isVisible = false
exportManuallyButton.action = {
withState(viewModel) { state ->
queryExportKeys(state.userId, QUERY_EXPORT_KEYS)
}
})
}
viewModel.keysBackupState.observe(viewLifecycleOwner, Observer {
if (viewModel.keysExportedToFile.value == true) {
// ignore this
return@Observer
}
TransitionManager.beginDelayedTransition(rootLayout)
setupMegolmBackupButton.action = {
startActivityForResult(KeysBackupSetupActivity.intent(requireContext(), true), EXPORT_REQ)
}
viewModel.observeViewEvents {
when (it) {
KeysBackupState.ReadyToBackUp -> {
signoutClickableView.isVisible = true
dontWantClickableView.isVisible = false
setupClickableView.isVisible = false
activateClickableView.isVisible = false
backingUpStatusGroup.isVisible = true
is SignoutCheckViewModel.ViewEvents.ExportKeys -> {
it.exporter
.export(requireContext(),
it.passphrase,
it.uri,
object : MatrixCallback<Boolean> {
override fun onSuccess(data: Boolean) {
if (data) {
viewModel.handle(SignoutCheckViewModel.Actions.KeySuccessfullyManuallyExported)
} else {
viewModel.handle(SignoutCheckViewModel.Actions.KeyExportFailed)
}
}
override fun onFailure(failure: Throwable) {
Timber.e("## Failed to export manually keys ${failure.localizedMessage}")
viewModel.handle(SignoutCheckViewModel.Actions.KeyExportFailed)
}
})
}
}
}
}
override fun invalidate() = withState(viewModel) { state ->
signoutExportingLoading.isVisible = false
if (state.crossSigningSetupAllKeysKnown && !state.backupIsSetup) {
sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_no_backup)
backingUpStatusGroup.isVisible = false
// we should show option to setup 4S
setupRecoveryButton.isVisible = true
setupMegolmBackupButton.isVisible = false
signOutButton.isVisible = false
// We let the option to ignore and quit
exportManuallyButton.isVisible = true
exitAnywayButton.isVisible = true
} else if (state.keysBackupState == KeysBackupState.Unknown || state.keysBackupState == KeysBackupState.Disabled) {
sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_no_backup)
backingUpStatusGroup.isVisible = false
// no key backup and cannot setup full 4S
// we propose to setup
// we should show option to setup 4S
setupRecoveryButton.isVisible = false
setupMegolmBackupButton.isVisible = true
signOutButton.isVisible = false
// We let the option to ignore and quit
exportManuallyButton.isVisible = true
exitAnywayButton.isVisible = true
} else {
// so keybackup is setup
// You should wait until all are uploaded
setupRecoveryButton.isVisible = false
when (state.keysBackupState) {
KeysBackupState.ReadyToBackUp -> {
sheetTitle.text = getString(R.string.action_sign_out_confirmation_simple)
// Ok all keys are backedUp
backingUpStatusGroup.isVisible = true
backupProgress.isVisible = false
backupCompleteImage.isVisible = true
backupStatusTex.text = getString(R.string.keys_backup_info_keys_all_backup_up)
sheetTitle.text = getString(R.string.action_sign_out_confirmation_simple)
hideViews(setupMegolmBackupButton, exportManuallyButton, exitAnywayButton)
// You can signout
signOutButton.isVisible = true
}
KeysBackupState.BackingUp,
KeysBackupState.WillBackUp -> {
backingUpStatusGroup.isVisible = true
sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_backing_up)
dontWantClickableView.isVisible = true
setupClickableView.isVisible = false
activateClickableView.isVisible = false
KeysBackupState.WillBackUp,
KeysBackupState.BackingUp -> {
sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_backing_up)
// save in progress
backingUpStatusGroup.isVisible = true
backupProgress.isVisible = true
backupCompleteImage.isVisible = false
backupStatusTex.text = getString(R.string.sign_out_bottom_sheet_backing_up_keys)
hideViews(setupMegolmBackupButton, setupMegolmBackupButton, signOutButton, exportManuallyButton)
exitAnywayButton.isVisible = true
}
KeysBackupState.NotTrusted -> {
backingUpStatusGroup.isVisible = false
dontWantClickableView.isVisible = true
setupClickableView.isVisible = false
activateClickableView.isVisible = true
sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_backup_not_active)
// It's not trusted and we know there are unsaved keys..
backingUpStatusGroup.isVisible = false
exportManuallyButton.isVisible = true
// option to enter pass/key
setupMegolmBackupButton.isVisible = true
exitAnywayButton.isVisible = true
}
else -> {
backingUpStatusGroup.isVisible = false
dontWantClickableView.isVisible = true
setupClickableView.isVisible = true
activateClickableView.isVisible = false
sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_no_backup)
// mmm.. strange state
exitAnywayButton.isVisible = true
}
}
}
// updateSignOutSection()
})
// final call if keys have been exported
when (state.hasBeenExportedToFile) {
is Loading -> {
signoutExportingLoading.isVisible = true
hideViews(setupRecoveryButton,
setupMegolmBackupButton,
exportManuallyButton,
backingUpStatusGroup,
signOutButton)
exitAnywayButton.isVisible = true
}
is Success -> {
if (state.hasBeenExportedToFile.invoke()) {
sheetTitle.text = getString(R.string.action_sign_out_confirmation_simple)
hideViews(setupRecoveryButton,
setupMegolmBackupButton,
exportManuallyButton,
backingUpStatusGroup,
exitAnywayButton)
signOutButton.isVisible = true
}
}
else -> {
}
}
super.invalidate()
}
override fun getLayoutResId() = R.layout.bottom_sheet_logout_and_backup
@ -243,11 +296,26 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(),
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
if (requestCode == EXPORT_REQ) {
val manualExportDone = data?.getBooleanExtra(KeysBackupSetupActivity.MANUAL_EXPORT, false)
viewModel.keysExportedToFile.value = manualExportDone
if (requestCode == QUERY_EXPORT_KEYS) {
val uri = data?.data
if (resultCode == Activity.RESULT_OK && uri != null) {
activity?.let { activity ->
ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener {
override fun onPassphrase(passphrase: String) {
viewModel.handle(SignoutCheckViewModel.Actions.ExportKeys(passphrase, uri))
}
})
}
}
} else if (requestCode == EXPORT_REQ) {
if (data?.getBooleanExtra(KeysBackupSetupActivity.MANUAL_EXPORT, false) == true) {
viewModel.handle(SignoutCheckViewModel.Actions.KeySuccessfullyManuallyExported)
}
}
}
}
private fun hideViews(vararg views: View) {
views.forEach { it.isVisible = false }
}
}

View File

@ -21,7 +21,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.FragmentActivity
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.extensions.hasUnsavedKeys
import im.vector.riotx.core.extensions.cannotLogoutSafely
import im.vector.riotx.core.extensions.vectorComponent
import im.vector.riotx.features.MainActivity
import im.vector.riotx.features.MainActivityArgs
@ -33,7 +33,7 @@ class SignOutUiWorker(private val activity: FragmentActivity) {
fun perform(context: Context) {
activeSessionHolder = context.vectorComponent().activeSessionHolder()
val session = activeSessionHolder.getActiveSession()
if (session.hasUnsavedKeys()) {
if (session.cannotLogoutSafely()) {
// The backup check on logout flow has to be displayed if there are keys in the store, and the keys backup state is not Ready
val signOutDialog = SignOutBottomSheetDialogFragment.newInstance()
signOutDialog.onSignOut = Runnable {

View File

@ -0,0 +1,95 @@
/*
* Copyright (c) 2020 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 android.content.Context
import android.content.res.ColorStateList
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.isVisible
import butterknife.BindView
import butterknife.ButterKnife
import im.vector.riotx.R
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.features.themes.ThemeUtils
class SignoutBottomSheetActionButton @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
@BindView(R.id.actionTitleText)
lateinit var actionTextView: TextView
@BindView(R.id.actionIconImageView)
lateinit var iconImageView: ImageView
@BindView(R.id.signedOutActionClickable)
lateinit var clickableZone: View
var action: (() -> Unit)? = null
var title: String? = null
set(value) {
field = value
actionTextView.setTextOrHide(value)
}
var leftIcon: Drawable? = null
set(value) {
field = value
if (value == null) {
iconImageView.isVisible = false
iconImageView.setImageDrawable(null)
} else {
iconImageView.isVisible = true
iconImageView.setImageDrawable(value)
}
}
var tint: Int? = null
set(value) {
field = value
iconImageView.imageTintList = value?.let { ColorStateList.valueOf(value) }
}
var textColor: Int? = null
set(value) {
field = value
textColor?.let { actionTextView.setTextColor(it) }
}
init {
inflate(context, R.layout.item_signout_action, this)
ButterKnife.bind(this)
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.SignoutBottomSheetActionButton, 0, 0)
title = typedArray.getString(R.styleable.SignoutBottomSheetActionButton_actionTitle) ?: ""
leftIcon = typedArray.getDrawable(R.styleable.SignoutBottomSheetActionButton_leftIcon)
tint = typedArray.getColor(R.styleable.SignoutBottomSheetActionButton_iconTint, ThemeUtils.getColor(context, android.R.attr.textColor))
textColor = typedArray.getColor(R.styleable.SignoutBottomSheetActionButton_textColor, ThemeUtils.getColor(context, android.R.attr.textColor))
typedArray.recycle()
clickableZone.setOnClickListener {
action?.invoke()
}
}
}

View File

@ -0,0 +1,148 @@
/*
* Copyright (c) 2020 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 android.net.Uri
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
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.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.rx.rx
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.platform.VectorViewModelAction
import im.vector.riotx.features.crypto.keys.KeysExporter
data class SignoutCheckViewState(
val userId: String = "",
val backupIsSetup: Boolean = false,
val crossSigningSetupAllKeysKnown: Boolean = false,
val keysBackupState: KeysBackupState = KeysBackupState.Unknown,
val hasBeenExportedToFile: Async<Boolean> = Uninitialized
) : MvRxState
class SignoutCheckViewModel @AssistedInject constructor(@Assisted initialState: SignoutCheckViewState,
private val session: Session)
: VectorViewModel<SignoutCheckViewState, SignoutCheckViewModel.Actions, SignoutCheckViewModel.ViewEvents>(initialState), KeysBackupStateListener {
sealed class Actions : VectorViewModelAction {
data class ExportKeys(val passphrase: String, val uri: Uri) : Actions()
object KeySuccessfullyManuallyExported : Actions()
object KeyExportFailed : Actions()
}
sealed class ViewEvents : VectorViewEvents {
data class ExportKeys(val exporter: KeysExporter, val passphrase: String, val uri: Uri) : ViewEvents()
}
@AssistedInject.Factory
interface Factory {
fun create(initialState: SignoutCheckViewState): SignoutCheckViewModel
}
companion object : MvRxViewModelFactory<SignoutCheckViewModel, SignoutCheckViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: SignoutCheckViewState): SignoutCheckViewModel? {
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")
}
}
init {
session.cryptoService().keysBackupService().addListener(this)
session.cryptoService().keysBackupService().checkAndStartKeysBackup()
val quad4SIsSetup = session.sharedSecretStorageService.isRecoverySetup()
val allKeysKnown = session.cryptoService().crossSigningService().allPrivateKeysKnown()
val backupState = session.cryptoService().keysBackupService().state
setState {
copy(
userId = session.myUserId,
crossSigningSetupAllKeysKnown = allKeysKnown,
backupIsSetup = quad4SIsSetup,
keysBackupState = backupState
)
}
session.rx().liveAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME))
.map {
session.sharedSecretStorageService.isRecoverySetup()
}
.distinctUntilChanged()
.execute {
copy(backupIsSetup = it.invoke() == true)
}
}
override fun onCleared() {
super.onCleared()
session.cryptoService().keysBackupService().removeListener(this)
}
override fun onStateChange(newState: KeysBackupState) {
setState {
copy(
keysBackupState = newState
)
}
}
fun refreshRemoteStateIfNeeded() = withState { state ->
if (state.keysBackupState == KeysBackupState.Disabled) {
session.cryptoService().keysBackupService().checkAndStartKeysBackup()
}
}
override fun handle(action: Actions) {
when (action) {
is Actions.ExportKeys -> {
setState {
copy(hasBeenExportedToFile = Loading())
}
_viewEvents.post(ViewEvents.ExportKeys(KeysExporter(session), action.passphrase, action.uri))
}
Actions.KeySuccessfullyManuallyExported -> {
setState {
copy(hasBeenExportedToFile = Success(true))
}
}
Actions.KeyExportFailed -> {
setState {
copy(hasBeenExportedToFile = Uninitialized)
}
}
}.exhaustive
}
}

View File

@ -70,137 +70,60 @@
</LinearLayout>
<LinearLayout
android:id="@+id/keys_backup_setup"
android:id="@+id/signoutExportingLoading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:foreground="?attr/selectableItemBackground"
android:minHeight="50dp"
android:orientation="horizontal"
android:paddingLeft="@dimen/layout_horizontal_margin"
android:paddingTop="8dp"
android:paddingRight="@dimen/layout_horizontal_margin"
android:paddingBottom="8dp">
android:layout_height="44dp"
android:gravity="center">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:scaleType="fitCenter"
android:src="@drawable/backup_keys"
android:tint="?riotx_text_primary" />
<TextView
<ProgressBar
style="?android:attr/progressBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/keys_backup_setup"
android:textColor="?riotx_text_secondary"
android:textSize="17sp" />
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:id="@+id/keys_backup_activate"
<im.vector.riotx.features.workers.signout.SignoutBottomSheetActionButton
android:id="@+id/setupRecoveryButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:foreground="?attr/selectableItemBackground"
android:minHeight="50dp"
android:orientation="horizontal"
android:paddingLeft="@dimen/layout_horizontal_margin"
android:paddingTop="8dp"
android:paddingRight="@dimen/layout_horizontal_margin"
android:paddingBottom="8dp"
android:visibility="gone"
tools:visibility="visible">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:scaleType="fitCenter"
android:src="@drawable/backup_keys"
android:tint="?riotx_text_primary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textColor="?riotx_text_secondary"
android:text="@string/keys_backup_activate"
android:textSize="17sp" />
</LinearLayout>
app:actionTitle="@string/secure_backup_setup"
app:iconTint="?riotx_text_primary"
app:leftIcon="@drawable/ic_secure_backup"
app:textColor="?riotx_text_secondary" />
<LinearLayout
android:id="@+id/keys_backup_dont_want"
<im.vector.riotx.features.workers.signout.SignoutBottomSheetActionButton
android:id="@+id/setupMegolmBackupButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:foreground="?attr/selectableItemBackground"
android:minHeight="50dp"
android:orientation="horizontal"
android:paddingLeft="@dimen/layout_horizontal_margin"
android:paddingTop="8dp"
android:paddingRight="@dimen/layout_horizontal_margin"
android:paddingBottom="8dp">
app:actionTitle="@string/keys_backup_setup"
app:iconTint="?riotx_text_primary"
app:leftIcon="@drawable/backup_keys"
app:textColor="?riotx_text_secondary" />
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_material_leave"
android:tint="@color/riotx_notice" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/sign_out_bottom_sheet_dont_want_secure_messages"
android:textColor="@color/riotx_notice"
android:textSize="17sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/bottom_sheet_signout_button"
<im.vector.riotx.features.workers.signout.SignoutBottomSheetActionButton
android:id="@+id/exportManuallyButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:foreground="?attr/selectableItemBackground"
android:minHeight="50dp"
android:orientation="horizontal"
android:paddingLeft="@dimen/layout_horizontal_margin"
android:paddingTop="8dp"
android:paddingRight="@dimen/layout_horizontal_margin"
android:paddingBottom="8dp"
android:visibility="gone"
tools:visibility="visible">
app:actionTitle="@string/keys_backup_setup_step1_manual_export"
app:iconTint="?riotx_text_primary"
app:leftIcon="@drawable/ic_download"
app:textColor="?riotx_text_secondary" />
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:src="@drawable/ic_material_exit_to_app"
android:tint="@color/riotx_notice" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/action_sign_out"
android:textColor="@color/riotx_notice"
android:textSize="17sp" />
</LinearLayout>
<im.vector.riotx.features.workers.signout.SignoutBottomSheetActionButton
android:id="@+id/exitAnywayButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:actionTitle="@string/sign_out_bottom_sheet_dont_want_secure_messages"
app:iconTint="@color/riotx_destructive_accent"
app:leftIcon="@drawable/ic_material_leave"
app:textColor="@color/riotx_destructive_accent" />
<im.vector.riotx.features.workers.signout.SignoutBottomSheetActionButton
android:id="@+id/signOutButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:actionTitle="@string/action_sign_out"
app:iconTint="@color/riotx_notice"
app:leftIcon="@drawable/ic_material_exit_to_app"
app:textColor="@color/riotx_notice" />
</LinearLayout>

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:id="@+id/signedOutActionClickable"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:foreground="?attr/selectableItemBackground"
android:minHeight="50dp"
android:orientation="horizontal"
android:paddingLeft="@dimen/layout_horizontal_margin"
android:paddingTop="8dp"
android:paddingRight="@dimen/layout_horizontal_margin"
android:paddingBottom="8dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<ImageView
android:id="@+id/actionIconImageView"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_secure_backup"
android:tint="?riotx_text_primary" />
<TextView
android:id="@+id/actionTitleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/secure_backup_setup"
android:textColor="?riotx_text_secondary"
android:textSize="17sp" />
</LinearLayout>

View File

@ -114,4 +114,10 @@
<attr name="forceStartPadding" format="boolean" />
</declare-styleable>
<declare-styleable name="SignoutBottomSheetActionButton">
<attr name="iconTint" format="color" />
<attr name="actionTitle"/>
<attr name="leftIcon" />
<attr name="textColor" format="color" />
</declare-styleable>
</resources>

View File

@ -1048,6 +1048,7 @@
<string name="encryption_export_export">Export</string>
<string name="encryption_export_notice">Please create a passphrase to encrypt the exported keys. You will need to enter the same passphrase to be able to import the keys.</string>
<string name="encryption_export_saved_as">The E2E room keys have been saved to \'%s\'.\n\nWarning: this file may be deleted if the application is uninstalled.</string>
<string name="encryption_exported_successfully">Keys successfully exported</string>
<string name="encryption_message_recovery">Encrypted Messages Recovery</string>
<string name="encryption_settings_manage_message_recovery_summary">Manage Key Backup</string>
@ -1506,6 +1507,9 @@ Why choose Riot.im?
<string name="keys_backup_banner_in_progress">Backing up your keys. This may take several minutes…</string>
<string name="secure_backup_setup">Set Up Secure Backup</string>
<!-- Keys backup info -->
<string name="keys_backup_info_keys_all_backup_up">All keys backed up</string>
<plurals name="keys_backup_info_keys_backing_up">