diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt index a2d1e4de8e357..b32fe3fef00d9 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -42,11 +42,13 @@ import com.android.credentialmanager.createflow.DisabledProviderInfo import com.android.credentialmanager.createflow.EnabledProviderInfo import com.android.credentialmanager.createflow.RequestDisplayInfo import com.android.credentialmanager.getflow.GetCredentialUiState +import com.android.credentialmanager.getflow.findAutoSelectEntry import androidx.credentials.CreateCredentialRequest.DisplayInfo import androidx.credentials.CreatePublicKeyCredentialRequest import androidx.credentials.CreatePasswordRequest import androidx.credentials.GetPasswordOption import androidx.credentials.GetPublicKeyCredentialOption +import com.android.credentialmanager.common.ProviderActivityState import java.time.Instant @@ -128,10 +130,20 @@ class CredentialManagerRepo( getCredentialUiState = null, ) } - RequestInfo.TYPE_GET -> UiState( - createCredentialUiState = null, - getCredentialUiState = getCredentialInitialUiState(originName)!!, - ) + RequestInfo.TYPE_GET -> { + val getCredentialInitialUiState = getCredentialInitialUiState(originName)!! + val autoSelectEntry = + findAutoSelectEntry(getCredentialInitialUiState.providerDisplayInfo) + UiState( + createCredentialUiState = null, + getCredentialUiState = getCredentialInitialUiState, + selectedEntry = autoSelectEntry, + providerActivityState = + if (autoSelectEntry == null) ProviderActivityState.NOT_APPLICABLE + else ProviderActivityState.READY_TO_LAUNCH, + isAutoSelectFlow = autoSelectEntry != null, + ) + } else -> throw IllegalStateException("Unrecognized request type: ${requestInfo.type}") } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt index a5c7494327531..ca30c53a6d83a 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt @@ -44,6 +44,9 @@ data class UiState( val selectedEntry: BaseEntry? = null, val providerActivityState: ProviderActivityState = ProviderActivityState.NOT_APPLICABLE, val dialogState: DialogState = DialogState.ACTIVE, + // True if the UI has one and onely one auto selectable entry. Its provider activiey will be + // launched immediately, and canceling it will cancel the whole UI flow. + val isAutoSelectFlow: Boolean = false, ) class CredentialSelectorViewModel( @@ -96,13 +99,20 @@ class CredentialSelectorViewModel( val resultCode = providerActivityResult.resultCode val resultData = providerActivityResult.data if (resultCode == Activity.RESULT_CANCELED) { - // Re-display the CredMan UI if the user canceled from the provider UI. - Log.d(Constants.LOG_TAG, "The provider activity was cancelled," + - " re-displaying our UI.") - uiState = uiState.copy( - selectedEntry = null, - providerActivityState = ProviderActivityState.NOT_APPLICABLE, - ) + // Re-display the CredMan UI if the user canceled from the provider UI, or cancel + // the UI if this is the auto select flow. + if (uiState.isAutoSelectFlow) { + Log.d(Constants.LOG_TAG, "The auto selected provider activity was cancelled," + + " ending the credential manager activity.") + onUserCancel() + } else { + Log.d(Constants.LOG_TAG, "The provider activity was cancelled," + + " re-displaying our UI.") + uiState = uiState.copy( + selectedEntry = null, + providerActivityState = ProviderActivityState.NOT_APPLICABLE, + ) + } } else { if (entry != null) { Log.d( diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt index 50edbc15e8162..e61633f441f7d 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -228,6 +228,8 @@ class GetFlowUtils { icon = credentialEntry.icon.loadDrawable(context), shouldTintIcon = credentialEntry.isDefaultIcon ?: false, lastUsedTimeMillis = credentialEntry.lastUsedTime, + isAutoSelectable = credentialEntry.isAutoSelectAllowed && + credentialEntry.autoSelectAllowedFromOption, )) } is PublicKeyCredentialEntry -> { @@ -245,6 +247,8 @@ class GetFlowUtils { icon = credentialEntry.icon.loadDrawable(context), shouldTintIcon = credentialEntry.isDefaultIcon, lastUsedTimeMillis = credentialEntry.lastUsedTime, + isAutoSelectable = credentialEntry.isAutoSelectAllowed && + credentialEntry.autoSelectAllowedFromOption, )) } is CustomCredentialEntry -> { @@ -262,6 +266,8 @@ class GetFlowUtils { icon = credentialEntry.icon.loadDrawable(context), shouldTintIcon = credentialEntry.isDefaultIcon, lastUsedTimeMillis = credentialEntry.lastUsedTime, + isAutoSelectable = credentialEntry.isAutoSelectAllowed && + credentialEntry.autoSelectAllowedFromOption, )) } else -> Log.d( diff --git a/packages/CredentialManager/src/com/android/credentialmanager/TestUtils.kt b/packages/CredentialManager/src/com/android/credentialmanager/TestUtils.kt index 75b12ff756ada..82d6952bf826d 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/TestUtils.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/TestUtils.kt @@ -155,21 +155,23 @@ class GetTestUtils { userName: String, userDisplayName: String?, lastUsedTime: Instant?, + isAutoSelectAllowed: Boolean = false, ): Entry { - val intent = Intent("com.androidauth.androidvault.CONFIRM_PASSWORD") - .setPackage("com.androidauth.androidvault") - intent.putExtra("provider_extra_sample", "testprovider") - val pendingIntent = PendingIntent.getActivity( - context, 1, - intent, (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - or PendingIntent.FLAG_ONE_SHOT) + val intent = Intent(Settings.ACTION_SYNC_SETTINGS) + val pendingIntent = + PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) + val candidateQueryData = Bundle() + candidateQueryData.putBoolean( + "androidx.credentials.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED", + isAutoSelectAllowed ) val passkeyEntry = PublicKeyCredentialEntry.Builder( context, userName, pendingIntent, - BeginGetPublicKeyCredentialOption(Bundle(), "id", "requestjson") - ).setDisplayName(userDisplayName).setLastUsedTime(lastUsedTime).build() + BeginGetPublicKeyCredentialOption(candidateQueryData, "id", "requestjson") + ).setDisplayName(userDisplayName).setLastUsedTime(lastUsedTime) + .setAutoSelectAllowed(isAutoSelectAllowed).build() return Entry(key, subkey, passkeyEntry.slice, Intent()) } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt index 2cb0af4fdeda8..9fe789934f1b5 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt @@ -1,4 +1,18 @@ -@file:OptIn(ExperimentalMaterial3Api::class) +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.android.credentialmanager.createflow @@ -15,7 +29,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material3.Divider -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.NewReleases diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt index 7bf8dafdea7e6..263a632ef5ee0 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt @@ -41,6 +41,23 @@ internal fun hasContentToDisplay(state: GetCredentialUiState): Boolean { !state.requestDisplayInfo.preferImmediatelyAvailableCredentials) } +internal fun findAutoSelectEntry(providerDisplayInfo: ProviderDisplayInfo): CredentialEntryInfo? { + if (providerDisplayInfo.authenticationEntryList.isNotEmpty()) { + return null + } + if (providerDisplayInfo.sortedUserNameToCredentialEntryList.size == 1) { + val entryList = providerDisplayInfo.sortedUserNameToCredentialEntryList.firstOrNull() + ?: return null + if (entryList.sortedCredentialEntryList.size == 1) { + val entry = entryList.sortedCredentialEntryList.firstOrNull() ?: return null + if (entry.isAutoSelectable) { + return entry + } + } + } + return null +} + data class ProviderInfo( /** * Unique id (component name) of this provider. @@ -83,6 +100,7 @@ class CredentialEntryInfo( val icon: Drawable?, val shouldTintIcon: Boolean, val lastUsedTimeMillis: Instant?, + val isAutoSelectable: Boolean, ) : BaseEntry( providerId, entryKey,