From fdf1238905a755b9163d3a0e25605b612dd952ca Mon Sep 17 00:00:00 2001 From: jeffrey-signal Date: Fri, 27 Jun 2025 09:42:32 -0400 Subject: [PATCH] Improve backup key password manager save error handling. - Add "go to settings" button to missing credential manager error dialog. - Add handling for disabled password manager save prompt on Android API <= 33. --- .../MessageBackupsFlowFragment.kt | 7 ++- .../MessageBackupsKeyRecordScreen.kt | 42 +++++++++++++---- .../BackupKeyCredentialManagerHandler.kt | 47 +++++++++++++++++++ .../remote/BackupKeyDisplayFragment.kt | 6 ++- app/src/main/res/values/strings.xml | 8 +++- 5 files changed, 97 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt index c7c9d0234a..9bdc66d7cc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt @@ -33,6 +33,7 @@ import org.signal.core.util.getSerializableCompat import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.backup.DeletionState import org.thoughtcrime.securesms.backup.v2.MessageBackupTier +import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeyCredentialManagerHandler import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentCheckoutDelegate import org.thoughtcrime.securesms.compose.ComposeFragment import org.thoughtcrime.securesms.compose.Nav @@ -150,7 +151,11 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega }, onRequestSaveToPasswordManager = viewModel::onBackupKeySaveRequested, onConfirmSaveToPasswordManager = viewModel::onBackupKeySaveConfirmed, - onSaveToPasswordManagerComplete = viewModel::onBackupKeySaveCompleted + onSaveToPasswordManagerComplete = viewModel::onBackupKeySaveCompleted, + onGoToDeviceSettingsClick = { + val intent = BackupKeyCredentialManagerHandler.getCredentialManagerSettingsIntent(requireContext()) + requireContext().startActivity(intent) + } ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyRecordScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyRecordScreen.kt index 772e31be12..4c2dfcf784 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyRecordScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyRecordScreen.kt @@ -51,6 +51,7 @@ import org.signal.core.ui.compose.Snackbars import org.signal.core.ui.compose.theme.SignalTheme import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeyCredentialManagerHandler import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeySaveState import org.thoughtcrime.securesms.components.settings.app.backups.remote.CredentialManagerError import org.thoughtcrime.securesms.components.settings.app.backups.remote.CredentialManagerResult @@ -72,7 +73,8 @@ fun MessageBackupsKeyRecordScreen( onRequestSaveToPasswordManager: () -> Unit = {}, onConfirmSaveToPasswordManager: () -> Unit = {}, onSaveToPasswordManagerComplete: (CredentialManagerResult) -> Unit = {}, - onNextClick: () -> Unit = {} + onNextClick: () -> Unit = {}, + onGoToDeviceSettingsClick: () -> Unit = {} ) { val snackbarHostState = remember { SnackbarHostState() } val backupKeyString = remember(backupKey) { @@ -163,7 +165,7 @@ fun MessageBackupsKeyRecordScreen( } } - if (Build.VERSION.SDK_INT >= 24) { + if (BackupKeyCredentialManagerHandler.isCredentialManagerSupported) { item { Buttons.Small( onClick = { onRequestSaveToPasswordManager() } @@ -220,13 +222,30 @@ fun MessageBackupsKeyRecordScreen( } is BackupKeySaveState.Error -> { - if (keySaveState.errorType is CredentialManagerError.MissingCredentialManager) { - Dialogs.SimpleMessageDialog( - title = stringResource(R.string.MessageBackupsKeyRecordScreen__missing_password_manager_title), - message = stringResource(R.string.MessageBackupsKeyRecordScreen__missing_password_manager_message), - dismiss = stringResource(android.R.string.ok), - onDismiss = { onSaveToPasswordManagerComplete(CredentialManagerResult.UserCanceled) } - ) + when (keySaveState.errorType) { + is CredentialManagerError.MissingCredentialManager -> { + Dialogs.SimpleAlertDialog( + title = stringResource(R.string.MessageBackupsKeyRecordScreen__cant_save_to_password_manager_title), + body = stringResource(R.string.MessageBackupsKeyRecordScreen__missing_password_manager_message), + confirm = stringResource(R.string.MessageBackupsKeyRecordScreen__go_to_settings), + onConfirm = { onGoToDeviceSettingsClick() }, + dismiss = stringResource(android.R.string.cancel), + onDismiss = { onSaveToPasswordManagerComplete(CredentialManagerResult.UserCanceled) } + ) + } + + is CredentialManagerError.SavePromptDisabled -> { + Dialogs.SimpleAlertDialog( + title = stringResource(R.string.MessageBackupsKeyRecordScreen__cant_save_to_password_manager_title), + body = stringResource(R.string.MessageBackupsKeyRecordScreen__missing_password_manager_message), + confirm = stringResource(R.string.MessageBackupsKeyRecordScreen__go_to_settings), + onConfirm = { onGoToDeviceSettingsClick() }, + dismiss = stringResource(android.R.string.cancel), + onDismiss = { onSaveToPasswordManagerComplete(CredentialManagerResult.UserCanceled) } + ) + } + + is CredentialManagerError.Unexpected -> Unit } } @@ -271,6 +290,11 @@ private suspend fun saveKeyToCredentialManager( CredentialManagerError.MissingCredentialManager(e) } + e.message?.contains("[28435]") == true -> { + Log.w(TAG, "CreateCredentialUnknownException: \"${e.message}\"") + CredentialManagerError.SavePromptDisabled(e) + } + else -> CredentialManagerError.Unexpected(e) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyCredentialManagerHandler.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyCredentialManagerHandler.kt index bc99ccb00a..255afeee3a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyCredentialManagerHandler.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyCredentialManagerHandler.kt @@ -5,6 +5,13 @@ package org.thoughtcrime.securesms.components.settings.app.backups.remote +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.provider.Settings +import android.view.autofill.AutofillManager +import androidx.core.content.getSystemService import org.signal.core.util.logging.Log import org.signal.core.util.logging.logW @@ -14,6 +21,37 @@ import org.signal.core.util.logging.logW interface BackupKeyCredentialManagerHandler { companion object { private val TAG = Log.tag(BackupKeyCredentialManagerHandler::class) + + val isCredentialManagerSupported: Boolean = Build.VERSION.SDK_INT >= 19 + + /** + * Returns an [Intent] that can be used to launch the device's password manager settings. + */ + fun getCredentialManagerSettingsIntent(context: Context): Intent? { + if (Build.VERSION.SDK_INT >= 34) { + val intent = Intent( + Settings.ACTION_CREDENTIAL_PROVIDER, + Uri.fromParts("package", context.packageName, null) + ) + + if (intent.resolveActivity(context.packageManager) != null) { + return intent + } + } + + if (Build.VERSION.SDK_INT >= 26) { + val isAutofillSupported = context.getSystemService()?.isAutofillSupported() == true + if (isAutofillSupported) { + val intent = Intent( + Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE, + Uri.fromParts("package", context.packageName, null) + ) + return intent.takeIf { it.resolveActivity(context.packageManager) != null } + } + } + + return null + } } /** Updates the [BackupKeySaveState]. Implementers must update their associated state to match [newState]. */ @@ -48,6 +86,11 @@ interface BackupKeyCredentialManagerHandler { updateBackupKeySaveState(newState = BackupKeySaveState.Error(result)) } + is CredentialManagerError.SavePromptDisabled -> { + Log.w(TAG, "Error saving backup key to credential manager: the user has disabled the save prompt.", result.exception) + updateBackupKeySaveState(newState = BackupKeySaveState.Error(result)) + } + is CredentialManagerError.Unexpected -> { throw result.exception.logW(TAG, "Unexpected error when saving backup key to credential manager.") } @@ -79,5 +122,9 @@ sealed class CredentialManagerError : CredentialManagerResult { /** No password manager is configured on the device. */ data class MissingCredentialManager(override val exception: Exception) : CredentialManagerError() + + /** The user has added this app to the "never save" list in the smart lock for passwords settings. **/ + data class SavePromptDisabled(override val exception: Exception) : CredentialManagerError() + data class Unexpected(override val exception: Exception) : CredentialManagerError() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyDisplayFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyDisplayFragment.kt index da93b4af88..5cfe8b6c70 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyDisplayFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyDisplayFragment.kt @@ -38,7 +38,11 @@ class BackupKeyDisplayFragment : ComposeFragment() { onRequestSaveToPasswordManager = viewModel::onBackupKeySaveRequested, onConfirmSaveToPasswordManager = viewModel::onBackupKeySaveConfirmed, onSaveToPasswordManagerComplete = viewModel::onBackupKeySaveCompleted, - onNextClick = { findNavController().popBackStack() } + onNextClick = { findNavController().popBackStack() }, + onGoToDeviceSettingsClick = { + val intent = BackupKeyCredentialManagerHandler.getCredentialManagerSettingsIntent(requireContext()) + requireContext().startActivity(intent) + } ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bbe1d99146..6a576b8680 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8360,10 +8360,14 @@ Only store your backup key in a password manager that you trust is secure. Signal does not make a recommendation on which password manager is right for you. Saved to your password manager - - Can\'t save to password manager + + Can\'t save to password manager You don\'t have a password manager set up, or the password manager you\'re using isn\'t supported. + + You previously chose not to save passwords for Signal to Google Password Manager. In your Google Password Manager settings, remove Signal from \"Declined sites and apps\" and try again. + + Go to settings Next