mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-09-20 00:35:47 +01:00
Move password manager stuff into a lib.
This commit is contained in:
committed by
Alex Hart
parent
0aaa921833
commit
8bf47713ea
@@ -702,6 +702,7 @@ dependencies {
|
||||
implementation(project(":lib:libsignal-service"))
|
||||
implementation(project(":lib:network"))
|
||||
implementation(project(":lib:paging"))
|
||||
implementation(project(":lib:password-manager"))
|
||||
implementation(project(":lib:photoview"))
|
||||
implementation(project(":lib:qr"))
|
||||
implementation(project(":lib:sticky-header-grid"))
|
||||
@@ -808,8 +809,6 @@ dependencies {
|
||||
implementation(libs.rxjava3.rxandroid)
|
||||
implementation(libs.rxjava3.rxkotlin)
|
||||
implementation(libs.rxdogtag)
|
||||
implementation(libs.androidx.credentials)
|
||||
implementation(libs.androidx.credentials.compat)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
implementation(project(":lib:billing"))
|
||||
|
||||
+4
-4
@@ -42,12 +42,12 @@ import androidx.compose.ui.unit.sp
|
||||
import org.signal.core.models.AccountEntropyPool
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.horizontalGutters
|
||||
import org.signal.passwordmanager.compose.attachPasswordAutoFillHelper
|
||||
import org.signal.passwordmanager.compose.passwordAutoFillHelper
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.TemporaryScreenshotSecurity
|
||||
import org.thoughtcrime.securesms.fonts.MonoTypeface
|
||||
import org.thoughtcrime.securesms.registration.ui.restore.BackupKeyVisualTransformation
|
||||
import org.thoughtcrime.securesms.registration.ui.restore.attachBackupKeyAutoFillHelper
|
||||
import org.thoughtcrime.securesms.registration.ui.restore.backupKeyAutoFillHelper
|
||||
|
||||
/**
|
||||
* Screen to enter backup key with an option to view the backup key again
|
||||
@@ -99,7 +99,7 @@ fun EnterKeyScreen(
|
||||
}
|
||||
|
||||
var requestFocus: Boolean by remember { mutableStateOf(true) }
|
||||
val autoFillHelper = backupKeyAutoFillHelper { updateEnteredBackupKey(it) }
|
||||
val autoFillHelper = passwordAutoFillHelper { updateEnteredBackupKey(it) }
|
||||
|
||||
TextField(
|
||||
value = enteredBackupKey,
|
||||
@@ -140,7 +140,7 @@ fun EnterKeyScreen(
|
||||
.testTag("message-backups-key-verify-screen-backup-key-input-field")
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester)
|
||||
.attachBackupKeyAutoFillHelper(autoFillHelper)
|
||||
.attachPasswordAutoFillHelper(autoFillHelper)
|
||||
.onGloballyPositioned {
|
||||
if (requestFocus) {
|
||||
focusRequester.requestFocus()
|
||||
|
||||
+4
-4
@@ -34,6 +34,7 @@ import org.signal.core.ui.compose.Dialogs
|
||||
import org.signal.core.util.Util
|
||||
import org.signal.core.util.concurrent.SignalDispatchers
|
||||
import org.signal.core.util.getSerializableCompat
|
||||
import org.signal.passwordmanager.SignalCredentialManager
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.DeletionState
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
@@ -44,7 +45,6 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil
|
||||
import org.thoughtcrime.securesms.util.storage.AndroidCredentialRepository
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
/**
|
||||
@@ -69,7 +69,7 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
||||
MessageBackupsFlowViewModel(
|
||||
initialTierSelection = requireArguments().getSerializableCompat(TIER, MessageBackupTier::class.java),
|
||||
googlePlayApiAvailability = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(requireContext()),
|
||||
isCredentialManagerSupported = AndroidCredentialRepository.isCredentialManagerSupported(requireContext())
|
||||
isCredentialManagerSupported = SignalCredentialManager.isSupported(requireContext())
|
||||
)
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
||||
|
||||
composable(route = MessageBackupsStage.Route.BACKUP_KEY_RECORD.name) {
|
||||
val context = LocalContext.current
|
||||
val passwordManagerSettingsIntent = AndroidCredentialRepository.getCredentialManagerSettingsIntent(requireContext())
|
||||
val passwordManagerSettingsIntent = SignalCredentialManager.getSettingsIntent(requireContext())
|
||||
|
||||
MessageBackupsKeyRecordScreen(
|
||||
backupKey = state.accountEntropyPool.displayValue,
|
||||
@@ -180,7 +180,7 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
||||
|
||||
composable(route = MessageBackupsStage.Route.BACKUP_KEY_RECORD_MANUALLY.name) {
|
||||
val context = LocalContext.current
|
||||
val passwordManagerSettingsIntent = AndroidCredentialRepository.getCredentialManagerSettingsIntent(requireContext())
|
||||
val passwordManagerSettingsIntent = SignalCredentialManager.getSettingsIntent(requireContext())
|
||||
|
||||
MessageBackupsKeyRecordScreen(
|
||||
backupKey = state.accountEntropyPool.displayValue,
|
||||
|
||||
+7
-7
@@ -76,6 +76,9 @@ import org.signal.core.ui.compose.Snackbars
|
||||
import org.signal.core.ui.compose.horizontalGutters
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.signal.core.util.Util
|
||||
import org.signal.passwordmanager.CredentialManagerError
|
||||
import org.signal.passwordmanager.CredentialManagerResult
|
||||
import org.signal.passwordmanager.SignalCredentialManager
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.warning.ClipStage
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.warning.RecoveryKeyWarningSheetContent
|
||||
@@ -86,9 +89,6 @@ import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupK
|
||||
import org.thoughtcrime.securesms.fonts.MonoTypeface
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.storage.AndroidCredentialRepository
|
||||
import org.thoughtcrime.securesms.util.storage.CredentialManagerError
|
||||
import org.thoughtcrime.securesms.util.storage.CredentialManagerResult
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
private const val CLIPBOARD_TIMEOUT_SECONDS = 60
|
||||
@@ -123,7 +123,7 @@ fun MessageBackupsKeyRecordScreen(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val passwordManagerSettingsIntent = remember {
|
||||
AndroidCredentialRepository.getCredentialManagerSettingsIntent(context)
|
||||
SignalCredentialManager.getSettingsIntent(context)
|
||||
}
|
||||
|
||||
val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
|
||||
@@ -414,7 +414,7 @@ fun MessageBackupsKeyRecordScreen(
|
||||
}
|
||||
}
|
||||
|
||||
if (!showAsPasskey && AndroidCredentialRepository.isCredentialManagerSupported(context)) {
|
||||
if (!showAsPasskey && SignalCredentialManager.isSupported(context)) {
|
||||
item {
|
||||
Buttons.Small(
|
||||
onClick = { onRequestSaveToPasswordManager() }
|
||||
@@ -764,7 +764,7 @@ private suspend fun saveKeyToCredentialManager(
|
||||
@UiContext activityContext: Context,
|
||||
backupKey: String
|
||||
): CredentialManagerResult {
|
||||
return AndroidCredentialRepository.saveCredential(
|
||||
return SignalCredentialManager.saveCredential(
|
||||
activityContext = activityContext,
|
||||
username = activityContext.getString(R.string.MessageBackupsKeyRecordScreen__backup_key_password_manager_id),
|
||||
password = backupKey
|
||||
@@ -775,7 +775,7 @@ private suspend fun getKeyFromCredentialManager(
|
||||
@UiContext activityContext: Context,
|
||||
id: String
|
||||
): String? {
|
||||
return AndroidCredentialRepository.getCredential(activityContext, id)
|
||||
return SignalCredentialManager.getCredential(activityContext, id)
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
|
||||
+2
-2
@@ -26,6 +26,7 @@ import kotlinx.coroutines.rx3.asFlowable
|
||||
import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment
|
||||
import org.signal.core.ui.compose.Dialogs
|
||||
import org.signal.core.util.concurrent.SignalDispatchers
|
||||
import org.signal.passwordmanager.SignalCredentialManager
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsFlowViewModel
|
||||
@@ -34,7 +35,6 @@ import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentCheckoutDelegate
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.util.storage.AndroidCredentialRepository
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
/**
|
||||
@@ -62,7 +62,7 @@ abstract class UpgradeToPaidTierBottomSheet : ComposeBottomSheetDialogFragment()
|
||||
MessageBackupsFlowViewModel(
|
||||
initialTierSelection = MessageBackupTier.PAID,
|
||||
googlePlayApiAvailability = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(requireContext()),
|
||||
isCredentialManagerSupported = AndroidCredentialRepository.isCredentialManagerSupported(requireContext()),
|
||||
isCredentialManagerSupported = SignalCredentialManager.isSupported(requireContext()),
|
||||
startScreen = MessageBackupsStage.TYPE_SELECTION
|
||||
)
|
||||
}
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ package org.thoughtcrime.securesms.components.settings.app.backups.remote
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.logging.logW
|
||||
import org.thoughtcrime.securesms.util.storage.CredentialManagerError
|
||||
import org.thoughtcrime.securesms.util.storage.CredentialManagerResult
|
||||
import org.signal.passwordmanager.CredentialManagerError
|
||||
import org.signal.passwordmanager.CredentialManagerResult
|
||||
|
||||
/**
|
||||
* Handles the process of storing a backup key to the device password manager.
|
||||
|
||||
+4
-2
@@ -55,6 +55,8 @@ import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.horizontalGutters
|
||||
import org.signal.passwordmanager.compose.attachPasswordAutoFillHelper
|
||||
import org.signal.passwordmanager.compose.passwordAutoFillHelper
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupsIconColors
|
||||
import org.thoughtcrime.securesms.components.TemporaryScreenshotSecurity
|
||||
@@ -137,7 +139,7 @@ fun EnterBackupKeyScreen(
|
||||
val visualTransform = remember(chunkLength) { BackupKeyVisualTransformation(chunkSize = chunkLength) }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
val autoFillHelper = backupKeyAutoFillHelper { onBackupKeyChanged(it) }
|
||||
val autoFillHelper = passwordAutoFillHelper { onBackupKeyChanged(it) }
|
||||
|
||||
TextField(
|
||||
value = enteredText,
|
||||
@@ -173,7 +175,7 @@ fun EnterBackupKeyScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester)
|
||||
.attachBackupKeyAutoFillHelper(autoFillHelper)
|
||||
.attachPasswordAutoFillHelper(autoFillHelper)
|
||||
.onGloballyPositioned {
|
||||
if (requestFocus) {
|
||||
focusRequester.requestFocus()
|
||||
|
||||
+4
-4
@@ -35,6 +35,8 @@ import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.CircularProgressWrapper
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.passwordmanager.compose.attachPasswordAutoFillHelper
|
||||
import org.signal.passwordmanager.compose.passwordAutoFillHelper
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.TemporaryScreenshotSecurity
|
||||
import org.thoughtcrime.securesms.fonts.MonoTypeface
|
||||
@@ -42,8 +44,6 @@ import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResul
|
||||
import org.thoughtcrime.securesms.registration.ui.restore.AccountEntropyPoolVerification
|
||||
import org.thoughtcrime.securesms.registration.ui.restore.BackupKeyVisualTransformation
|
||||
import org.thoughtcrime.securesms.registration.ui.restore.RegistrationErrorDialogs
|
||||
import org.thoughtcrime.securesms.registration.ui.restore.attachBackupKeyAutoFillHelper
|
||||
import org.thoughtcrime.securesms.registration.ui.restore.backupKeyAutoFillHelper
|
||||
import org.thoughtcrime.securesms.registration.ui.shared.RegistrationScreen
|
||||
|
||||
@Composable
|
||||
@@ -67,7 +67,7 @@ fun EnterLocalBackupKeyScreen(
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
var requestFocus by remember { mutableStateOf(true) }
|
||||
|
||||
val autoFillHelper = backupKeyAutoFillHelper { onBackupKeyChanged(it) }
|
||||
val autoFillHelper = passwordAutoFillHelper { onBackupKeyChanged(it) }
|
||||
|
||||
RegistrationScreen(
|
||||
title = stringResource(R.string.EnterLocalBackupKeyScreen__enter_your_recovery_key),
|
||||
@@ -128,7 +128,7 @@ fun EnterLocalBackupKeyScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester)
|
||||
.attachBackupKeyAutoFillHelper(autoFillHelper)
|
||||
.attachPasswordAutoFillHelper(autoFillHelper)
|
||||
.onGloballyPositioned {
|
||||
if (requestFocus) {
|
||||
focusRequester.requestFocus()
|
||||
|
||||
@@ -62,6 +62,7 @@ dependencies {
|
||||
implementation(project(":core:models-jvm"))
|
||||
implementation(project(":core:serialization"))
|
||||
implementation(project(":lib:device-transfer"))
|
||||
implementation(project(":lib:password-manager"))
|
||||
implementation(project(":lib:signal-login"))
|
||||
implementation(libs.libsignal.android)
|
||||
|
||||
@@ -98,10 +99,6 @@ dependencies {
|
||||
implementation(libs.google.play.services.auth)
|
||||
implementation(libs.kotlinx.coroutines.play.services)
|
||||
|
||||
// Credential Manager (password manager retrieval)
|
||||
implementation(libs.androidx.credentials)
|
||||
implementation(libs.androidx.credentials.compat)
|
||||
|
||||
// Testing
|
||||
testImplementation(testFixtures(project(":core:ui")))
|
||||
testImplementation(testLibs.junit.junit)
|
||||
|
||||
+5
-5
@@ -47,6 +47,7 @@ import org.signal.core.util.LinkActions.OpenUrlError
|
||||
import org.signal.core.util.serialization.AccountEntropyPoolSerializer
|
||||
import org.signal.network.api.RegistrationApiV2.SessionMetadata
|
||||
import org.signal.network.api.RegistrationApiV2.SvrCredentials
|
||||
import org.signal.passwordmanager.SignalCredentialManager
|
||||
import org.signal.registration.screens.accountlocked.AccountLockedScreen
|
||||
import org.signal.registration.screens.accountlocked.AccountLockedScreenEvents
|
||||
import org.signal.registration.screens.accountlocked.AccountLockedState
|
||||
@@ -124,7 +125,6 @@ import org.signal.registration.screens.welcome.WelcomeScreen
|
||||
import org.signal.registration.screens.welcome.WelcomeScreenActions
|
||||
import org.signal.registration.screens.welcome.WelcomeScreenViewModel
|
||||
import org.signal.registration.util.AccountEntropyPoolParceler
|
||||
import org.signal.registration.util.RegistrationCredentialManager
|
||||
import org.signal.registration.util.SessionMetadataParceler
|
||||
import org.signal.registration.util.SvrCredentialsParceler
|
||||
|
||||
@@ -659,7 +659,7 @@ private fun EntryProviderScope<NavKey>.navigationEntries(
|
||||
factory = SignalLoginInfoViewModel.Factory(
|
||||
repository = registrationRepository,
|
||||
parentEventEmitter = registrationViewModel::onEvent,
|
||||
isPasswordManagerAvailable = RegistrationCredentialManager.isSupported(context)
|
||||
isPasswordManagerAvailable = SignalCredentialManager.isSupported(context)
|
||||
)
|
||||
)
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
@@ -904,7 +904,7 @@ private fun EntryProviderScope<NavKey>.navigationEntries(
|
||||
parentEventEmitter = registrationViewModel::onEvent,
|
||||
resultBus = registrationViewModel.resultBus,
|
||||
resultKey = AEP_FOR_LOCAL_BACKUP_RESULT,
|
||||
isPasswordManagerAvailable = RegistrationCredentialManager.isSupported(context)
|
||||
isPasswordManagerAvailable = SignalCredentialManager.isSupported(context)
|
||||
)
|
||||
)
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
@@ -922,7 +922,7 @@ private fun EntryProviderScope<NavKey>.navigationEntries(
|
||||
e164 = key.e164,
|
||||
repository = registrationRepository,
|
||||
parentEventEmitter = registrationViewModel::onEvent,
|
||||
isPasswordManagerAvailable = RegistrationCredentialManager.isSupported(context)
|
||||
isPasswordManagerAvailable = SignalCredentialManager.isSupported(context)
|
||||
)
|
||||
)
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
@@ -939,7 +939,7 @@ private fun EntryProviderScope<NavKey>.navigationEntries(
|
||||
factory = EnterAepForRemoteBackupPostRegistrationViewModel.Factory(
|
||||
repository = registrationRepository,
|
||||
parentEventEmitter = registrationViewModel::onEvent,
|
||||
isPasswordManagerAvailable = RegistrationCredentialManager.isSupported(context)
|
||||
isPasswordManagerAvailable = SignalCredentialManager.isSupported(context)
|
||||
)
|
||||
)
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
+6
-6
@@ -56,16 +56,16 @@ import org.signal.core.ui.compose.AllDevicePreviews
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.Dialogs
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.passwordmanager.SignalCredentialManager
|
||||
import org.signal.passwordmanager.compose.attachPasswordAutoFillHelper
|
||||
import org.signal.passwordmanager.compose.passwordAutoFillHelper
|
||||
import org.signal.registration.R
|
||||
import org.signal.registration.fonts.MonoTypeface
|
||||
import org.signal.registration.screens.OnePaneRegistrationScaffold
|
||||
import org.signal.registration.screens.RegistrationScaffold
|
||||
import org.signal.registration.screens.TwoPaneRegistrationScaffold
|
||||
import org.signal.registration.screens.attachDebugLogHelper
|
||||
import org.signal.registration.screens.localbackuprestore.attachBackupKeyAutoFillHelper
|
||||
import org.signal.registration.screens.localbackuprestore.backupKeyAutoFillHelper
|
||||
import org.signal.registration.test.TestTags
|
||||
import org.signal.registration.util.RegistrationCredentialManager
|
||||
|
||||
@Composable
|
||||
fun EnterAepScreen(
|
||||
@@ -264,7 +264,7 @@ private fun RecoveryKeyTextField(state: EnterAepState, onEvent: (EnterAepEvents)
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
var requestFocus by remember { mutableStateOf(true) }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val autoFillHelper = backupKeyAutoFillHelper { onEvent(EnterAepEvents.BackupKeyChanged(it)) }
|
||||
val autoFillHelper = passwordAutoFillHelper { onEvent(EnterAepEvents.BackupKeyChanged(it)) }
|
||||
|
||||
TextField(
|
||||
value = state.enteredText,
|
||||
@@ -311,7 +311,7 @@ private fun RecoveryKeyTextField(state: EnterAepState, onEvent: (EnterAepEvents)
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.ENTER_AEP_INPUT)
|
||||
.focusRequester(focusRequester)
|
||||
.attachBackupKeyAutoFillHelper(autoFillHelper)
|
||||
.attachPasswordAutoFillHelper(autoFillHelper)
|
||||
.onGloballyPositioned {
|
||||
if (requestFocus) {
|
||||
focusRequester.requestFocus()
|
||||
@@ -330,7 +330,7 @@ private fun FillFromPasswordManagerButton(onEvent: (EnterAepEvents) -> Unit, mod
|
||||
modifier = modifier,
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
val password = RegistrationCredentialManager.getPasswordCredential(context)
|
||||
val password = SignalCredentialManager.getCredential(context)
|
||||
if (password != null) {
|
||||
onEvent(EnterAepEvents.BackupKeyChanged(password))
|
||||
}
|
||||
|
||||
-103
@@ -1,103 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.registration.screens.localbackuprestore
|
||||
|
||||
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
|
||||
|
||||
@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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Modifier.attachBackupKeyAutoFillHelper(helper: BackupKeyAutoFillHelper): Modifier {
|
||||
return this.then(
|
||||
Modifier
|
||||
.onFocusChanged {
|
||||
if (it.isFocused) {
|
||||
helper.request()
|
||||
} else {
|
||||
helper.cancel()
|
||||
}
|
||||
}
|
||||
.onGloballyPositioned {
|
||||
helper.updateNodeBounds(it.boundsInWindow())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.registration.util
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.view.autofill.AutofillManager
|
||||
import androidx.annotation.UiContext
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.credentials.CredentialManager
|
||||
import androidx.credentials.GetCredentialRequest
|
||||
import androidx.credentials.GetPasswordOption
|
||||
import androidx.credentials.PasswordCredential
|
||||
import androidx.credentials.exceptions.GetCredentialException
|
||||
import org.signal.core.util.PlayServicesUtil
|
||||
import org.signal.core.util.logging.Log
|
||||
|
||||
/**
|
||||
* Retrieves a previously saved password credential (e.g. a backup/recovery key) using Android's
|
||||
* Credential Manager.
|
||||
*/
|
||||
object RegistrationCredentialManager {
|
||||
|
||||
private val TAG = Log.tag(RegistrationCredentialManager::class)
|
||||
|
||||
/**
|
||||
* Whether a password manager / credential provider is available to fill credentials. On API 26+
|
||||
* this tracks whether the user has an autofill service enabled; older devices fall back to the
|
||||
* Credential Manager Play Services backend, so they are supported only when Play Services is.
|
||||
*/
|
||||
fun isSupported(context: Context): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= 26) {
|
||||
context.getSystemService<AutofillManager>()?.isEnabled == true
|
||||
} else {
|
||||
PlayServicesUtil.getPlayServicesStatus(context) == PlayServicesUtil.PlayServicesStatus.SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts the device password manager to let the user pick a saved password credential and returns
|
||||
* its value, or null if none was chosen or retrieval failed. Must be called with an Activity
|
||||
* context so the Credential Manager UI can be shown.
|
||||
*/
|
||||
suspend fun getPasswordCredential(@UiContext activityContext: Context): String? {
|
||||
return try {
|
||||
val result = CredentialManager.create(activityContext).getCredential(activityContext, GetCredentialRequest(listOf(GetPasswordOption())))
|
||||
(result.credential as? PasswordCredential)?.password
|
||||
} catch (e: GetCredentialException) {
|
||||
Log.w(TAG, "Failed to retrieve credential from password manager.", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,7 @@ androidx-compose-material3-adaptive = { module = "androidx.compose.material3.ada
|
||||
androidx-compose-material3-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout"}
|
||||
androidx-compose-material3-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation"}
|
||||
androidx-compose-material-icons-extended = "androidx.compose.material:material-icons-extended:1.7.8"
|
||||
androidx-compose-ui = { module = "androidx.compose.ui:ui" }
|
||||
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
||||
androidx-compose-ui-tooling-core = { module = "androidx.compose.ui:ui-tooling" }
|
||||
androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
|
||||
|
||||
@@ -626,6 +626,16 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
||||
<sha256 value="8397c169c6ef7218801eeae044f8d11b37ac355bba51841075eb02a3d1b2ab47" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.animation" name="animation-core" version="1.8.1">
|
||||
<artifact name="animation-core-1.8.1.module">
|
||||
<sha256 value="209bfe0d6bc8a700597762c6944eba401f1ae920dd7f6f959738bbce3f4dd1d2" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.animation" name="animation-core" version="1.9.0">
|
||||
<artifact name="animation-core-1.9.0.module">
|
||||
<sha256 value="4c6c8ea8e5773499eeab487336c083f6bd966997520b208f05691fd700b052ac" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.animation" name="animation-core-android" version="1.11.4">
|
||||
<artifact name="animation-core-android-1.11.4.module">
|
||||
<sha256 value="6fc0c420dba0eebfe09d4b8b90c8b2424c7fda4d541faf9784afeb8bec54fe77" origin="Generated by Gradle"/>
|
||||
@@ -1642,6 +1652,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
||||
<sha256 value="639ae0bdc1d09a4d0c3cd198850fd8530363cf65c55c7c870b17596883346163" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.credentials" name="credentials" version="1.6.0-beta01">
|
||||
<artifact name="credentials-1.6.0-beta01.aar">
|
||||
<sha256 value="b15506c9fd24c1cad961aabb6da8fe2d4394a7e7993bcc3ef51d3ac917d1b0c7" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="credentials-1.6.0-beta01.module">
|
||||
<sha256 value="64dca04761a2cc080e8bea1c198fd14bc6ba7aef3d310acf802c51739ea0e3ec" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.credentials" name="credentials-play-services-auth" version="1.6.0">
|
||||
<artifact name="credentials-play-services-auth-1.6.0.aar">
|
||||
<sha256 value="837a0a01988f93182bbe3e01007cbcc2a9ac9185c934153a136b37ac1bd8cc48" origin="Generated by Gradle"/>
|
||||
@@ -4433,6 +4451,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
||||
<sha256 value="96f6e6d9f5e5db840ee3da20449e16828ed4ba2c07783dde3397d13aaf814e19" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.google.android.gms" name="play-services-auth" version="21.1.1">
|
||||
<artifact name="play-services-auth-21.1.1.aar">
|
||||
<sha256 value="d73155e83e97875e93634ca9fd27d0159e2218f3347c53aeaaf0faa2efaa280f" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.google.android.gms" name="play-services-auth" version="21.5.1">
|
||||
<artifact name="play-services-auth-21.5.1.aar">
|
||||
<sha256 value="2b3f114af97c5b256ae244c4059e8470231f3218087a095930e34c4fbc243844" origin="Generated by Gradle"/>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
plugins {
|
||||
id("signal-library")
|
||||
alias(libs.plugins.compose.compiler)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "org.signal.passwordmanager"
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
lintChecks(project(":lintchecks"))
|
||||
|
||||
implementation(project(":core:util"))
|
||||
|
||||
implementation(libs.androidx.credentials)
|
||||
implementation(libs.androidx.credentials.compat)
|
||||
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.compose.ui)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
|
||||
+32
-19
@@ -1,9 +1,9 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.util.storage
|
||||
package org.signal.passwordmanager
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -11,6 +11,7 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import android.view.autofill.AutofillManager
|
||||
import androidx.annotation.UiContext
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.credentials.CreatePasswordRequest
|
||||
import androidx.credentials.CredentialManager
|
||||
@@ -23,28 +24,40 @@ import androidx.credentials.exceptions.CreateCredentialNoCreateOptionException
|
||||
import androidx.credentials.exceptions.CreateCredentialProviderConfigurationException
|
||||
import androidx.credentials.exceptions.CreateCredentialUnknownException
|
||||
import androidx.credentials.exceptions.GetCredentialException
|
||||
import org.signal.core.util.PlayServicesUtil
|
||||
import org.signal.core.util.logging.Log
|
||||
|
||||
/**
|
||||
* Responsible for storing and retrieving credentials using Android's Credential Manager.
|
||||
* Stores and retrieves password credentials (e.g. backup/recovery keys) using Android's
|
||||
* Credential Manager, which delegates to the user's password manager.
|
||||
*/
|
||||
object AndroidCredentialRepository {
|
||||
private val TAG = Log.tag(AndroidCredentialRepository::class)
|
||||
object SignalCredentialManager {
|
||||
|
||||
private val TAG = Log.tag(SignalCredentialManager::class)
|
||||
|
||||
private const val ERROR_CODE_GOOGLE_AUTOFILL_SUCCESS = "[28431]"
|
||||
private const val ERROR_CODE_MISSING_CREDENTIAL_MANAGER = "[28434]"
|
||||
private const val ERROR_CODE_SAVE_PROMPT_DISABLED = "[28435]"
|
||||
|
||||
fun isCredentialManagerSupported(context: Context): Boolean {
|
||||
/**
|
||||
* Whether a password manager / credential provider is available. On API 26+ this tracks whether
|
||||
* the user has an autofill service enabled; older devices fall back to the Credential Manager
|
||||
* Play Services backend, so they are supported only when Play Services is.
|
||||
*/
|
||||
fun isSupported(context: Context): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= 26) {
|
||||
context.getSystemService<AutofillManager>()?.isEnabled == true
|
||||
} else {
|
||||
true
|
||||
PlayServicesUtil.getPlayServicesStatus(context) == PlayServicesUtil.PlayServicesStatus.SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts the user to save a password credential to their password manager. Must be called with
|
||||
* an Activity context so the Credential Manager UI can be shown.
|
||||
*/
|
||||
suspend fun saveCredential(
|
||||
activityContext: Context,
|
||||
@UiContext activityContext: Context,
|
||||
username: String,
|
||||
password: String
|
||||
): CredentialManagerResult = try {
|
||||
@@ -92,30 +105,29 @@ object AndroidCredentialRepository {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a previously saved password credential by username. Returns null if the credential
|
||||
* cannot be found or if retrieval fails.
|
||||
* Prompts the device password manager to let the user pick a saved password credential and
|
||||
* returns its value, or null if none was chosen or retrieval failed. If [id] is provided, only a
|
||||
* credential with that id will be returned. Must be called with an Activity context so the
|
||||
* Credential Manager UI can be shown.
|
||||
*/
|
||||
suspend fun getCredential(
|
||||
activityContext: Context,
|
||||
id: String
|
||||
): String? = try {
|
||||
suspend fun getCredential(@UiContext activityContext: Context, id: String? = null): String? = try {
|
||||
val result = CredentialManager.create(activityContext).getCredential(activityContext, GetCredentialRequest(listOf(GetPasswordOption())))
|
||||
val credential = result.credential
|
||||
if (credential is PasswordCredential && credential.id == id) {
|
||||
if (credential is PasswordCredential && (id == null || credential.id == id)) {
|
||||
credential.password
|
||||
} else {
|
||||
Log.w(TAG, "Failed to find credential from password manager")
|
||||
Log.w(TAG, "Failed to find a matching credential from the password manager.")
|
||||
null
|
||||
}
|
||||
} catch (e: GetCredentialException) {
|
||||
Log.w(TAG, "Failed to find credential from password manager.", e)
|
||||
Log.w(TAG, "Failed to retrieve credential from password manager.", e)
|
||||
null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an [Intent] that can be used to launch the device's password manager settings.
|
||||
*/
|
||||
fun getCredentialManagerSettingsIntent(context: Context): Intent? {
|
||||
fun getSettingsIntent(context: Context): Intent? {
|
||||
if (Build.VERSION.SDK_INT >= 34) {
|
||||
val intent = Intent(
|
||||
Settings.ACTION_CREDENTIAL_PROVIDER,
|
||||
@@ -142,11 +154,12 @@ object AndroidCredentialRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/** Represents the result of a [SignalCredentialManager] save operation. */
|
||||
sealed interface CredentialManagerResult {
|
||||
data object Success : CredentialManagerResult
|
||||
data object UserCanceled : CredentialManagerResult
|
||||
|
||||
/** The backup key save operation was interrupted and should be retried. */
|
||||
/** The save operation was interrupted and should be retried. */
|
||||
data class Interrupted(val exception: Exception) : CredentialManagerResult
|
||||
}
|
||||
|
||||
+10
-10
@@ -1,9 +1,9 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.ui.restore
|
||||
package org.signal.passwordmanager.compose
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
@@ -29,14 +29,14 @@ 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.
|
||||
* Provide a compose friendly way to autofill a password (e.g. a backup key) from a password manager.
|
||||
*/
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@SuppressLint("NewApi")
|
||||
@Composable
|
||||
fun backupKeyAutoFillHelper(
|
||||
fun passwordAutoFillHelper(
|
||||
onFill: (String) -> Unit
|
||||
): BackupKeyAutoFillHelper {
|
||||
): PasswordAutoFillHelper {
|
||||
val view = LocalView.current
|
||||
val context = LocalContext.current
|
||||
val autofill: Autofill? = LocalAutofill.current
|
||||
@@ -45,7 +45,7 @@ fun backupKeyAutoFillHelper(
|
||||
LocalAutofillTree.current += node
|
||||
|
||||
return remember {
|
||||
object : BackupKeyAutoFillHelper(context) {
|
||||
object : PasswordAutoFillHelper(context) {
|
||||
override fun request() {
|
||||
if (node.boundingBox != null) {
|
||||
autofill?.requestAutofillForNode(node)
|
||||
@@ -75,10 +75,10 @@ fun backupKeyAutoFillHelper(
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a [BackupKeyAutoFillHelper] return from [backupKeyAutoFillHelper] to setup the default
|
||||
* Attach a [PasswordAutoFillHelper] returned from [passwordAutoFillHelper] to setup the default
|
||||
* callbacks needed to make requests on the view's behalf.
|
||||
*/
|
||||
fun Modifier.attachBackupKeyAutoFillHelper(helper: BackupKeyAutoFillHelper): Modifier {
|
||||
fun Modifier.attachPasswordAutoFillHelper(helper: PasswordAutoFillHelper): Modifier {
|
||||
return this.then(
|
||||
Modifier
|
||||
.onFocusChanged {
|
||||
@@ -95,10 +95,10 @@ fun Modifier.attachBackupKeyAutoFillHelper(helper: BackupKeyAutoFillHelper): Mod
|
||||
}
|
||||
|
||||
/**
|
||||
* Weird compose-interop abstract class to let us return something to the caller of [backupKeyAutoFillHelper]
|
||||
* Weird compose-interop abstract class to let us return something to the caller of [passwordAutoFillHelper]
|
||||
* and capture inner compose data to implement the methods that need various compose provided things.
|
||||
*/
|
||||
abstract class BackupKeyAutoFillHelper(context: Context) {
|
||||
abstract class PasswordAutoFillHelper(context: Context) {
|
||||
protected val autoFillManager: AutofillManager? = if (Build.VERSION.SDK_INT >= 26) {
|
||||
ContextCompat.getSystemService(context, AutofillManager::class.java)
|
||||
} else {
|
||||
@@ -126,6 +126,7 @@ include(":lib:emoji")
|
||||
include(":lib:archive")
|
||||
include(":lib:ui-components")
|
||||
include(":lib:signal-login")
|
||||
include(":lib:password-manager")
|
||||
|
||||
// Feature modules
|
||||
include(":feature:app-settings")
|
||||
|
||||
Reference in New Issue
Block a user