blob: 716f47450ae9b38370d1e467ca527e2d2963bf00 [file] [log] [blame]
/*
* 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.getflow
import android.app.PendingIntent
import android.content.Intent
import android.graphics.drawable.Drawable
import com.android.credentialmanager.common.BaseEntry
import com.android.credentialmanager.common.CredentialType
import com.android.internal.util.Preconditions
import java.time.Instant
data class GetCredentialUiState(
val providerInfoList: List<ProviderInfo>,
val requestDisplayInfo: RequestDisplayInfo,
val providerDisplayInfo: ProviderDisplayInfo = toProviderDisplayInfo(providerInfoList),
val currentScreenState: GetScreenState = toGetScreenState(providerDisplayInfo),
val activeEntry: BaseEntry? = toActiveEntry(providerDisplayInfo),
val isNoAccount: Boolean = false,
)
internal fun hasContentToDisplay(state: GetCredentialUiState): Boolean {
return state.providerDisplayInfo.sortedUserNameToCredentialEntryList.isNotEmpty() ||
state.providerDisplayInfo.authenticationEntryList.isNotEmpty() ||
(state.providerDisplayInfo.remoteEntry != null &&
!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.
* Not for display purpose - [displayName] should be used for ui rendering.
*/
val id: String,
val icon: Drawable,
val displayName: String,
val credentialEntryList: List<CredentialEntryInfo>,
val authenticationEntryList: List<AuthenticationEntryInfo>,
val remoteEntry: RemoteEntryInfo?,
val actionEntryList: List<ActionEntryInfo>,
)
/** Display-centric data structure derived from the [ProviderInfo]. This abstraction is not grouping
* by the provider id but instead focuses on structures convenient for display purposes. */
data class ProviderDisplayInfo(
/**
* The credential entries grouped by userName, derived from all entries of the [providerInfoList].
* Note that the list order matters to the display order.
*/
val sortedUserNameToCredentialEntryList: List<PerUserNameCredentialEntryList>,
val authenticationEntryList: List<AuthenticationEntryInfo>,
val remoteEntry: RemoteEntryInfo?
)
class CredentialEntryInfo(
providerId: String,
entryKey: String,
entrySubkey: String,
pendingIntent: PendingIntent?,
fillInIntent: Intent?,
/** Type of this credential used for sorting. Not localized so must not be directly displayed. */
val credentialType: CredentialType,
/** Localized type value of this credential used for display purpose. */
val credentialTypeDisplayName: String,
val providerDisplayName: String,
val userName: String,
val displayName: String?,
val icon: Drawable?,
val shouldTintIcon: Boolean,
val lastUsedTimeMillis: Instant?,
val isAutoSelectable: Boolean,
) : BaseEntry(
providerId,
entryKey,
entrySubkey,
pendingIntent,
fillInIntent,
shouldTerminateUiUponSuccessfulProviderResult = true,
)
class AuthenticationEntryInfo(
providerId: String,
entryKey: String,
entrySubkey: String,
pendingIntent: PendingIntent?,
fillInIntent: Intent?,
val title: String,
val providerDisplayName: String,
val icon: Drawable,
// The entry had been unlocked and turned out to be empty. Used to determine whether to
// show "Tap to unlock" or "No sign-in info" for this entry.
val isUnlockedAndEmpty: Boolean,
// True if the entry was the last one unlocked. Used to show the no sign-in info snackbar.
val isLastUnlocked: Boolean,
) : BaseEntry(
providerId,
entryKey, entrySubkey,
pendingIntent,
fillInIntent,
shouldTerminateUiUponSuccessfulProviderResult = false,
)
class RemoteEntryInfo(
providerId: String,
entryKey: String,
entrySubkey: String,
pendingIntent: PendingIntent?,
fillInIntent: Intent?,
) : BaseEntry(
providerId,
entryKey,
entrySubkey,
pendingIntent,
fillInIntent,
shouldTerminateUiUponSuccessfulProviderResult = true,
)
class ActionEntryInfo(
providerId: String,
entryKey: String,
entrySubkey: String,
pendingIntent: PendingIntent?,
fillInIntent: Intent?,
val title: String,
val icon: Drawable,
val subTitle: String?,
) : BaseEntry(
providerId,
entryKey,
entrySubkey,
pendingIntent,
fillInIntent,
shouldTerminateUiUponSuccessfulProviderResult = true,
)
data class RequestDisplayInfo(
val appName: String,
val preferImmediatelyAvailableCredentials: Boolean,
val preferIdentityDocUi: Boolean,
// A top level branding icon + display name preferred by the app.
val preferTopBrandingContent: TopBrandingContent?,
)
data class TopBrandingContent(
val icon: Drawable,
val displayName: String,
)
/**
* @property userName the userName that groups all the entries in this list
* @property sortedCredentialEntryList the credential entries associated with the [userName] sorted
* by last used timestamps and then by credential types
*/
data class PerUserNameCredentialEntryList(
val userName: String,
val sortedCredentialEntryList: List<CredentialEntryInfo>,
)
/** The name of the current screen. */
enum class GetScreenState {
/** The primary credential selection page. */
PRIMARY_SELECTION,
/** The secondary credential selection page, where all sign-in options are listed. */
ALL_SIGN_IN_OPTIONS,
/** The snackbar only page when there's no account but only a remoteEntry. */
REMOTE_ONLY,
/** The snackbar when there are only auth entries and all of them turn out to be empty. */
UNLOCKED_AUTH_ENTRIES_ONLY,
}
// IMPORTANT: new invocation should be mindful that this method will throw if more than 1 remote
// entry exists
private fun toProviderDisplayInfo(
providerInfoList: List<ProviderInfo>
): ProviderDisplayInfo {
val userNameToCredentialEntryMap = mutableMapOf<String, MutableList<CredentialEntryInfo>>()
val authenticationEntryList = mutableListOf<AuthenticationEntryInfo>()
val remoteEntryList = mutableListOf<RemoteEntryInfo>()
providerInfoList.forEach { providerInfo ->
authenticationEntryList.addAll(providerInfo.authenticationEntryList)
if (providerInfo.remoteEntry != null) {
remoteEntryList.add(providerInfo.remoteEntry)
}
// There can only be at most one remote entry
Preconditions.checkState(remoteEntryList.size <= 1)
providerInfo.credentialEntryList.forEach {
userNameToCredentialEntryMap.compute(
it.userName
) { _, v ->
if (v == null) {
mutableListOf(it)
} else {
v.add(it)
v
}
}
}
}
// Compose sortedUserNameToCredentialEntryList
val comparator = CredentialEntryInfoComparatorByTypeThenTimestamp()
// Sort per username
userNameToCredentialEntryMap.values.forEach {
it.sortWith(comparator)
}
// Transform to list of PerUserNameCredentialEntryLists and then sort across usernames
val sortedUserNameToCredentialEntryList = userNameToCredentialEntryMap.map {
PerUserNameCredentialEntryList(it.key, it.value)
}.sortedWith(
compareByDescending { it.sortedCredentialEntryList.first().lastUsedTimeMillis }
)
return ProviderDisplayInfo(
sortedUserNameToCredentialEntryList = sortedUserNameToCredentialEntryList,
authenticationEntryList = authenticationEntryList,
remoteEntry = remoteEntryList.getOrNull(0),
)
}
private fun toActiveEntry(
providerDisplayInfo: ProviderDisplayInfo,
): BaseEntry? {
val sortedUserNameToCredentialEntryList =
providerDisplayInfo.sortedUserNameToCredentialEntryList
val authenticationEntryList = providerDisplayInfo.authenticationEntryList
var activeEntry: BaseEntry? = null
if (sortedUserNameToCredentialEntryList
.size == 1 && authenticationEntryList.isEmpty()
) {
activeEntry = sortedUserNameToCredentialEntryList.first().sortedCredentialEntryList.first()
} else if (
sortedUserNameToCredentialEntryList
.isEmpty() && authenticationEntryList.size == 1
) {
activeEntry = authenticationEntryList.first()
}
return activeEntry
}
private fun toGetScreenState(
providerDisplayInfo: ProviderDisplayInfo
): GetScreenState {
return if (providerDisplayInfo.sortedUserNameToCredentialEntryList.isEmpty() &&
providerDisplayInfo.remoteEntry == null &&
providerDisplayInfo.authenticationEntryList.all { it.isUnlockedAndEmpty })
GetScreenState.UNLOCKED_AUTH_ENTRIES_ONLY
else if (providerDisplayInfo.sortedUserNameToCredentialEntryList.isEmpty() &&
providerDisplayInfo.authenticationEntryList.isEmpty() &&
providerDisplayInfo.remoteEntry != null)
GetScreenState.REMOTE_ONLY
else GetScreenState.PRIMARY_SELECTION
}
internal class CredentialEntryInfoComparatorByTypeThenTimestamp : Comparator<CredentialEntryInfo> {
override fun compare(p0: CredentialEntryInfo, p1: CredentialEntryInfo): Int {
// First prefer passkey type for its security benefits
if (p0.credentialType != p1.credentialType) {
if (CredentialType.PASSKEY == p0.credentialType) {
return -1
} else if (CredentialType.PASSKEY == p1.credentialType) {
return 1
}
}
// Then order by last used timestamp
if (p0.lastUsedTimeMillis != null && p1.lastUsedTimeMillis != null) {
if (p0.lastUsedTimeMillis < p1.lastUsedTimeMillis) {
return 1
} else if (p0.lastUsedTimeMillis > p1.lastUsedTimeMillis) {
return -1
}
} else if (p0.lastUsedTimeMillis != null) {
return -1
} else if (p1.lastUsedTimeMillis != null) {
return 1
}
return 0
}
}