/* * 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, 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, val authenticationEntryList: List, val remoteEntry: RemoteEntryInfo?, val actionEntryList: List, ) /** 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, val authenticationEntryList: List, 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, ) /** 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 ): ProviderDisplayInfo { val userNameToCredentialEntryMap = mutableMapOf>() val authenticationEntryList = mutableListOf() val remoteEntryList = mutableListOf() 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 { 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 } }