mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 17:29:32 +01:00
Prompt to save PIN to device password manager.
Fixes an issue where the system auto-fill could overwrite the Signal backup key saved to the device password manager with the Signal PIN. The PIN confirmation screen now explicitly uses `CredentialManager` to save the `Signal PIN` under a separate username from the `Signal Backups` key, allowing both credentials to be stored and auto-filled correctly. - Add `com.google.android.libraries.identity.googleid` dependency so `CredentialManager` works on Android < 14. - Prompt to save Signal PIN to credential manager after PIN is created/edited.
This commit is contained in:
committed by
Alex Hart
parent
ef874c4091
commit
6d58e89c18
@@ -33,7 +33,6 @@ 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
|
||||
@@ -41,6 +40,7 @@ import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.storage.AndroidCredentialRepository
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
/**
|
||||
@@ -140,7 +140,7 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
||||
|
||||
composable(route = MessageBackupsStage.Route.BACKUP_KEY_RECORD.name) {
|
||||
val context = LocalContext.current
|
||||
val passwordManagerSettingsIntent = BackupKeyCredentialManagerHandler.getCredentialManagerSettingsIntent(requireContext())
|
||||
val passwordManagerSettingsIntent = AndroidCredentialRepository.getCredentialManagerSettingsIntent(requireContext())
|
||||
|
||||
MessageBackupsKeyRecordScreen(
|
||||
backupKey = state.accountEntropyPool.displayValue,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.annotation.UiContext
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -36,13 +36,6 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.credentials.CreatePasswordRequest
|
||||
import androidx.credentials.CredentialManager
|
||||
import androidx.credentials.exceptions.CreateCredentialCancellationException
|
||||
import androidx.credentials.exceptions.CreateCredentialInterruptedException
|
||||
import androidx.credentials.exceptions.CreateCredentialNoCreateOptionException
|
||||
import androidx.credentials.exceptions.CreateCredentialProviderConfigurationException
|
||||
import androidx.credentials.exceptions.CreateCredentialUnknownException
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.Dialogs
|
||||
import org.signal.core.ui.compose.Previews
|
||||
@@ -50,17 +43,14 @@ import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
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
|
||||
import org.thoughtcrime.securesms.fonts.MonoTypeface
|
||||
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 TAG = "MessageBackupsKeyRecordScreen"
|
||||
|
||||
/**
|
||||
* Screen displaying the backup key allowing the user to write it down
|
||||
* or copy it.
|
||||
@@ -167,7 +157,7 @@ fun MessageBackupsKeyRecordScreen(
|
||||
}
|
||||
}
|
||||
|
||||
if (BackupKeyCredentialManagerHandler.isCredentialManagerSupported) {
|
||||
if (AndroidCredentialRepository.isCredentialManagerSupported) {
|
||||
item {
|
||||
Buttons.Small(
|
||||
onClick = { onRequestSaveToPasswordManager() }
|
||||
@@ -279,52 +269,14 @@ private fun BackupKeySaveErrorDialog(
|
||||
}
|
||||
|
||||
private suspend fun saveKeyToCredentialManager(
|
||||
context: Context,
|
||||
@UiContext activityContext: Context,
|
||||
backupKey: String
|
||||
): CredentialManagerResult {
|
||||
return try {
|
||||
CredentialManager.create(context)
|
||||
.createCredential(
|
||||
context = context,
|
||||
request = CreatePasswordRequest(
|
||||
id = context.getString(R.string.RemoteBackupsSettingsFragment__signal_backups),
|
||||
password = backupKey,
|
||||
preferImmediatelyAvailableCredentials = false,
|
||||
isAutoSelectAllowed = false
|
||||
)
|
||||
)
|
||||
CredentialManagerResult.Success
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is CreateCredentialCancellationException -> CredentialManagerResult.UserCanceled
|
||||
is CreateCredentialInterruptedException -> CredentialManagerResult.Interrupted(e)
|
||||
is CreateCredentialNoCreateOptionException, is CreateCredentialProviderConfigurationException -> CredentialManagerError.MissingCredentialManager(e)
|
||||
is CreateCredentialUnknownException -> {
|
||||
when {
|
||||
Build.VERSION.SDK_INT <= 33 && e.message?.contains("[28431]") == true -> {
|
||||
// This error only impacts Android 13 and earlier, when Google is the designated autofill provider. The error can be safely disregarded, since users
|
||||
// will receive a save prompt from autofill and the password will be stored in Google Password Manager, which syncs with the Credential Manager API.
|
||||
Log.d(TAG, "Disregarding CreateCredentialUnknownException and treating credential creation as success: \"${e.message}\".")
|
||||
CredentialManagerResult.Success
|
||||
}
|
||||
|
||||
e.message?.contains("[28434]") == true -> {
|
||||
Log.w(TAG, "Detected MissingCredentialManager error based on CreateCredentialUnknownException message: \"${e.message}\"")
|
||||
CredentialManagerError.MissingCredentialManager(e)
|
||||
}
|
||||
|
||||
e.message?.contains("[28435]") == true -> {
|
||||
Log.w(TAG, "CreateCredentialUnknownException: \"${e.message}\"")
|
||||
CredentialManagerError.SavePromptDisabled(e)
|
||||
}
|
||||
|
||||
else -> CredentialManagerError.Unexpected(e)
|
||||
}
|
||||
}
|
||||
|
||||
else -> CredentialManagerError.Unexpected(e)
|
||||
}
|
||||
}
|
||||
return AndroidCredentialRepository.saveCredential(
|
||||
activityContext = activityContext,
|
||||
username = activityContext.getString(R.string.MessageBackupsKeyRecordScreen__backup_key_password_manager_id),
|
||||
password = backupKey
|
||||
)
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
|
||||
@@ -5,15 +5,10 @@
|
||||
|
||||
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
|
||||
import org.thoughtcrime.securesms.util.storage.CredentialManagerError
|
||||
import org.thoughtcrime.securesms.util.storage.CredentialManagerResult
|
||||
|
||||
/**
|
||||
* Handles the process of storing a backup key to the device password manager.
|
||||
@@ -21,37 +16,6 @@ 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<AutofillManager>()?.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]. */
|
||||
@@ -108,23 +72,3 @@ sealed interface BackupKeySaveState {
|
||||
data object Success : BackupKeySaveState
|
||||
data class Error(val errorType: CredentialManagerError) : BackupKeySaveState
|
||||
}
|
||||
|
||||
sealed interface CredentialManagerResult {
|
||||
data object Success : CredentialManagerResult
|
||||
data object UserCanceled : CredentialManagerResult
|
||||
|
||||
/** The backup key save operation was interrupted and should be retried. */
|
||||
data class Interrupted(val exception: Exception) : CredentialManagerResult
|
||||
}
|
||||
|
||||
sealed class CredentialManagerError : CredentialManagerResult {
|
||||
abstract val exception: Exception
|
||||
|
||||
/** 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()
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsKeyRec
|
||||
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
|
||||
|
||||
/**
|
||||
@@ -29,7 +30,7 @@ class BackupKeyDisplayFragment : ComposeFragment() {
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val passwordManagerSettingsIntent = BackupKeyCredentialManagerHandler.getCredentialManagerSettingsIntent(requireContext())
|
||||
val passwordManagerSettingsIntent = AndroidCredentialRepository.getCredentialManagerSettingsIntent(requireContext())
|
||||
|
||||
MessageBackupsKeyRecordScreen(
|
||||
backupKey = SignalStore.account.accountEntropyPool.displayValue,
|
||||
|
||||
@@ -8,6 +8,9 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.lock.v2.ConfirmSvrPinViewModel.SaveAnimation
|
||||
@@ -15,6 +18,7 @@ import org.thoughtcrime.securesms.megaphone.Megaphones
|
||||
import org.thoughtcrime.securesms.registration.util.RegistrationUtil
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.storage.AndroidCredentialRepository
|
||||
|
||||
internal class ConfirmSvrPinFragment : BaseSvrPinFragment<ConfirmSvrPinViewModel>() {
|
||||
|
||||
@@ -25,6 +29,8 @@ internal class ConfirmSvrPinFragment : BaseSvrPinFragment<ConfirmSvrPinViewModel
|
||||
} else {
|
||||
initializeViewStatesForPinCreate()
|
||||
}
|
||||
|
||||
ViewCompat.setImportantForAutofill(input, View.IMPORTANT_FOR_AUTOFILL_YES)
|
||||
ViewCompat.setAutofillHints(input, HintConstants.AUTOFILL_HINT_NEW_PASSWORD)
|
||||
}
|
||||
|
||||
@@ -64,6 +70,7 @@ internal class ConfirmSvrPinFragment : BaseSvrPinFragment<ConfirmSvrPinViewModel
|
||||
label.setText(R.string.ConfirmKbsPinFragment__creating_pin)
|
||||
input.isEnabled = false
|
||||
}
|
||||
|
||||
ConfirmSvrPinViewModel.LabelState.RE_ENTER_PIN -> label.setText(R.string.ConfirmKbsPinFragment__re_enter_your_pin)
|
||||
ConfirmSvrPinViewModel.LabelState.PIN_DOES_NOT_MATCH -> {
|
||||
label.text = SpanUtil.color(
|
||||
@@ -85,7 +92,9 @@ internal class ConfirmSvrPinFragment : BaseSvrPinFragment<ConfirmSvrPinViewModel
|
||||
closeNavGraphBranch()
|
||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
showSavePinToPasswordManagerPrompt()
|
||||
}
|
||||
|
||||
SaveAnimation.FAILURE -> {
|
||||
confirm.cancelSpinning()
|
||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||
@@ -118,4 +127,14 @@ internal class ConfirmSvrPinFragment : BaseSvrPinFragment<ConfirmSvrPinViewModel
|
||||
private fun markMegaphoneSeenIfNecessary() {
|
||||
AppDependencies.megaphoneRepository.markSeen(Megaphones.Event.PINS_FOR_ALL)
|
||||
}
|
||||
|
||||
private fun showSavePinToPasswordManagerPrompt() {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
AndroidCredentialRepository.saveCredential(
|
||||
activityContext = requireActivity(),
|
||||
username = getString(R.string.ConfirmKbsPinFragment__pin_password_manager_id),
|
||||
password = input.text.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@ import android.view.animation.Animation
|
||||
import android.view.animation.TranslateAnimation
|
||||
import android.widget.EditText
|
||||
import androidx.annotation.PluralsRes
|
||||
import androidx.autofill.HintConstants
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.Navigation.findNavController
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -19,16 +17,15 @@ class CreateSvrPinFragment : BaseSvrPinFragment<CreateSvrPinViewModel?>() {
|
||||
override fun initializeViewStates() {
|
||||
val args = CreateSvrPinFragmentArgs.fromBundle(requireArguments())
|
||||
if (args.isPinChange) {
|
||||
initializeViewStatesForPinChange(args.isForgotPin)
|
||||
initializeViewStatesForPinChange()
|
||||
} else {
|
||||
initializeViewStatesForPinCreate()
|
||||
}
|
||||
label.text = getPinLengthRestrictionText(R.plurals.CreateKbsPinFragment__pin_must_be_at_least_digits)
|
||||
confirm.isEnabled = false
|
||||
ViewCompat.setAutofillHints(input, HintConstants.AUTOFILL_HINT_NEW_PASSWORD)
|
||||
}
|
||||
|
||||
private fun initializeViewStatesForPinChange(isForgotPin: Boolean) {
|
||||
private fun initializeViewStatesForPinChange() {
|
||||
title.setText(R.string.CreateKbsPinFragment__create_a_new_pin)
|
||||
description.setText(R.string.CreateKbsPinFragment__you_can_choose_a_new_pin_as_long_as_this_device_is_registered)
|
||||
description.setLearnMoreVisible(true)
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.util.storage
|
||||
|
||||
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 androidx.credentials.CreatePasswordRequest
|
||||
import androidx.credentials.CredentialManager
|
||||
import androidx.credentials.exceptions.CreateCredentialCancellationException
|
||||
import androidx.credentials.exceptions.CreateCredentialInterruptedException
|
||||
import androidx.credentials.exceptions.CreateCredentialNoCreateOptionException
|
||||
import androidx.credentials.exceptions.CreateCredentialProviderConfigurationException
|
||||
import androidx.credentials.exceptions.CreateCredentialUnknownException
|
||||
import org.signal.core.util.logging.Log
|
||||
|
||||
/**
|
||||
* Responsible for storing and retrieving credentials using Android's Credential Manager.
|
||||
*/
|
||||
object AndroidCredentialRepository {
|
||||
private val TAG = Log.tag(AndroidCredentialRepository::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]"
|
||||
|
||||
val isCredentialManagerSupported: Boolean = Build.VERSION.SDK_INT >= 19
|
||||
|
||||
suspend fun saveCredential(
|
||||
activityContext: Context,
|
||||
username: String,
|
||||
password: String
|
||||
): CredentialManagerResult = try {
|
||||
CredentialManager.create(activityContext)
|
||||
.createCredential(
|
||||
context = activityContext,
|
||||
request = CreatePasswordRequest(
|
||||
id = username,
|
||||
password = password,
|
||||
preferImmediatelyAvailableCredentials = false,
|
||||
isAutoSelectAllowed = false
|
||||
)
|
||||
)
|
||||
CredentialManagerResult.Success
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is CreateCredentialCancellationException -> CredentialManagerResult.UserCanceled
|
||||
is CreateCredentialInterruptedException -> CredentialManagerResult.Interrupted(e)
|
||||
is CreateCredentialNoCreateOptionException, is CreateCredentialProviderConfigurationException -> CredentialManagerError.MissingCredentialManager(e)
|
||||
is CreateCredentialUnknownException -> {
|
||||
when {
|
||||
Build.VERSION.SDK_INT <= 33 && e.message?.contains(ERROR_CODE_GOOGLE_AUTOFILL_SUCCESS) == true -> {
|
||||
// This error only impacts Android 13 and earlier, when Google is the designated autofill provider. The error can be safely disregarded, since users
|
||||
// will receive a save prompt from autofill and the password will be stored in Google Password Manager, which syncs with the Credential Manager API.
|
||||
Log.d(TAG, "Disregarding CreateCredentialUnknownException and treating credential creation as success: \"${e.message}\".")
|
||||
CredentialManagerResult.Success
|
||||
}
|
||||
|
||||
e.message?.contains(ERROR_CODE_MISSING_CREDENTIAL_MANAGER) == true -> {
|
||||
Log.w(TAG, "Detected MissingCredentialManager error based on CreateCredentialUnknownException message: \"${e.message}\"")
|
||||
CredentialManagerError.MissingCredentialManager(e)
|
||||
}
|
||||
|
||||
e.message?.contains(ERROR_CODE_SAVE_PROMPT_DISABLED) == true -> {
|
||||
Log.w(TAG, "CreateCredentialUnknownException: \"${e.message}\"")
|
||||
CredentialManagerError.SavePromptDisabled(e)
|
||||
}
|
||||
|
||||
else -> CredentialManagerError.Unexpected(e)
|
||||
}
|
||||
}
|
||||
|
||||
else -> CredentialManagerError.Unexpected(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<AutofillManager>()?.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
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface CredentialManagerResult {
|
||||
data object Success : CredentialManagerResult
|
||||
data object UserCanceled : CredentialManagerResult
|
||||
|
||||
/** The backup key save operation was interrupted and should be retried. */
|
||||
data class Interrupted(val exception: Exception) : CredentialManagerResult
|
||||
}
|
||||
|
||||
sealed class CredentialManagerError : CredentialManagerResult {
|
||||
abstract val exception: Exception
|
||||
|
||||
/** 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()
|
||||
}
|
||||
Reference in New Issue
Block a user