Enable reset all and skip options (#7721)

* Dismiss bottomsheet on skipping verification

* Enable reset all and skip options

* Change ResetAll bottomsheet event to no-op for user verification

* Fix strings and improve state step logic in SharedSecureStorageViewModel
This commit is contained in:
Amit Kumar 2022-12-07 19:40:22 +05:30 committed by GitHub
parent 6965c0c5ab
commit 17d25e2597
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 89 additions and 63 deletions

View file

@ -411,6 +411,7 @@
<string name="action_play">Play</string> <string name="action_play">Play</string>
<string name="action_dismiss">Dismiss</string> <string name="action_dismiss">Dismiss</string>
<string name="action_reset">Reset</string> <string name="action_reset">Reset</string>
<string name="action_proceed_to_reset">Proceed to reset</string>
<string name="action_learn_more">Learn more</string> <string name="action_learn_more">Learn more</string>
<string name="action_next">Next</string> <string name="action_next">Next</string>
<string name="action_got_it">Got it</string> <string name="action_got_it">Got it</string>
@ -2635,8 +2636,10 @@
<string name="bad_passphrase_key_reset_all_action">Forgot or lost all recovery options? Reset everything</string> <string name="bad_passphrase_key_reset_all_action">Forgot or lost all recovery options? Reset everything</string>
<string name="secure_backup_reset_all">Reset everything</string> <string name="secure_backup_reset_all">Reset everything</string>
<string name="secure_backup_reset_all_no_other_devices">Only do this if you have no other device you can verify this device with.</string> <string name="secure_backup_reset_all_no_other_devices">Only do this if you have no other device you can verify this device with.</string>
<string name="secure_backup_reset_all_no_other_devices_long">Resetting your verification keys cannot be undone. After resetting, you won\'t have access to old encrypted messages, and any friends who have previously verified you will see security warnings until you re-verify with them.</string>
<string name="secure_backup_reset_if_you_reset_all">If you reset everything</string> <string name="secure_backup_reset_if_you_reset_all">If you reset everything</string>
<string name="secure_backup_reset_no_history">You will restart with no history, no messages, trusted devices or trusted users</string> <string name="secure_backup_reset_no_history">You will restart with no history, no messages, trusted devices or trusted users</string>
<string name="secure_backup_reset_danger_warning">Please only proceed if you\'re sure you\'ve lost all of your other devices and your security key.</string>
<plurals name="secure_backup_reset_devices_you_can_verify"> <plurals name="secure_backup_reset_devices_you_can_verify">
<item quantity="one">Show the device you can verify with now</item> <item quantity="one">Show the device you can verify with now</item>
<item quantity="other">Show %d devices you can verify with now</item> <item quantity="other">Show %d devices you can verify with now</item>

View file

@ -18,9 +18,7 @@ package org.matrix.android.sdk.internal.session.room.summary
import io.realm.Realm import io.realm.Realm
import io.realm.kotlin.createObject import io.realm.kotlin.createObject
import kotlinx.coroutines.runBlocking
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
@ -41,7 +39,6 @@ import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary
import org.matrix.android.sdk.api.session.sync.model.RoomSyncUnreadNotifications import org.matrix.android.sdk.api.session.sync.model.RoomSyncUnreadNotifications
import org.matrix.android.sdk.api.session.sync.model.RoomSyncUnreadThreadNotifications import org.matrix.android.sdk.api.session.sync.model.RoomSyncUnreadThreadNotifications
import org.matrix.android.sdk.internal.crypto.EventDecryptor
import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.mapper.ContentMapper
import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity

View file

@ -50,9 +50,9 @@ internal class RoomSummaryEventDecryptor @Inject constructor(
} }
private val scope: CoroutineScope = CoroutineScope( private val scope: CoroutineScope = CoroutineScope(
cryptoCoroutineScope.coroutineContext cryptoCoroutineScope.coroutineContext +
+ SupervisorJob() SupervisorJob() +
+ CoroutineName("RoomSummaryDecryptor") CoroutineName("RoomSummaryDecryptor")
) )
private val channel = Channel<Message>(capacity = 300) private val channel = Channel<Message>(capacity = 300)
@ -116,8 +116,8 @@ internal class RoomSummaryEventDecryptor @Inject constructor(
} }
} }
if (failure.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID if (failure.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID ||
|| failure.errorType == MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX) { failure.errorType == MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX) {
(event.content["session_id"] as? String)?.let { sessionId -> (event.content["session_id"] as? String)?.let { sessionId ->
unknownSessionsFailure.getOrPut(sessionId) { mutableSetOf() } unknownSessionsFailure.getOrPut(sessionId) { mutableSetOf() }
.add(event) .add(event)

View file

@ -16,8 +16,6 @@
package org.matrix.android.sdk.api.session.crypto.keysbackup package org.matrix.android.sdk.api.session.crypto.keysbackup
import org.matrix.android.sdk.api.session.securestorage.SsssPassphrase
object BackupUtils { object BackupUtils {
fun recoveryKeyFromBase58(key: String): IBackupRecoveryKey? = BackupRecoveryKey.fromBase58(key) fun recoveryKeyFromBase58(key: String): IBackupRecoveryKey? = BackupRecoveryKey.fromBase58(key)
fun recoveryKeyFromPassphrase(passphrase: String): IBackupRecoveryKey? = BackupRecoveryKey.newFromPassphrase(passphrase) fun recoveryKeyFromPassphrase(passphrase: String): IBackupRecoveryKey? = BackupRecoveryKey.newFromPassphrase(passphrase)

View file

@ -36,7 +36,6 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo
import org.matrix.android.sdk.api.session.crypto.crosssigning.UserTrustResult import org.matrix.android.sdk.api.session.crypto.crosssigning.UserTrustResult
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
@ -52,7 +51,6 @@ import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.coroutines.builder.safeInvokeOnClose import org.matrix.android.sdk.internal.coroutines.builder.safeInvokeOnClose
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.DefaultKeysAlgorithmAndData import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.DefaultKeysAlgorithmAndData
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysAlgorithmAndData import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysAlgorithmAndData
import org.matrix.android.sdk.internal.crypto.network.RequestSender import org.matrix.android.sdk.internal.crypto.network.RequestSender

View file

@ -49,6 +49,7 @@ class SharedSecureStorageActivity :
val requestedSecrets: List<String> = emptyList(), val requestedSecrets: List<String> = emptyList(),
val resultKeyStoreAlias: String, val resultKeyStoreAlias: String,
val writeSecrets: List<Pair<String, String>> = emptyList(), val writeSecrets: List<Pair<String, String>> = emptyList(),
val currentStep: SharedSecureStorageViewState.Step = SharedSecureStorageViewState.Step.EnterPassphrase,
) : Parcelable ) : Parcelable
private val viewModel: SharedSecureStorageViewModel by viewModel() private val viewModel: SharedSecureStorageViewModel by viewModel()
@ -150,7 +151,8 @@ class SharedSecureStorageActivity :
context: Context, context: Context,
keyId: String? = null, keyId: String? = null,
requestedSecrets: List<String>, requestedSecrets: List<String>,
resultKeyStoreAlias: String = DEFAULT_RESULT_KEYSTORE_ALIAS resultKeyStoreAlias: String = DEFAULT_RESULT_KEYSTORE_ALIAS,
initialStep: SharedSecureStorageViewState.Step = SharedSecureStorageViewState.Step.EnterPassphrase
): Intent { ): Intent {
require(requestedSecrets.isNotEmpty()) require(requestedSecrets.isNotEmpty())
return Intent(context, SharedSecureStorageActivity::class.java).also { return Intent(context, SharedSecureStorageActivity::class.java).also {
@ -159,7 +161,8 @@ class SharedSecureStorageActivity :
Args( Args(
keyId = keyId, keyId = keyId,
requestedSecrets = requestedSecrets, requestedSecrets = requestedSecrets,
resultKeyStoreAlias = resultKeyStoreAlias resultKeyStoreAlias = resultKeyStoreAlias,
currentStep = initialStep
) )
) )
} }
@ -169,7 +172,8 @@ class SharedSecureStorageActivity :
context: Context, context: Context,
keyId: String? = null, keyId: String? = null,
writeSecrets: List<Pair<String, String>>, writeSecrets: List<Pair<String, String>>,
resultKeyStoreAlias: String = DEFAULT_RESULT_KEYSTORE_ALIAS resultKeyStoreAlias: String = DEFAULT_RESULT_KEYSTORE_ALIAS,
initialStep: SharedSecureStorageViewState.Step = SharedSecureStorageViewState.Step.EnterPassphrase
): Intent { ): Intent {
require(writeSecrets.isNotEmpty()) require(writeSecrets.isNotEmpty())
return Intent(context, SharedSecureStorageActivity::class.java).also { return Intent(context, SharedSecureStorageActivity::class.java).also {
@ -178,7 +182,8 @@ class SharedSecureStorageActivity :
Args( Args(
keyId = keyId, keyId = keyId,
writeSecrets = writeSecrets, writeSecrets = writeSecrets,
resultKeyStoreAlias = resultKeyStoreAlias resultKeyStoreAlias = resultKeyStoreAlias,
currentStep = initialStep,
) )
) )
} }

