Merge pull request #1648 from vector-im/feature/server_recovery_banner

Feature/server recovery banner
This commit is contained in:
Benoit Marty 2020-07-10 15:53:58 +02:00 committed by GitHub
commit a8ad57a9b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 992 additions and 497 deletions

View file

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

View file

@ -507,6 +507,13 @@ internal class DefaultCrossSigningService @Inject constructor(
&& cryptoStore.getCrossSigningPrivateKeys()?.user != null && 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>) { override fun trustUser(otherUserId: String, callback: MatrixCallback<Unit>) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
Timber.d("## CrossSigning - Mark user $userId as trusted ") Timber.d("## CrossSigning - Mark user $userId as trusted ")

View file

@ -73,6 +73,7 @@ import im.vector.riotx.features.terms.ReviewTermsActivity
import im.vector.riotx.features.ui.UiStateRepository import im.vector.riotx.features.ui.UiStateRepository
import im.vector.riotx.features.widgets.WidgetActivity import im.vector.riotx.features.widgets.WidgetActivity
import im.vector.riotx.features.widgets.permissions.RoomWidgetPermissionBottomSheet import im.vector.riotx.features.widgets.permissions.RoomWidgetPermissionBottomSheet
import im.vector.riotx.features.workers.signout.SignOutBottomSheetDialogFragment
@Component( @Component(
dependencies = [ dependencies = [
@ -154,6 +155,7 @@ interface ScreenComponent {
fun inject(bottomSheet: RoomWidgetPermissionBottomSheet) fun inject(bottomSheet: RoomWidgetPermissionBottomSheet)
fun inject(bottomSheet: RoomWidgetsBottomSheet) fun inject(bottomSheet: RoomWidgetsBottomSheet)
fun inject(bottomSheet: CallControlsBottomSheet) fun inject(bottomSheet: CallControlsBottomSheet)
fun inject(bottomSheet: SignOutBottomSheetDialogFragment)
/* ========================================================================================== /* ==========================================================================================
* Others * Others

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.roomdirectory.RoomDirectorySharedActionViewModel
import im.vector.riotx.features.roomprofile.RoomProfileSharedActionViewModel import im.vector.riotx.features.roomprofile.RoomProfileSharedActionViewModel
import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel
import im.vector.riotx.features.workers.signout.SignOutViewModel
@Module @Module
interface ViewModelModule { interface ViewModelModule {
@ -51,11 +50,6 @@ interface ViewModelModule {
* Below are bindings for the androidx view models (which extend ViewModel). Will be converted to MvRx ViewModel in the future. * 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 @Binds
@IntoMap @IntoMap
@ViewModelKey(EmojiChooserViewModel::class) @ViewModelKey(EmojiChooserViewModel::class)

View file

@ -16,9 +16,16 @@
package im.vector.riotx.core.extensions package im.vector.riotx.core.extensions
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Parcelable import android.os.Parcelable
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment 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) { fun VectorBaseFragment.addFragment(frameId: Int, fragment: Fragment) {
parentFragmentManager.commitTransactionNow { add(frameId, fragment) } parentFragmentManager.commitTransactionNow { add(frameId, fragment) }
@ -89,3 +96,29 @@ fun Fragment.getAllChildFragments(): List<Fragment> {
// Define a missing constant // Define a missing constant
const val POP_BACK_STACK_EXCLUSIVE = 0 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 return cryptoService().inboundGroupSessionsCount(false) > 0
&& cryptoService().keysBackupService().state != KeysBackupState.ReadyToBackUp && 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

@ -17,15 +17,14 @@
package im.vector.riotx.core.ui.views package im.vector.riotx.core.ui.views
import android.content.Context import android.content.Context
import androidx.preference.PreferenceManager
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.AbsListView
import android.widget.TextView import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.preference.PreferenceManager
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
import butterknife.BindView import butterknife.BindView
import butterknife.ButterKnife import butterknife.ButterKnife
@ -58,22 +57,12 @@ class KeysBackupBanner @JvmOverloads constructor(
var delegate: Delegate? = null var delegate: Delegate? = null
private var state: State = State.Initial 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 { init {
setupView() setupView()
PreferenceManager.getDefaultSharedPreferences(context).edit {
putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false)
putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, "")
}
} }
/** /**
@ -91,7 +80,8 @@ class KeysBackupBanner @JvmOverloads constructor(
state = newState state = newState
hideAll() hideAll()
val parent = parent as ViewGroup
TransitionManager.beginDelayedTransition(parent)
when (newState) { when (newState) {
State.Initial -> renderInitial() State.Initial -> renderInitial()
State.Hidden -> renderHidden() State.Hidden -> renderHidden()
@ -102,22 +92,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?) { override fun onClick(v: View?) {
when (state) { when (state) {
is State.Setup -> { is State.Setup -> {
@ -166,6 +140,8 @@ class KeysBackupBanner @JvmOverloads constructor(
ButterKnife.bind(this) ButterKnife.bind(this)
setOnClickListener(this) setOnClickListener(this)
textView1.setOnClickListener(this)
textView2.setOnClickListener(this)
} }
private fun renderInitial() { private fun renderInitial() {
@ -184,9 +160,9 @@ class KeysBackupBanner @JvmOverloads constructor(
} else { } else {
isVisible = true isVisible = true
textView1.setText(R.string.keys_backup_banner_setup_line1) textView1.setText(R.string.secure_backup_banner_setup_line1)
textView2.isVisible = true textView2.isVisible = true
textView2.setText(R.string.keys_backup_banner_setup_line2) textView2.setText(R.string.secure_backup_banner_setup_line2)
close.isVisible = true close.isVisible = true
} }
} }
@ -218,10 +194,10 @@ class KeysBackupBanner @JvmOverloads constructor(
} }
private fun renderBackingUp() { private fun renderBackingUp() {
// Do not render when backing up anymore isVisible = true
isVisible = false textView1.setText(R.string.secure_backup_banner_setup_line1)
textView2.isVisible = true
textView1.setText(R.string.keys_backup_banner_in_progress) textView2.setText(R.string.keys_backup_banner_in_progress)
loading.isVisible = true loading.isVisible = true
} }

View file

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

View file

@ -15,6 +15,8 @@
*/ */
package im.vector.riotx.features.crypto.keysbackup.setup package im.vector.riotx.features.crypto.keysbackup.setup
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
@ -132,36 +134,22 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
this, this,
PERMISSION_REQUEST_CODE_EXPORT_KEYS, PERMISSION_REQUEST_CODE_EXPORT_KEYS,
R.string.permissions_rationale_msg_keys_backup_export)) { R.string.permissions_rationale_msg_keys_backup_export)) {
ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener { try {
override fun onPassphrase(passphrase: String) { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
showWaitingView() intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_TITLE, "riot-megolm-export-${session.myUserId}-${System.currentTimeMillis()}.txt")
KeysExporter(session) startActivityForResult(
.export(this@KeysBackupSetupActivity, Intent.createChooser(
passphrase, intent,
object : MatrixCallback<String> { getString(R.string.keys_backup_setup_step1_manual_export)
override fun onSuccess(data: String) { ),
hideWaitingView() REQUEST_CODE_SAVE_MEGOLM_EXPORT
)
AlertDialog.Builder(this@KeysBackupSetupActivity) } catch (activityNotFoundException: ActivityNotFoundException) {
.setMessage(getString(R.string.encryption_export_saved_as, data)) toast(R.string.error_no_external_application_found)
.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()
}
})
}
})
} }
} }
@ -173,6 +161,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() { override fun onBackPressed() {
if (viewModel.shouldPromptOnBack) { if (viewModel.shouldPromptOnBack) {
if (waitingView?.isVisible == true) { if (waitingView?.isVisible == true) {
@ -205,6 +234,7 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
const val KEYS_VERSION = "KEYS_VERSION" const val KEYS_VERSION = "KEYS_VERSION"
const val MANUAL_EXPORT = "MANUAL_EXPORT" const val MANUAL_EXPORT = "MANUAL_EXPORT"
const val EXTRA_SHOW_MANUAL_EXPORT = "SHOW_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 { fun intent(context: Context, showManualExport: Boolean): Intent {
val intent = Intent(context, KeysBackupSetupActivity::class.java) val intent = Intent(context, KeysBackupSetupActivity::class.java)

View file

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

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.crosssigning.toBase64NoPadding
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo 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.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.keysbackup.util.extractCurveKeyFromRecoveryKey
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.matrix.android.internal.util.awaitCallback import im.vector.matrix.android.internal.util.awaitCallback
@ -84,8 +85,10 @@ class BootstrapCrossSigningTask @Inject constructor(
override suspend fun execute(params: Params): BootstrapResult { override suspend fun execute(params: Params): BootstrapResult {
val crossSigningService = session.cryptoService().crossSigningService() 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 // Ensure cross-signing is initialized. Due to migration it is maybe not always correctly initialized
if (!crossSigningService.isCrossSigningInitialized()) { if (!crossSigningService.isCrossSigningInitialized()) {
Timber.d("## BootstrapCrossSigningTask: Cross signing not enabled, so initialize")
params.progressListener?.onProgress( params.progressListener?.onProgress(
WaitingViewData( WaitingViewData(
stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing), stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing),
@ -104,8 +107,9 @@ class BootstrapCrossSigningTask @Inject constructor(
return handleInitializeXSigningError(failure) return handleInitializeXSigningError(failure)
} }
} else { } else {
// not sure how this can happen?? Timber.d("## BootstrapCrossSigningTask: Cross signing already setup, go to 4S setup")
if (params.initOnlyCrossSigning) { if (params.initOnlyCrossSigning) {
// not sure how this can happen??
return handleInitializeXSigningError(IllegalArgumentException("Cross signing already setup")) return handleInitializeXSigningError(IllegalArgumentException("Cross signing already setup"))
} }
} }
@ -119,6 +123,8 @@ class BootstrapCrossSigningTask @Inject constructor(
stringProvider.getString(R.string.bootstrap_crosssigning_progress_pbkdf2), stringProvider.getString(R.string.bootstrap_crosssigning_progress_pbkdf2),
isIndeterminate = true) isIndeterminate = true)
) )
Timber.d("## BootstrapCrossSigningTask: Creating 4S key with pass: ${params.passphrase != null}")
try { try {
keyInfo = awaitCallback { keyInfo = awaitCallback {
params.passphrase?.let { passphrase -> params.passphrase?.let { passphrase ->
@ -141,6 +147,7 @@ class BootstrapCrossSigningTask @Inject constructor(
} }
} }
} catch (failure: Failure) { } catch (failure: Failure) {
Timber.e("## BootstrapCrossSigningTask: Creating 4S - Failed to generate key <${failure.localizedMessage}>")
return BootstrapResult.FailedToCreateSSSSKey(failure) return BootstrapResult.FailedToCreateSSSSKey(failure)
} }
@ -149,19 +156,24 @@ class BootstrapCrossSigningTask @Inject constructor(
stringProvider.getString(R.string.bootstrap_crosssigning_progress_default_key), stringProvider.getString(R.string.bootstrap_crosssigning_progress_default_key),
isIndeterminate = true) isIndeterminate = true)
) )
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Set default key")
try { try {
awaitCallback<Unit> { awaitCallback<Unit> {
ssssService.setDefaultKey(keyInfo.keyId, it) ssssService.setDefaultKey(keyInfo.keyId, it)
} }
} catch (failure: Failure) { } catch (failure: Failure) {
// Maybe we could just ignore this error? // Maybe we could just ignore this error?
Timber.e("## BootstrapCrossSigningTask: Creating 4S - Set default key error <${failure.localizedMessage}>")
return BootstrapResult.FailedToSetDefaultSSSSKey(failure) return BootstrapResult.FailedToSetDefaultSSSSKey(failure)
} }
Timber.d("## BootstrapCrossSigningTask: Creating 4S - gathering private keys")
val xKeys = crossSigningService.getCrossSigningPrivateKeys() val xKeys = crossSigningService.getCrossSigningPrivateKeys()
val mskPrivateKey = xKeys?.master ?: return BootstrapResult.MissingPrivateKey val mskPrivateKey = xKeys?.master ?: return BootstrapResult.MissingPrivateKey
val sskPrivateKey = xKeys.selfSigned ?: return BootstrapResult.MissingPrivateKey val sskPrivateKey = xKeys.selfSigned ?: return BootstrapResult.MissingPrivateKey
val uskPrivateKey = xKeys.user ?: return BootstrapResult.MissingPrivateKey val uskPrivateKey = xKeys.user ?: return BootstrapResult.MissingPrivateKey
Timber.d("## BootstrapCrossSigningTask: Creating 4S - gathering private keys success")
try { try {
params.progressListener?.onProgress( params.progressListener?.onProgress(
@ -170,6 +182,7 @@ class BootstrapCrossSigningTask @Inject constructor(
isIndeterminate = true isIndeterminate = true
) )
) )
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing MSK...")
awaitCallback<Unit> { awaitCallback<Unit> {
ssssService.storeSecret( ssssService.storeSecret(
MASTER_KEY_SSSS_NAME, MASTER_KEY_SSSS_NAME,
@ -183,6 +196,7 @@ class BootstrapCrossSigningTask @Inject constructor(
isIndeterminate = true isIndeterminate = true
) )
) )
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing USK...")
awaitCallback<Unit> { awaitCallback<Unit> {
ssssService.storeSecret( ssssService.storeSecret(
USER_SIGNING_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME,
@ -196,6 +210,7 @@ class BootstrapCrossSigningTask @Inject constructor(
stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_ssk), isIndeterminate = true stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_ssk), isIndeterminate = true
) )
) )
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing SSK...")
awaitCallback<Unit> { awaitCallback<Unit> {
ssssService.storeSecret( ssssService.storeSecret(
SELF_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME,
@ -204,6 +219,7 @@ class BootstrapCrossSigningTask @Inject constructor(
) )
} }
} catch (failure: Failure) { } catch (failure: Failure) {
Timber.e("## BootstrapCrossSigningTask: Creating 4S - Failed to store keys <${failure.localizedMessage}>")
// Maybe we could just ignore this error? // Maybe we could just ignore this error?
return BootstrapResult.FailedToStorePrivateKeyInSSSS(failure) return BootstrapResult.FailedToStorePrivateKeyInSSSS(failure)
} }
@ -215,7 +231,14 @@ class BootstrapCrossSigningTask @Inject constructor(
) )
) )
try { 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> { val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it) session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
} }
@ -223,6 +246,7 @@ class BootstrapCrossSigningTask @Inject constructor(
session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it) session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
} }
// Save it for gossiping // Save it for gossiping
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Save megolm backup key for gossiping")
session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version) session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
awaitCallback<Unit> { awaitCallback<Unit> {
@ -239,6 +263,7 @@ class BootstrapCrossSigningTask @Inject constructor(
Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup") Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup")
} }
Timber.d("## BootstrapCrossSigningTask: initXSOnly:${params.initOnlyCrossSigning} Finished")
return BootstrapResult.Success(keyInfo) return BootstrapResult.Success(keyInfo)
} }

