mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-16 02:05:06 +08:00
Add option to reset 4S if lost pass/key
This commit is contained in:
parent
2b90f1395f
commit
c20517599e
@ -27,6 +27,7 @@ import im.vector.app.features.contactsbook.ContactsBookFragment
|
||||
import im.vector.app.features.crypto.keysbackup.settings.KeysBackupSettingsFragment
|
||||
import im.vector.app.features.crypto.quads.SharedSecuredStorageKeyFragment
|
||||
import im.vector.app.features.crypto.quads.SharedSecuredStoragePassphraseFragment
|
||||
import im.vector.app.features.crypto.quads.SharedSecuredStorageResetAllFragment
|
||||
import im.vector.app.features.crypto.recover.BootstrapAccountPasswordFragment
|
||||
import im.vector.app.features.crypto.recover.BootstrapConclusionFragment
|
||||
import im.vector.app.features.crypto.recover.BootstrapConfirmPassphraseFragment
|
||||
@ -530,6 +531,11 @@ interface FragmentModule {
|
||||
@FragmentKey(SharedSecuredStorageKeyFragment::class)
|
||||
fun bindSharedSecuredStorageKeyFragment(fragment: SharedSecuredStorageKeyFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(SharedSecuredStorageResetAllFragment::class)
|
||||
fun bindSharedSecuredStorageResetAllFragment(fragment: SharedSecuredStorageResetAllFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(SetIdentityServerFragment::class)
|
||||
|
@ -17,6 +17,7 @@ package im.vector.app.core.platform
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
@ -86,6 +87,24 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
|
||||
|
||||
open val showExpanded = false
|
||||
|
||||
interface ResultListener {
|
||||
fun onBottomSheetResult(resultCode: Int, data: Any?)
|
||||
|
||||
companion object {
|
||||
const val RESULT_OK = 1
|
||||
const val RESULT_CANCEL = 0
|
||||
}
|
||||
}
|
||||
|
||||
var resultListener : ResultListener? = null
|
||||
var bottomSheetResult: Int = ResultListener.RESULT_OK
|
||||
var bottomSheetResultData: Any? = null
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
resultListener?.onBottomSheetResult(bottomSheetResult, bottomSheetResultData)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val view = inflater.inflate(getLayoutResId(), container, false)
|
||||
unBinder = ButterKnife.bind(this, view)
|
||||
|
@ -24,6 +24,7 @@ import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.withStyledAttributes
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isInvisible
|
||||
@ -107,6 +108,12 @@ class BottomSheetActionButton @JvmOverloads constructor(
|
||||
leftIconImageView.imageTintList = value?.let { ColorStateList.valueOf(value) }
|
||||
}
|
||||
|
||||
var titleTextColor: Int? = null
|
||||
set(value) {
|
||||
field = value
|
||||
value?.let { actionTextView.setTextColor(it) }
|
||||
}
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.item_verification_action, this)
|
||||
ButterKnife.bind(this)
|
||||
@ -120,6 +127,7 @@ class BottomSheetActionButton @JvmOverloads constructor(
|
||||
rightIcon = getDrawable(R.styleable.BottomSheetActionButton_rightIcon)
|
||||
|
||||
tint = getColor(R.styleable.BottomSheetActionButton_tint, ThemeUtils.getColor(context, android.R.attr.textColor))
|
||||
titleTextColor = getColor(R.styleable.BottomSheetActionButton_titleTextColor, ContextCompat.getColor(context, R.color.riotx_accent))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,8 @@ sealed class SharedSecureStorageAction : VectorViewModelAction {
|
||||
object Cancel : SharedSecureStorageAction()
|
||||
data class SubmitPassphrase(val passphrase: String) : SharedSecureStorageAction()
|
||||
data class SubmitKey(val recoveryKey: String) : SharedSecureStorageAction()
|
||||
object ForgotResetAll : SharedSecureStorageAction()
|
||||
object DoResetAll : SharedSecureStorageAction()
|
||||
}
|
||||
|
||||
sealed class SharedSecureStorageViewEvent : VectorViewEvents {
|
||||
@ -40,4 +42,5 @@ sealed class SharedSecureStorageViewEvent : VectorViewEvents {
|
||||
object ShowModalLoading : SharedSecureStorageViewEvent()
|
||||
object HideModalLoading : SharedSecureStorageViewEvent()
|
||||
data class UpdateLoadingState(val waitingData: WaitingViewData) : SharedSecureStorageViewEvent()
|
||||
object ShowResetBottomSheet : SharedSecureStorageViewEvent()
|
||||
}
|
||||
|
@ -31,12 +31,14 @@ import im.vector.app.core.di.ScreenComponent
|
||||
import im.vector.app.core.error.ErrorFormatter
|
||||
import im.vector.app.core.extensions.commitTransaction
|
||||
import im.vector.app.core.platform.SimpleFragmentActivity
|
||||
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
import im.vector.app.features.crypto.recover.SetupMode
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.activity.*
|
||||
import javax.inject.Inject
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class SharedSecureStorageActivity : SimpleFragmentActivity() {
|
||||
class SharedSecureStorageActivity : SimpleFragmentActivity(), VectorBaseBottomSheetDialogFragment.ResultListener {
|
||||
|
||||
@Parcelize
|
||||
data class Args(
|
||||
@ -69,18 +71,22 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() {
|
||||
|
||||
private fun renderState(state: SharedSecureStorageViewState) {
|
||||
if (!state.ready) return
|
||||
val fragment = if (state.hasPassphrase) {
|
||||
if (state.useKey) SharedSecuredStorageKeyFragment::class else SharedSecuredStoragePassphraseFragment::class
|
||||
} else SharedSecuredStorageKeyFragment::class
|
||||
val fragment =
|
||||
when (state.step) {
|
||||
SharedSecureStorageViewState.Step.EnterPassphrase -> SharedSecuredStoragePassphraseFragment::class
|
||||
SharedSecureStorageViewState.Step.EnterKey -> SharedSecuredStorageKeyFragment::class
|
||||
SharedSecureStorageViewState.Step.ResetAll -> SharedSecuredStorageResetAllFragment::class
|
||||
}
|
||||
|
||||
showFragment(fragment, Bundle())
|
||||
}
|
||||
|
||||
private fun observeViewEvents(it: SharedSecureStorageViewEvent?) {
|
||||
when (it) {
|
||||
is SharedSecureStorageViewEvent.Dismiss -> {
|
||||
is SharedSecureStorageViewEvent.Dismiss -> {
|
||||
finish()
|
||||
}
|
||||
is SharedSecureStorageViewEvent.Error -> {
|
||||
is SharedSecureStorageViewEvent.Error -> {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(getString(R.string.dialog_title_error))
|
||||
.setMessage(it.message)
|
||||
@ -92,21 +98,24 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() {
|
||||
}
|
||||
.show()
|
||||
}
|
||||
is SharedSecureStorageViewEvent.ShowModalLoading -> {
|
||||
is SharedSecureStorageViewEvent.ShowModalLoading -> {
|
||||
showWaitingView()
|
||||
}
|
||||
is SharedSecureStorageViewEvent.HideModalLoading -> {
|
||||
is SharedSecureStorageViewEvent.HideModalLoading -> {
|
||||
hideWaitingView()
|
||||
}
|
||||
is SharedSecureStorageViewEvent.UpdateLoadingState -> {
|
||||
is SharedSecureStorageViewEvent.UpdateLoadingState -> {
|
||||
updateWaitingView(it.waitingData)
|
||||
}
|
||||
is SharedSecureStorageViewEvent.FinishSuccess -> {
|
||||
is SharedSecureStorageViewEvent.FinishSuccess -> {
|
||||
val dataResult = Intent()
|
||||
dataResult.putExtra(EXTRA_DATA_RESULT, it.cypherResult)
|
||||
setResult(Activity.RESULT_OK, dataResult)
|
||||
finish()
|
||||
}
|
||||
is SharedSecureStorageViewEvent.ShowResetBottomSheet -> {
|
||||
navigator.open4SSetup(this, SetupMode.HARD_RESET, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,6 +133,7 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() {
|
||||
|
||||
companion object {
|
||||
const val EXTRA_DATA_RESULT = "EXTRA_DATA_RESULT"
|
||||
const val EXTRA_DATA_RESET = "EXTRA_DATA_RESET"
|
||||
const val DEFAULT_RESULT_KEYSTORE_ALIAS = "SharedSecureStorageActivity"
|
||||
|
||||
fun newIntent(context: Context,
|
||||
@ -140,4 +150,12 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBottomSheetResult(resultCode: Int, data: Any?) {
|
||||
if (resultCode == VectorBaseBottomSheetDialogFragment.ResultListener.RESULT_OK) {
|
||||
// the 4S has been reset
|
||||
setResult(Activity.RESULT_OK, Intent().apply { putExtra(EXTRA_DATA_RESET, true) })
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,9 @@ import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.platform.WaitingViewData
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.android.sdk.api.listeners.ProgressListener
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.securestorage.IntegrityResult
|
||||
@ -40,19 +43,26 @@ import org.matrix.android.sdk.api.session.securestorage.KeyInfoResult
|
||||
import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec
|
||||
import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
|
||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.android.sdk.rx.rx
|
||||
import timber.log.Timber
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
data class SharedSecureStorageViewState(
|
||||
val ready: Boolean = false,
|
||||
val hasPassphrase: Boolean = true,
|
||||
val useKey: Boolean = false,
|
||||
val passphraseVisible: Boolean = false,
|
||||
val checkingSSSSAction: Async<Unit> = Uninitialized
|
||||
) : MvRxState
|
||||
val checkingSSSSAction: Async<Unit> = Uninitialized,
|
||||
val step: Step = Step.EnterPassphrase,
|
||||
val activeDeviceCount: Int = 0,
|
||||
val showResetAllAction: Boolean = false,
|
||||
val userId: String = ""
|
||||
) : MvRxState {
|
||||
enum class Step {
|
||||
EnterPassphrase,
|
||||
EnterKey,
|
||||
ResetAll
|
||||
}
|
||||
}
|
||||
|
||||
class SharedSecureStorageViewModel @AssistedInject constructor(
|
||||
@Assisted initialState: SharedSecureStorageViewState,
|
||||
@ -67,6 +77,10 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
init {
|
||||
|
||||
setState {
|
||||
copy(userId = session.myUserId)
|
||||
}
|
||||
val isValid = session.sharedSecretStorageService.checkShouldBeAbleToAccessSecrets(args.requestedSecrets, args.keyId) is IntegrityResult.Success
|
||||
if (!isValid) {
|
||||
_viewEvents.post(
|
||||
@ -86,20 +100,30 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
|
||||
if (info.content.passphrase != null) {
|
||||
setState {
|
||||
copy(
|
||||
ready = true,
|
||||
hasPassphrase = true,
|
||||
useKey = false
|
||||
ready = true,
|
||||
step = SharedSecureStorageViewState.Step.EnterPassphrase
|
||||
)
|
||||
}
|
||||
} else {
|
||||
setState {
|
||||
copy(
|
||||
hasPassphrase = false,
|
||||
ready = true,
|
||||
hasPassphrase = false
|
||||
step = SharedSecureStorageViewState.Step.EnterKey
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
session.rx()
|
||||
.liveUserCryptoDevices(session.myUserId)
|
||||
.distinctUntilChanged()
|
||||
.execute {
|
||||
copy(
|
||||
activeDeviceCount = it.invoke()?.size ?: 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handle(action: SharedSecureStorageAction) = withState {
|
||||
@ -110,27 +134,52 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
|
||||
SharedSecureStorageAction.UseKey -> handleUseKey()
|
||||
is SharedSecureStorageAction.SubmitKey -> handleSubmitKey(action)
|
||||
SharedSecureStorageAction.Back -> handleBack()
|
||||
SharedSecureStorageAction.ForgotResetAll -> handleResetAll()
|
||||
SharedSecureStorageAction.DoResetAll -> handleDoResetAll()
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun handleDoResetAll() {
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.ShowResetBottomSheet)
|
||||
}
|
||||
|
||||
private fun handleResetAll() {
|
||||
setState {
|
||||
copy(
|
||||
step = SharedSecureStorageViewState.Step.ResetAll
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUseKey() {
|
||||
setState {
|
||||
copy(
|
||||
useKey = true
|
||||
step = SharedSecureStorageViewState.Step.EnterKey
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBack() = withState { state ->
|
||||
if (state.checkingSSSSAction is Loading) return@withState // ignore
|
||||
if (state.hasPassphrase && state.useKey) {
|
||||
setState {
|
||||
copy(
|
||||
useKey = false
|
||||
)
|
||||
when (state.step) {
|
||||
SharedSecureStorageViewState.Step.EnterKey -> {
|
||||
setState {
|
||||
copy(
|
||||
step = SharedSecureStorageViewState.Step.EnterPassphrase
|
||||
)
|
||||
}
|
||||
}
|
||||
SharedSecureStorageViewState.Step.ResetAll -> {
|
||||
setState {
|
||||
copy(
|
||||
step = if (state.hasPassphrase) SharedSecureStorageViewState.Step.EnterPassphrase
|
||||
else SharedSecureStorageViewState.Step.EnterKey
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.Dismiss)
|
||||
}
|
||||
} else {
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.Dismiss)
|
||||
}
|
||||
}
|
||||
|
||||
@ -158,6 +207,7 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
|
||||
val keySpec = RawBytesKeySpec.fromRecoveryKey(recoveryKey) ?: return@launch Unit.also {
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.KeyInlineError(stringProvider.getString(R.string.bootstrap_invalid_recovery_key)))
|
||||
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
|
||||
setState { copy(checkingSSSSAction = Fail(IllegalArgumentException(stringProvider.getString(R.string.bootstrap_invalid_recovery_key)))) }
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
|
@ -27,9 +27,9 @@ import com.jakewharton.rxbinding3.widget.textChanges
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.utils.startImportTextFromFileIntent
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.android.synthetic.main.fragment_ssss_access_from_key.*
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -63,6 +63,10 @@ class SharedSecuredStorageKeyFragment @Inject constructor() : VectorBaseFragment
|
||||
|
||||
ssss_key_use_file.debouncedClicks { startImportTextFromFileIntent(this, IMPORT_FILE_REQ) }
|
||||
|
||||
ssss_key_reset.clickableView.debouncedClicks {
|
||||
sharedViewModel.handle(SharedSecureStorageAction.ForgotResetAll)
|
||||
}
|
||||
|
||||
sharedViewModel.observeViewEvents {
|
||||
when (it) {
|
||||
is SharedSecureStorageViewEvent.KeyInlineError -> {
|
||||
|
@ -74,6 +74,10 @@ class SharedSecuredStoragePassphraseFragment @Inject constructor(
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
ssss_passphrase_reset.clickableView.debouncedClicks {
|
||||
sharedViewModel.handle(SharedSecureStorageAction.ForgotResetAll)
|
||||
}
|
||||
|
||||
sharedViewModel.observeViewEvents {
|
||||
when (it) {
|
||||
is SharedSecureStorageViewEvent.InlineError -> {
|
||||
|
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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.app.features.crypto.quads
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.mvrx.activityViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.roommemberprofile.devices.DeviceListBottomSheet
|
||||
import kotlinx.android.synthetic.main.fragment_ssss_reset_all.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class SharedSecuredStorageResetAllFragment @Inject constructor(
|
||||
private val stringProvider: StringProvider
|
||||
) : VectorBaseFragment() {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_ssss_reset_all
|
||||
|
||||
val sharedViewModel: SharedSecureStorageViewModel by activityViewModel()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
ssss_reset_button_reset.debouncedClicks {
|
||||
sharedViewModel.handle(SharedSecureStorageAction.DoResetAll)
|
||||
}
|
||||
|
||||
ssss_reset_button_cancel.debouncedClicks {
|
||||
sharedViewModel.handle(SharedSecureStorageAction.Back)
|
||||
}
|
||||
|
||||
ssss_reset_other_devices.debouncedClicks {
|
||||
withState(sharedViewModel) {
|
||||
DeviceListBottomSheet.newInstance(it.userId, false).show(childFragmentManager, "DEV_LIST")
|
||||
}
|
||||
}
|
||||
|
||||
sharedViewModel.subscribe(this) {
|
||||
if (it.activeDeviceCount == 0) {
|
||||
ssss_reset_other_devices.isVisible = false
|
||||
} else {
|
||||
ssss_reset_other_devices.isVisible = true
|
||||
ssss_reset_other_devices.text = stringProvider.getQuantityString(
|
||||
R.plurals.secure_backup_reset_devices_you_can_verify,
|
||||
it.activeDeviceCount,
|
||||
it.activeDeviceCount
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -45,8 +45,10 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
|
||||
@Parcelize
|
||||
data class Args(
|
||||
val initCrossSigningOnly: Boolean,
|
||||
val forceReset4S: Boolean
|
||||
// val initCrossSigningOnly: Boolean,
|
||||
val setUpMode: SetupMode = SetupMode.NORMAL
|
||||
// val forceReset4S: Boolean,
|
||||
// val resetAllUnknownSecrets: Boolean
|
||||
) : Parcelable
|
||||
|
||||
override val showExpanded = true
|
||||
@ -66,7 +68,10 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewModel.observeViewEvents { event ->
|
||||
when (event) {
|
||||
is BootstrapViewEvents.Dismiss -> dismiss()
|
||||
is BootstrapViewEvents.Dismiss -> {
|
||||
bottomSheetResult = if (event.success) ResultListener.RESULT_OK else ResultListener.RESULT_CANCEL
|
||||
dismiss()
|
||||
}
|
||||
is BootstrapViewEvents.ModalError -> {
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.dialog_title_error)
|
||||
@ -90,6 +95,7 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
.setMessage(R.string.bootstrap_cancel_text)
|
||||
.setPositiveButton(R.string._continue, null)
|
||||
.setNegativeButton(R.string.skip) { _, _ ->
|
||||
bottomSheetResult = ResultListener.RESULT_CANCEL
|
||||
dismiss()
|
||||
}
|
||||
.show()
|
||||
@ -181,16 +187,15 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
|
||||
const val EXTRA_ARGS = "EXTRA_ARGS"
|
||||
|
||||
fun show(fragmentManager: FragmentManager, initCrossSigningOnly: Boolean, forceReset4S: Boolean) {
|
||||
BootstrapBottomSheet().apply {
|
||||
fun show(fragmentManager: FragmentManager, mode: SetupMode): BootstrapBottomSheet {
|
||||
return BootstrapBottomSheet().apply {
|
||||
isCancelable = false
|
||||
arguments = Bundle().apply {
|
||||
this.putParcelable(EXTRA_ARGS, Args(
|
||||
initCrossSigningOnly,
|
||||
forceReset4S
|
||||
))
|
||||
this.putParcelable(EXTRA_ARGS, Args(setUpMode = mode))
|
||||
}
|
||||
}.show(fragmentManager, "BootstrapBottomSheet")
|
||||
}.also {
|
||||
it.show(fragmentManager, "BootstrapBottomSheet")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,10 +69,12 @@ interface BootstrapProgressListener {
|
||||
|
||||
data class Params(
|
||||
val userPasswordAuth: UserPasswordAuth? = null,
|
||||
val initOnlyCrossSigning: Boolean = false,
|
||||
// val initOnlyCrossSigning: Boolean = false,
|
||||
val progressListener: BootstrapProgressListener? = null,
|
||||
val passphrase: String?,
|
||||
val keySpec: SsssKeySpec? = null
|
||||
val keySpec: SsssKeySpec? = null,
|
||||
// val resetAllIfNeeded: Boolean = false,
|
||||
val setupMode: SetupMode
|
||||
)
|
||||
|
||||
// TODO Rename to CreateServerRecovery
|
||||
@ -84,9 +86,13 @@ class BootstrapCrossSigningTask @Inject constructor(
|
||||
override suspend fun execute(params: Params): BootstrapResult {
|
||||
val crossSigningService = session.cryptoService().crossSigningService()
|
||||
|
||||
Timber.d("## BootstrapCrossSigningTask: initXSOnly:${params.initOnlyCrossSigning} Starting...")
|
||||
Timber.d("## BootstrapCrossSigningTask: mode:${params.setupMode} Starting...")
|
||||
// Ensure cross-signing is initialized. Due to migration it is maybe not always correctly initialized
|
||||
if (!crossSigningService.isCrossSigningInitialized()) {
|
||||
|
||||
val shouldSetCrossSigning = !crossSigningService.isCrossSigningInitialized()
|
||||
|| (params.setupMode == SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET && !crossSigningService.allPrivateKeysKnown())
|
||||
|| (params.setupMode == SetupMode.HARD_RESET)
|
||||
if (shouldSetCrossSigning) {
|
||||
Timber.d("## BootstrapCrossSigningTask: Cross signing not enabled, so initialize")
|
||||
params.progressListener?.onProgress(
|
||||
WaitingViewData(
|
||||
@ -99,7 +105,7 @@ class BootstrapCrossSigningTask @Inject constructor(
|
||||
awaitCallback<Unit> {
|
||||
crossSigningService.initializeCrossSigning(params.userPasswordAuth, it)
|
||||
}
|
||||
if (params.initOnlyCrossSigning) {
|
||||
if (params.setupMode == SetupMode.CROSS_SIGNING_ONLY) {
|
||||
return BootstrapResult.SuccessCrossSigningOnly
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
@ -107,7 +113,7 @@ class BootstrapCrossSigningTask @Inject constructor(
|
||||
}
|
||||
} else {
|
||||
Timber.d("## BootstrapCrossSigningTask: Cross signing already setup, go to 4S setup")
|
||||
if (params.initOnlyCrossSigning) {
|
||||
if (params.setupMode == SetupMode.CROSS_SIGNING_ONLY) {
|
||||
// not sure how this can happen??
|
||||
return handleInitializeXSigningError(IllegalArgumentException("Cross signing already setup"))
|
||||
}
|
||||
@ -236,7 +242,13 @@ class BootstrapCrossSigningTask @Inject constructor(
|
||||
val serverVersion = awaitCallback<KeysVersionResult?> {
|
||||
session.cryptoService().keysBackupService().getCurrentVersion(it)
|
||||
}
|
||||
if (serverVersion == null) {
|
||||
|
||||
val knownMegolmSecret = session.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()
|
||||
val isMegolmBackupSecretKnown = knownMegolmSecret != null && knownMegolmSecret.version == serverVersion?.version
|
||||
val shouldCreateKeyBackup = serverVersion == null
|
||||
|| (params.setupMode == SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET && !isMegolmBackupSecretKnown)
|
||||
|| (params.setupMode == SetupMode.HARD_RESET)
|
||||
if (shouldCreateKeyBackup) {
|
||||
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Create megolm backup")
|
||||
val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
|
||||
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
|
||||
@ -260,16 +272,15 @@ class BootstrapCrossSigningTask @Inject constructor(
|
||||
} else {
|
||||
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Existing megolm backup found")
|
||||
// ensure we store existing backup secret if we have it!
|
||||
val knownSecret = session.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()
|
||||
if (knownSecret != null && knownSecret.version == serverVersion.version) {
|
||||
if (isMegolmBackupSecretKnown) {
|
||||
// check it matches
|
||||
val isValid = awaitCallback<Boolean> {
|
||||
session.cryptoService().keysBackupService().isValidRecoveryKeyForCurrentVersion(knownSecret.recoveryKey, it)
|
||||
session.cryptoService().keysBackupService().isValidRecoveryKeyForCurrentVersion(knownMegolmSecret!!.recoveryKey, it)
|
||||
}
|
||||
if (isValid) {
|
||||
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Megolm key valid and known")
|
||||
awaitCallback<Unit> {
|
||||
extractCurveKeyFromRecoveryKey(knownSecret.recoveryKey)?.toBase64NoPadding()?.let { secret ->
|
||||
extractCurveKeyFromRecoveryKey(knownMegolmSecret!!.recoveryKey)?.toBase64NoPadding()?.let { secret ->
|
||||
ssssService.storeSecret(
|
||||
KEYBACKUP_SECRET_SSSS_NAME,
|
||||
secret,
|
||||
@ -286,7 +297,7 @@ class BootstrapCrossSigningTask @Inject constructor(
|
||||
Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup")
|
||||
}
|
||||
|
||||
Timber.d("## BootstrapCrossSigningTask: initXSOnly:${params.initOnlyCrossSigning} Finished")
|
||||
Timber.d("## BootstrapCrossSigningTask: mode:${params.setupMode} Finished")
|
||||
return BootstrapResult.Success(keyInfo)
|
||||
}
|
||||
|
||||
|
@ -34,6 +34,8 @@ import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.platform.WaitingViewData
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.login.ReAuthHelper
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec
|
||||
@ -41,8 +43,6 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionR
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.OutputStream
|
||||
|
||||
class BootstrapSharedViewModel @AssistedInject constructor(
|
||||
@ -69,46 +69,52 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||
|
||||
init {
|
||||
|
||||
if (args.forceReset4S) {
|
||||
setState {
|
||||
copy(step = BootstrapStep.FirstForm(keyBackUpExist = false, reset = true))
|
||||
}
|
||||
} else if (args.initCrossSigningOnly) {
|
||||
// Go straight to account password
|
||||
setState {
|
||||
copy(step = BootstrapStep.AccountPassword(false))
|
||||
}
|
||||
} else {
|
||||
// need to check if user have an existing keybackup
|
||||
setState {
|
||||
copy(step = BootstrapStep.CheckingMigration)
|
||||
}
|
||||
|
||||
// We need to check if there is an existing backup
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val version = awaitCallback<KeysVersionResult?> {
|
||||
session.cryptoService().keysBackupService().getCurrentVersion(it)
|
||||
when (args.setUpMode) {
|
||||
SetupMode.PASSPHRASE_RESET,
|
||||
SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET,
|
||||
SetupMode.HARD_RESET -> {
|
||||
setState {
|
||||
copy(step = BootstrapStep.FirstForm(keyBackUpExist = false, reset = true))
|
||||
}
|
||||
if (version == null) {
|
||||
// we just resume plain bootstrap
|
||||
doesKeyBackupExist = false
|
||||
setState {
|
||||
copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist))
|
||||
}
|
||||
SetupMode.CROSS_SIGNING_ONLY -> {
|
||||
// Go straight to account password
|
||||
setState {
|
||||
copy(step = BootstrapStep.AccountPassword(false))
|
||||
}
|
||||
}
|
||||
SetupMode.NORMAL -> {
|
||||
// need to check if user have an existing keybackup
|
||||
setState {
|
||||
copy(step = BootstrapStep.CheckingMigration)
|
||||
}
|
||||
|
||||
// We need to check if there is an existing backup
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val version = awaitCallback<KeysVersionResult?> {
|
||||
session.cryptoService().keysBackupService().getCurrentVersion(it)
|
||||
}
|
||||
} else {
|
||||
// we need to get existing backup passphrase/key and convert to SSSS
|
||||
val keyVersion = awaitCallback<KeysVersionResult?> {
|
||||
session.cryptoService().keysBackupService().getVersion(version.version ?: "", it)
|
||||
}
|
||||
if (keyVersion == null) {
|
||||
// strange case... just finish?
|
||||
_viewEvents.post(BootstrapViewEvents.Dismiss)
|
||||
} else {
|
||||
doesKeyBackupExist = true
|
||||
isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null
|
||||
if (version == null) {
|
||||
// we just resume plain bootstrap
|
||||
doesKeyBackupExist = false
|
||||
setState {
|
||||
copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist))
|
||||
}
|
||||
} else {
|
||||
// we need to get existing backup passphrase/key and convert to SSSS
|
||||
val keyVersion = awaitCallback<KeysVersionResult?> {
|
||||
session.cryptoService().keysBackupService().getVersion(version.version ?: "", it)
|
||||
}
|
||||
if (keyVersion == null) {
|
||||
// strange case... just finish?
|
||||
_viewEvents.post(BootstrapViewEvents.Dismiss(false))
|
||||
} else {
|
||||
doesKeyBackupExist = true
|
||||
isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null
|
||||
setState {
|
||||
copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -234,7 +240,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
BootstrapActions.Completed -> {
|
||||
_viewEvents.post(BootstrapViewEvents.Dismiss)
|
||||
_viewEvents.post(BootstrapViewEvents.Dismiss(true))
|
||||
}
|
||||
BootstrapActions.GoToCompleted -> {
|
||||
setState {
|
||||
@ -395,16 +401,16 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||
bootstrapTask.invoke(this,
|
||||
Params(
|
||||
userPasswordAuth = userPasswordAuth,
|
||||
initOnlyCrossSigning = args.initCrossSigningOnly,
|
||||
progressListener = progressListener,
|
||||
passphrase = state.passphrase,
|
||||
keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } }
|
||||
keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } },
|
||||
setupMode = args.setUpMode
|
||||
)
|
||||
) { bootstrapResult ->
|
||||
when (bootstrapResult) {
|
||||
is BootstrapResult.SuccessCrossSigningOnly -> {
|
||||
is BootstrapResult.SuccessCrossSigningOnly -> {
|
||||
// TPD
|
||||
_viewEvents.post(BootstrapViewEvents.Dismiss)
|
||||
_viewEvents.post(BootstrapViewEvents.Dismiss(true))
|
||||
}
|
||||
is BootstrapResult.Success -> {
|
||||
setState {
|
||||
@ -428,7 +434,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||
}
|
||||
is BootstrapResult.UnsupportedAuthFlow -> {
|
||||
_viewEvents.post(BootstrapViewEvents.ModalError(stringProvider.getString(R.string.auth_flow_not_supported)))
|
||||
_viewEvents.post(BootstrapViewEvents.Dismiss)
|
||||
_viewEvents.post(BootstrapViewEvents.Dismiss(false))
|
||||
}
|
||||
is BootstrapResult.InvalidPasswordError -> {
|
||||
// it's a bad password
|
||||
@ -558,7 +564,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||
override fun create(viewModelContext: ViewModelContext, state: BootstrapViewState): BootstrapSharedViewModel? {
|
||||
val fragment: BootstrapBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
|
||||
val args: BootstrapBottomSheet.Args = fragment.arguments?.getParcelable(BootstrapBottomSheet.EXTRA_ARGS)
|
||||
?: BootstrapBottomSheet.Args(initCrossSigningOnly = true, forceReset4S = false)
|
||||
?: BootstrapBottomSheet.Args(SetupMode.CROSS_SIGNING_ONLY)
|
||||
return fragment.bootstrapViewModelFactory.create(state, args)
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ package im.vector.app.features.crypto.recover
|
||||
import im.vector.app.core.platform.VectorViewEvents
|
||||
|
||||
sealed class BootstrapViewEvents : VectorViewEvents {
|
||||
object Dismiss : BootstrapViewEvents()
|
||||
data class Dismiss(val success: Boolean) : BootstrapViewEvents()
|
||||
data class ModalError(val error: String) : BootstrapViewEvents()
|
||||
object RecoveryKeySaved: BootstrapViewEvents()
|
||||
data class SkipBootstrap(val genKeyOption: Boolean = true): BootstrapViewEvents()
|
||||
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.app.features.crypto.recover
|
||||
|
||||
enum class SetupMode {
|
||||
|
||||
/**
|
||||
* Only setup cross signing, no 4S or megolm backup
|
||||
*/
|
||||
CROSS_SIGNING_ONLY,
|
||||
|
||||
/**
|
||||
* Normal setup mode.
|
||||
*/
|
||||
NORMAL,
|
||||
|
||||
/**
|
||||
* Only reset the 4S passphrase/key, but do not touch
|
||||
* to existing cross-signing or megolm backup
|
||||
* It take the local known secrets and put them in 4S
|
||||
*/
|
||||
PASSPHRASE_RESET,
|
||||
|
||||
/**
|
||||
* Resets the passphrase/key, and all missing secrets
|
||||
* are re-created. Meaning that if cross signing is setup and the secrets
|
||||
* keys are not known, cross signing will be reset (if secret is known we just keep same cross signing)
|
||||
* Same apply to megolm
|
||||
*/
|
||||
PASSPHRASE_AND_NEEDED_SECRETS_RESET,
|
||||
|
||||
/**
|
||||
* Resets the passphrase/key, cross signing and megolm backup
|
||||
*/
|
||||
HARD_RESET
|
||||
}
|
@ -31,4 +31,5 @@ sealed class VerificationAction : VectorViewModelAction {
|
||||
object SkipVerification : VerificationAction()
|
||||
object VerifyFromPassphrase : VerificationAction()
|
||||
data class GotResultFromSsss(val cypherData: String, val alias: String) : VerificationAction()
|
||||
object SecuredStorageHasBeenReseted : VerificationAction()
|
||||
}
|
||||
|
@ -48,6 +48,7 @@ import im.vector.app.features.crypto.verification.qrconfirmation.VerificationQrS
|
||||
import im.vector.app.features.crypto.verification.request.VerificationRequestFragment
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.settings.VectorSettingsActivity
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
||||
@ -55,7 +56,6 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_S
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlin.reflect.KClass
|
||||
@ -76,6 +76,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
|
||||
@Inject
|
||||
lateinit var verificationViewModelFactory: VerificationBottomSheetViewModel.Factory
|
||||
|
||||
@Inject
|
||||
lateinit var avatarRenderer: AvatarRenderer
|
||||
|
||||
@ -146,8 +147,13 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (resultCode == Activity.RESULT_OK && requestCode == SECRET_REQUEST_CODE) {
|
||||
data?.getStringExtra(SharedSecureStorageActivity.EXTRA_DATA_RESULT)?.let {
|
||||
viewModel.handle(VerificationAction.GotResultFromSsss(it, SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS))
|
||||
val result = data?.getStringExtra(SharedSecureStorageActivity.EXTRA_DATA_RESULT)
|
||||
val reseted = data?.getBooleanExtra(SharedSecureStorageActivity.EXTRA_DATA_RESET, false) ?: false
|
||||
if (result != null) {
|
||||
viewModel.handle(VerificationAction.GotResultFromSsss(result, SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS))
|
||||
} else if (reseted) {
|
||||
// all have been reset, so we are verified?
|
||||
viewModel.handle(VerificationAction.SecuredStorageHasBeenReseted)
|
||||
}
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
@ -182,6 +188,17 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
if (state.quadsHasBeenReseted) {
|
||||
showFragment(VerificationConclusionFragment::class, Bundle().apply {
|
||||
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(
|
||||
isSuccessFull = true,
|
||||
isMe = true,
|
||||
cancelReason = null
|
||||
))
|
||||
})
|
||||
return@withState
|
||||
}
|
||||
|
||||
if (state.userThinkItsNotHim) {
|
||||
otherUserNameText.text = getString(R.string.dialog_title_warning)
|
||||
showFragment(VerificationNotMeFragment::class, Bundle())
|
||||
@ -356,6 +373,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun forSelfVerification(session: Session, outgoingRequest: String): VerificationBottomSheet {
|
||||
return VerificationBottomSheet().apply {
|
||||
arguments = Bundle().apply {
|
||||
|
@ -31,6 +31,7 @@ import im.vector.app.R
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||
@ -74,7 +75,8 @@ data class VerificationBottomSheetViewState(
|
||||
val currentDeviceCanCrossSign: Boolean = false,
|
||||
val userWantsToCancel: Boolean = false,
|
||||
val userThinkItsNotHim: Boolean = false,
|
||||
val quadSContainsSecrets: Boolean = true
|
||||
val quadSContainsSecrets: Boolean = true,
|
||||
val quadsHasBeenReseted: Boolean = false
|
||||
) : MvRxState
|
||||
|
||||
class VerificationBottomSheetViewModel @AssistedInject constructor(
|
||||
@ -349,6 +351,14 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
|
||||
is VerificationAction.GotResultFromSsss -> {
|
||||
handleSecretBackFromSSSS(action)
|
||||
}
|
||||
VerificationAction.SecuredStorageHasBeenReseted -> {
|
||||
if (session.cryptoService().crossSigningService().allPrivateKeysKnown()) {
|
||||
setState {
|
||||
copy(quadsHasBeenReseted = true)
|
||||
}
|
||||
}
|
||||
Unit
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
@ -393,7 +403,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
private fun tentativeRestoreBackup(res: Map<String, String>?) {
|
||||
viewModelScope.launch {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val secret = res?.get(KEYBACKUP_SECRET_SSSS_NAME) ?: return@launch Unit.also {
|
||||
Timber.v("## Keybackup secret not restored from SSSS")
|
||||
|
@ -30,6 +30,7 @@ import im.vector.app.R
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.core.error.fatalError
|
||||
import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
import im.vector.app.core.utils.toast
|
||||
import im.vector.app.features.call.conference.JitsiCallViewModel
|
||||
import im.vector.app.features.call.conference.VectorJitsiActivity
|
||||
@ -37,6 +38,7 @@ import im.vector.app.features.createdirect.CreateDirectRoomActivity
|
||||
import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||
import im.vector.app.features.crypto.keysbackup.setup.KeysBackupSetupActivity
|
||||
import im.vector.app.features.crypto.recover.BootstrapBottomSheet
|
||||
import im.vector.app.features.crypto.recover.SetupMode
|
||||
import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
|
||||
import im.vector.app.features.crypto.verification.VerificationBottomSheet
|
||||
import im.vector.app.features.debug.DebugMenuActivity
|
||||
@ -153,7 +155,10 @@ class DefaultNavigator @Inject constructor(
|
||||
|
||||
override fun upgradeSessionSecurity(context: Context, initCrossSigningOnly: Boolean) {
|
||||
if (context is VectorBaseActivity) {
|
||||
BootstrapBottomSheet.show(context.supportFragmentManager, initCrossSigningOnly, false)
|
||||
BootstrapBottomSheet.show(
|
||||
context.supportFragmentManager,
|
||||
if (initCrossSigningOnly) SetupMode.CROSS_SIGNING_ONLY else SetupMode.NORMAL
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -226,13 +231,21 @@ class DefaultNavigator @Inject constructor(
|
||||
// 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, initCrossSigningOnly = false, forceReset4S = false)
|
||||
BootstrapBottomSheet.show(context.supportFragmentManager, SetupMode.NORMAL)
|
||||
} else {
|
||||
context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun open4SSetup(context: Context, setupMode: SetupMode, listener: VectorBaseBottomSheetDialogFragment.ResultListener?) {
|
||||
if (context is VectorBaseActivity) {
|
||||
BootstrapBottomSheet.show(context.supportFragmentManager, setupMode).also {
|
||||
it.resultListener = listener
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun openKeysBackupManager(context: Context) {
|
||||
context.startActivity(KeysBackupManageActivity.intent(context))
|
||||
}
|
||||
|
@ -21,6 +21,8 @@ import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.core.util.Pair
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
import im.vector.app.features.crypto.recover.SetupMode
|
||||
import im.vector.app.features.home.room.detail.widget.WidgetRequestCodes
|
||||
import im.vector.app.features.media.AttachmentData
|
||||
import im.vector.app.features.pin.PinActivity
|
||||
@ -71,6 +73,8 @@ interface Navigator {
|
||||
|
||||
fun openKeysBackupSetup(context: Context, showManualExport: Boolean)
|
||||
|
||||
fun open4SSetup(context: Context, setupMode: SetupMode, listener: VectorBaseBottomSheetDialogFragment.ResultListener?)
|
||||
|
||||
fun openKeysBackupManager(context: Context)
|
||||
|
||||
fun openGroupDetail(groupId: String, context: Context, buildTask: Boolean = false)
|
||||
|
@ -18,6 +18,7 @@ package im.vector.app.features.roommemberprofile.devices
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.KeyEvent
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.airbnb.mvrx.MvRx
|
||||
@ -29,6 +30,7 @@ import im.vector.app.core.extensions.commitTransaction
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
import im.vector.app.features.crypto.verification.VerificationBottomSheet
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import javax.inject.Inject
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@ -104,10 +106,16 @@ class DeviceListBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Args(
|
||||
val userId: String,
|
||||
val allowDeviceAction: Boolean
|
||||
) : Parcelable
|
||||
|
||||
companion object {
|
||||
fun newInstance(userId: String): DeviceListBottomSheet {
|
||||
fun newInstance(userId: String, allowDeviceAction: Boolean = true): DeviceListBottomSheet {
|
||||
val args = Bundle()
|
||||
args.putString(MvRx.KEY_ARG, userId)
|
||||
args.putParcelable(MvRx.KEY_ARG, Args(userId, allowDeviceAction))
|
||||
return DeviceListBottomSheet().apply { arguments = args }
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.airbnb.mvrx.args
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.app.core.di.HasScreenInjector
|
||||
@ -44,24 +45,24 @@ data class DeviceListViewState(
|
||||
) : MvRxState
|
||||
|
||||
class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted private val initialState: DeviceListViewState,
|
||||
@Assisted private val userId: String,
|
||||
@Assisted private val args: DeviceListBottomSheet.Args,
|
||||
private val session: Session)
|
||||
: VectorViewModel<DeviceListViewState, DeviceListAction, DeviceListBottomSheetViewEvents>(initialState) {
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(initialState: DeviceListViewState, userId: String): DeviceListBottomSheetViewModel
|
||||
fun create(initialState: DeviceListViewState, args: DeviceListBottomSheet.Args): DeviceListBottomSheetViewModel
|
||||
}
|
||||
|
||||
init {
|
||||
session.rx().liveUserCryptoDevices(userId)
|
||||
session.rx().liveUserCryptoDevices(args.userId)
|
||||
.execute {
|
||||
copy(cryptoDevices = it).also {
|
||||
refreshSelectedId()
|
||||
}
|
||||
}
|
||||
|
||||
session.rx().liveCrossSigningInfo(userId)
|
||||
session.rx().liveCrossSigningInfo(args.userId)
|
||||
.execute {
|
||||
copy(memberCrossSigningKey = it.invoke()?.getOrNull())
|
||||
}
|
||||
@ -88,6 +89,7 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva
|
||||
}
|
||||
|
||||
private fun selectDevice(action: DeviceListAction.SelectDevice) {
|
||||
if (!args.allowDeviceAction) return
|
||||
setState {
|
||||
copy(selectedDevice = action.device)
|
||||
}
|
||||
@ -100,8 +102,9 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva
|
||||
}
|
||||
|
||||
private fun manuallyVerify(action: DeviceListAction.ManuallyVerify) {
|
||||
session.cryptoService().verificationService().beginKeyVerification(VerificationMethod.SAS, userId, action.deviceId, null)?.let { txID ->
|
||||
_viewEvents.post(DeviceListBottomSheetViewEvents.Verify(userId, txID))
|
||||
if (!args.allowDeviceAction) return
|
||||
session.cryptoService().verificationService().beginKeyVerification(VerificationMethod.SAS, args.userId, action.deviceId, null)?.let { txID ->
|
||||
_viewEvents.post(DeviceListBottomSheetViewEvents.Verify(args.userId, txID))
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,12 +112,12 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva
|
||||
@JvmStatic
|
||||
override fun create(viewModelContext: ViewModelContext, state: DeviceListViewState): DeviceListBottomSheetViewModel? {
|
||||
val fragment: DeviceListBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
|
||||
val userId = viewModelContext.args<String>()
|
||||
return fragment.viewModelFactory.create(state, userId)
|
||||
val args = viewModelContext.args<DeviceListBottomSheet.Args>()
|
||||
return fragment.viewModelFactory.create(state, args)
|
||||
}
|
||||
|
||||
override fun initialState(viewModelContext: ViewModelContext): DeviceListViewState? {
|
||||
val userId = viewModelContext.args<String>()
|
||||
val userId = viewModelContext.args<DeviceListBottomSheet.Args>().userId
|
||||
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
|
||||
return session.getUser(userId)?.toMatrixItem()?.let {
|
||||
DeviceListViewState(
|
||||
|
@ -53,6 +53,7 @@ import im.vector.app.features.crypto.keys.KeysExporter
|
||||
import im.vector.app.features.crypto.keys.KeysImporter
|
||||
import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||
import im.vector.app.features.crypto.recover.BootstrapBottomSheet
|
||||
import im.vector.app.features.crypto.recover.SetupMode
|
||||
import im.vector.app.features.navigation.Navigator
|
||||
import im.vector.app.features.pin.PinActivity
|
||||
import im.vector.app.features.pin.PinCodeStore
|
||||
@ -193,7 +194,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
||||
secureBackupCategory.isVisible = true
|
||||
secureBackupPreference.title = getString(R.string.settings_secure_backup_setup)
|
||||
secureBackupPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
BootstrapBottomSheet.show(parentFragmentManager, initCrossSigningOnly = false, forceReset4S = false)
|
||||
BootstrapBottomSheet.show(parentFragmentManager, SetupMode.NORMAL)
|
||||
true
|
||||
}
|
||||
} else {
|
||||
@ -212,7 +213,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
||||
secureBackupCategory.isVisible = true
|
||||
secureBackupPreference.title = getString(R.string.settings_secure_backup_reset)
|
||||
secureBackupPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
BootstrapBottomSheet.show(parentFragmentManager, initCrossSigningOnly = false, forceReset4S = true)
|
||||
BootstrapBottomSheet.show(parentFragmentManager, SetupMode.PASSPHRASE_RESET)
|
||||
true
|
||||
}
|
||||
} else if (!info.megolmSecretKnown) {
|
||||
|
@ -44,6 +44,7 @@ import im.vector.app.core.extensions.queryExportKeys
|
||||
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
import im.vector.app.features.crypto.keysbackup.setup.KeysBackupSetupActivity
|
||||
import im.vector.app.features.crypto.recover.BootstrapBottomSheet
|
||||
import im.vector.app.features.crypto.recover.SetupMode
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -121,7 +122,7 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(),
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
setupRecoveryButton.action = {
|
||||
BootstrapBottomSheet.show(parentFragmentManager, initCrossSigningOnly = false, forceReset4S = false)
|
||||
BootstrapBottomSheet.show(parentFragmentManager, SetupMode.NORMAL)
|
||||
}
|
||||
|
||||
exitAnywayButton.action = {
|
||||
|
@ -15,11 +15,11 @@
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:src="@drawable/ic_security_key_24dp"
|
||||
android:tint="?riotx_text_primary"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/ssss_restore_with_key"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/ssss_restore_with_key"
|
||||
android:src="@drawable/ic_security_key_24dp" />
|
||||
app:layout_constraintTop_toTopOf="@+id/ssss_restore_with_key" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/ssss_restore_with_key"
|
||||
@ -56,8 +56,8 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:errorEnabled="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
@ -97,16 +97,32 @@
|
||||
tools:ignore="MissingConstraints" />
|
||||
|
||||
<androidx.constraintlayout.helper.widget.Flow
|
||||
android:id="@+id/ssss_key_flow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginBottom="@dimen/layout_vertical_margin_big"
|
||||
app:constraint_referenced_ids="ssss_key_use_file,ssss_key_submit"
|
||||
app:flow_horizontalStyle="spread_inside"
|
||||
app:flow_wrapMode="chain"
|
||||
app:layout_constraintBottom_toTopOf="@+id/ssss_key_reset"
|
||||
app:layout_constraintTop_toBottomOf="@+id/ssss_key_enter_til"
|
||||
app:layout_goneMarginBottom="@dimen/layout_vertical_margin_big" />
|
||||
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/ssss_key_reset"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
app:actionTitle="@string/bad_passphrase_key_reset_all_action"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/ssss_key_enter_til" />
|
||||
app:layout_constraintTop_toBottomOf="@id/ssss_key_flow"
|
||||
app:leftIcon="@drawable/ic_alert_triangle"
|
||||
app:tint="@color/vector_error_color"
|
||||
app:titleTextColor="?riotx_text_secondary" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
@ -109,16 +109,33 @@
|
||||
tools:ignore="MissingConstraints" />
|
||||
|
||||
<androidx.constraintlayout.helper.widget.Flow
|
||||
android:id="@+id/ssss_passphrase_flow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginBottom="@dimen/layout_vertical_margin_big"
|
||||
app:constraint_referenced_ids="ssss_passphrase_use_key,ssss_passphrase_submit"
|
||||
app:flow_horizontalStyle="spread_inside"
|
||||
app:flow_wrapMode="chain"
|
||||
app:layout_constraintBottom_toTopOf="@+id/ssss_passphrase_reset"
|
||||
app:layout_constraintTop_toBottomOf="@+id/ssss_passphrase_enter_til"
|
||||
app:layout_goneMarginBottom="32dp" />
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/ssss_passphrase_reset"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
app:actionTitle="@string/bad_passphrase_key_reset_all_action"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/ssss_passphrase_enter_til" />
|
||||
app:layout_constraintTop_toBottomOf="@id/ssss_passphrase_flow"
|
||||
app:leftIcon="@drawable/ic_alert_triangle"
|
||||
app:tint="@color/vector_error_color"
|
||||
app:titleTextColor="?riotx_text_secondary" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
116
vector/src/main/res/layout/fragment_ssss_reset_all.xml
Normal file
116
vector/src/main/res/layout/fragment_ssss_reset_all.xml
Normal file
@ -0,0 +1,116 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/ssss__root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/reset_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:drawableStart="@drawable/ic_alert_triangle"
|
||||
android:drawablePadding="8dp"
|
||||
android:drawableTint="?riot_primary_text_color"
|
||||
android:text="@string/secure_backup_reset_all"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:tint="?riot_primary_text_color"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/ssss_reset_all_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/secure_backup_reset_all_no_other_devices"
|
||||
android:textColor="?riotx_text_primary"
|
||||
app:layout_constraintBottom_toTopOf="@id/ssss_reset_other_devices"
|
||||
app:layout_constraintTop_toBottomOf="@id/reset_title" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/ssss_reset_other_devices"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:drawableLeft="@drawable/ic_smartphone"
|
||||
android:drawablePadding="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/ssss_reset_all_description"
|
||||
tools:text="Show 2 devices you can verify with now" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/ssss_reset_text3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/secure_backup_reset_if_you_reset_all"
|
||||
android:textColor="@color/riotx_destructive_accent"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
android:tint="?riot_primary_text_color"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
||||
app:layout_constraintTop_toBottomOf="@id/ssss_reset_other_devices" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/ssss_reset_text4"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/secure_backup_reset_no_history"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintTop_toBottomOf="@id/ssss_reset_text3" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/ssss_reset_button_cancel"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton.Icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/cancel"
|
||||
tools:ignore="MissingConstraints" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/ssss_reset_button_reset"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/reset"
|
||||
tools:ignore="MissingConstraints" />
|
||||
|
||||
<androidx.constraintlayout.helper.widget.Flow
|
||||
android:id="@+id/ssss_passphrase_flow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:constraint_referenced_ids="ssss_reset_button_cancel, ssss_reset_button_reset"
|
||||
app:flow_horizontalStyle="spread_inside"
|
||||
app:flow_wrapMode="chain"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/ssss_reset_text4" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
@ -52,6 +52,7 @@
|
||||
|
||||
<declare-styleable name="BottomSheetActionButton">
|
||||
<attr name="tint" format="color" />
|
||||
<attr name="titleTextColor" format="color" />
|
||||
<attr name="actionTitle" format="string" />
|
||||
<attr name="actionDescription" format="string" />
|
||||
<attr name="leftIcon" format="reference" />
|
||||
|
@ -78,6 +78,7 @@
|
||||
<string name="play_video">Play</string>
|
||||
<string name="pause_video">Pause</string>
|
||||
<string name="dismiss">Dismiss</string>
|
||||
<string name="reset">Reset</string>
|
||||
|
||||
|
||||
<!-- First param will be replace by the value of ongoing_conference_call_voice, and second one by the value of ongoing_conference_call_video -->
|
||||
@ -2434,6 +2435,15 @@
|
||||
<string name="enter_secret_storage_input_key">Select your Recovery Key, or input it manually by typing it or pasting from your clipboard</string>
|
||||
<string name="keys_backup_recovery_key_error_decrypt">Backup could not be decrypted with this Recovery Key: please verify that you entered the correct Recovery Key.</string>
|
||||
<string name="failed_to_access_secure_storage">Failed to access secure storage</string>
|
||||
<string name="bad_passphrase_key_reset_all_action">Forgot or lost all recovery options? Reset everything</string>
|
||||
<string name="secure_backup_reset_all">Reset everything</string>
|
||||
<string name="secure_backup_reset_all_no_other_devices">Only do this if you have no other device you can verify this device with.</string>
|
||||
<string name="secure_backup_reset_if_you_reset_all">If you reset everything</string>
|
||||
<string name="secure_backup_reset_no_history">You will restart with no history, no messages, trusted devices or trusted users</string>
|
||||
<plurals name="secure_backup_reset_devices_you_can_verify">
|
||||
<item quantity="one">Show the device you can verify with now</item>
|
||||
<item quantity="other">Show %d devices you can verify with now</item>
|
||||
</plurals>
|
||||
|
||||
<string name="unencrypted">Unencrypted</string>
|
||||
<string name="encrypted_unverified">Encrypted by an unverified device</string>
|
||||
|
Loading…
Reference in New Issue
Block a user