View file

@ -58,7 +58,7 @@ data class SharedSecureStorageViewState(
val ready: Boolean = false, val ready: Boolean = false,
val hasPassphrase: Boolean = true, val hasPassphrase: Boolean = true,
val checkingSSSSAction: Async<Unit> = Uninitialized, val checkingSSSSAction: Async<Unit> = Uninitialized,
val step: Step = Step.EnterPassphrase, val step: Step = Step.ResetAll,
val activeDeviceCount: Int = 0, val activeDeviceCount: Int = 0,
val showResetAllAction: Boolean = false, val showResetAllAction: Boolean = false,
val userId: String = "", val userId: String = "",
@ -74,7 +74,8 @@ data class SharedSecureStorageViewState(
} else { } else {
RequestType.ReadSecrets(args.requestedSecrets) RequestType.ReadSecrets(args.requestedSecrets)
}, },
resultKeyStoreAlias = args.resultKeyStoreAlias resultKeyStoreAlias = args.resultKeyStoreAlias,
step = args.currentStep,
) )
enum class Step { enum class Step {
@ -113,30 +114,35 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
) )
} }
} }
val keyResult = initialState.keyId?.let { session.sharedSecretStorageService().getKey(it) }
?: session.sharedSecretStorageService().getDefaultKey()
if (!keyResult.isSuccess()) { if (initialState.step != SharedSecureStorageViewState.Step.ResetAll) {
_viewEvents.post(SharedSecureStorageViewEvent.Dismiss) val keyResult = initialState.keyId?.let { session.sharedSecretStorageService().getKey(it) }
} else { ?: session.sharedSecretStorageService().getDefaultKey()
val info = (keyResult as KeyInfoResult.Success).keyInfo
if (info.content.passphrase != null) { if (!keyResult.isSuccess()) {
setState { _viewEvents.post(SharedSecureStorageViewEvent.Dismiss)
copy(
hasPassphrase = true,
ready = true,
step = SharedSecureStorageViewState.Step.EnterPassphrase
)
}
} else { } else {
setState { val info = (keyResult as KeyInfoResult.Success).keyInfo
copy( if (info.content.passphrase != null) {
hasPassphrase = false, setState {
ready = true, copy(
step = SharedSecureStorageViewState.Step.EnterKey hasPassphrase = true,
) ready = true,
step = SharedSecureStorageViewState.Step.EnterPassphrase
)
}
} else {
setState {
copy(
hasPassphrase = false,
ready = true,
step = SharedSecureStorageViewState.Step.EnterKey
)
}
} }
} }
} else {
setState { copy(ready = true) }
} }
session.flow() session.flow()
@ -203,6 +209,7 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
_viewEvents.post(SharedSecureStorageViewEvent.Dismiss) _viewEvents.post(SharedSecureStorageViewEvent.Dismiss)
} }
} }
/*
SharedSecureStorageViewState.Step.ResetAll -> { SharedSecureStorageViewState.Step.ResetAll -> {
setState { setState {
copy( copy(
@ -211,6 +218,7 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
) )
} }
} }
*/
else -> { else -> {
_viewEvents.post(SharedSecureStorageViewEvent.Dismiss) _viewEvents.post(SharedSecureStorageViewEvent.Dismiss)
} }