View file

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

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.popup.VerificationVectorAlert
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotx.features.settings.VectorPreferences 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 im.vector.riotx.push.fcm.FcmHelper
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.activity_home.* import kotlinx.android.synthetic.main.activity_home.*
@ -60,13 +61,16 @@ data class HomeActivityArgs(
val accountCreation: Boolean val accountCreation: Boolean
) : Parcelable ) : Parcelable
class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory { class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory, ServerBackupStatusViewModel.Factory {
private lateinit var sharedActionViewModel: HomeSharedActionViewModel private lateinit var sharedActionViewModel: HomeSharedActionViewModel
private val homeActivityViewModel: HomeActivityViewModel by viewModel() private val homeActivityViewModel: HomeActivityViewModel by viewModel()
@Inject lateinit var viewModelFactory: HomeActivityViewModel.Factory @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 activeSessionHolder: ActiveSessionHolder
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
@Inject lateinit var pushManager: PushersManager @Inject lateinit var pushManager: PushersManager
@ -92,6 +96,10 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
return unknownDeviceViewModelFactory.create(initialState) return unknownDeviceViewModelFactory.create(initialState)
} }
override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel {
return serverBackupviewModelFactory.create(initialState)
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager, vectorPreferences.areNotificationEnabledForDevice()) FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager, vectorPreferences.areNotificationEnabledForDevice())
@ -234,7 +242,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
} }
// Force remote backup state update to update the banner if needed // Force remote backup state update to update the banner if needed
viewModelProvider.get(SignOutViewModel::class.java).refreshRemoteStateIfNeeded() serverBackupStatusViewModel.refreshRemoteStateIfNeeded()
} }
override fun configure(toolbar: Toolbar) { override fun configure(toolbar: Toolbar) {

View file

@ -27,7 +27,6 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.google.android.material.bottomnavigation.BottomNavigationItemView import com.google.android.material.bottomnavigation.BottomNavigationItemView
import com.google.android.material.bottomnavigation.BottomNavigationMenuView 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.session.group.model.GroupSummary
import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
@ -49,13 +48,10 @@ import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView
import im.vector.riotx.features.popup.PopupAlertManager import im.vector.riotx.features.popup.PopupAlertManager
import im.vector.riotx.features.popup.VerificationVectorAlert 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.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.*
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 timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -65,15 +61,17 @@ private const val INDEX_ROOMS = 2
class HomeDetailFragment @Inject constructor( class HomeDetailFragment @Inject constructor(
val homeDetailViewModelFactory: HomeDetailViewModel.Factory, val homeDetailViewModelFactory: HomeDetailViewModel.Factory,
private val serverBackupStatusViewModelFactory: ServerBackupStatusViewModel.Factory,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val alertManager: PopupAlertManager, private val alertManager: PopupAlertManager,
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager
) : VectorBaseFragment(), KeysBackupBanner.Delegate, ActiveCallView.Callback { ) : VectorBaseFragment(), KeysBackupBanner.Delegate, ActiveCallView.Callback, ServerBackupStatusViewModel.Factory {
private val unreadCounterBadgeViews = arrayListOf<UnreadCounterBadgeView>() private val unreadCounterBadgeViews = arrayListOf<UnreadCounterBadgeView>()
private val viewModel: HomeDetailViewModel by fragmentViewModel() private val viewModel: HomeDetailViewModel by fragmentViewModel()
private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel() private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel()
private val serverBackupStatusViewModel: ServerBackupStatusViewModel by activityViewModel()
private lateinit var sharedActionViewModel: HomeSharedActionViewModel private lateinit var sharedActionViewModel: HomeSharedActionViewModel
private lateinit var sharedCallActionViewModel: SharedActiveCallViewModel private lateinit var sharedCallActionViewModel: SharedActiveCallViewModel
@ -195,34 +193,14 @@ class HomeDetailFragment @Inject constructor(
} }
private fun setupKeysBackupBanner() { private fun setupKeysBackupBanner() {
// Keys backup banner serverBackupStatusViewModel.subscribe(this) {
// Use the SignOutViewModel, it observe the keys backup state and this is what we need here when (val banState = it.bannerState.invoke()) {
val model = fragmentViewModelProvider.get(SignOutViewModel::class.java) is BannerState.Setup -> homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(banState.numberOfKeys), false)
BannerState.BackingUp -> homeKeysBackupBanner.render(KeysBackupBanner.State.BackingUp, false)
model.keysBackupState.observe(viewLifecycleOwner, Observer { keysBackupState -> null,
when (keysBackupState) { BannerState.Hidden -> homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false)
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)
} }
}) }.disposeOnDestroyView()
homeKeysBackupBanner.delegate = this homeKeysBackupBanner.delegate = this
} }
@ -331,4 +309,8 @@ class HomeDetailFragment @Inject constructor(
} }
} }
} }
override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel {
return serverBackupStatusViewModelFactory.create(initialState)
}
} }

