Full bootstrap flow initial commit

This commit is contained in:
Valere 2020-04-02 16:51:03 +02:00
parent 8ecdac7c31
commit bf5ba99653
44 changed files with 1616 additions and 174 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()
}
}

View File

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

View File

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

View File

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

View File

@ -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()
}

View File

@ -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()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()
}
}

View File

@ -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(" ")
}
}

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

@ -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 &amp; 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 &amp; 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">Dont 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">Youre 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 &amp; %1$s are now set.\n\nKeep them safe! Youll 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 -->