View file

@ -31,6 +31,7 @@ sealed class VerificationAction : VectorViewModelAction {
data class GotItConclusion(val verified: Boolean) : VerificationAction() data class GotItConclusion(val verified: Boolean) : VerificationAction()
object FailedToGetKeysFrom4S : VerificationAction() object FailedToGetKeysFrom4S : VerificationAction()
object SkipVerification : VerificationAction() object SkipVerification : VerificationAction()
object ForgotResetAll : VerificationAction()
object VerifyFromPassphrase : VerificationAction() object VerifyFromPassphrase : VerificationAction()
object ReadyPendingVerification : VerificationAction() object ReadyPendingVerification : VerificationAction()
object CancelPendingVerification : VerificationAction() object CancelPendingVerification : VerificationAction()

View file

@ -24,6 +24,7 @@ import im.vector.app.core.platform.VectorViewEvents
sealed class VerificationBottomSheetViewEvents : VectorViewEvents { sealed class VerificationBottomSheetViewEvents : VectorViewEvents {
object Dismiss : VerificationBottomSheetViewEvents() object Dismiss : VerificationBottomSheetViewEvents()
object AccessSecretStore : VerificationBottomSheetViewEvents() object AccessSecretStore : VerificationBottomSheetViewEvents()
object ResetAll : VerificationBottomSheetViewEvents()
object GoToSettings : VerificationBottomSheetViewEvents() object GoToSettings : VerificationBottomSheetViewEvents()
data class ModalError(val errorMessage: CharSequence) : VerificationBottomSheetViewEvents() data class ModalError(val errorMessage: CharSequence) : VerificationBottomSheetViewEvents()
} }

View file

@ -34,6 +34,7 @@ import im.vector.app.core.extensions.toMvRxBundle
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.databinding.BottomSheetVerificationBinding import im.vector.app.databinding.BottomSheetVerificationBinding
import im.vector.app.features.crypto.quads.SharedSecureStorageActivity import im.vector.app.features.crypto.quads.SharedSecureStorageActivity
import im.vector.app.features.crypto.quads.SharedSecureStorageViewState
import im.vector.app.features.crypto.verification.VerificationAction import im.vector.app.features.crypto.verification.VerificationAction
import im.vector.app.features.crypto.verification.VerificationBottomSheetViewEvents import im.vector.app.features.crypto.verification.VerificationBottomSheetViewEvents
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ -92,7 +93,18 @@ class SelfVerificationBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSh
requireContext(), requireContext(),
null, // use default key null, // use default key
listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME), listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME),
SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS,
)
)
}
VerificationBottomSheetViewEvents.ResetAll -> {
secretStartForActivityResult.launch(
SharedSecureStorageActivity.newReadIntent(
requireContext(),
null, // use default key
listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME),
SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS,
SharedSecureStorageViewState.Step.ResetAll
) )
) )
} }

