mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-15 01:35:07 +08:00
Signout to setup 4S
This commit is contained in:
parent
a98b2ecce3
commit
332f227bc1
@ -61,6 +61,8 @@ interface CrossSigningService {
|
||||
|
||||
fun canCrossSign(): Boolean
|
||||
|
||||
fun allPrivateKeysKnown(): Boolean
|
||||
|
||||
fun trustUser(otherUserId: String,
|
||||
callback: MatrixCallback<Unit>)
|
||||
|
||||
|
@ -507,6 +507,13 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||
&& cryptoStore.getCrossSigningPrivateKeys()?.user != null
|
||||
}
|
||||
|
||||
override fun allPrivateKeysKnown(): Boolean {
|
||||
return checkSelfTrust().isVerified()
|
||||
&& cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null
|
||||
&& cryptoStore.getCrossSigningPrivateKeys()?.user != null
|
||||
&& cryptoStore.getCrossSigningPrivateKeys()?.master != null
|
||||
}
|
||||
|
||||
override fun trustUser(otherUserId: String, callback: MatrixCallback<Unit>) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
Timber.d("## CrossSigning - Mark user $userId as trusted ")
|
||||
|
@ -36,7 +36,6 @@ import im.vector.riotx.features.reactions.EmojiChooserViewModel
|
||||
import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel
|
||||
import im.vector.riotx.features.roomprofile.RoomProfileSharedActionViewModel
|
||||
import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel
|
||||
import im.vector.riotx.features.workers.signout.ServerBackupStatusViewModel
|
||||
|
||||
@Module
|
||||
interface ViewModelModule {
|
||||
|
@ -16,9 +16,16 @@
|
||||
|
||||
package im.vector.riotx.core.extensions
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Parcelable
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import im.vector.riotx.core.utils.toast
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
fun VectorBaseFragment.addFragment(frameId: Int, fragment: Fragment) {
|
||||
parentFragmentManager.commitTransactionNow { add(frameId, fragment) }
|
||||
@ -89,3 +96,29 @@ fun Fragment.getAllChildFragments(): List<Fragment> {
|
||||
|
||||
// Define a missing constant
|
||||
const val POP_BACK_STACK_EXCLUSIVE = 0
|
||||
|
||||
fun Fragment.queryExportKeys(userId: String, requestCode: Int) {
|
||||
// We need WRITE_EXTERNAL permission
|
||||
// if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES,
|
||||
// this,
|
||||
// PERMISSION_REQUEST_CODE_EXPORT_KEYS,
|
||||
// R.string.permissions_rationale_msg_keys_backup_export)) {
|
||||
// WRITE permissions are not needed
|
||||
val timestamp = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).let {
|
||||
it.format(Date())
|
||||
}
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
intent.type = "text/plain"
|
||||
intent.putExtra(
|
||||
Intent.EXTRA_TITLE,
|
||||
"riot-megolm-export-$userId-$timestamp.txt"
|
||||
)
|
||||
|
||||
try {
|
||||
startActivityForResult(Intent.createChooser(intent, getString(R.string.keys_backup_setup_step1_manual_export)), requestCode)
|
||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||
activity?.toast(R.string.error_no_external_application_found)
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
@ -65,3 +65,12 @@ fun Session.hasUnsavedKeys(): Boolean {
|
||||
return cryptoService().inboundGroupSessionsCount(false) > 0
|
||||
&& cryptoService().keysBackupService().state != KeysBackupState.ReadyToBackUp
|
||||
}
|
||||
|
||||
fun Session.cannotLogoutSafely(): Boolean {
|
||||
// has some encrypted chat
|
||||
return hasUnsavedKeys()
|
||||
// has local cross signing keys
|
||||
|| (cryptoService().crossSigningService().allPrivateKeysKnown()
|
||||
// That are not backed up
|
||||
&& !sharedSecretStorageService.isRecoverySetup())
|
||||
}
|
||||
|
@ -21,13 +21,10 @@ import androidx.preference.PreferenceManager
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AbsListView
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.transition.TransitionManager
|
||||
import butterknife.BindView
|
||||
import butterknife.ButterKnife
|
||||
|
@ -17,37 +17,34 @@
|
||||
package im.vector.riotx.features.crypto.keys
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Environment
|
||||
import android.net.Uri
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.internal.extensions.foldToCallback
|
||||
import im.vector.matrix.android.internal.util.awaitCallback
|
||||
import im.vector.riotx.core.files.addEntryToDownloadManager
|
||||
import im.vector.riotx.core.files.writeToFile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
class KeysExporter(private val session: Session) {
|
||||
|
||||
/**
|
||||
* Export keys and return the file path with the callback
|
||||
*/
|
||||
fun export(context: Context, password: String, callback: MatrixCallback<String>) {
|
||||
fun export(context: Context, password: String, uri: Uri, callback: MatrixCallback<Boolean>) {
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
runCatching {
|
||||
val data = awaitCallback<ByteArray> { session.cryptoService().exportRoomKeys(password, it) }
|
||||
withContext(Dispatchers.IO) {
|
||||
val parentDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
|
||||
val file = File(parentDir, "riotx-keys-" + System.currentTimeMillis() + ".txt")
|
||||
|
||||
writeToFile(data, file)
|
||||
|
||||
addEntryToDownloadManager(context, file, "text/plain")
|
||||
|
||||
file.absolutePath
|
||||
val data = awaitCallback<ByteArray> { session.cryptoService().exportRoomKeys(password, it) }
|
||||
val os = context.contentResolver?.openOutputStream(uri)
|
||||
if (os == null) {
|
||||
false
|
||||
} else {
|
||||
os.write(data)
|
||||
os.flush()
|
||||
true
|
||||
}
|
||||
}
|
||||
}.foldToCallback(callback)
|
||||
}
|
||||
|
@ -15,6 +15,8 @@
|
||||
*/
|
||||
package im.vector.riotx.features.crypto.keysbackup.setup
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
@ -132,36 +134,16 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
|
||||
this,
|
||||
PERMISSION_REQUEST_CODE_EXPORT_KEYS,
|
||||
R.string.permissions_rationale_msg_keys_backup_export)) {
|
||||
ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
|
||||
override fun onPassphrase(passphrase: String) {
|
||||
showWaitingView()
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
intent.type = "text/plain"
|
||||
intent.putExtra(Intent.EXTRA_TITLE, "riot-megolm-export-${session.myUserId}-${System.currentTimeMillis()}.txt")
|
||||
|
||||
KeysExporter(session)
|
||||
.export(this@KeysBackupSetupActivity,
|
||||
passphrase,
|
||||
object : MatrixCallback<String> {
|
||||
override fun onSuccess(data: String) {
|
||||
hideWaitingView()
|
||||
|
||||
AlertDialog.Builder(this@KeysBackupSetupActivity)
|
||||
.setMessage(getString(R.string.encryption_export_saved_as, data))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
val resultIntent = Intent()
|
||||
resultIntent.putExtra(MANUAL_EXPORT, true)
|
||||
setResult(RESULT_OK, resultIntent)
|
||||
finish()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
toast(failure.localizedMessage ?: getString(R.string.unexpected_error))
|
||||
hideWaitingView()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
startActivityForResult(Intent.createChooser(intent, getString(R.string.keys_backup_setup_step1_manual_export)), REQUEST_CODE_SAVE_MEGOLM_EXPORT)
|
||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||
toast(R.string.error_no_external_application_found)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,6 +155,47 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == REQUEST_CODE_SAVE_MEGOLM_EXPORT) {
|
||||
val uri = data?.data
|
||||
if (resultCode == Activity.RESULT_OK && uri != null) {
|
||||
ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
|
||||
override fun onPassphrase(passphrase: String) {
|
||||
showWaitingView()
|
||||
|
||||
KeysExporter(session)
|
||||
.export(this@KeysBackupSetupActivity,
|
||||
passphrase,
|
||||
uri,
|
||||
object : MatrixCallback<Boolean> {
|
||||
override fun onSuccess(data: Boolean) {
|
||||
if (data) {
|
||||
toast(getString(R.string.encryption_exported_successfully))
|
||||
Intent().apply {
|
||||
putExtra(MANUAL_EXPORT, true)
|
||||
}.let {
|
||||
setResult(Activity.RESULT_OK, it)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
hideWaitingView()
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
toast(failure.localizedMessage ?: getString(R.string.unexpected_error))
|
||||
hideWaitingView()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
toast(getString(R.string.unexpected_error))
|
||||
hideWaitingView()
|
||||
}
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (viewModel.shouldPromptOnBack) {
|
||||
if (waitingView?.isVisible == true) {
|
||||
@ -205,6 +228,7 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
|
||||
const val KEYS_VERSION = "KEYS_VERSION"
|
||||
const val MANUAL_EXPORT = "MANUAL_EXPORT"
|
||||
const val EXTRA_SHOW_MANUAL_EXPORT = "SHOW_MANUAL_EXPORT"
|
||||
const val REQUEST_CODE_SAVE_MEGOLM_EXPORT = 101
|
||||
|
||||
fun intent(context: Context, showManualExport: Boolean): Intent {
|
||||
val intent = Intent(context, KeysBackupSetupActivity::class.java)
|
||||
|
@ -15,13 +15,13 @@
|
||||
*/
|
||||
package im.vector.riotx.features.crypto.keysbackup.setup
|
||||
|
||||
import android.os.AsyncTask
|
||||
import android.os.Bundle
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.transition.TransitionManager
|
||||
import butterknife.BindView
|
||||
import butterknife.OnClick
|
||||
@ -33,6 +33,8 @@ import im.vector.riotx.core.extensions.showPassword
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import im.vector.riotx.core.ui.views.PasswordStrengthBar
|
||||
import im.vector.riotx.features.settings.VectorLocale
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment() {
|
||||
@ -117,9 +119,9 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment()
|
||||
if (newValue.isEmpty()) {
|
||||
viewModel.passwordStrength.value = null
|
||||
} else {
|
||||
AsyncTask.execute {
|
||||
viewModel.viewModelScope.launch(Dispatchers.IO) {
|
||||
val strength = zxcvbn.measure(newValue)
|
||||
activity?.runOnUiThread {
|
||||
launch(Dispatchers.Main) {
|
||||
viewModel.passwordStrength.value = strength
|
||||
}
|
||||
}
|
||||
|
@ -168,7 +168,6 @@ class BootstrapCrossSigningTask @Inject constructor(
|
||||
return BootstrapResult.FailedToSetDefaultSSSSKey(failure)
|
||||
}
|
||||
|
||||
|
||||
Timber.d("## BootstrapCrossSigningTask: Creating 4S - gathering private keys")
|
||||
val xKeys = crossSigningService.getCrossSigningPrivateKeys()
|
||||
val mskPrivateKey = xKeys?.master ?: return BootstrapResult.MissingPrivateKey
|
||||
|
@ -406,7 +406,10 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||
setState {
|
||||
copy(
|
||||
recoveryKeyCreationInfo = bootstrapResult.keyInfo,
|
||||
step = BootstrapStep.SaveRecoveryKey(false)
|
||||
step = BootstrapStep.SaveRecoveryKey(
|
||||
// If a passphrase was used, saving key is optional
|
||||
state.passphrase != null
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -68,7 +68,6 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
|
||||
private val homeActivityViewModel: HomeActivityViewModel by viewModel()
|
||||
@Inject lateinit var viewModelFactory: HomeActivityViewModel.Factory
|
||||
|
||||
|
||||
private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel()
|
||||
@Inject lateinit var serverBackupviewModelFactory: ServerBackupStatusViewModel.Factory
|
||||
|
||||
|
@ -21,7 +21,6 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.forEachIndexed
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import com.airbnb.mvrx.activityViewModel
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
@ -194,7 +193,6 @@ class HomeDetailFragment @Inject constructor(
|
||||
}
|
||||
|
||||
private fun setupKeysBackupBanner() {
|
||||
|
||||
serverBackupStatusViewModel.subscribe(this) {
|
||||
when (val banState = it.bannerState.invoke()) {
|
||||
is BannerState.Setup -> homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(banState.numberOfKeys), false)
|
||||
|
@ -207,7 +207,6 @@ class DefaultNavigator @Inject constructor(
|
||||
context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun openKeysBackupManager(context: Context) {
|
||||
|
@ -34,16 +34,16 @@ import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||
import im.vector.riotx.core.dialogs.ExportKeysDialog
|
||||
import im.vector.riotx.core.extensions.queryExportKeys
|
||||
import im.vector.riotx.core.intent.ExternalIntentData
|
||||
import im.vector.riotx.core.intent.analyseIntent
|
||||
import im.vector.riotx.core.intent.getFilenameFromUri
|
||||
import im.vector.riotx.core.platform.SimpleTextWatcher
|
||||
import im.vector.riotx.core.preference.VectorPreference
|
||||
import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
|
||||
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_EXPORT_KEYS
|
||||
import im.vector.riotx.core.utils.allGranted
|
||||
import im.vector.riotx.core.utils.checkPermissions
|
||||
import im.vector.riotx.core.utils.openFileSelection
|
||||
import im.vector.riotx.core.utils.toast
|
||||
import im.vector.riotx.features.crypto.keys.KeysExporter
|
||||
@ -52,7 +52,8 @@ import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActiv
|
||||
import javax.inject.Inject
|
||||
|
||||
class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
||||
private val vectorPreferences: VectorPreferences
|
||||
private val vectorPreferences: VectorPreferences,
|
||||
private val activeSessionHolder: ActiveSessionHolder
|
||||
) : VectorSettingsBaseFragment() {
|
||||
|
||||
override var titleRes = R.string.settings_security_and_privacy
|
||||
@ -119,38 +120,69 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
||||
}
|
||||
|
||||
private fun refreshXSigningStatus() {
|
||||
val xSigningIsEnableInAccount = session.cryptoService().crossSigningService().isCrossSigningInitialized()
|
||||
val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified()
|
||||
val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign()
|
||||
val crossSigningKeys = session.cryptoService().crossSigningService().getMyCrossSigningKeys()
|
||||
val xSigningIsEnableInAccount = crossSigningKeys != null
|
||||
val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified()
|
||||
val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign()
|
||||
|
||||
if (xSigningKeyCanSign) {
|
||||
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_trusted)
|
||||
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_complete)
|
||||
} else if (xSigningKeysAreTrusted) {
|
||||
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_custom)
|
||||
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_trusted)
|
||||
} else if (xSigningIsEnableInAccount) {
|
||||
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_black)
|
||||
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_not_trusted)
|
||||
} else {
|
||||
mCrossSigningStatePreference.setIcon(android.R.color.transparent)
|
||||
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_disabled)
|
||||
}
|
||||
if (xSigningKeyCanSign) {
|
||||
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_trusted)
|
||||
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_complete)
|
||||
} else if (xSigningKeysAreTrusted) {
|
||||
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_custom)
|
||||
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_trusted)
|
||||
} else if (xSigningIsEnableInAccount) {
|
||||
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_black)
|
||||
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_not_trusted)
|
||||
} else {
|
||||
mCrossSigningStatePreference.setIcon(android.R.color.transparent)
|
||||
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_disabled)
|
||||
}
|
||||
|
||||
mCrossSigningStatePreference.isVisible = true
|
||||
mCrossSigningStatePreference.isVisible = true
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
if (allGranted(grantResults)) {
|
||||
if (requestCode == PERMISSION_REQUEST_CODE_EXPORT_KEYS) {
|
||||
exportKeys()
|
||||
queryExportKeys(activeSessionHolder.getSafeActiveSession()?.myUserId ?: "", REQUEST_CODE_SAVE_MEGOLM_EXPORT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (requestCode == REQUEST_CODE_SAVE_MEGOLM_EXPORT) {
|
||||
val uri = data?.data
|
||||
if (resultCode == Activity.RESULT_OK && uri != null) {
|
||||
activity?.let { activity ->
|
||||
ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener {
|
||||
override fun onPassphrase(passphrase: String) {
|
||||
displayLoadingView()
|
||||
|
||||
KeysExporter(session)
|
||||
.export(requireContext(),
|
||||
passphrase,
|
||||
uri,
|
||||
object : MatrixCallback<Boolean> {
|
||||
override fun onSuccess(data: Boolean) {
|
||||
if (data) {
|
||||
requireActivity().toast(getString(R.string.encryption_exported_successfully))
|
||||
} else {
|
||||
requireActivity().toast(getString(R.string.unexpected_error))
|
||||
}
|
||||
hideLoadingView()
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
onCommonDone(failure.localizedMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
when (requestCode) {
|
||||
REQUEST_E2E_FILE_REQUEST_CODE -> importKeys(data)
|
||||
@ -169,7 +201,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
||||
}
|
||||
|
||||
exportPref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
exportKeys()
|
||||
queryExportKeys(activeSessionHolder.getSafeActiveSession()?.myUserId ?: "", REQUEST_CODE_SAVE_MEGOLM_EXPORT)
|
||||
true
|
||||
}
|
||||
|
||||
@ -179,46 +211,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manage the e2e keys export.
|
||||
*/
|
||||
private fun exportKeys() {
|
||||
// We need WRITE_EXTERNAL permission
|
||||
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES,
|
||||
this,
|
||||
PERMISSION_REQUEST_CODE_EXPORT_KEYS,
|
||||
R.string.permissions_rationale_msg_keys_backup_export)) {
|
||||
activity?.let { activity ->
|
||||
ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener {
|
||||
override fun onPassphrase(passphrase: String) {
|
||||
displayLoadingView()
|
||||
|
||||
KeysExporter(session)
|
||||
.export(requireContext(),
|
||||
passphrase,
|
||||
object : MatrixCallback<String> {
|
||||
override fun onSuccess(data: String) {
|
||||
if (isAdded) {
|
||||
hideLoadingView()
|
||||
|
||||
AlertDialog.Builder(activity)
|
||||
.setMessage(getString(R.string.encryption_export_saved_as, data))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
onCommonDone(failure.localizedMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manage the e2e keys import.
|
||||
*/
|
||||
@ -515,6 +507,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
||||
|
||||
companion object {
|
||||
private const val REQUEST_E2E_FILE_REQUEST_CODE = 123
|
||||
private const val REQUEST_CODE_SAVE_MEGOLM_EXPORT = 124
|
||||
|
||||
private const val PUSHER_PREFERENCE_KEY_BASE = "PUSHER_PREFERENCE_KEY_BASE"
|
||||
private const val DEVICES_PREFERENCE_KEY_BASE = "DEVICES_PREFERENCE_KEY_BASE"
|
||||
|
@ -94,9 +94,8 @@ class ServerBackupStatusViewModel @AssistedInject constructor(@Assisted initialS
|
||||
init {
|
||||
session.cryptoService().keysBackupService().addListener(this)
|
||||
|
||||
keyBackupPublishSubject.onNext(session.cryptoService().keysBackupService().state)
|
||||
keysBackupState.value = session.cryptoService().keysBackupService().state
|
||||
session.rx().liveCrossSigningPrivateKeys()
|
||||
|
||||
Observable.combineLatest<List<UserAccountData>, Optional<MXCrossSigningInfo>, KeysBackupState, Optional<PrivateKeysInfo>, BannerState>(
|
||||
session.rx().liveAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME)),
|
||||
session.rx().liveCrossSigningInfo(session.myUserId),
|
||||
@ -126,13 +125,15 @@ class ServerBackupStatusViewModel @AssistedInject constructor(@Assisted initialS
|
||||
BannerState.Hidden
|
||||
}
|
||||
)
|
||||
.throttleLast(2000, TimeUnit.MILLISECONDS) // we don't want to flicker or catch transient states
|
||||
.throttleLast(1000, TimeUnit.MILLISECONDS) // we don't want to flicker or catch transient states
|
||||
.distinctUntilChanged()
|
||||
.execute { async ->
|
||||
copy(
|
||||
bannerState = async
|
||||
)
|
||||
}
|
||||
|
||||
keyBackupPublishSubject.onNext(session.cryptoService().keysBackupService().state)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -28,24 +28,27 @@ import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.transition.TransitionManager
|
||||
import butterknife.BindView
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.dialogs.ExportKeysDialog
|
||||
import im.vector.riotx.core.extensions.queryExportKeys
|
||||
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
import im.vector.riotx.core.utils.toast
|
||||
import im.vector.riotx.features.attachments.preview.AttachmentsPreviewViewModel
|
||||
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||
import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity
|
||||
import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsViewModel
|
||||
import im.vector.riotx.features.crypto.recover.BootstrapBottomSheet
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(), ServerBackupStatusViewModel.Factory {
|
||||
// TODO this needs to be refactored to current standard and remove legacy
|
||||
class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(), SignoutCheckViewModel.Factory {
|
||||
|
||||
@BindView(R.id.bottom_sheet_signout_warning_text)
|
||||
lateinit var sheetTitle: TextView
|
||||
@ -53,14 +56,20 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(),
|
||||
@BindView(R.id.bottom_sheet_signout_backingup_status_group)
|
||||
lateinit var backingUpStatusGroup: ViewGroup
|
||||
|
||||
@BindView(R.id.keys_backup_setup)
|
||||
lateinit var setupClickableView: View
|
||||
@BindView(R.id.setupRecoveryButton)
|
||||
lateinit var setupRecoveryButton: SignoutBottomSheetActionButton
|
||||
|
||||
@BindView(R.id.keys_backup_activate)
|
||||
lateinit var activateClickableView: View
|
||||
@BindView(R.id.setupMegolmBackupButton)
|
||||
lateinit var setupMegolmBackupButton: SignoutBottomSheetActionButton
|
||||
|
||||
@BindView(R.id.keys_backup_dont_want)
|
||||
lateinit var dontWantClickableView: View
|
||||
@BindView(R.id.exportManuallyButton)
|
||||
lateinit var exportManuallyButton: SignoutBottomSheetActionButton
|
||||
|
||||
@BindView(R.id.exitAnywayButton)
|
||||
lateinit var exitAnywayButton: SignoutBottomSheetActionButton
|
||||
|
||||
@BindView(R.id.signOutButton)
|
||||
lateinit var signOutButton: SignoutBottomSheetActionButton
|
||||
|
||||
@BindView(R.id.bottom_sheet_signout_icon_progress_bar)
|
||||
lateinit var backupProgress: ProgressBar
|
||||
@ -71,8 +80,8 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(),
|
||||
@BindView(R.id.bottom_sheet_backup_status_text)
|
||||
lateinit var backupStatusTex: TextView
|
||||
|
||||
@BindView(R.id.bottom_sheet_signout_button)
|
||||
lateinit var signoutClickableView: View
|
||||
@BindView(R.id.signoutExportingLoading)
|
||||
lateinit var signoutExportingLoading: View
|
||||
|
||||
@BindView(R.id.root_layout)
|
||||
lateinit var rootLayout: ViewGroup
|
||||
@ -83,6 +92,7 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(),
|
||||
fun newInstance() = SignOutBottomSheetDialogFragment()
|
||||
|
||||
private const val EXPORT_REQ = 0
|
||||
private const val QUERY_EXPORT_KEYS = 1
|
||||
}
|
||||
|
||||
init {
|
||||
@ -90,65 +100,36 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(),
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ServerBackupStatusViewModel.Factory
|
||||
lateinit var viewModelFactory: SignoutCheckViewModel.Factory
|
||||
|
||||
override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel {
|
||||
override fun create(initialState: SignoutCheckViewState): SignoutCheckViewModel {
|
||||
return viewModelFactory.create(initialState)
|
||||
}
|
||||
|
||||
private val viewModel: ServerBackupStatusViewModel by fragmentViewModel(ServerBackupStatusViewModel::class)
|
||||
private val viewModel: SignoutCheckViewModel by fragmentViewModel(SignoutCheckViewModel::class)
|
||||
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.refreshRemoteStateIfNeeded()
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
setupClickableView.setOnClickListener {
|
||||
context?.let { context ->
|
||||
startActivityForResult(KeysBackupSetupActivity.intent(context, true), EXPORT_REQ)
|
||||
}
|
||||
setupRecoveryButton.action = {
|
||||
BootstrapBottomSheet.show(parentFragmentManager, false)
|
||||
}
|
||||
|
||||
activateClickableView.setOnClickListener {
|
||||
context?.let { context ->
|
||||
startActivity(KeysBackupManageActivity.intent(context))
|
||||
}
|
||||
}
|
||||
|
||||
signoutClickableView.setOnClickListener {
|
||||
this.onSignOut?.run()
|
||||
}
|
||||
|
||||
dontWantClickableView.setOnClickListener { _ ->
|
||||
exitAnywayButton.action = {
|
||||
context?.let {
|
||||
AlertDialog.Builder(it)
|
||||
.setTitle(R.string.are_you_sure)
|
||||
.setMessage(R.string.sign_out_bottom_sheet_will_lose_secure_messages)
|
||||
.setPositiveButton(R.string.backup) { _, _ ->
|
||||
when (viewModel.keysBackupState.value) {
|
||||
KeysBackupState.NotTrusted -> {
|
||||
context?.let { context ->
|
||||
startActivity(KeysBackupManageActivity.intent(context))
|
||||
}
|
||||
}
|
||||
KeysBackupState.Disabled -> {
|
||||
context?.let { context ->
|
||||
startActivityForResult(KeysBackupSetupActivity.intent(context, true), EXPORT_REQ)
|
||||
}
|
||||
}
|
||||
KeysBackupState.BackingUp,
|
||||
KeysBackupState.WillBackUp -> {
|
||||
// keys are already backing up please wait
|
||||
context?.toast(R.string.keys_backup_is_not_finished_please_wait)
|
||||
}
|
||||
else -> {
|
||||
// nop
|
||||
}
|
||||
}
|
||||
}
|
||||
.setPositiveButton(R.string.backup, null)
|
||||
.setNegativeButton(R.string.action_sign_out) { _, _ ->
|
||||
onSignOut?.run()
|
||||
}
|
||||
@ -156,71 +137,143 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(),
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.keysExportedToFile.observe(viewLifecycleOwner, Observer {
|
||||
val hasExportedToFile = it ?: false
|
||||
if (hasExportedToFile) {
|
||||
// We can allow to sign out
|
||||
|
||||
sheetTitle.text = getString(R.string.action_sign_out_confirmation_simple)
|
||||
|
||||
signoutClickableView.isVisible = true
|
||||
dontWantClickableView.isVisible = false
|
||||
setupClickableView.isVisible = false
|
||||
activateClickableView.isVisible = false
|
||||
backingUpStatusGroup.isVisible = false
|
||||
exportManuallyButton.action = {
|
||||
withState(viewModel) { state ->
|
||||
queryExportKeys(state.userId, QUERY_EXPORT_KEYS)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
viewModel.keysBackupState.observe(viewLifecycleOwner, Observer {
|
||||
if (viewModel.keysExportedToFile.value == true) {
|
||||
// ignore this
|
||||
return@Observer
|
||||
}
|
||||
TransitionManager.beginDelayedTransition(rootLayout)
|
||||
setupMegolmBackupButton.action = {
|
||||
startActivityForResult(KeysBackupSetupActivity.intent(requireContext(), true), EXPORT_REQ)
|
||||
}
|
||||
|
||||
viewModel.observeViewEvents {
|
||||
when (it) {
|
||||
KeysBackupState.ReadyToBackUp -> {
|
||||
signoutClickableView.isVisible = true
|
||||
dontWantClickableView.isVisible = false
|
||||
setupClickableView.isVisible = false
|
||||
activateClickableView.isVisible = false
|
||||
backingUpStatusGroup.isVisible = true
|
||||
is SignoutCheckViewModel.ViewEvents.ExportKeys -> {
|
||||
it.exporter
|
||||
.export(requireContext(),
|
||||
it.passphrase,
|
||||
it.uri,
|
||||
object : MatrixCallback<Boolean> {
|
||||
override fun onSuccess(data: Boolean) {
|
||||
if (data) {
|
||||
viewModel.handle(SignoutCheckViewModel.Actions.KeySuccessfullyManuallyExported)
|
||||
} else {
|
||||
viewModel.handle(SignoutCheckViewModel.Actions.KeyExportFailed)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
Timber.e("## Failed to export manually keys ${failure.localizedMessage}")
|
||||
viewModel.handle(SignoutCheckViewModel.Actions.KeyExportFailed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(viewModel) { state ->
|
||||
signoutExportingLoading.isVisible = false
|
||||
if (state.crossSigningSetupAllKeysKnown && !state.backupIsSetup) {
|
||||
sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_no_backup)
|
||||
backingUpStatusGroup.isVisible = false
|
||||
// we should show option to setup 4S
|
||||
setupRecoveryButton.isVisible = true
|
||||
setupMegolmBackupButton.isVisible = false
|
||||
signOutButton.isVisible = false
|
||||
// We let the option to ignore and quit
|
||||
exportManuallyButton.isVisible = true
|
||||
exitAnywayButton.isVisible = true
|
||||
} else if (state.keysBackupState == KeysBackupState.Unknown || state.keysBackupState == KeysBackupState.Disabled) {
|
||||
sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_no_backup)
|
||||
backingUpStatusGroup.isVisible = false
|
||||
// no key backup and cannot setup full 4S
|
||||
// we propose to setup
|
||||
// we should show option to setup 4S
|
||||
setupRecoveryButton.isVisible = false
|
||||
setupMegolmBackupButton.isVisible = true
|
||||
signOutButton.isVisible = false
|
||||
// We let the option to ignore and quit
|
||||
exportManuallyButton.isVisible = true
|
||||
exitAnywayButton.isVisible = true
|
||||
} else {
|
||||
// so keybackup is setup
|
||||
// You should wait until all are uploaded
|
||||
setupRecoveryButton.isVisible = false
|
||||
|
||||
when (state.keysBackupState) {
|
||||
KeysBackupState.ReadyToBackUp -> {
|
||||
sheetTitle.text = getString(R.string.action_sign_out_confirmation_simple)
|
||||
|
||||
// Ok all keys are backedUp
|
||||
backingUpStatusGroup.isVisible = true
|
||||
backupProgress.isVisible = false
|
||||
backupCompleteImage.isVisible = true
|
||||
backupStatusTex.text = getString(R.string.keys_backup_info_keys_all_backup_up)
|
||||
|
||||
sheetTitle.text = getString(R.string.action_sign_out_confirmation_simple)
|
||||
hideViews(setupMegolmBackupButton, exportManuallyButton, exitAnywayButton)
|
||||
// You can signout
|
||||
signOutButton.isVisible = true
|
||||
}
|
||||
KeysBackupState.BackingUp,
|
||||
KeysBackupState.WillBackUp -> {
|
||||
backingUpStatusGroup.isVisible = true
|
||||
sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_backing_up)
|
||||
dontWantClickableView.isVisible = true
|
||||
setupClickableView.isVisible = false
|
||||
activateClickableView.isVisible = false
|
||||
|
||||
KeysBackupState.WillBackUp,
|
||||
KeysBackupState.BackingUp -> {
|
||||
sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_backing_up)
|
||||
|
||||
// save in progress
|
||||
backingUpStatusGroup.isVisible = true
|
||||
backupProgress.isVisible = true
|
||||
backupCompleteImage.isVisible = false
|
||||
backupStatusTex.text = getString(R.string.sign_out_bottom_sheet_backing_up_keys)
|
||||
|
||||
hideViews(setupMegolmBackupButton, setupMegolmBackupButton, signOutButton, exportManuallyButton)
|
||||
exitAnywayButton.isVisible = true
|
||||
}
|
||||
KeysBackupState.NotTrusted -> {
|
||||
backingUpStatusGroup.isVisible = false
|
||||
dontWantClickableView.isVisible = true
|
||||
setupClickableView.isVisible = false
|
||||
activateClickableView.isVisible = true
|
||||
sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_backup_not_active)
|
||||
// It's not trusted and we know there are unsaved keys..
|
||||
backingUpStatusGroup.isVisible = false
|
||||
|
||||
exportManuallyButton.isVisible = true
|
||||
// option to enter pass/key
|
||||
setupMegolmBackupButton.isVisible = true
|
||||
exitAnywayButton.isVisible = true
|
||||
}
|
||||
else -> {
|
||||
backingUpStatusGroup.isVisible = false
|
||||
dontWantClickableView.isVisible = true
|
||||
setupClickableView.isVisible = true
|
||||
activateClickableView.isVisible = false
|
||||
sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_no_backup)
|
||||
// mmm.. strange state
|
||||
|
||||
exitAnywayButton.isVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateSignOutSection()
|
||||
})
|
||||
// final call if keys have been exported
|
||||
when (state.hasBeenExportedToFile) {
|
||||
is Loading -> {
|
||||
signoutExportingLoading.isVisible = true
|
||||
hideViews(setupRecoveryButton,
|
||||
setupMegolmBackupButton,
|
||||
exportManuallyButton,
|
||||
backingUpStatusGroup,
|
||||
signOutButton)
|
||||
exitAnywayButton.isVisible = true
|
||||
}
|
||||
is Success -> {
|
||||
if (state.hasBeenExportedToFile.invoke()) {
|
||||
sheetTitle.text = getString(R.string.action_sign_out_confirmation_simple)
|
||||
hideViews(setupRecoveryButton,
|
||||
setupMegolmBackupButton,
|
||||
exportManuallyButton,
|
||||
backingUpStatusGroup,
|
||||
exitAnywayButton)
|
||||
signOutButton.isVisible = true
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
super.invalidate()
|
||||
}
|
||||
|
||||
override fun getLayoutResId() = R.layout.bottom_sheet_logout_and_backup
|
||||
@ -243,11 +296,26 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(),
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
if (requestCode == EXPORT_REQ) {
|
||||
val manualExportDone = data?.getBooleanExtra(KeysBackupSetupActivity.MANUAL_EXPORT, false)
|
||||
viewModel.keysExportedToFile.value = manualExportDone
|
||||
if (requestCode == QUERY_EXPORT_KEYS) {
|
||||
val uri = data?.data
|
||||
if (resultCode == Activity.RESULT_OK && uri != null) {
|
||||
activity?.let { activity ->
|
||||
ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener {
|
||||
override fun onPassphrase(passphrase: String) {
|
||||
viewModel.handle(SignoutCheckViewModel.Actions.ExportKeys(passphrase, uri))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (requestCode == EXPORT_REQ) {
|
||||
if (data?.getBooleanExtra(KeysBackupSetupActivity.MANUAL_EXPORT, false) == true) {
|
||||
viewModel.handle(SignoutCheckViewModel.Actions.KeySuccessfullyManuallyExported)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideViews(vararg views: View) {
|
||||
views.forEach { it.isVisible = false }
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||
import im.vector.riotx.core.extensions.hasUnsavedKeys
|
||||
import im.vector.riotx.core.extensions.cannotLogoutSafely
|
||||
import im.vector.riotx.core.extensions.vectorComponent
|
||||
import im.vector.riotx.features.MainActivity
|
||||
import im.vector.riotx.features.MainActivityArgs
|
||||
@ -33,7 +33,7 @@ class SignOutUiWorker(private val activity: FragmentActivity) {
|
||||
fun perform(context: Context) {
|
||||
activeSessionHolder = context.vectorComponent().activeSessionHolder()
|
||||
val session = activeSessionHolder.getActiveSession()
|
||||
if (session.hasUnsavedKeys()) {
|
||||
if (session.cannotLogoutSafely()) {
|
||||
// The backup check on logout flow has to be displayed if there are keys in the store, and the keys backup state is not Ready
|
||||
val signOutDialog = SignOutBottomSheetDialogFragment.newInstance()
|
||||
signOutDialog.onSignOut = Runnable {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -70,137 +70,60 @@
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/keys_backup_setup"
|
||||
android:id="@+id/signoutExportingLoading"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:minHeight="50dp"
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="@dimen/layout_horizontal_margin"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingRight="@dimen/layout_horizontal_margin"
|
||||
android:paddingBottom="8dp">
|
||||
android:layout_height="44dp"
|
||||
android:gravity="center">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/backup_keys"
|
||||
android:tint="?riotx_text_primary" />
|
||||
|
||||
<TextView
|
||||
<ProgressBar
|
||||
style="?android:attr/progressBarStyleSmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:text="@string/keys_backup_setup"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textSize="17sp" />
|
||||
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/keys_backup_activate"
|
||||
<im.vector.riotx.features.workers.signout.SignoutBottomSheetActionButton
|
||||
android:id="@+id/setupRecoveryButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:minHeight="50dp"
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="@dimen/layout_horizontal_margin"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingRight="@dimen/layout_horizontal_margin"
|
||||
android:paddingBottom="8dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/backup_keys"
|
||||
android:tint="?riotx_text_primary" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:text="@string/keys_backup_activate"
|
||||
android:textSize="17sp" />
|
||||
|
||||
</LinearLayout>
|
||||
app:actionTitle="@string/secure_backup_setup"
|
||||
app:iconTint="?riotx_text_primary"
|
||||
app:leftIcon="@drawable/ic_secure_backup"
|
||||
app:textColor="?riotx_text_secondary" />
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/keys_backup_dont_want"
|
||||
<im.vector.riotx.features.workers.signout.SignoutBottomSheetActionButton
|
||||
android:id="@+id/setupMegolmBackupButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:minHeight="50dp"
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="@dimen/layout_horizontal_margin"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingRight="@dimen/layout_horizontal_margin"
|
||||
android:paddingBottom="8dp">
|
||||
app:actionTitle="@string/keys_backup_setup"
|
||||
app:iconTint="?riotx_text_primary"
|
||||
app:leftIcon="@drawable/backup_keys"
|
||||
app:textColor="?riotx_text_secondary" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_material_leave"
|
||||
android:tint="@color/riotx_notice" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:text="@string/sign_out_bottom_sheet_dont_want_secure_messages"
|
||||
android:textColor="@color/riotx_notice"
|
||||
android:textSize="17sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/bottom_sheet_signout_button"
|
||||
<im.vector.riotx.features.workers.signout.SignoutBottomSheetActionButton
|
||||
android:id="@+id/exportManuallyButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:minHeight="50dp"
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="@dimen/layout_horizontal_margin"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingRight="@dimen/layout_horizontal_margin"
|
||||
android:paddingBottom="8dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
app:actionTitle="@string/keys_backup_setup_step1_manual_export"
|
||||
app:iconTint="?riotx_text_primary"
|
||||
app:leftIcon="@drawable/ic_download"
|
||||
app:textColor="?riotx_text_secondary" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:src="@drawable/ic_material_exit_to_app"
|
||||
android:tint="@color/riotx_notice" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:text="@string/action_sign_out"
|
||||
android:textColor="@color/riotx_notice"
|
||||
android:textSize="17sp" />
|
||||
</LinearLayout>
|
||||
<im.vector.riotx.features.workers.signout.SignoutBottomSheetActionButton
|
||||
android:id="@+id/exitAnywayButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:actionTitle="@string/sign_out_bottom_sheet_dont_want_secure_messages"
|
||||
app:iconTint="@color/riotx_destructive_accent"
|
||||
app:leftIcon="@drawable/ic_material_leave"
|
||||
app:textColor="@color/riotx_destructive_accent" />
|
||||
|
||||
<im.vector.riotx.features.workers.signout.SignoutBottomSheetActionButton
|
||||
android:id="@+id/signOutButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:actionTitle="@string/action_sign_out"
|
||||
app:iconTint="@color/riotx_notice"
|
||||
app:leftIcon="@drawable/ic_material_exit_to_app"
|
||||
app:textColor="@color/riotx_notice" />
|
||||
</LinearLayout>
|
36
vector/src/main/res/layout/item_signout_action.xml
Normal file
36
vector/src/main/res/layout/item_signout_action.xml
Normal 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>
|
@ -114,4 +114,10 @@
|
||||
<attr name="forceStartPadding" format="boolean" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="SignoutBottomSheetActionButton">
|
||||
<attr name="iconTint" format="color" />
|
||||
<attr name="actionTitle"/>
|
||||
<attr name="leftIcon" />
|
||||
<attr name="textColor" format="color" />
|
||||
</declare-styleable>
|
||||
</resources>
|
||||
|
@ -1048,6 +1048,7 @@
|
||||
<string name="encryption_export_export">Export</string>
|
||||
<string name="encryption_export_notice">Please create a passphrase to encrypt the exported keys. You will need to enter the same passphrase to be able to import the keys.</string>
|
||||
<string name="encryption_export_saved_as">The E2E room keys have been saved to \'%s\'.\n\nWarning: this file may be deleted if the application is uninstalled.</string>
|
||||
<string name="encryption_exported_successfully">Keys successfully exported</string>
|
||||
|
||||
<string name="encryption_message_recovery">Encrypted Messages Recovery</string>
|
||||
<string name="encryption_settings_manage_message_recovery_summary">Manage Key Backup</string>
|
||||
@ -1506,6 +1507,9 @@ Why choose Riot.im?
|
||||
|
||||
<string name="keys_backup_banner_in_progress">Backing up your keys. This may take several minutes…</string>
|
||||
|
||||
|
||||
<string name="secure_backup_setup">Set Up Secure Backup</string>
|
||||
|
||||
<!-- Keys backup info -->
|
||||
<string name="keys_backup_info_keys_all_backup_up">All keys backed up</string>
|
||||
<plurals name="keys_backup_info_keys_backing_up">
|
||||
|
Loading…
Reference in New Issue
Block a user