Add auto-fill backup key support.

This commit is contained in:
Cody Henthorne
2025-03-21 14:51:27 -04:00
parent 2743bec704
commit e2961a3f6f
3 changed files with 159 additions and 15 deletions

View File

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

View File

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

View File

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