mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-26 11:51:10 +01:00
Add verify key megaphone.
This commit is contained in:
committed by
Jeffrey Starke
parent
c6afa17330
commit
359f473b59
@@ -0,0 +1,173 @@
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.horizontalGutters
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.fonts.MonoTypeface
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.restore.BackupKeyVisualTransformation
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.restore.attachBackupKeyAutoFillHelper
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.restore.backupKeyAutoFillHelper
|
||||
import org.whispersystems.signalservice.api.AccountEntropyPool
|
||||
|
||||
/**
|
||||
* Screen to enter backup key with an option to view the backup key again
|
||||
*/
|
||||
@Composable
|
||||
fun EnterKeyScreen(
|
||||
paddingValues: PaddingValues,
|
||||
backupKey: String,
|
||||
onNextClick: () -> Unit,
|
||||
captionContent: @Composable () -> Unit,
|
||||
seeKeyButton: @Composable () -> Unit
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.consumeWindowInsets(paddingValues)
|
||||
.imePadding()
|
||||
.fillMaxSize()
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val visualTransform = remember { BackupKeyVisualTransformation(chunkSize = 4) }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
var enteredBackupKey by remember { mutableStateOf("") }
|
||||
var isBackupKeyValid by remember { mutableStateOf(false) }
|
||||
var showError by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(scrollState)
|
||||
.weight(weight = 1f, fill = false)
|
||||
.horizontalGutters(),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
captionContent()
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
|
||||
val updateEnteredBackupKey = { input: String ->
|
||||
enteredBackupKey = AccountEntropyPool.removeIllegalCharacters(input).uppercase()
|
||||
isBackupKeyValid = enteredBackupKey == backupKey
|
||||
showError = !isBackupKeyValid && enteredBackupKey.length >= backupKey.length
|
||||
}
|
||||
|
||||
var requestFocus: Boolean by remember { mutableStateOf(true) }
|
||||
val autoFillHelper = backupKeyAutoFillHelper { updateEnteredBackupKey(it) }
|
||||
|
||||
TextField(
|
||||
value = enteredBackupKey,
|
||||
onValueChange = {
|
||||
updateEnteredBackupKey(it)
|
||||
autoFillHelper.onValueChanged(it)
|
||||
},
|
||||
label = {
|
||||
Text(text = stringResource(id = R.string.MessageBackupsKeyVerifyScreen__backup_key))
|
||||
},
|
||||
textStyle = LocalTextStyle.current.copy(
|
||||
fontFamily = MonoTypeface.fontFamily(),
|
||||
lineHeight = 36.sp
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Password,
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
imeAction = ImeAction.Next,
|
||||
autoCorrectEnabled = false
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onNext = {
|
||||
if (isBackupKeyValid) {
|
||||
keyboardController?.hide()
|
||||
onNextClick()
|
||||
}
|
||||
}
|
||||
),
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
),
|
||||
supportingText = { if (showError) Text(text = stringResource(R.string.MessageBackupsKeyVerifyScreen__incorrect_backup_key)) },
|
||||
isError = showError,
|
||||
minLines = 4,
|
||||
visualTransformation = visualTransform,
|
||||
modifier = Modifier
|
||||
.testTag("message-backups-key-verify-screen-backup-key-input-field")
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester)
|
||||
.attachBackupKeyAutoFillHelper(autoFillHelper)
|
||||
.onGloballyPositioned {
|
||||
if (requestFocus) {
|
||||
focusRequester.requestFocus()
|
||||
requestFocus = false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Surface(
|
||||
shadowElevation = if (scrollState.canScrollForward) 8.dp else 0.dp,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp, bottom = 24.dp)
|
||||
.horizontalGutters()
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
seeKeyButton()
|
||||
|
||||
Buttons.LargeTonal(
|
||||
enabled = isBackupKeyValid,
|
||||
onClick = onNextClick
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.RegistrationActivity_next)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,71 +6,40 @@
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.ui.compose.BottomSheets
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.ui.compose.horizontalGutters
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.fonts.MonoTypeface
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.restore.BackupKeyVisualTransformation
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.restore.attachBackupKeyAutoFillHelper
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.restore.backupKeyAutoFillHelper
|
||||
import org.whispersystems.signalservice.api.AccountEntropyPool
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import kotlin.random.Random
|
||||
import kotlin.random.nextInt
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
@@ -78,7 +47,7 @@ import org.signal.core.ui.R as CoreUiR
|
||||
/**
|
||||
* Prompt user to re-enter backup key (AEP) to confirm they have it still.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MessageBackupsKeyVerifyScreen(
|
||||
backupKey: String,
|
||||
@@ -96,127 +65,28 @@ fun MessageBackupsKeyVerifyScreen(
|
||||
onNavigationClick = onNavigationClick
|
||||
) { paddingValues ->
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.imePadding()
|
||||
.fillMaxSize()
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val visualTransform = remember { BackupKeyVisualTransformation(chunkSize = 4) }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
var enteredBackupKey by remember { mutableStateOf("") }
|
||||
var isBackupKeyValid by remember { mutableStateOf(false) }
|
||||
var showError by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(scrollState)
|
||||
.weight(weight = 1f, fill = false)
|
||||
.horizontalGutters(),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
EnterKeyScreen(
|
||||
paddingValues = paddingValues,
|
||||
backupKey = backupKey,
|
||||
onNextClick = {
|
||||
coroutineScope.launch { sheetState.show() }
|
||||
},
|
||||
captionContent = {
|
||||
Text(
|
||||
text = stringResource(R.string.MessageBackupsKeyVerifyScreen__enter_the_backup_key_that_you_just_recorded),
|
||||
style = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
|
||||
val updateEnteredBackupKey = { input: String ->
|
||||
enteredBackupKey = AccountEntropyPool.removeIllegalCharacters(input).uppercase()
|
||||
isBackupKeyValid = enteredBackupKey == backupKey
|
||||
showError = !isBackupKeyValid && enteredBackupKey.length >= backupKey.length
|
||||
}
|
||||
|
||||
var requestFocus: Boolean by remember { mutableStateOf(true) }
|
||||
val autoFillHelper = backupKeyAutoFillHelper { updateEnteredBackupKey(it) }
|
||||
|
||||
TextField(
|
||||
value = enteredBackupKey,
|
||||
onValueChange = {
|
||||
updateEnteredBackupKey(it)
|
||||
autoFillHelper.onValueChanged(it)
|
||||
},
|
||||
label = {
|
||||
Text(text = stringResource(id = R.string.MessageBackupsKeyVerifyScreen__backup_key))
|
||||
},
|
||||
textStyle = LocalTextStyle.current.copy(
|
||||
fontFamily = MonoTypeface.fontFamily(),
|
||||
lineHeight = 36.sp
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Password,
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
imeAction = ImeAction.Next,
|
||||
autoCorrectEnabled = false
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onNext = {
|
||||
if (isBackupKeyValid) {
|
||||
keyboardController?.hide()
|
||||
coroutineScope.launch { sheetState.show() }
|
||||
}
|
||||
}
|
||||
),
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
),
|
||||
supportingText = { if (showError) Text(text = stringResource(R.string.MessageBackupsKeyVerifyScreen__incorrect_backup_key)) },
|
||||
isError = showError,
|
||||
minLines = 4,
|
||||
visualTransformation = visualTransform,
|
||||
modifier = Modifier
|
||||
.testTag("message-backups-key-verify-screen-backup-key-input-field")
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester)
|
||||
.attachBackupKeyAutoFillHelper(autoFillHelper)
|
||||
.onGloballyPositioned {
|
||||
if (requestFocus) {
|
||||
focusRequester.requestFocus()
|
||||
requestFocus = false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Surface(
|
||||
shadowElevation = if (scrollState.canScrollForward) 8.dp else 0.dp,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp, bottom = 24.dp)
|
||||
.horizontalGutters()
|
||||
.fillMaxWidth()
|
||||
},
|
||||
seeKeyButton = {
|
||||
TextButton(
|
||||
onClick = onNavigationClick
|
||||
) {
|
||||
TextButton(
|
||||
onClick = onNavigationClick
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.MessageBackupsKeyVerifyScreen__see_key_again)
|
||||
)
|
||||
}
|
||||
|
||||
Buttons.LargeTonal(
|
||||
enabled = isBackupKeyValid,
|
||||
onClick = {
|
||||
coroutineScope.launch { sheetState.show() }
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.RegistrationActivity_next)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringResource(id = R.string.MessageBackupsKeyVerifyScreen__see_key_again)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (sheetState.isVisible) {
|
||||
ModalBottomSheet(
|
||||
@@ -225,6 +95,7 @@ fun MessageBackupsKeyVerifyScreen(
|
||||
containerColor = SignalTheme.colors.colorSurface1,
|
||||
onDismissRequest = {
|
||||
coroutineScope.launch {
|
||||
SignalStore.backup.lastVerifyKeyTime = System.currentTimeMillis()
|
||||
sheetState.hide()
|
||||
}
|
||||
}
|
||||
@@ -232,6 +103,7 @@ fun MessageBackupsKeyVerifyScreen(
|
||||
BottomSheetContent(
|
||||
onContinueClick = {
|
||||
coroutineScope.launch {
|
||||
SignalStore.backup.lastVerifyKeyTime = System.currentTimeMillis()
|
||||
sheetState.hide()
|
||||
}
|
||||
onNextClick()
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.verify
|
||||
|
||||
import android.app.Activity.RESULT_OK
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsKeyVerifyScreen
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
|
||||
/**
|
||||
* Fragment to confirm the backup key just shown after users forget it.
|
||||
*/
|
||||
class ConfirmBackupKeyDisplayFragment : ComposeFragment() {
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
MessageBackupsKeyVerifyScreen(
|
||||
backupKey = SignalStore.account.accountEntropyPool.displayValue,
|
||||
onNavigationClick = {
|
||||
requireActivity().supportFragmentManager.popBackStack()
|
||||
},
|
||||
onNextClick = {
|
||||
SignalStore.backup.lastVerifyKeyTime = System.currentTimeMillis()
|
||||
SignalStore.backup.hasVerifiedBefore = true
|
||||
SignalStore.backup.hasSnoozedVerified = false
|
||||
requireActivity().setResult(RESULT_OK)
|
||||
requireActivity().finish()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.verify
|
||||
|
||||
import android.R
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsKeyRecordScreen
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.storage.AndroidCredentialRepository
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
/**
|
||||
* Fragment which displays the backup key to the user after users forget it.
|
||||
*/
|
||||
class ForgotBackupKeyFragment : ComposeFragment() {
|
||||
|
||||
companion object {
|
||||
const val CLIPBOARD_TIMEOUT_SECONDS = 60
|
||||
}
|
||||
|
||||
private val viewModel: ForgotBackupKeyViewModel by viewModel { ForgotBackupKeyViewModel() }
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val passwordManagerSettingsIntent = AndroidCredentialRepository.getCredentialManagerSettingsIntent(requireContext())
|
||||
|
||||
MessageBackupsKeyRecordScreen(
|
||||
backupKey = SignalStore.account.accountEntropyPool.displayValue,
|
||||
keySaveState = state.keySaveState,
|
||||
canOpenPasswordManagerSettings = passwordManagerSettingsIntent != null,
|
||||
onNavigationClick = { requireActivity().supportFragmentManager.popBackStack() },
|
||||
onCopyToClipboardClick = { Util.copyToClipboard(requireContext(), it, CLIPBOARD_TIMEOUT_SECONDS) },
|
||||
onRequestSaveToPasswordManager = viewModel::onBackupKeySaveRequested,
|
||||
onConfirmSaveToPasswordManager = viewModel::onBackupKeySaveConfirmed,
|
||||
onSaveToPasswordManagerComplete = viewModel::onBackupKeySaveCompleted,
|
||||
onNextClick = {
|
||||
requireActivity()
|
||||
.supportFragmentManager
|
||||
.beginTransaction()
|
||||
.add(R.id.content, ConfirmBackupKeyDisplayFragment())
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
},
|
||||
onGoToPasswordManagerSettingsClick = { requireContext().startActivity(passwordManagerSettingsIntent) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.verify
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeyCredentialManagerHandler
|
||||
import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeySaveState
|
||||
|
||||
/**
|
||||
* View model for [ForgotBackupKeyFragment]
|
||||
*/
|
||||
class ForgotBackupKeyViewModel : ViewModel(), BackupKeyCredentialManagerHandler {
|
||||
private val _uiState = MutableStateFlow(BackupKeyDisplayUiState())
|
||||
val uiState: StateFlow<BackupKeyDisplayUiState> = _uiState
|
||||
|
||||
override fun updateBackupKeySaveState(newState: BackupKeySaveState?) {
|
||||
_uiState.update { it.copy(keySaveState = newState) }
|
||||
}
|
||||
}
|
||||
|
||||
data class BackupKeyDisplayUiState(
|
||||
val keySaveState: BackupKeySaveState? = null
|
||||
)
|
||||
@@ -0,0 +1,191 @@
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.verify
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.BiometricDeviceAuthentication
|
||||
import org.thoughtcrime.securesms.BiometricDeviceLockContract
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.EnterKeyScreen
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import kotlin.random.Random
|
||||
import kotlin.random.nextInt
|
||||
|
||||
/**
|
||||
* Screen to verify the backup key
|
||||
*/
|
||||
class VerifyBackupKeyActivity : PassphraseRequiredActivity() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(VerifyBackupKeyActivity::class)
|
||||
|
||||
@JvmStatic
|
||||
fun createIntent(context: Context): Intent {
|
||||
return Intent(context, VerifyBackupKeyActivity::class.java)
|
||||
}
|
||||
|
||||
const val REQUEST_CODE = 1204
|
||||
}
|
||||
|
||||
private lateinit var biometricDeviceAuthentication: BiometricDeviceAuthentication
|
||||
private lateinit var biometricDeviceLockLauncher: ActivityResultLauncher<String>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
enableEdgeToEdge()
|
||||
|
||||
setContent {
|
||||
SignalTheme {
|
||||
VerifyBackupPinScreen(
|
||||
backupKey = SignalStore.account.accountEntropyPool.displayValue,
|
||||
onForgotKeyClick = {
|
||||
if (!biometricDeviceAuthentication.authenticate(this, true) { biometricDeviceLockLauncher.launch(getString(R.string.RemoteBackupsSettingsFragment__unlock_to_view_backup_key)) }) {
|
||||
displayBackupKey()
|
||||
}
|
||||
},
|
||||
onNextClick = {
|
||||
SignalStore.backup.lastVerifyKeyTime = System.currentTimeMillis()
|
||||
SignalStore.backup.hasVerifiedBefore = true
|
||||
SignalStore.backup.hasSnoozedVerified = false
|
||||
setResult(RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
initializeBiometricAuth()
|
||||
}
|
||||
|
||||
private fun initializeBiometricAuth() {
|
||||
val biometricPrompt = BiometricPrompt(this, AuthListener())
|
||||
val promptInfo: BiometricPrompt.PromptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||
.setAllowedAuthenticators(BiometricDeviceAuthentication.ALLOWED_AUTHENTICATORS)
|
||||
.setTitle(getString(R.string.RemoteBackupsSettingsFragment__unlock_to_view_backup_key))
|
||||
.build()
|
||||
|
||||
biometricDeviceAuthentication = BiometricDeviceAuthentication(BiometricManager.from(this), biometricPrompt, promptInfo)
|
||||
biometricDeviceLockLauncher = registerForActivityResult(BiometricDeviceLockContract()) { result: Int ->
|
||||
if (result == BiometricDeviceAuthentication.AUTHENTICATED) {
|
||||
displayBackupKey()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun displayBackupKey() {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.add(android.R.id.content, ForgotBackupKeyFragment())
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
|
||||
private inner class AuthListener : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationFailed() {
|
||||
Log.w(TAG, "onAuthenticationFailed")
|
||||
Toast.makeText(this@VerifyBackupKeyActivity, R.string.RemoteBackupsSettingsFragment__authenticatino_required, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
Log.i(TAG, "onAuthenticationSucceeded")
|
||||
displayBackupKey()
|
||||
}
|
||||
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
Log.w(TAG, "onAuthenticationError: $errorCode, $errString")
|
||||
onAuthenticationFailed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VerifyBackupPinScreen(
|
||||
backupKey: String,
|
||||
onForgotKeyClick: () -> Unit = {},
|
||||
onNextClick: () -> Unit = {}
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
val text = buildAnnotatedString {
|
||||
append(stringResource(id = R.string.VerifyBackupPinScreen__enter_the_backup_key_that_you_recorded))
|
||||
append(" ")
|
||||
|
||||
withLink(
|
||||
LinkAnnotation.Clickable(tag = "learn-more") {
|
||||
CommunicationActions.openBrowserLink(context, context.getString(R.string.backup_failed_support_url))
|
||||
}
|
||||
) {
|
||||
withStyle(SpanStyle(color = MaterialTheme.colorScheme.primary)) {
|
||||
append(stringResource(id = R.string.BackupAlertBottomSheet__learn_more))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold { paddingValues ->
|
||||
EnterKeyScreen(
|
||||
paddingValues = paddingValues,
|
||||
backupKey = backupKey,
|
||||
onNextClick = onNextClick,
|
||||
captionContent = {
|
||||
Text(
|
||||
text = stringResource(R.string.VerifyBackupPinScreen__enter_your_backup_key),
|
||||
style = MaterialTheme.typography.headlineMedium.copy(color = MaterialTheme.colorScheme.onSurface),
|
||||
modifier = Modifier.padding(top = 40.dp, bottom = 16.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
)
|
||||
},
|
||||
seeKeyButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
keyboardController?.hide()
|
||||
onForgotKeyClick()
|
||||
}
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.VerifyBackupPinScreen__forgot_key))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun VerifyBackupKeyScreen() {
|
||||
Previews.Preview {
|
||||
VerifyBackupPinScreen(
|
||||
backupKey = (0 until 64).map { Random.nextInt(65..90).toChar() }.joinToString("").uppercase()
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user