Full bootstrap flow initial commit
This commit is contained in:
parent
8ecdac7c31
commit
bf5ba99653
@ -22,6 +22,7 @@ import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustResult
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.UserTrustResult
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
|
||||
|
||||
interface CrossSigningService {
|
||||
|
||||
@ -52,6 +53,8 @@ interface CrossSigningService {
|
||||
|
||||
fun getMyCrossSigningKeys(): MXCrossSigningInfo?
|
||||
|
||||
fun getCrossSigningPrivateKeys(): PrivateKeysInfo?
|
||||
|
||||
fun canCrossSign(): Boolean
|
||||
|
||||
fun trustUser(otherUserId: String,
|
||||
|
@ -19,3 +19,7 @@ package im.vector.matrix.android.api.session.securestorage
|
||||
interface KeySigner {
|
||||
fun sign(canonicalJson: String): Map<String, Map<String, String>>?
|
||||
}
|
||||
|
||||
class EmptyKeySigner : KeySigner {
|
||||
override fun sign(canonicalJson: String): Map<String, Map<String, String>>? = null
|
||||
}
|
||||
|
@ -19,5 +19,6 @@ package im.vector.matrix.android.api.session.securestorage
|
||||
data class SsssKeyCreationInfo(
|
||||
val keyId: String = "",
|
||||
var content: SecretStorageKeyContent?,
|
||||
val recoveryKey: String = ""
|
||||
val recoveryKey: String = "",
|
||||
val keySpec: SsssKeySpec
|
||||
)
|
||||
|
@ -641,7 +641,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||
throw MXCryptoError.OlmError(e)
|
||||
}
|
||||
|
||||
if (null != timeline) {
|
||||
if (timeline?.isNotBlank() == true) {
|
||||
val timelineSet = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableSetOf() }
|
||||
|
||||
val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex
|
||||
|
@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.crypto.model.KeyUsage
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UploadSignatureQueryBuilder
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
|
||||
import im.vector.matrix.android.internal.crypto.tasks.UploadSignaturesTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.UploadSigningKeysTask
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
@ -595,6 +596,10 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||
return cryptoStore.getMyCrossSigningInfo()
|
||||
}
|
||||
|
||||
override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? {
|
||||
return cryptoStore.getCrossSigningPrivateKeys()
|
||||
}
|
||||
|
||||
override fun canCrossSign(): Boolean {
|
||||
return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null
|
||||
&& cryptoStore.getCrossSigningPrivateKeys()?.user != null
|
||||
|
@ -28,7 +28,7 @@ import com.squareup.moshi.JsonClass
|
||||
* The user_signing_keys property will only be included when a user requests their own keys.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class KeysQueryResponse(
|
||||
internal data class KeysQueryResponse(
|
||||
/**
|
||||
* The device keys per devices per users.
|
||||
* Map from userId to map from deviceId to MXDeviceInfo
|
||||
|
@ -102,7 +102,8 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
||||
callback.onSuccess(SsssKeyCreationInfo(
|
||||
keyId = keyId,
|
||||
content = storageKeyContent,
|
||||
recoveryKey = computeRecoveryKey(key)
|
||||
recoveryKey = computeRecoveryKey(key),
|
||||
keySpec = RawBytesKeySpec(key)
|
||||
))
|
||||
}
|
||||
}
|
||||
@ -142,7 +143,8 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
||||
callback.onSuccess(SsssKeyCreationInfo(
|
||||
keyId = keyId,
|
||||
content = storageKeyContent,
|
||||
recoveryKey = computeRecoveryKey(privatePart.privateKey)
|
||||
recoveryKey = computeRecoveryKey(privatePart.privateKey),
|
||||
keySpec = RawBytesKeySpec(privatePart.privateKey)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@ -26,8 +26,12 @@ import im.vector.riotx.features.attachments.preview.AttachmentsPreviewFragment
|
||||
import im.vector.riotx.features.createdirect.CreateDirectRoomDirectoryUsersFragment
|
||||
import im.vector.riotx.features.createdirect.CreateDirectRoomKnownUsersFragment
|
||||
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment
|
||||
import im.vector.riotx.features.crypto.recover.BootstrapAccountPasswordFragment
|
||||
import im.vector.riotx.features.crypto.recover.BootstrapConclusionFragment
|
||||
import im.vector.riotx.features.crypto.recover.BootstrapConfirmPassphraseFragment
|
||||
import im.vector.riotx.features.crypto.recover.BootstrapEnterPassphraseFragment
|
||||
import im.vector.riotx.features.crypto.recover.BootstrapSaveRecoveryKeyFragment
|
||||
import im.vector.riotx.features.crypto.recover.BootstrapWaitingFragment
|
||||
import im.vector.riotx.features.crypto.verification.cancel.VerificationCancelFragment
|
||||
import im.vector.riotx.features.crypto.verification.cancel.VerificationNotMeFragment
|
||||
import im.vector.riotx.features.crypto.verification.choose.VerificationChooseMethodFragment
|
||||
@ -414,4 +418,24 @@ interface FragmentModule {
|
||||
@IntoMap
|
||||
@FragmentKey(BootstrapConfirmPassphraseFragment::class)
|
||||
fun bindBootstrapConfirmPassphraseFragment(fragment: BootstrapConfirmPassphraseFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(BootstrapWaitingFragment::class)
|
||||
fun bindBootstrapWaitingFragment(fragment: BootstrapWaitingFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(BootstrapSaveRecoveryKeyFragment::class)
|
||||
fun bindBootstrapSaveRecoveryKeyFragment(fragment: BootstrapSaveRecoveryKeyFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(BootstrapConclusionFragment::class)
|
||||
fun bindBootstrapConclusionFragment(fragment: BootstrapConclusionFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(BootstrapAccountPasswordFragment::class)
|
||||
fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment
|
||||
}
|
||||
|
@ -0,0 +1,133 @@
|
||||
/*
|
||||
* 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.core.ui.views
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isInvisible
|
||||
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 BottomSheetActionButton @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
@BindView(R.id.itemVerificationActionTitle)
|
||||
lateinit var actionTextView: TextView
|
||||
|
||||
@BindView(R.id.itemVerificationActionSubTitle)
|
||||
lateinit var descriptionTextView: TextView
|
||||
|
||||
@BindView(R.id.itemVerificationLeftIcon)
|
||||
lateinit var leftIconImageView: ImageView
|
||||
|
||||
@BindView(R.id.itemVerificationActionIcon)
|
||||
lateinit var rightIconImageView: ImageView
|
||||
|
||||
@BindView(R.id.itemVerificationClickableZone)
|
||||
lateinit var clickableView: View
|
||||
|
||||
|
||||
var title: String? = null
|
||||
set(value) {
|
||||
field = value
|
||||
actionTextView.setTextOrHide(value)
|
||||
}
|
||||
|
||||
var subTitle: String? = null
|
||||
set(value) {
|
||||
field = value
|
||||
descriptionTextView.setTextOrHide(value)
|
||||
}
|
||||
|
||||
var forceStartPadding: Boolean? = null
|
||||
set(value) {
|
||||
field = value
|
||||
if (leftIcon == null) {
|
||||
if (forceStartPadding == true) {
|
||||
leftIconImageView.isInvisible = true
|
||||
} else {
|
||||
leftIconImageView.isGone = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var leftIcon: Drawable? = null
|
||||
set(value) {
|
||||
field = value
|
||||
if (value == null) {
|
||||
if (forceStartPadding == true) {
|
||||
leftIconImageView.isInvisible = true
|
||||
} else {
|
||||
leftIconImageView.isGone = true
|
||||
}
|
||||
leftIconImageView.setImageDrawable(null)
|
||||
} else {
|
||||
leftIconImageView.isVisible = true
|
||||
leftIconImageView.setImageDrawable(value)
|
||||
}
|
||||
}
|
||||
|
||||
var rightIcon: Drawable? = null
|
||||
set(value) {
|
||||
field = value
|
||||
rightIconImageView.setImageDrawable(value)
|
||||
}
|
||||
|
||||
var tint: Int? = null
|
||||
set(value) {
|
||||
field = value
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
leftIconImageView.imageTintList = value?.let { ColorStateList.valueOf(value) }
|
||||
} else {
|
||||
leftIcon?.let {
|
||||
leftIcon = ThemeUtils.tintDrawable(context, it, value ?: ThemeUtils.getColor(context, android.R.attr.textColor))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.item_verification_action, this)
|
||||
ButterKnife.bind(this)
|
||||
|
||||
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.BottomSheetActionButton, 0, 0)
|
||||
title = typedArray.getString(R.styleable.BottomSheetActionButton_actionTitle) ?: ""
|
||||
subTitle = typedArray.getString(R.styleable.BottomSheetActionButton_actionDescription) ?: ""
|
||||
forceStartPadding = typedArray.getBoolean(R.styleable.BottomSheetActionButton_forceStartPadding, false)
|
||||
leftIcon = typedArray.getDrawable(R.styleable.BottomSheetActionButton_leftIcon)
|
||||
|
||||
rightIcon = typedArray.getDrawable(R.styleable.BottomSheetActionButton_rightIcon)
|
||||
|
||||
tint = typedArray.getColor(R.styleable.BottomSheetActionButton_tint, ThemeUtils.getColor(context, android.R.attr.textColor))
|
||||
|
||||
typedArray.recycle()
|
||||
}
|
||||
}
|
@ -153,7 +153,7 @@ fun startAddGoogleAccountIntent(context: AppCompatActivity, requestCode: Int) {
|
||||
}
|
||||
}
|
||||
|
||||
fun startSharePlainTextIntent(fragment: Fragment, chooserTitle: String?, text: String, subject: String? = null) {
|
||||
fun startSharePlainTextIntent(fragment: Fragment, chooserTitle: String?, text: String, subject: String? = null, requestCode: Int? = null) {
|
||||
val share = Intent(Intent.ACTION_SEND)
|
||||
share.type = "text/plain"
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
@ -165,7 +165,11 @@ fun startSharePlainTextIntent(fragment: Fragment, chooserTitle: String?, text: S
|
||||
share.putExtra(Intent.EXTRA_SUBJECT, subject)
|
||||
share.putExtra(Intent.EXTRA_TEXT, text)
|
||||
try {
|
||||
fragment.startActivity(Intent.createChooser(share, chooserTitle))
|
||||
if (requestCode != null) {
|
||||
fragment.startActivityForResult(Intent.createChooser(share, chooserTitle), requestCode)
|
||||
} else {
|
||||
fragment.startActivity(Intent.createChooser(share, chooserTitle))
|
||||
}
|
||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||
fragment.activity?.toast(R.string.error_no_external_application_found)
|
||||
}
|
||||
|
@ -195,8 +195,7 @@ class MainActivity : VectorBaseActivity() {
|
||||
// We have a session.
|
||||
// Check it can be opened
|
||||
if (sessionHolder.getActiveSession().isOpenable) {
|
||||
// DO NOT COMMIT
|
||||
HomeActivity.newIntent(this, accountCreation = true)
|
||||
HomeActivity.newIntent(this)
|
||||
} else {
|
||||
// The token is still invalid
|
||||
SoftLogoutActivity.newIntent(this)
|
||||
|
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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.crypto.recover
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.core.text.toSpannable
|
||||
import com.airbnb.mvrx.parentFragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.jakewharton.rxbinding3.view.clicks
|
||||
import com.jakewharton.rxbinding3.widget.editorActionEvents
|
||||
import com.jakewharton.rxbinding3.widget.textChanges
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.hideKeyboard
|
||||
import im.vector.riotx.core.extensions.showPassword
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import im.vector.riotx.core.resources.ColorProvider
|
||||
import im.vector.riotx.core.utils.colorizeMatchingText
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.android.synthetic.main.fragment_bootstrap_enter_account_password.*
|
||||
import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.bootstrapDescriptionText
|
||||
import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.ssss_view_show_password
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
class BootstrapAccountPasswordFragment @Inject constructor(
|
||||
private val colorProvider: ColorProvider
|
||||
) : VectorBaseFragment() {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_bootstrap_enter_account_password
|
||||
|
||||
val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val recPassPhrase = getString(R.string.account_password)
|
||||
bootstrapDescriptionText.text = getString(R.string.enter_account_password, recPassPhrase)
|
||||
.toSpannable()
|
||||
.colorizeMatchingText(recPassPhrase, colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
|
||||
|
||||
bootstrapAccountPasswordEditText.hint = getString(R.string.account_password)
|
||||
|
||||
bootstrapAccountPasswordEditText.editorActionEvents()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
if (it.actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
submit()
|
||||
}
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
bootstrapAccountPasswordEditText.textChanges()
|
||||
.distinct()
|
||||
.subscribe {
|
||||
if (!it.isNullOrBlank()) {
|
||||
bootstrapAccountPasswordTil.error = null
|
||||
}
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
ssss_view_show_password.clicks()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility)
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
withState(sharedViewModel) {state ->
|
||||
(state.step as? BootstrapStep.AccountPassword)?.failure?.let {
|
||||
bootstrapAccountPasswordTil.error = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun submit() = withState(sharedViewModel) { state ->
|
||||
if (state.step !is BootstrapStep.AccountPassword) {
|
||||
return@withState
|
||||
}
|
||||
val accountPassword = bootstrapAccountPasswordEditText.text?.toString()
|
||||
if (accountPassword.isNullOrBlank()) {
|
||||
bootstrapAccountPasswordTil.error = getString(R.string.error_empty_field_your_password)
|
||||
} else {
|
||||
view?.hideKeyboard()
|
||||
sharedViewModel.handle(BootstrapActions.ReAuth(accountPassword))
|
||||
}
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(sharedViewModel) { state ->
|
||||
super.invalidate()
|
||||
|
||||
if (state.step is BootstrapStep.AccountPassword) {
|
||||
val isPasswordVisible = state.step.isPasswordVisible
|
||||
bootstrapAccountPasswordEditText.showPassword(isPasswordVisible, updateCursor = false)
|
||||
ssss_view_show_password.setImageResource(if (isPasswordVisible) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -16,14 +16,28 @@
|
||||
|
||||
package im.vector.riotx.features.crypto.recover
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||
import java.io.OutputStream
|
||||
|
||||
sealed class BootstrapActions : VectorViewModelAction {
|
||||
|
||||
// Navigation
|
||||
|
||||
object GoBack : BootstrapActions()
|
||||
data class GoToConfirmPassphrase(val passphrase: String) : BootstrapActions()
|
||||
object GoToCompleted : BootstrapActions()
|
||||
object GoToEnterAccountPassword : BootstrapActions()
|
||||
|
||||
|
||||
data class DoInitialize(val passphrase: String, val auth: UserPasswordAuth? = null) : BootstrapActions()
|
||||
object TogglePasswordVisibility : BootstrapActions()
|
||||
data class UpdateCandidatePassphrase(val pass: String) : BootstrapActions()
|
||||
data class UpdateConfirmCandidatePassphrase(val pass: String) : BootstrapActions()
|
||||
|
||||
data class ReAuth(val pass: String) : BootstrapActions()
|
||||
object RecoveryKeySaved : BootstrapActions()
|
||||
object Completed : BootstrapActions()
|
||||
object SaveReqQueryStarted : BootstrapActions()
|
||||
data class SaveKeyToUri(val os: OutputStream) : BootstrapActions()
|
||||
object SaveReqFailed : BootstrapActions()
|
||||
}
|
||||
|
@ -23,11 +23,15 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.dialogs.PromptPasswordDialog
|
||||
import im.vector.riotx.core.extensions.commitTransaction
|
||||
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
import kotlinx.android.synthetic.main.bottom_sheet_bootstrap.*
|
||||
@ -51,9 +55,19 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewModel.observeViewEvents {
|
||||
when (it) {
|
||||
is BootstrapViewEvents.Dismiss -> dismiss()
|
||||
viewModel.observeViewEvents { event ->
|
||||
when (event) {
|
||||
is BootstrapViewEvents.Dismiss -> dismiss()
|
||||
is BootstrapViewEvents.ModalError -> {
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.dialog_title_error)
|
||||
.setMessage(event.error)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
BootstrapViewEvents.RecoveryKeySaved -> {
|
||||
KeepItSafeDialog().show(requireActivity())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -81,14 +95,35 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
|
||||
when (state.step) {
|
||||
is BootstrapStep.SetupPassphrase -> {
|
||||
bootstrapTitleText.text = getString(R.string.recovery_passphrase)
|
||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password))
|
||||
bootstrapTitleText.text = getString(R.string.set_recovery_passphrase, getString(R.string.recovery_passphrase))
|
||||
showFragment(BootstrapEnterPassphraseFragment::class, Bundle())
|
||||
}
|
||||
is BootstrapStep.ConfirmPassphrase -> {
|
||||
bootstrapTitleText.text = getString(R.string.passphrase_confirm_passphrase)
|
||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password))
|
||||
bootstrapTitleText.text = getString(R.string.confirm_recovery_passphrase, getString(R.string.recovery_passphrase))
|
||||
showFragment(BootstrapConfirmPassphraseFragment::class, Bundle())
|
||||
}
|
||||
is BootstrapStep.Initializing -> TODO()
|
||||
is BootstrapStep.AccountPassword -> {
|
||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_user))
|
||||
bootstrapTitleText.text = getString(R.string.account_password)
|
||||
showFragment(BootstrapAccountPasswordFragment::class, Bundle())
|
||||
}
|
||||
is BootstrapStep.Initializing -> {
|
||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key))
|
||||
bootstrapTitleText.text = getString(R.string.bootstrap_loading_title)
|
||||
showFragment(BootstrapWaitingFragment::class, Bundle())
|
||||
}
|
||||
is BootstrapStep.SaveRecoveryKey -> {
|
||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key))
|
||||
bootstrapTitleText.text = getString(R.string.keys_backup_setup_step3_please_make_copy)
|
||||
showFragment(BootstrapSaveRecoveryKeyFragment::class, Bundle())
|
||||
}
|
||||
is BootstrapStep.DoneSuccess -> {
|
||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key))
|
||||
bootstrapTitleText.text = getString(R.string.bootstrap_finish_title)
|
||||
showFragment(BootstrapConclusionFragment::class, Bundle())
|
||||
}
|
||||
}
|
||||
super.invalidate()
|
||||
}
|
||||
|
@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.crypto.recover
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.text.toSpannable
|
||||
import com.airbnb.mvrx.parentFragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.jakewharton.rxbinding3.view.clicks
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import im.vector.riotx.core.resources.ColorProvider
|
||||
import im.vector.riotx.core.utils.colorizeMatchingText
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.android.synthetic.main.fragment_bootstrap_conclusion.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
class BootstrapConclusionFragment @Inject constructor(
|
||||
private val colorProvider: ColorProvider
|
||||
) : VectorBaseFragment() {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_bootstrap_conclusion
|
||||
|
||||
val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
bootstrapConclusionContinue.clickableView.clicks()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
sharedViewModel.handle(BootstrapActions.Completed)
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(sharedViewModel) { state ->
|
||||
if (state.step !is BootstrapStep.DoneSuccess) return@withState
|
||||
|
||||
bootstrapConclusionText.text = getString(R.string.bootstrap_cross_signing_success, getString(R.string.recovery_passphrase), getString(R.string.message_key))
|
||||
.toSpannable()
|
||||
.colorizeMatchingText(getString(R.string.recovery_passphrase), colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
|
||||
.colorizeMatchingText(getString(R.string.message_key), colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
|
||||
|
||||
}
|
||||
}
|
@ -27,6 +27,7 @@ import com.jakewharton.rxbinding3.view.clicks
|
||||
import com.jakewharton.rxbinding3.widget.editorActionEvents
|
||||
import com.jakewharton.rxbinding3.widget.textChanges
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.hideKeyboard
|
||||
import im.vector.riotx.core.extensions.showPassword
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import im.vector.riotx.core.resources.ColorProvider
|
||||
@ -56,6 +57,12 @@ class BootstrapConfirmPassphraseFragment @Inject constructor(
|
||||
|
||||
ssss_passphrase_enter_edittext.hint = getString(R.string.passphrase_confirm_passphrase)
|
||||
|
||||
withState(sharedViewModel) {
|
||||
// set initial value (useful when coming back)
|
||||
ssss_passphrase_enter_edittext.setText(it.passphraseRepeat ?: "")
|
||||
ssss_passphrase_enter_edittext.requestFocus()
|
||||
}
|
||||
|
||||
ssss_passphrase_enter_edittext.editorActionEvents()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
@ -68,9 +75,8 @@ class BootstrapConfirmPassphraseFragment @Inject constructor(
|
||||
|
||||
ssss_passphrase_enter_edittext.textChanges()
|
||||
.subscribe {
|
||||
// ssss_passphrase_enter_til.error = null
|
||||
ssss_passphrase_enter_til.error = null
|
||||
sharedViewModel.handle(BootstrapActions.UpdateConfirmCandidatePassphrase(it?.toString() ?: ""))
|
||||
// ssss_passphrase_submit.isEnabled = it.isNotBlank()
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
@ -95,16 +101,15 @@ class BootstrapConfirmPassphraseFragment @Inject constructor(
|
||||
if (state.step !is BootstrapStep.ConfirmPassphrase) {
|
||||
return@withState
|
||||
}
|
||||
|
||||
// val score = state.passphraseStrength.invoke()?.score
|
||||
// val passphrase = ssss_passphrase_enter_edittext.text?.toString()
|
||||
// if (passphrase.isNullOrBlank()) {
|
||||
// ssss_passphrase_enter_til.error = getString(R.string.passphrase_empty_error_message)
|
||||
// } else if (score != 4) {
|
||||
// ssss_passphrase_enter_til.error = getString(R.string.passphrase_passphrase_too_weak)
|
||||
// } else {
|
||||
// sharedViewModel.handle(BootstrapActions.GoToConfirmPassphrase(passphrase))
|
||||
// }
|
||||
val passphrase = ssss_passphrase_enter_edittext.text?.toString()
|
||||
if (passphrase.isNullOrBlank()) {
|
||||
ssss_passphrase_enter_til.error = getString(R.string.passphrase_empty_error_message)
|
||||
} else if (passphrase != state.passphrase) {
|
||||
ssss_passphrase_enter_til.error = getString(R.string.passphrase_passphrase_does_not_match)
|
||||
} else {
|
||||
view?.hideKeyboard()
|
||||
sharedViewModel.handle(BootstrapActions.DoInitialize(passphrase))
|
||||
}
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(sharedViewModel) { state ->
|
||||
@ -112,7 +117,7 @@ class BootstrapConfirmPassphraseFragment @Inject constructor(
|
||||
|
||||
if (state.step is BootstrapStep.ConfirmPassphrase) {
|
||||
val isPasswordVisible = state.step.isPasswordVisible
|
||||
ssss_passphrase_enter_edittext.showPassword(isPasswordVisible)
|
||||
ssss_passphrase_enter_edittext.showPassword(isPasswordVisible, updateCursor = false)
|
||||
ssss_view_show_password.setImageResource(if (isPasswordVisible) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black)
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,177 @@
|
||||
/*
|
||||
* 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.crypto.recover
|
||||
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.failure.MatrixError
|
||||
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.securestorage.EmptyKeySigner
|
||||
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService
|
||||
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
|
||||
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
||||
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.util.awaitCallback
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.platform.WaitingViewData
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
sealed class BootstrapResult {
|
||||
|
||||
data class Success(val keyInfo: SsssKeyCreationInfo) : BootstrapResult()
|
||||
|
||||
abstract class Failure(val error: String?) : BootstrapResult()
|
||||
|
||||
class UnsupportedAuthFlow : Failure(null)
|
||||
|
||||
data class GenericError(val failure: Throwable) : Failure(failure.localizedMessage)
|
||||
data class InvalidPasswordError(val matrixError: MatrixError) : Failure(null)
|
||||
class FailedToCreateSSSSKey(failure: Throwable) : Failure(failure.localizedMessage)
|
||||
class FailedToSetDefaultSSSSKey(failure: Throwable) : Failure(failure.localizedMessage)
|
||||
class FailedToStorePrivateKeyInSSSS(failure: Throwable) : Failure(failure.localizedMessage)
|
||||
object MissingPrivateKey : Failure(null)
|
||||
|
||||
data class PasswordAuthFlowMissing(val sessionId: String, val userId: String) : Failure(null)
|
||||
}
|
||||
|
||||
interface BootstrapProgressListener {
|
||||
fun onProgress(data: WaitingViewData)
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val userPasswordAuth: UserPasswordAuth? = null,
|
||||
val progressListener: BootstrapProgressListener? = null,
|
||||
val passphrase: String
|
||||
)
|
||||
|
||||
class BootstrapCrossSigningTask @Inject constructor(
|
||||
private val session: Session,
|
||||
private val stringProvider: StringProvider
|
||||
) {
|
||||
|
||||
operator fun invoke(
|
||||
scope: CoroutineScope,
|
||||
params: Params,
|
||||
onResult: (BootstrapResult) -> Unit = {}
|
||||
) {
|
||||
val backgroundJob = scope.async { execute(params) }
|
||||
scope.launch { onResult(backgroundJob.await()) }
|
||||
}
|
||||
|
||||
suspend fun execute(params: Params): BootstrapResult {
|
||||
|
||||
params.progressListener?.onProgress(WaitingViewData(stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing), isIndeterminate = true))
|
||||
val crossSigningService = session.cryptoService().crossSigningService()
|
||||
|
||||
try {
|
||||
awaitCallback<Unit> {
|
||||
crossSigningService.initializeCrossSigning(params.userPasswordAuth, it)
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
return handleInitializeXSigningError(failure)
|
||||
}
|
||||
|
||||
val keyInfo: SsssKeyCreationInfo
|
||||
|
||||
val ssssService = session.sharedSecretStorageService
|
||||
|
||||
|
||||
params.progressListener?.onProgress(WaitingViewData(stringProvider.getString(R.string.bootstrap_crosssigning_progress_pbkdf2), isIndeterminate = true))
|
||||
try {
|
||||
keyInfo = awaitCallback {
|
||||
ssssService.generateKeyWithPassphrase(
|
||||
UUID.randomUUID().toString(),
|
||||
"ssss_key",
|
||||
params.passphrase,
|
||||
EmptyKeySigner(),
|
||||
null,
|
||||
it
|
||||
)
|
||||
}
|
||||
} catch (failure: Failure) {
|
||||
return BootstrapResult.FailedToCreateSSSSKey(failure)
|
||||
}
|
||||
|
||||
|
||||
params.progressListener?.onProgress(WaitingViewData(stringProvider.getString(R.string.bootstrap_crosssigning_progress_default_key), isIndeterminate = true))
|
||||
try {
|
||||
awaitCallback<Unit> {
|
||||
ssssService.setDefaultKey(keyInfo.keyId, it)
|
||||
}
|
||||
} catch (failure: Failure) {
|
||||
// Maybe we could just ignore this error?
|
||||
return BootstrapResult.FailedToSetDefaultSSSSKey(failure)
|
||||
}
|
||||
|
||||
val xKeys = crossSigningService.getCrossSigningPrivateKeys()
|
||||
val mskPrivateKey = xKeys?.master ?: return BootstrapResult.MissingPrivateKey
|
||||
val sskPrivateKey = xKeys.selfSigned ?: return BootstrapResult.MissingPrivateKey
|
||||
val uskPrivateKey = xKeys.user ?: return BootstrapResult.MissingPrivateKey
|
||||
|
||||
try {
|
||||
|
||||
params.progressListener?.onProgress(WaitingViewData(stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_msk), isIndeterminate = true))
|
||||
awaitCallback<Unit> {
|
||||
ssssService.storeSecret(MASTER_KEY_SSSS_NAME, mskPrivateKey, listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), it)
|
||||
}
|
||||
params.progressListener?.onProgress(WaitingViewData(stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_usk), isIndeterminate = true))
|
||||
awaitCallback<Unit> {
|
||||
ssssService.storeSecret(USER_SIGNING_KEY_SSSS_NAME, uskPrivateKey, listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), it)
|
||||
}
|
||||
params.progressListener?.onProgress(WaitingViewData(stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_ssk), isIndeterminate = true))
|
||||
awaitCallback<Unit> {
|
||||
ssssService.storeSecret(SELF_SIGNING_KEY_SSSS_NAME, sskPrivateKey, listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), it)
|
||||
}
|
||||
} catch (failure: Failure) {
|
||||
// Maybe we could just ignore this error?
|
||||
return BootstrapResult.FailedToStorePrivateKeyInSSSS(failure)
|
||||
}
|
||||
|
||||
// TODO configure key backup?
|
||||
|
||||
return BootstrapResult.Success(keyInfo)
|
||||
}
|
||||
|
||||
private fun handleInitializeXSigningError(failure: Throwable): BootstrapResult {
|
||||
if (failure is Failure.ServerError && failure.error.code == MatrixError.M_FORBIDDEN) {
|
||||
return BootstrapResult.InvalidPasswordError(failure.error)
|
||||
} else if (failure is Failure.OtherServerError && failure.httpCode == 401) {
|
||||
try {
|
||||
MoshiProvider.providesMoshi()
|
||||
.adapter(RegistrationFlowResponse::class.java)
|
||||
.fromJson(failure.errorBody)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}?.let { flowResponse ->
|
||||
if (flowResponse.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } != true) {
|
||||
// can't do this from here
|
||||
return BootstrapResult.UnsupportedAuthFlow()
|
||||
}
|
||||
}
|
||||
}
|
||||
return BootstrapResult.GenericError(failure)
|
||||
}
|
||||
}
|
@ -33,11 +33,6 @@ import im.vector.riotx.core.utils.colorizeMatchingText
|
||||
import im.vector.riotx.features.settings.VectorLocale
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.*
|
||||
import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.ssss_passphrase_enter_edittext
|
||||
import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.ssss_passphrase_enter_til
|
||||
import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.ssss_view_show_password
|
||||
import kotlinx.android.synthetic.main.fragment_ssss_access_from_passphrase.*
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -52,7 +47,6 @@ class BootstrapEnterPassphraseFragment @Inject constructor(
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
|
||||
val recPassPhrase = getString(R.string.recovery_passphrase)
|
||||
bootstrapDescriptionText.text = getString(R.string.bootstrap_info_text, recPassPhrase)
|
||||
.toSpannable()
|
||||
@ -89,22 +83,6 @@ class BootstrapEnterPassphraseFragment @Inject constructor(
|
||||
// }
|
||||
}
|
||||
|
||||
// ssss_passphrase_submit.clicks()
|
||||
// .debounce(300, TimeUnit.MILLISECONDS)
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe {
|
||||
// submit()
|
||||
// }
|
||||
// .disposeOnDestroyView()
|
||||
|
||||
// ssss_passphrase_cancel.clicks()
|
||||
// .debounce(300, TimeUnit.MILLISECONDS)
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe {
|
||||
// sharedViewModel.handle(SharedSecureStorageAction.Cancel)
|
||||
// }
|
||||
// .disposeOnDestroyView()
|
||||
|
||||
ssss_view_show_password.clicks()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
@ -118,7 +96,6 @@ class BootstrapEnterPassphraseFragment @Inject constructor(
|
||||
if (state.step !is BootstrapStep.SetupPassphrase) {
|
||||
return@withState
|
||||
}
|
||||
|
||||
val score = state.passphraseStrength.invoke()?.score
|
||||
val passphrase = ssss_passphrase_enter_edittext.text?.toString()
|
||||
if (passphrase.isNullOrBlank()) {
|
||||
@ -135,24 +112,16 @@ class BootstrapEnterPassphraseFragment @Inject constructor(
|
||||
|
||||
if (state.step is BootstrapStep.SetupPassphrase) {
|
||||
val isPasswordVisible = state.step.isPasswordVisible
|
||||
ssss_passphrase_enter_edittext.showPassword(isPasswordVisible)
|
||||
ssss_passphrase_enter_edittext.showPassword(isPasswordVisible, updateCursor = false)
|
||||
ssss_view_show_password.setImageResource(if (isPasswordVisible) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black)
|
||||
|
||||
state.passphraseStrength.invoke()?.let { strength ->
|
||||
val score = strength.score
|
||||
ssss_passphrase_security_progress.strength = score
|
||||
|
||||
Timber.e("## Strength info: $strength")
|
||||
Timber.e("## Strength info score: $score")
|
||||
Timber.e("## Strength info getWarning: ${strength.feedback?.getWarning(VectorLocale.applicationLocale)}")
|
||||
Timber.e("## Strength info getSuggestions: ${strength.feedback?.getSuggestions(VectorLocale.applicationLocale)}")
|
||||
Timber.e("## Strength info getFirstSuggestions: ${strength.feedback?.getSuggestions(VectorLocale.applicationLocale)?.firstOrNull()}")
|
||||
if (score in 1..3) {
|
||||
val hint =
|
||||
strength.feedback?.getWarning(VectorLocale.applicationLocale)?.takeIf { it.isNotBlank() }
|
||||
?: strength.feedback?.getSuggestions(VectorLocale.applicationLocale)?.firstOrNull()
|
||||
Timber.e("## Strength info: $hint")
|
||||
Timber.e("## Strength currentValue : ${ssss_passphrase_enter_til.error}")
|
||||
if (hint != null && hint != ssss_passphrase_enter_til.error.toString()) {
|
||||
ssss_passphrase_enter_til.error = hint
|
||||
}
|
||||
@ -160,9 +129,7 @@ class BootstrapEnterPassphraseFragment @Inject constructor(
|
||||
ssss_passphrase_enter_til.error = null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,147 @@
|
||||
/*
|
||||
* 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.crypto.recover
|
||||
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.mvrx.parentFragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.jakewharton.rxbinding3.view.clicks
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import im.vector.riotx.core.resources.ColorProvider
|
||||
import im.vector.riotx.core.utils.colorizeMatchingText
|
||||
import im.vector.riotx.core.utils.startSharePlainTextIntent
|
||||
import im.vector.riotx.core.utils.toast
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.android.synthetic.main.fragment_bootstrap_save_key.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
class BootstrapSaveRecoveryKeyFragment @Inject constructor(
|
||||
private val colorProvider: ColorProvider
|
||||
) : VectorBaseFragment() {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_bootstrap_save_key
|
||||
|
||||
val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
bootstrapSaveText.text = getString(R.string.bootstrap_save_key_description, getString(R.string.message_key), getString(R.string.recovery_passphrase))
|
||||
.toSpannable()
|
||||
.colorizeMatchingText(getString(R.string.recovery_passphrase), colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
|
||||
.colorizeMatchingText(getString(R.string.message_key), colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
|
||||
|
||||
recoverySave.clickableView.clicks()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
downloadRecoveryKey()
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
recoveryCopy.clickableView.clicks()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
shareRecoveryKey()
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
recoveryContinue.clickableView.clicks()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
sharedViewModel.handle(BootstrapActions.GoToCompleted)
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
}
|
||||
|
||||
private fun downloadRecoveryKey() = withState(sharedViewModel) { _ ->
|
||||
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
intent.type = "text/plain"
|
||||
intent.putExtra(Intent.EXTRA_TITLE, "riot-recovery-key.txt")
|
||||
|
||||
try {
|
||||
sharedViewModel.handle(BootstrapActions.SaveReqQueryStarted)
|
||||
startActivityForResult(Intent.createChooser(intent, getString(R.string.keys_backup_setup_step3_please_make_copy)), REQUEST_CODE_SAVE)
|
||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||
requireActivity().toast(R.string.error_no_external_application_found)
|
||||
sharedViewModel.handle(BootstrapActions.SaveReqFailed)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == REQUEST_CODE_SAVE) {
|
||||
val uri = data?.data
|
||||
if (resultCode == RESULT_OK && uri != null) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
sharedViewModel.handle(BootstrapActions.SaveKeyToUri(context!!.contentResolver!!.openOutputStream(uri)!!))
|
||||
} catch (failure: Throwable) {
|
||||
sharedViewModel.handle(BootstrapActions.SaveReqFailed)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// result code seems to be always cancelled here.. so act as if it was saved
|
||||
sharedViewModel.handle(BootstrapActions.SaveReqFailed)
|
||||
}
|
||||
return
|
||||
} else if (requestCode == REQUEST_CODE_COPY) {
|
||||
sharedViewModel.handle(BootstrapActions.RecoveryKeySaved)
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
private fun shareRecoveryKey() = withState(sharedViewModel) { state ->
|
||||
val recoveryKey = state.recoveryKeyCreationInfo?.recoveryKey?.formatRecoveryKey()
|
||||
?: return@withState
|
||||
|
||||
startSharePlainTextIntent(this,
|
||||
context?.getString(R.string.keys_backup_setup_step3_share_intent_chooser_title),
|
||||
recoveryKey,
|
||||
context?.getString(R.string.recovery_key)
|
||||
, REQUEST_CODE_COPY)
|
||||
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(sharedViewModel) { state ->
|
||||
val step = state.step
|
||||
if (step !is BootstrapStep.SaveRecoveryKey) return@withState
|
||||
|
||||
recoveryContinue.isVisible = step.isSaved
|
||||
bootstrapRecoveryKeyText.text = state.recoveryKeyCreationInfo?.recoveryKey?.formatRecoveryKey()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val REQUEST_CODE_SAVE = 123
|
||||
const val REQUEST_CODE_COPY = 124
|
||||
}
|
||||
}
|
@ -16,8 +16,11 @@
|
||||
|
||||
package im.vector.riotx.features.crypto.recover
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.Fail
|
||||
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
|
||||
@ -28,26 +31,44 @@ import com.nulabinc.zxcvbn.Zxcvbn
|
||||
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.securestorage.SsssKeyCreationInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.exhaustive
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
import im.vector.riotx.core.platform.WaitingViewData
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.features.login.ReAuthHelper
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.OutputStream
|
||||
|
||||
data class BootstrapViewState(
|
||||
val step: BootstrapStep = BootstrapStep.SetupPassphrase(false),
|
||||
val passphrase: String? = null,
|
||||
val passphraseRepeat: String? = null,
|
||||
val crossSigningInitialization: Async<Unit> = Uninitialized,
|
||||
val passphraseStrength: Async<Strength> = Uninitialized
|
||||
val passphraseStrength: Async<Strength> = Uninitialized,
|
||||
val passphraseConfirmMatch: Async<Unit> = Uninitialized,
|
||||
val recoveryKeyCreationInfo: SsssKeyCreationInfo? = null,
|
||||
val initializationWaitingViewData: WaitingViewData? = null,
|
||||
val currentReAuth: UserPasswordAuth? = null,
|
||||
val recoverySaveFileProcess: Async<Unit> = Uninitialized
|
||||
) : MvRxState
|
||||
|
||||
sealed class BootstrapStep {
|
||||
data class SetupPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
|
||||
data class ConfirmPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
|
||||
data class AccountPassword(val isPasswordVisible: Boolean, val failure: String? = null) : BootstrapStep()
|
||||
object Initializing : BootstrapStep()
|
||||
data class SaveRecoveryKey(val isSaved: Boolean) : BootstrapStep()
|
||||
object DoneSuccess : BootstrapStep()
|
||||
}
|
||||
|
||||
class BootstrapSharedViewModel @AssistedInject constructor(
|
||||
@Assisted initialState: BootstrapViewState,
|
||||
private val stringProvider: StringProvider,
|
||||
private val session: Session,
|
||||
private val bootstrapTask: BootstrapCrossSigningTask,
|
||||
private val reAuthHelper: ReAuthHelper
|
||||
) : VectorViewModel<BootstrapViewState, BootstrapActions, BootstrapViewEvents>(initialState) {
|
||||
|
||||
@ -60,8 +81,8 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||
|
||||
override fun handle(action: BootstrapActions) = withState { state ->
|
||||
when (action) {
|
||||
is BootstrapActions.GoBack -> queryBack()
|
||||
BootstrapActions.TogglePasswordVisibility -> {
|
||||
is BootstrapActions.GoBack -> queryBack()
|
||||
BootstrapActions.TogglePasswordVisibility -> {
|
||||
when (state.step) {
|
||||
is BootstrapStep.SetupPassphrase -> {
|
||||
setState {
|
||||
@ -73,11 +94,18 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
|
||||
}
|
||||
}
|
||||
|
||||
is BootstrapStep.AccountPassword -> {
|
||||
setState {
|
||||
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
is BootstrapActions.UpdateCandidatePassphrase -> {
|
||||
|
||||
is BootstrapActions.UpdateCandidatePassphrase -> {
|
||||
val strength = zxcvbn.measure(action.pass)
|
||||
setState {
|
||||
copy(
|
||||
@ -86,7 +114,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
is BootstrapActions.GoToConfirmPassphrase -> {
|
||||
is BootstrapActions.GoToConfirmPassphrase -> {
|
||||
setState {
|
||||
copy(
|
||||
passphrase = action.passphrase,
|
||||
@ -97,18 +125,181 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
is BootstrapActions.UpdateConfirmCandidatePassphrase -> {
|
||||
|
||||
setState {
|
||||
copy(
|
||||
passphraseRepeat = action.pass
|
||||
)
|
||||
}
|
||||
}
|
||||
is BootstrapActions.DoInitialize -> {
|
||||
if (state.passphrase == state.passphraseRepeat) {
|
||||
val auth = action.auth ?: reAuthHelper.rememberedAuth()
|
||||
if (auth == null) {
|
||||
setState {
|
||||
copy(
|
||||
step = BootstrapStep.AccountPassword(false)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
startInitializeFlow(action.auth)
|
||||
}
|
||||
} else {
|
||||
setState {
|
||||
copy(
|
||||
passphraseConfirmMatch = Fail(Throwable(stringProvider.getString(R.string.passphrase_passphrase_does_not_match)))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
BootstrapActions.RecoveryKeySaved -> {
|
||||
_viewEvents.post(BootstrapViewEvents.RecoveryKeySaved)
|
||||
setState {
|
||||
copy(step = BootstrapStep.SaveRecoveryKey(true))
|
||||
}
|
||||
}
|
||||
BootstrapActions.Completed -> {
|
||||
_viewEvents.post(BootstrapViewEvents.Dismiss)
|
||||
}
|
||||
BootstrapActions.GoToCompleted -> {
|
||||
setState {
|
||||
copy(step = BootstrapStep.DoneSuccess)
|
||||
}
|
||||
}
|
||||
BootstrapActions.SaveReqQueryStarted -> {
|
||||
setState {
|
||||
copy(recoverySaveFileProcess = Loading())
|
||||
}
|
||||
}
|
||||
is BootstrapActions.SaveKeyToUri -> {
|
||||
saveRecoveryKeyToUri(action.os)
|
||||
}
|
||||
BootstrapActions.SaveReqFailed -> {
|
||||
setState {
|
||||
copy(recoverySaveFileProcess = Uninitialized)
|
||||
}
|
||||
}
|
||||
BootstrapActions.GoToEnterAccountPassword -> {
|
||||
setState {
|
||||
copy(step = BootstrapStep.AccountPassword(false))
|
||||
}
|
||||
}
|
||||
is BootstrapActions.ReAuth -> {
|
||||
startInitializeFlow(
|
||||
state.currentReAuth?.copy(password = action.pass)
|
||||
?: UserPasswordAuth(user = session.myUserId, password = action.pass)
|
||||
)
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
// =======================================
|
||||
// Business Logic
|
||||
// =======================================
|
||||
private fun saveRecoveryKeyToUri(os: OutputStream) = withState { state ->
|
||||
viewModelScope.launch {
|
||||
kotlin.runCatching {
|
||||
os.use {
|
||||
os.write((state.recoveryKeyCreationInfo?.recoveryKey?.formatRecoveryKey() ?: "").toByteArray())
|
||||
}
|
||||
}.fold({
|
||||
setState {
|
||||
_viewEvents.post(BootstrapViewEvents.RecoveryKeySaved)
|
||||
copy(
|
||||
recoverySaveFileProcess = Success(Unit),
|
||||
step = BootstrapStep.SaveRecoveryKey(isSaved = true)
|
||||
)
|
||||
}
|
||||
}, {
|
||||
setState {
|
||||
copy(recoverySaveFileProcess = Fail(it))
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun startInitializeFlow(auth: UserPasswordAuth?) {
|
||||
|
||||
setState {
|
||||
copy(step = BootstrapStep.Initializing)
|
||||
}
|
||||
|
||||
val progressListener = object : BootstrapProgressListener {
|
||||
override fun onProgress(data: WaitingViewData) {
|
||||
setState {
|
||||
copy(
|
||||
initializationWaitingViewData = data
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
withState { state ->
|
||||
viewModelScope.launch {
|
||||
bootstrapTask.invoke(this, Params(
|
||||
userPasswordAuth = auth ?: reAuthHelper.rememberedAuth(),
|
||||
progressListener = progressListener,
|
||||
passphrase = state.passphrase!!
|
||||
)) {
|
||||
when (it) {
|
||||
is BootstrapResult.Success -> {
|
||||
setState {
|
||||
copy(
|
||||
recoveryKeyCreationInfo = it.keyInfo,
|
||||
step = BootstrapStep.SaveRecoveryKey(false)
|
||||
)
|
||||
}
|
||||
}
|
||||
is BootstrapResult.PasswordAuthFlowMissing -> {
|
||||
setState {
|
||||
copy(
|
||||
currentReAuth = UserPasswordAuth(session = it.sessionId, user = it.userId),
|
||||
step = BootstrapStep.AccountPassword(false)
|
||||
)
|
||||
}
|
||||
}
|
||||
is BootstrapResult.UnsupportedAuthFlow -> {
|
||||
_viewEvents.post(BootstrapViewEvents.ModalError(stringProvider.getString(R.string.auth_flow_not_supported)))
|
||||
_viewEvents.post(BootstrapViewEvents.Dismiss)
|
||||
}
|
||||
is BootstrapResult.InvalidPasswordError -> {
|
||||
// it's a bad password
|
||||
setState {
|
||||
copy(
|
||||
// We clear the auth session, to avoid 'Requested operation has changed during the UI authentication session' error
|
||||
currentReAuth = UserPasswordAuth(session = null, user = session.myUserId),
|
||||
step = BootstrapStep.AccountPassword(false, stringProvider.getString(R.string.auth_invalid_login_param))
|
||||
)
|
||||
}
|
||||
}
|
||||
is BootstrapResult.Failure -> {
|
||||
if (it is BootstrapResult.GenericError
|
||||
&& it.failure is im.vector.matrix.android.api.failure.Failure.OtherServerError
|
||||
&& it.failure.httpCode == 401) {
|
||||
|
||||
} else {
|
||||
_viewEvents.post(BootstrapViewEvents.ModalError(it.error ?: stringProvider.getString(R.string.matrix_error)))
|
||||
setState {
|
||||
copy(
|
||||
step = BootstrapStep.ConfirmPassphrase(false)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =======================================
|
||||
// Fragment interaction
|
||||
// =======================================
|
||||
|
||||
private fun queryBack() = withState { state ->
|
||||
when (state.step) {
|
||||
is BootstrapStep.SetupPassphrase -> {
|
||||
is BootstrapStep.SetupPassphrase -> {
|
||||
|
||||
}
|
||||
is BootstrapStep.ConfirmPassphrase -> {
|
||||
|
@ -20,6 +20,9 @@ import im.vector.riotx.core.platform.VectorViewEvents
|
||||
|
||||
sealed class BootstrapViewEvents : VectorViewEvents {
|
||||
object Dismiss : BootstrapViewEvents()
|
||||
// data class RequestPassword(val sessionId: String, val userId: String) : BootstrapViewEvents()
|
||||
data class ModalError(val error: String) : BootstrapViewEvents()
|
||||
object RecoveryKeySaved: BootstrapViewEvents()
|
||||
// data class Failure(val throwable: Throwable) : DevicesViewEvents()
|
||||
//
|
||||
// object RequestPassword : DevicesViewEvents()
|
||||
|
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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.crypto.recover
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import com.airbnb.mvrx.parentFragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import kotlinx.android.synthetic.main.fragment_bootstrap_waiting.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class BootstrapWaitingFragment @Inject constructor() : VectorBaseFragment() {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_bootstrap_waiting
|
||||
|
||||
val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(sharedViewModel) { state ->
|
||||
if (state.step !is BootstrapStep.Initializing) return@withState
|
||||
bootstrapLoadingStatusText.text = state.initializationWaitingViewData?.message
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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.crypto.recover
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.DialogInterface
|
||||
import android.view.KeyEvent
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import im.vector.riotx.R
|
||||
import me.gujun.android.span.image
|
||||
import me.gujun.android.span.span
|
||||
|
||||
class KeepItSafeDialog {
|
||||
|
||||
fun show(activity: Activity) {
|
||||
val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_recovery_key_saved_info, null)
|
||||
|
||||
val descriptionText = dialogLayout.findViewById<TextView>(R.id.keepItSafeText)
|
||||
|
||||
descriptionText.text = span {
|
||||
span {
|
||||
image(ContextCompat.getDrawable(activity, R.drawable.ic_check_on)!!)
|
||||
+" "
|
||||
+activity.getString(R.string.bootstrap_crosssigning_print_it)
|
||||
+"\n\n"
|
||||
image(ContextCompat.getDrawable(activity, R.drawable.ic_check_on)!!)
|
||||
+" "
|
||||
+activity.getString(R.string.bootstrap_crosssigning_save_usb)
|
||||
+"\n\n"
|
||||
image(ContextCompat.getDrawable(activity, R.drawable.ic_check_on)!!)
|
||||
+" "
|
||||
+activity.getString(R.string.bootstrap_crosssigning_save_cloud)
|
||||
+"\n\n"
|
||||
}
|
||||
}
|
||||
|
||||
AlertDialog.Builder(activity)
|
||||
// .setIcon(android.R.drawable.ic_dialog_alert)
|
||||
// .setTitle(R.string.devices_delete_dialog_title)
|
||||
.setView(dialogLayout)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event ->
|
||||
if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
dialog.cancel()
|
||||
return@OnKeyListener true
|
||||
}
|
||||
false
|
||||
})
|
||||
.create()
|
||||
.show()
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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.crypto.recover
|
||||
|
||||
fun String.formatRecoveryKey(): String {
|
||||
return this.replace(" ", "")
|
||||
.chunked(16)
|
||||
.joinToString("\n") {
|
||||
it
|
||||
.chunked(4)
|
||||
.joinToString(" ")
|
||||
}
|
||||
}
|
@ -25,7 +25,6 @@ import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import butterknife.BindView
|
||||
@ -130,7 +129,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return super.onCreateDialog(savedInstanceState).apply {
|
||||
setOnKeyListener { _, keyCode, keyEvent ->
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK && keyEvent.action == KeyEvent.ACTION_UP) {
|
||||
@ -352,11 +351,11 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
fun View.getParentCoordinatorLayout(): CoordinatorLayout? {
|
||||
var current = this as? View
|
||||
while (current != null) {
|
||||
if (current is CoordinatorLayout) return current
|
||||
current = current.parent as? View
|
||||
}
|
||||
return null
|
||||
}
|
||||
//fun View.getParentCoordinatorLayout(): CoordinatorLayout? {
|
||||
// var current = this as? View
|
||||
// while (current != null) {
|
||||
// if (current is CoordinatorLayout) return current
|
||||
// current = current.parent as? View
|
||||
// }
|
||||
// return null
|
||||
//}
|
||||
|
@ -219,7 +219,7 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||
if (loginViewState.isUserLogged()) {
|
||||
val intent = HomeActivity.newIntent(
|
||||
this,
|
||||
accountCreation = true //loginViewState.signMode == SignMode.SignUp
|
||||
accountCreation = loginViewState.signMode == SignMode.SignUp
|
||||
)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
|
@ -31,17 +31,18 @@ class ReAuthHelper @Inject constructor() {
|
||||
|
||||
private var rememberedInfo: UserPasswordAuth? = null
|
||||
|
||||
private var clearTask = object : TimerTask() {
|
||||
override fun run() {
|
||||
rememberedInfo = null
|
||||
fun rememberAuth(password: UserPasswordAuth?) {
|
||||
timer?.cancel()
|
||||
timer = null
|
||||
rememberedInfo = password
|
||||
timer = Timer().apply {
|
||||
schedule(object : TimerTask() {
|
||||
override fun run() {
|
||||
rememberedInfo = null
|
||||
}
|
||||
}, THREE_MINUTES)
|
||||
}
|
||||
}
|
||||
|
||||
fun rememberAuth(password: UserPasswordAuth?) {
|
||||
timer?.cancel()
|
||||
rememberedInfo = password
|
||||
timer = Timer().also {
|
||||
it.schedule(clearTask, THREE_MINUTES)
|
||||
}
|
||||
}
|
||||
fun rememberedAuth() = rememberedInfo
|
||||
}
|
||||
|
@ -162,7 +162,9 @@ class VectorFileLogger @Inject constructor(val context: Context, private val vec
|
||||
|
||||
companion object {
|
||||
private val LINE_SEPARATOR = System.getProperty("line.separator") ?: "\n"
|
||||
private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
|
||||
// private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
|
||||
private val DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss*SSSZZZZ", Locale.US)
|
||||
|
||||
private var mIsTimeZoneSet = false
|
||||
}
|
||||
}
|
||||
|
21
vector/src/main/res/drawable/ic_clipboard.xml
Normal file
21
vector/src/main/res/drawable/ic_clipboard.xml
Normal file
@ -0,0 +1,21 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M16,4H18C19.1046,4 20,4.8954 20,6V20C20,21.1046 19.1046,22 18,22H6C4.8954,22 4,21.1046 4,20V6C4,4.8954 4.8954,4 6,4H8"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M8,3C8,2.4477 8.4477,2 9,2H15C15.5523,2 16,2.4477 16,3V5C16,5.5523 15.5523,6 15,6H9C8.4477,6 8,5.5523 8,5V3Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
27
vector/src/main/res/drawable/ic_download.xml
Normal file
27
vector/src/main/res/drawable/ic_download.xml
Normal file
@ -0,0 +1,27 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M3,17V20C3,21.1046 3.8954,22 5,22H19C20.1046,22 21,21.1046 21,20V17"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M8,12L12,16L16,12"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M12,2V16"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
22
vector/src/main/res/drawable/ic_message_key.xml
Normal file
22
vector/src/main/res/drawable/ic_message_key.xml
Normal file
@ -0,0 +1,22 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="22dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="22"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h22v6h-22zM0,17h22v7h-22z"/>
|
||||
<path
|
||||
android:pathData="M21,11.4445C21.0038,12.911 20.6612,14.3577 20,15.6667C18.401,18.8659 15.1321,20.8875 11.5555,20.8889C10.089,20.8927 8.6423,20.5501 7.3333,19.8889L1,22L3.1111,15.6667C2.4499,14.3577 2.1073,12.911 2.1111,11.4445C2.1125,7.8679 4.1341,4.599 7.3333,3C8.6423,2.3388 10.089,1.9962 11.5555,2H12.1111C16.9064,2.2646 20.7354,6.0936 21,10.8889V11.4445V11.4445Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
</group>
|
||||
<path
|
||||
android:pathData="M2,8C0.8954,8 0,8.8954 0,10V13C0,14.1046 0.8954,15 2,15H20C21.1046,15 22,14.1046 22,13V10C22,8.8954 21.1046,8 20,8H2ZM4.25,9.5C3.8358,9.5 3.5,9.8358 3.5,10.25C3.5,10.6642 3.8358,11 4.25,11H6.75C7.1642,11 7.5,10.6642 7.5,10.25C7.5,9.8358 7.1642,9.5 6.75,9.5H4.25ZM8.5,10.25C8.5,9.8358 8.8358,9.5 9.25,9.5H9.75C10.1642,9.5 10.5,9.8358 10.5,10.25C10.5,10.6642 10.1642,11 9.75,11H9.25C8.8358,11 8.5,10.6642 8.5,10.25ZM12.25,9.5C11.8358,9.5 11.5,9.8358 11.5,10.25C11.5,10.6642 11.8358,11 12.25,11H14.75C15.1642,11 15.5,10.6642 15.5,10.25C15.5,9.8358 15.1642,9.5 14.75,9.5H12.25ZM16.5,10.25C16.5,9.8358 16.8358,9.5 17.25,9.5H17.75C18.1642,9.5 18.5,9.8358 18.5,10.25C18.5,10.6642 18.1642,11 17.75,11H17.25C16.8358,11 16.5,10.6642 16.5,10.25ZM4.25,12C3.8358,12 3.5,12.3358 3.5,12.75C3.5,13.1642 3.8358,13.5 4.25,13.5H4.75C5.1642,13.5 5.5,13.1642 5.5,12.75C5.5,12.3358 5.1642,12 4.75,12H4.25ZM6.5,12.75C6.5,12.3358 6.8358,12 7.25,12H9.75C10.1642,12 10.5,12.3358 10.5,12.75C10.5,13.1642 10.1642,13.5 9.75,13.5H7.25C6.8358,13.5 6.5,13.1642 6.5,12.75ZM12.25,12C11.8358,12 11.5,12.3358 11.5,12.75C11.5,13.1642 11.8358,13.5 12.25,13.5H12.75C13.1642,13.5 13.5,13.1642 13.5,12.75C13.5,12.3358 13.1642,12 12.75,12H12.25Z"
|
||||
android:fillColor="#2E2F32"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
22
vector/src/main/res/drawable/ic_message_password.xml
Normal file
22
vector/src/main/res/drawable/ic_message_password.xml
Normal file
@ -0,0 +1,22 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="22dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="22"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h22v6h-22zM0,17h22v7h-22z"/>
|
||||
<path
|
||||
android:pathData="M21,11.4445C21.0038,12.911 20.6612,14.3577 20,15.6667C18.401,18.8659 15.1321,20.8875 11.5555,20.8889C10.089,20.8927 8.6423,20.5501 7.3333,19.8889L1,22L3.1111,15.6667C2.4499,14.3577 2.1073,12.911 2.1111,11.4445C2.1125,7.8679 4.1341,4.599 7.3333,3C8.6423,2.3388 10.089,1.9962 11.5555,2H12.1111C16.9064,2.2646 20.7354,6.0936 21,10.8889V11.4445V11.4445Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
</group>
|
||||
<path
|
||||
android:pathData="M0,10C0,8.8954 0.8954,8 2,8H20C21.1046,8 22,8.8954 22,10V13C22,14.1046 21.1046,15 20,15H2C0.8954,15 0,14.1046 0,13V10ZM5,11.5C5,12.3284 4.3284,13 3.5,13C2.6716,13 2,12.3284 2,11.5C2,10.6716 2.6716,10 3.5,10C4.3284,10 5,10.6716 5,11.5ZM8.5,13C9.3284,13 10,12.3284 10,11.5C10,10.6716 9.3284,10 8.5,10C7.6716,10 7,10.6716 7,11.5C7,12.3284 7.6716,13 8.5,13ZM15,11.5C15,12.3284 14.3284,13 13.5,13C12.6716,13 12,12.3284 12,11.5C12,10.6716 12.6716,10 13.5,10C14.3284,10 15,10.6716 15,11.5ZM18.5,13C19.3284,13 20,12.3284 20,11.5C20,10.6716 19.3284,10 18.5,10C17.6716,10 17,10.6716 17,11.5C17,12.3284 17.6716,13 18.5,13Z"
|
||||
android:fillColor="#2E2F32"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
@ -1,22 +1,21 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="15dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="15"
|
||||
android:viewportHeight="16">
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M14,15v-1.556c0,-1.718 -1.455,-3.11 -3.25,-3.11h-6.5c-1.795,0 -3.25,1.392 -3.25,3.11L1,15"
|
||||
android:pathData="M20,21V19C20,16.7909 18.2091,15 16,15H8C5.7909,15 4,16.7909 4,19V21"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.167"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#7E899C"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M4.25,4.111a3.25,3.111 0,1 0,6.5 0a3.25,3.111 0,1 0,-6.5 0z"
|
||||
android:pathData="M12,11C14.2091,11 16,9.2091 16,7C16,4.7909 14.2091,3 12,3C9.7909,3 8,4.7909 8,7C8,9.2091 9.7909,11 12,11Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.167"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#7E899C"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
|
@ -12,17 +12,19 @@
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp">
|
||||
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/bootstrapIcon"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:layout_marginStart="16dp"
|
||||
android:contentDescription="@string/avatar"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/ic_shield_black"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_message_password"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
@ -32,6 +34,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
@ -52,64 +55,6 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/bootstrapTitleText" />
|
||||
|
||||
<!-- <TextView-->
|
||||
<!-- android:id="@+id/bootstrapDescriptionText"-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="wrap_content"-->
|
||||
<!-- android:layout_marginTop="24dp"-->
|
||||
<!-- android:text="@string/bootstrap_info_text"-->
|
||||
<!-- android:textColor="?riotx_text_primary"-->
|
||||
<!-- android:textSize="14sp"-->
|
||||
<!-- app:layout_constraintTop_toBottomOf="@+id/bootstrapTitleText" />-->
|
||||
|
||||
<!-- <com.google.android.material.textfield.TextInputLayout-->
|
||||
<!-- android:id="@+id/ssss_passphrase_enter_til"-->
|
||||
<!-- style="@style/VectorTextInputLayout"-->
|
||||
<!-- android:layout_width="0dp"-->
|
||||
<!-- android:layout_height="wrap_content"-->
|
||||
<!-- android:layout_marginTop="16dp"-->
|
||||
<!-- app:errorEnabled="true"-->
|
||||
<!-- app:layout_constraintEnd_toStartOf="@id/ssss_view_show_password"-->
|
||||
<!-- app:layout_constraintStart_toStartOf="parent"-->
|
||||
<!-- app:layout_constraintTop_toBottomOf="@id/bootstrapDescriptionText">-->
|
||||
|
||||
<!-- <com.google.android.material.textfield.TextInputEditText-->
|
||||
<!-- android:id="@+id/ssss_passphrase_enter_edittext"-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="wrap_content"-->
|
||||
<!-- android:hint="@string/passphrase_enter_passphrase"-->
|
||||
<!-- android:imeOptions="actionDone"-->
|
||||
<!-- android:maxLines="3"-->
|
||||
<!-- android:singleLine="false"-->
|
||||
<!-- android:textColor="?android:textColorPrimary"-->
|
||||
<!-- tools:inputType="textPassword" />-->
|
||||
|
||||
<!-- </com.google.android.material.textfield.TextInputLayout>-->
|
||||
|
||||
<!-- <ImageView-->
|
||||
<!-- android:id="@+id/ssss_view_show_password"-->
|
||||
<!-- android:layout_width="@dimen/layout_touch_size"-->
|
||||
<!-- android:layout_height="@dimen/layout_touch_size"-->
|
||||
<!-- android:layout_marginTop="8dp"-->
|
||||
<!-- android:background="?attr/selectableItemBackground"-->
|
||||
<!-- android:scaleType="center"-->
|
||||
<!-- android:src="@drawable/ic_eye_black"-->
|
||||
<!-- android:tint="?colorAccent"-->
|
||||
<!-- app:layout_constraintEnd_toEndOf="parent"-->
|
||||
<!-- app:layout_constraintStart_toEndOf="@+id/ssss_passphrase_enter_til"-->
|
||||
<!-- app:layout_constraintTop_toTopOf="@+id/ssss_passphrase_enter_til" />-->
|
||||
|
||||
|
||||
<!-- <TextView-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="wrap_content"-->
|
||||
<!-- android:layout_marginTop="8dp"-->
|
||||
<!-- android:drawableStart="@drawable/e2e_warning"-->
|
||||
<!-- android:drawablePadding="4dp"-->
|
||||
<!-- android:text="@string/bootstrap_dont_reuse_pwd"-->
|
||||
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
|
||||
<!-- app:layout_constraintTop_toBottomOf="@id/ssss_passphrase_enter_til" />-->
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
@ -58,11 +58,10 @@
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/bottomSheetFragmentContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/verificationRequestAvatar" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/layout_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="?dialogPreferredPadding"
|
||||
android:paddingLeft="?dialogPreferredPadding"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingEnd="?dialogPreferredPadding"
|
||||
android:paddingRight="?dialogPreferredPadding"
|
||||
android:paddingBottom="12dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/bootstrapIcon"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:contentDescription="@string/avatar"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_message_key" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bootstrapTitleText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:text="@string/keep_it_safe"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/keepItSafeText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16sp"
|
||||
tools:text="@string/bootstrap_crosssigning_save_usb" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
50
vector/src/main/res/layout/fragment_bootstrap_conclusion.xml
Normal file
50
vector/src/main/res/layout/fragment_bootstrap_conclusion.xml
Normal file
@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="200dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="16dp"
|
||||
android:paddingTop="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bootstrapConclusionText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/bootstrap_cross_signing_success"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<Space
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/vctr_list_divider_color" />
|
||||
|
||||
<im.vector.riotx.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/bootstrapConclusionContinue"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="50dp"
|
||||
app:actionTitle="@string/_continue"
|
||||
app:forceStartPadding="false"
|
||||
app:rightIcon="@drawable/ic_arrow_right"
|
||||
app:tint="?colorAccent" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/vctr_list_divider_color" />
|
||||
|
||||
</LinearLayout>
|
@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bootstrapDescriptionText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
tools:text="@string/enter_account_password"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toTopOf="@id/bootstrapAccountPasswordTil"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/bootstrapAccountPasswordTil"
|
||||
style="@style/VectorTextInputLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:errorEnabled="true"
|
||||
app:layout_constraintBottom_toTopOf="@+id/bootstrapPasswordButton"
|
||||
app:layout_constraintEnd_toStartOf="@id/ssss_view_show_password"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/bootstrapDescriptionText">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/bootstrapAccountPasswordEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionDone"
|
||||
android:maxLines="3"
|
||||
android:singleLine="false"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
tools:hint="@string/passphrase_enter_passphrase"
|
||||
tools:inputType="textPassword" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ssss_view_show_password"
|
||||
android:layout_width="@dimen/layout_touch_size"
|
||||
android:layout_height="@dimen/layout_touch_size"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/ic_eye_black"
|
||||
android:tint="?colorAccent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/bootstrapAccountPasswordTil"
|
||||
app:layout_constraintTop_toTopOf="@+id/bootstrapAccountPasswordTil" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/bootstrapPasswordButton"
|
||||
style="@style/VectorButtonStyleText"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginTop="@dimen/layout_vertical_margin"
|
||||
android:padding="8dp"
|
||||
android:text="@string/_continue"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/bootstrapAccountPasswordTil" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -5,7 +5,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:padding="16dp">
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bootstrapDescriptionText"
|
||||
|
88
vector/src/main/res/layout/fragment_bootstrap_save_key.xml
Normal file
88
vector/src/main/res/layout/fragment_bootstrap_save_key.xml
Normal file
@ -0,0 +1,88 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<LinearLayout 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:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="200dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bootstrapSaveText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/bootstrap_save_key_description"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bootstrapRecoveryKeyText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="@dimen/layout_horizontal_margin"
|
||||
android:fontFamily="monospace"
|
||||
android:gravity="center_horizontal"
|
||||
android:padding="4dp"
|
||||
android:textColor="?vctr_notice_secondary"
|
||||
android:textSize="15sp"
|
||||
tools:text="HHWJ Y8DK RDR4\nBQEN FQ4V M4F8\nBQEN FQ4V M4A8" />
|
||||
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/vctr_list_divider_color" />
|
||||
|
||||
<im.vector.riotx.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/recoveryCopy"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="50dp"
|
||||
app:actionTitle="@string/copy_value"
|
||||
app:leftIcon="@drawable/ic_clipboard"
|
||||
app:rightIcon="@drawable/ic_arrow_right"
|
||||
app:tint="?colorAccent" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/vctr_list_divider_color" />
|
||||
|
||||
<im.vector.riotx.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/recoverySave"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="50dp"
|
||||
app:actionTitle="@string/keys_backup_setup_step3_save_button_title"
|
||||
app:leftIcon="@drawable/ic_download"
|
||||
app:rightIcon="@drawable/ic_arrow_right"
|
||||
app:tint="?colorAccent" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/vctr_list_divider_color" />
|
||||
|
||||
<im.vector.riotx.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/recoveryContinue"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="50dp"
|
||||
app:actionTitle="@string/_continue"
|
||||
app:forceStartPadding="true"
|
||||
app:rightIcon="@drawable/ic_arrow_right"
|
||||
app:tint="?colorAccent" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/vctr_list_divider_color" />
|
||||
|
||||
|
||||
</LinearLayout>
|
41
vector/src/main/res/layout/fragment_bootstrap_waiting.xml
Normal file
41
vector/src/main/res/layout/fragment_bootstrap_waiting.xml
Normal file
@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="200dp"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bootstrapDescriptionText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/bootstrap_loading_text"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/bootstrapWaitingProgress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/bootstrapDescriptionText" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bootstrapLoadingStatusText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintTop_toBottomOf="@id/bootstrapWaitingProgress"
|
||||
tools:text="Bending the spoon..." />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -2,6 +2,7 @@
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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/itemVerificationClickableZone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
@ -13,18 +14,34 @@
|
||||
android:paddingRight="@dimen/layout_horizontal_margin"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/itemVerificationLeftIcon"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:scaleType="center"
|
||||
android:tint="?riotx_text_primary"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/ic_share"
|
||||
tools:visibility="visible" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemVerificationActionTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:textColor="@color/riotx_accent"
|
||||
android:textSize="16sp"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toTopOf="@+id/itemVerificationActionSubTitle"
|
||||
app:layout_constraintEnd_toStartOf="@+id/itemVerificationActionIcon"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/itemVerificationLeftIcon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
app:layout_goneMarginStart="0dp"
|
||||
tools:text="@string/start_verification" />
|
||||
|
||||
<TextView
|
||||
@ -38,7 +55,7 @@
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/itemVerificationActionIcon"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/itemVerificationActionTitle"
|
||||
app:layout_constraintTop_toBottomOf="@+id/itemVerificationActionTitle"
|
||||
tools:text="For maximum security, do this in person"
|
||||
tools:visibility="visible" />
|
||||
|
@ -105,4 +105,13 @@
|
||||
<attr name="optionIsWinner" format="boolean" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="BottomSheetActionButton">
|
||||
<attr name="tint" format="color" />
|
||||
<attr name="actionTitle" format="string" />
|
||||
<attr name="actionDescription" format="string" />
|
||||
<attr name="leftIcon" format="reference" />
|
||||
<attr name="rightIcon" format="reference" />
|
||||
<attr name="forceStartPadding" format="boolean" />
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
||||
|
@ -32,13 +32,56 @@
|
||||
<string name="verify_cancelled_notice">Verify your devices from Settings.</string>
|
||||
<string name="verification_cancelled">Verification Cancelled</string>
|
||||
|
||||
<string name="recovery_passphrase">Recovery Passphrase</string>
|
||||
<string name="recovery_passphrase">Message Password</string>
|
||||
<string name="message_key">Message Key</string>
|
||||
<string name="account_password">Account Password</string>
|
||||
|
||||
|
||||
<!-- %s will be replaced by recovery_passphrase -->
|
||||
<string name="bootstrap_info_text">Secure & unlock information with a %s so only you can access encrypted messages and secure information.</string>
|
||||
<string name="set_recovery_passphrase">Set a %s</string>
|
||||
|
||||
<!-- %s will be replaced by recovery_passphrase -->
|
||||
<string name="confirm_recovery_passphrase">Confirm %s</string>
|
||||
|
||||
|
||||
<!-- %s will be replaced by account_password -->
|
||||
<string name="enter_account_password">Enter your %s to continue.</string>
|
||||
|
||||
<!-- %s will be replaced by recovery_passphrase -->
|
||||
<string name="bootstrap_info_text">Secure & unlock encrypted messages and trust with a %s.</string>
|
||||
<!-- %s will be replaced by recovery_passphrase -->
|
||||
<string name="bootstrap_info_confirm_text">Enter your %s again to confirm it.</string>
|
||||
<string name="bootstrap_dont_reuse_pwd">Don’t re-use your account password.</string>
|
||||
|
||||
|
||||
<string name="bootstrap_loading_text">This might take several seconds, please be patient.</string>
|
||||
<string name="bootstrap_loading_title">Setting up recovery.</string>
|
||||
<string name="your_recovery_key">Your recovery key</string>
|
||||
<string name="bootstrap_finish_title">You‘re done!</string>
|
||||
<string name="keep_it_safe">Keep it safe</string>
|
||||
|
||||
<!-- %1$s is replaced by message_key and %2$s by recovery_passphrase -->
|
||||
<string name="bootstrap_save_key_description">Use this %1$s as a safety net in case you forget your %2$s.</string>
|
||||
|
||||
<string name="bootstrap_crosssigning_progress_initializing">Publishing created identity keys</string>
|
||||
<string name="bootstrap_crosssigning_progress_pbkdf2">Generating secure key from passphrase</string>
|
||||
<string name="bootstrap_crosssigning_progress_default_key">Defining SSSS default Key</string>
|
||||
<string name="bootstrap_crosssigning_progress_save_msk">Synchronizing Master key</string>
|
||||
<string name="bootstrap_crosssigning_progress_save_usk">Synchronizing User key</string>
|
||||
<string name="bootstrap_crosssigning_progress_save_ssk">Synchronizing Self Signing key</string>
|
||||
|
||||
|
||||
<!-- %1$s is replaced by message_key and %2$s by recovery_passphrase -->
|
||||
<string name="bootstrap_cross_signing_success">Your %2$s & %1$s are now set.\n\nKeep them safe! You’ll need them to unlock encrypted messages and secure information if you lose all of your active sessions.</string>
|
||||
|
||||
<!-- the %s will be replaced by a check mark on screen-->
|
||||
<string name="bootstrap_crosssigning_print_it">Print it and store it somewhere safe</string>
|
||||
<string name="bootstrap_crosssigning_save_usb">Save it on a USB key or backup drive</string>
|
||||
<string name="bootstrap_crosssigning_save_cloud">Copy it to your personal cloud storage</string>
|
||||
|
||||
<string name="auth_flow_not_supported">You cannot do that from mobile</string>
|
||||
|
||||
|
||||
<!-- END Strings added by Valere -->
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user