View file

@ -218,7 +218,14 @@ class DefaultNavigator @Inject constructor(
} }
override fun openKeysBackupSetup(context: Context, showManualExport: Boolean) { 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) { 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.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.dialogs.ExportKeysDialog 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.ExternalIntentData
import im.vector.riotx.core.intent.analyseIntent import im.vector.riotx.core.intent.analyseIntent
import im.vector.riotx.core.intent.getFilenameFromUri import im.vector.riotx.core.intent.getFilenameFromUri
import im.vector.riotx.core.platform.SimpleTextWatcher import im.vector.riotx.core.platform.SimpleTextWatcher
import im.vector.riotx.core.preference.VectorPreference 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.PERMISSION_REQUEST_CODE_EXPORT_KEYS
import im.vector.riotx.core.utils.allGranted 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.openFileSelection
import im.vector.riotx.core.utils.toast import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.crypto.keys.KeysExporter 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 import javax.inject.Inject
class VectorSettingsSecurityPrivacyFragment @Inject constructor( class VectorSettingsSecurityPrivacyFragment @Inject constructor(
private val vectorPreferences: VectorPreferences private val vectorPreferences: VectorPreferences,
private val activeSessionHolder: ActiveSessionHolder
) : VectorSettingsBaseFragment() { ) : VectorSettingsBaseFragment() {
override var titleRes = R.string.settings_security_and_privacy override var titleRes = R.string.settings_security_and_privacy
@ -119,38 +120,69 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
} }
private fun refreshXSigningStatus() { private fun refreshXSigningStatus() {
val xSigningIsEnableInAccount = session.cryptoService().crossSigningService().isCrossSigningInitialized() val crossSigningKeys = session.cryptoService().crossSigningService().getMyCrossSigningKeys()
val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified() val xSigningIsEnableInAccount = crossSigningKeys != null
val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign() val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified()
val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign()
if (xSigningKeyCanSign) { if (xSigningKeyCanSign) {
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_trusted) mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_trusted)
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_complete) mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_complete)
} else if (xSigningKeysAreTrusted) { } else if (xSigningKeysAreTrusted) {
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_custom) mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_custom)
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_trusted) mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_trusted)
} else if (xSigningIsEnableInAccount) { } else if (xSigningIsEnableInAccount) {
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_black) mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_black)
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_not_trusted) mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_not_trusted)
} else { } else {
mCrossSigningStatePreference.setIcon(android.R.color.transparent) mCrossSigningStatePreference.setIcon(android.R.color.transparent)
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_disabled) 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) { override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
if (allGranted(grantResults)) { if (allGranted(grantResults)) {
if (requestCode == PERMISSION_REQUEST_CODE_EXPORT_KEYS) { 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?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) 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) { if (resultCode == Activity.RESULT_OK) {
when (requestCode) { when (requestCode) {
REQUEST_E2E_FILE_REQUEST_CODE -> importKeys(data) REQUEST_E2E_FILE_REQUEST_CODE -> importKeys(data)
@ -169,7 +201,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
} }
exportPref.onPreferenceClickListener = Preference.OnPreferenceClickListener { exportPref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
exportKeys() queryExportKeys(activeSessionHolder.getSafeActiveSession()?.myUserId ?: "", REQUEST_CODE_SAVE_MEGOLM_EXPORT)
true 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. * Manage the e2e keys import.
*/ */
@ -515,6 +507,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
companion object { companion object {
private const val REQUEST_E2E_FILE_REQUEST_CODE = 123 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 PUSHER_PREFERENCE_KEY_BASE = "PUSHER_PREFERENCE_KEY_BASE"
private const val DEVICES_PREFERENCE_KEY_BASE = "DEVICES_PREFERENCE_KEY_BASE" private const val DEVICES_PREFERENCE_KEY_BASE = "DEVICES_PREFERENCE_KEY_BASE"

View file

@ -0,0 +1,177 @@
/*
* 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)
keysBackupState.value = session.cryptoService().keysBackupService().state
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(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)
}
/**
* 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

@ -28,19 +28,27 @@ import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.transition.TransitionManager
import butterknife.BindView 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.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog 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.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.riotx.R 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.platform.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity
import im.vector.riotx.features.crypto.recover.BootstrapBottomSheet
import timber.log.Timber
import javax.inject.Inject
class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment() { // 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) @BindView(R.id.bottom_sheet_signout_warning_text)
lateinit var sheetTitle: TextView lateinit var sheetTitle: TextView
@ -48,14 +56,20 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment() {
@BindView(R.id.bottom_sheet_signout_backingup_status_group) @BindView(R.id.bottom_sheet_signout_backingup_status_group)
lateinit var backingUpStatusGroup: ViewGroup lateinit var backingUpStatusGroup: ViewGroup
@BindView(R.id.keys_backup_setup) @BindView(R.id.setupRecoveryButton)
lateinit var setupClickableView: View lateinit var setupRecoveryButton: SignoutBottomSheetActionButton
@BindView(R.id.keys_backup_activate) @BindView(R.id.setupMegolmBackupButton)
lateinit var activateClickableView: View lateinit var setupMegolmBackupButton: SignoutBottomSheetActionButton
@BindView(R.id.keys_backup_dont_want) @BindView(R.id.exportManuallyButton)
lateinit var dontWantClickableView: View 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) @BindView(R.id.bottom_sheet_signout_icon_progress_bar)
lateinit var backupProgress: ProgressBar lateinit var backupProgress: ProgressBar
@ -66,8 +80,8 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment() {
@BindView(R.id.bottom_sheet_backup_status_text) @BindView(R.id.bottom_sheet_backup_status_text)
lateinit var backupStatusTex: TextView lateinit var backupStatusTex: TextView
@BindView(R.id.bottom_sheet_signout_button) @BindView(R.id.signoutExportingLoading)
lateinit var signoutClickableView: View lateinit var signoutExportingLoading: View
@BindView(R.id.root_layout) @BindView(R.id.root_layout)
lateinit var rootLayout: ViewGroup lateinit var rootLayout: ViewGroup
@ -78,62 +92,44 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment() {
fun newInstance() = SignOutBottomSheetDialogFragment() fun newInstance() = SignOutBottomSheetDialogFragment()
private const val EXPORT_REQ = 0 private const val EXPORT_REQ = 0
private const val QUERY_EXPORT_KEYS = 1
} }
init { init {
isCancelable = true isCancelable = true
} }
private lateinit var viewModel: SignOutViewModel @Inject
lateinit var viewModelFactory: SignoutCheckViewModel.Factory
override fun create(initialState: SignoutCheckViewState): SignoutCheckViewModel {
return viewModelFactory.create(initialState)
}
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?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
viewModel = fragmentViewModelProvider.get(SignOutViewModel::class.java) setupRecoveryButton.action = {
BootstrapBottomSheet.show(parentFragmentManager, false)
setupClickableView.setOnClickListener {
context?.let { context ->
startActivityForResult(KeysBackupSetupActivity.intent(context, true), EXPORT_REQ)
}
} }
activateClickableView.setOnClickListener { exitAnywayButton.action = {
context?.let { context ->
startActivity(KeysBackupManageActivity.intent(context))
}
}
signoutClickableView.setOnClickListener {
this.onSignOut?.run()
}
dontWantClickableView.setOnClickListener { _ ->
context?.let { context?.let {
AlertDialog.Builder(it) AlertDialog.Builder(it)
.setTitle(R.string.are_you_sure) .setTitle(R.string.are_you_sure)
.setMessage(R.string.sign_out_bottom_sheet_will_lose_secure_messages) .setMessage(R.string.sign_out_bottom_sheet_will_lose_secure_messages)
.setPositiveButton(R.string.backup) { _, _ -> .setPositiveButton(R.string.backup, null)
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
}
}
}
.setNegativeButton(R.string.action_sign_out) { _, _ -> .setNegativeButton(R.string.action_sign_out) { _, _ ->
onSignOut?.run() onSignOut?.run()
} }
@ -141,71 +137,143 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment() {
} }
} }
viewModel.keysExportedToFile.observe(viewLifecycleOwner, Observer { exportManuallyButton.action = {
val hasExportedToFile = it ?: false withState(viewModel) { state ->
if (hasExportedToFile) { queryExportKeys(state.userId, QUERY_EXPORT_KEYS)
// 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
} }
}) }
viewModel.keysBackupState.observe(viewLifecycleOwner, Observer { setupMegolmBackupButton.action = {
if (viewModel.keysExportedToFile.value == true) { startActivityForResult(KeysBackupSetupActivity.intent(requireContext(), true), EXPORT_REQ)
// ignore this }
return@Observer
} viewModel.observeViewEvents {
TransitionManager.beginDelayedTransition(rootLayout)
when (it) { when (it) {
KeysBackupState.ReadyToBackUp -> { is SignoutCheckViewModel.ViewEvents.ExportKeys -> {
signoutClickableView.isVisible = true it.exporter
dontWantClickableView.isVisible = false .export(requireContext(),
setupClickableView.isVisible = false it.passphrase,
activateClickableView.isVisible = false it.uri,
backingUpStatusGroup.isVisible = true 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 backupProgress.isVisible = false
backupCompleteImage.isVisible = true backupCompleteImage.isVisible = true
backupStatusTex.text = getString(R.string.keys_backup_info_keys_all_backup_up) 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 backupProgress.isVisible = true
backupCompleteImage.isVisible = false backupCompleteImage.isVisible = false
backupStatusTex.text = getString(R.string.sign_out_bottom_sheet_backing_up_keys) backupStatusTex.text = getString(R.string.sign_out_bottom_sheet_backing_up_keys)
hideViews(setupMegolmBackupButton, setupMegolmBackupButton, signOutButton, exportManuallyButton)
exitAnywayButton.isVisible = true
} }
KeysBackupState.NotTrusted -> { 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) 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 -> { else -> {
backingUpStatusGroup.isVisible = false // mmm.. strange state
dontWantClickableView.isVisible = true
setupClickableView.isVisible = true exitAnywayButton.isVisible = true
activateClickableView.isVisible = false
sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_no_backup)
} }
} }
}
// 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 override fun getLayoutResId() = R.layout.bottom_sheet_logout_and_backup
@ -228,10 +296,26 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment() {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
if (requestCode == EXPORT_REQ) { if (requestCode == QUERY_EXPORT_KEYS) {
val manualExportDone = data?.getBooleanExtra(KeysBackupSetupActivity.MANUAL_EXPORT, false) val uri = data?.data
viewModel.keysExportedToFile.value = manualExportDone 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 androidx.fragment.app.FragmentActivity
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder 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.core.extensions.vectorComponent
import im.vector.riotx.features.MainActivity import im.vector.riotx.features.MainActivity
import im.vector.riotx.features.MainActivityArgs import im.vector.riotx.features.MainActivityArgs
@ -33,7 +33,7 @@ class SignOutUiWorker(private val activity: FragmentActivity) {
fun perform(context: Context) { fun perform(context: Context) {
activeSessionHolder = context.vectorComponent().activeSessionHolder() activeSessionHolder = context.vectorComponent().activeSessionHolder()
val session = activeSessionHolder.getActiveSession() 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 // 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() val signOutDialog = SignOutBottomSheetDialogFragment.newInstance()
signOutDialog.onSignOut = Runnable { signOutDialog.onSignOut = Runnable {

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

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

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

View file

@ -59,6 +59,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?riotx_keys_backup_banner_accent_color" android:background="?riotx_keys_backup_banner_accent_color"
android:minHeight="67dp" android:minHeight="67dp"
android:visibility="gone"
tools:visibility="visible"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/syncStateView" /> app:layout_constraintTop_toBottomOf="@id/syncStateView" />

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

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

View file

@ -114,4 +114,10 @@
<attr name="forceStartPadding" format="boolean" /> <attr name="forceStartPadding" format="boolean" />
</declare-styleable> </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> </resources>

View file

@ -1052,6 +1052,7 @@
<string name="encryption_export_export">Export</string> <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_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_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_message_recovery">Encrypted Messages Recovery</string>
<string name="encryption_settings_manage_message_recovery_summary">Manage Key Backup</string> <string name="encryption_settings_manage_message_recovery_summary">Manage Key Backup</string>
@ -1497,17 +1498,24 @@ Why choose Riot.im?
<string name="new_recovery_method_popup_title">New Key Backup</string> <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_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> <string name="new_recovery_method_popup_was_me">It was me</string>
<!-- Keys backup banner --> <!-- Keys backup banner -->
<string name="keys_backup_banner_setup_line1">Never lose encrypted messages</string> <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_line2">Start using Key Backup</string>
<string name="secure_backup_banner_setup_line1">Secure Backup</string>
<string name="secure_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_line1">Never lose encrypted messages</string>
<string name="keys_backup_banner_recover_line2">Use Key Backup</string> <string name="keys_backup_banner_recover_line2">Use Key Backup</string>
<string name="keys_backup_banner_update_line1">New secure message keys</string> <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_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>
<string name="secure_backup_setup">Set Up Secure Backup</string>
<!-- Keys backup info --> <!-- Keys backup info -->
<string name="keys_backup_info_keys_all_backup_up">All keys backed up</string> <string name="keys_backup_info_keys_all_backup_up">All keys backed up</string>