View file

@ -254,8 +254,8 @@ class SelfVerificationController @Inject constructor(
} }
} }
is Success -> { is Success -> {
val invoke = action.invoke() val value = action.invoke()
if (invoke) { if (value) {
verifiedSuccessTile() verifiedSuccessTile()
bottomDone { (host.listener as? InteractionListener)?.onDoneFrom4S() } bottomDone { (host.listener as? InteractionListener)?.onDoneFrom4S() }
} else { } else {

View file

@ -77,11 +77,11 @@ class SelfVerificationFragment : VectorBaseFragment<BottomSheetVerificationChil
} }
override fun onClickSkip() { override fun onClickSkip() {
TODO("Not yet implemented") viewModel.handle(VerificationAction.SkipVerification)
} }
override fun onClickResetSecurity() { override fun onClickResetSecurity() {
TODO("Not yet implemented") viewModel.handle(VerificationAction.ForgotResetAll)
} }
override fun onDoneFrom4S() { override fun onDoneFrom4S() {

View file

@ -286,8 +286,17 @@ class SelfVerificationViewModel @AssistedInject constructor(
} }
} }
} }
VerificationAction.SecuredStorageHasBeenReset -> TODO() VerificationAction.SecuredStorageHasBeenReset -> {
VerificationAction.SkipVerification -> TODO() if (session.cryptoService().crossSigningService().allPrivateKeysKnown()) {
_viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)
}
}
VerificationAction.SkipVerification -> {
_viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)
}
VerificationAction.ForgotResetAll -> {
_viewEvents.post(VerificationBottomSheetViewEvents.ResetAll)
}
VerificationAction.StartSASVerification -> { VerificationAction.StartSASVerification -> {
withState { state -> withState { state ->
val request = state.pendingRequest.invoke() ?: return@withState val request = state.pendingRequest.invoke() ?: return@withState

View file

@ -90,6 +90,9 @@ class UserVerificationBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSh
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
} }
VerificationBottomSheetViewEvents.ResetAll -> {
// no-op for user verification
}
} }
} }
} }

