mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-25 19:05:56 +03:00
Merge pull request #1648 from vector-im/feature/server_recovery_banner
Feature/server recovery banner
This commit is contained in:
commit
a8ad57a9b0
29 changed files with 992 additions and 497 deletions
|
@ -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>)
|
||||||
|
|
||||||
|
|
|
@ -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 ")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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) {}
|
||||||
|
}
|
|
@ -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 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
20
vector/src/main/res/drawable/ic_secure_backup.xml
Normal file
20
vector/src/main/res/drawable/ic_secure_backup.xml
Normal 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>
|
|
@ -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>
|
|
@ -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" />
|
||||||
|
|
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>
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 didn’t set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.</string>
|
<string name="new_recovery_method_popup_description">A new secure message key backup has been detected.\n\nIf you didn’t set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.</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 & 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>
|
||||||
|
|
Loading…
Reference in a new issue