mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-23 19:26:17 +00:00
Add auto-fill backup key support.
This commit is contained in:
@@ -32,16 +32,17 @@ import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
@@ -63,6 +64,8 @@ import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.ui.compose.horizontalGutters
|
||||
import org.thoughtcrime.securesms.R
|
||||
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 kotlin.random.Random
|
||||
import kotlin.random.nextInt
|
||||
@@ -71,7 +74,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)
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun MessageBackupsKeyVerifyScreen(
|
||||
backupKey: String,
|
||||
@@ -119,12 +122,20 @@ fun MessageBackupsKeyVerifyScreen(
|
||||
|
||||
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 = {
|
||||
enteredBackupKey = AccountEntropyPool.removeIllegalCharacters(it).uppercase()
|
||||
isBackupKeyValid = enteredBackupKey == backupKey
|
||||
showError = !isBackupKeyValid && enteredBackupKey.length >= backupKey.length
|
||||
updateEnteredBackupKey(it)
|
||||
autoFillHelper.onValueChanged(it)
|
||||
},
|
||||
label = {
|
||||
Text(text = stringResource(id = R.string.MessageBackupsKeyVerifyScreen__backup_key))
|
||||
@@ -154,13 +165,16 @@ fun MessageBackupsKeyVerifyScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester)
|
||||
.attachBackupKeyAutoFillHelper(autoFillHelper)
|
||||
.onGloballyPositioned {
|
||||
if (requestFocus) {
|
||||
focusRequester.requestFocus()
|
||||
requestFocus = false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
|
||||
Surface(
|
||||
shadowElevation = if (scrollState.canScrollForward) 8.dp else 0.dp,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registrationv3.ui.restore
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.view.autofill.AutofillManager
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.Autofill
|
||||
import androidx.compose.ui.autofill.AutofillNode
|
||||
import androidx.compose.ui.autofill.AutofillType
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalAutofill
|
||||
import androidx.compose.ui.platform.LocalAutofillTree
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.content.ContextCompat
|
||||
import kotlin.math.roundToInt
|
||||
import android.graphics.Rect as ViewRect
|
||||
import androidx.compose.ui.geometry.Rect as ComposeRect
|
||||
|
||||
/**
|
||||
* Provide a compose friendly way to autofill the backup key from a password manager.
|
||||
*/
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@SuppressLint("NewApi")
|
||||
@Composable
|
||||
fun backupKeyAutoFillHelper(
|
||||
onFill: (String) -> Unit
|
||||
): BackupKeyAutoFillHelper {
|
||||
val view = LocalView.current
|
||||
val context = LocalContext.current
|
||||
val autofill: Autofill? = LocalAutofill.current
|
||||
|
||||
val node = remember { AutofillNode(autofillTypes = listOf(AutofillType.Password), onFill = onFill) }
|
||||
LocalAutofillTree.current += node
|
||||
|
||||
return remember {
|
||||
object : BackupKeyAutoFillHelper(context) {
|
||||
override fun request() {
|
||||
if (node.boundingBox != null) {
|
||||
autofill?.requestAutofillForNode(node)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
autofill?.cancelAutofillForNode(node)
|
||||
}
|
||||
|
||||
/**
|
||||
* Call when need to manually prompt auto-fill options when text field is empty. For some reason calling
|
||||
* [request] like we do for on focus changes is not enough.
|
||||
*/
|
||||
override fun requestDirectly() {
|
||||
val bounds = node.boundingBox?.let { ViewRect(it.left.roundToInt(), it.top.roundToInt(), it.right.roundToInt(), it.bottom.roundToInt()) }
|
||||
if (bounds != null) {
|
||||
autoFillManager?.requestAutofill(view, node.id, bounds)
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateNodeBounds(boundsInWindow: ComposeRect) {
|
||||
node.boundingBox = boundsInWindow
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a [BackupKeyAutoFillHelper] return from [backupKeyAutoFillHelper] to setup the default
|
||||
* callbacks needed to make requests on the view's behalf.
|
||||
*/
|
||||
fun Modifier.attachBackupKeyAutoFillHelper(helper: BackupKeyAutoFillHelper): Modifier {
|
||||
return this.then(
|
||||
Modifier
|
||||
.onFocusChanged {
|
||||
if (it.isFocused) {
|
||||
helper.request()
|
||||
} else {
|
||||
helper.cancel()
|
||||
}
|
||||
}
|
||||
.onGloballyPositioned {
|
||||
helper.updateNodeBounds(it.boundsInWindow())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Weird compose-interop abstract class to let us return something to the caller of [backupKeyAutoFillHelper]
|
||||
* and capture inner compose data to implement the methods that need various compose provided things.
|
||||
*/
|
||||
abstract class BackupKeyAutoFillHelper(context: Context) {
|
||||
protected val autoFillManager: AutofillManager? = if (Build.VERSION.SDK_INT >= 26) {
|
||||
ContextCompat.getSystemService(context, AutofillManager::class.java)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
fun onValueChanged(value: String) {
|
||||
if (value.isEmpty()) {
|
||||
requestDirectly()
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun request()
|
||||
abstract fun cancel()
|
||||
abstract fun requestDirectly()
|
||||
abstract fun updateNodeBounds(boundsInWindow: ComposeRect)
|
||||
}
|
||||
@@ -30,13 +30,16 @@ import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.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.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -124,12 +127,18 @@ fun EnterBackupKeyScreen(
|
||||
}
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
var requestFocus: Boolean by remember { mutableStateOf(true) }
|
||||
val visualTransform = remember(chunkLength) { BackupKeyVisualTransformation(chunkSize = chunkLength) }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
val autoFillHelper = backupKeyAutoFillHelper { onBackupKeyChanged(it) }
|
||||
|
||||
TextField(
|
||||
value = backupKey,
|
||||
onValueChange = onBackupKeyChanged,
|
||||
onValueChange = {
|
||||
onBackupKeyChanged(it)
|
||||
autoFillHelper.onValueChanged(it)
|
||||
},
|
||||
label = {
|
||||
Text(text = stringResource(id = R.string.EnterBackupKey_backup_key))
|
||||
},
|
||||
@@ -158,12 +167,15 @@ fun EnterBackupKeyScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester)
|
||||
.attachBackupKeyAutoFillHelper(autoFillHelper)
|
||||
.onGloballyPositioned {
|
||||
if (requestFocus) {
|
||||
focusRequester.requestFocus()
|
||||
requestFocus = false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
|
||||
if (sheetState.isVisible) {
|
||||
ModalBottomSheet(
|
||||
dragHandle = null,
|
||||
|
||||
Reference in New Issue
Block a user