View file

@ -385,6 +385,9 @@ class UserVerificationViewModel @AssistedInject constructor(
// Not applicable for user verification // Not applicable for user verification
} }
VerificationAction.RequestSelfVerification -> TODO() VerificationAction.RequestSelfVerification -> TODO()
VerificationAction.ForgotResetAll -> {
// Not applicable for user verification
}
} }
} }

View file

@ -37,7 +37,7 @@
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
android:text="@string/secure_backup_reset_all_no_other_devices" android:text="@string/secure_backup_reset_all_no_other_devices_long"
android:textColor="?vctr_content_primary" android:textColor="?vctr_content_primary"
app:layout_constraintBottom_toTopOf="@id/ssss_reset_other_devices" app:layout_constraintBottom_toTopOf="@id/ssss_reset_other_devices"
app:layout_constraintTop_toBottomOf="@id/reset_title" /> app:layout_constraintTop_toBottomOf="@id/reset_title" />
@ -60,19 +60,6 @@
tools:text="Show 2 devices you can verify with now" tools:text="Show 2 devices you can verify with now"
tools:visibility="visible" /> tools:visibility="visible" />
<TextView
android:id="@+id/ssss_reset_text3"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="@string/secure_backup_reset_if_you_reset_all"
android:textColor="?colorError"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ssss_reset_other_devices" />
<TextView <TextView
android:id="@+id/ssss_reset_text4" android:id="@+id/ssss_reset_text4"
style="@style/Widget.Vector.TextView.Body" style="@style/Widget.Vector.TextView.Body"
@ -82,9 +69,10 @@
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
android:text="@string/secure_backup_reset_no_history" android:text="@string/secure_backup_reset_danger_warning"
android:textColor="?vctr_content_primary" android:textColor="?vctr_content_primary"
app:layout_constraintTop_toBottomOf="@id/ssss_reset_text3" /> app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ssss_reset_other_devices" />
<Button <Button
android:id="@+id/ssss_reset_button_cancel" android:id="@+id/ssss_reset_button_cancel"
@ -99,7 +87,7 @@
style="@style/Widget.Vector.Button.Text" style="@style/Widget.Vector.Button.Text"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/action_reset" android:text="@string/action_proceed_to_reset"
tools:ignore="MissingConstraints" /> tools:ignore="MissingConstraints" />
<androidx.constraintlayout.helper.widget.Flow <androidx.constraintlayout.helper.widget.Flow