1 /*
<lambda>null2 * Copyright (C) 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.credentialmanager
18
19 import android.app.slice.Slice
20 import android.content.ComponentName
21 import android.content.Context
22 import android.content.pm.PackageManager
23 import android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL
24 import android.credentials.ui.AuthenticationEntry
25 import android.credentials.ui.CreateCredentialProviderData
26 import android.credentials.ui.DisabledProviderData
27 import android.credentials.ui.Entry
28 import android.credentials.ui.GetCredentialProviderData
29 import android.credentials.ui.RequestInfo
30 import android.graphics.drawable.Drawable
31 import android.text.TextUtils
32 import android.util.Log
33 import com.android.credentialmanager.common.Constants
34 import com.android.credentialmanager.common.CredentialType
35 import com.android.credentialmanager.createflow.ActiveEntry
36 import com.android.credentialmanager.createflow.CreateCredentialUiState
37 import com.android.credentialmanager.createflow.CreateOptionInfo
38 import com.android.credentialmanager.createflow.CreateScreenState
39 import com.android.credentialmanager.createflow.DisabledProviderInfo
40 import com.android.credentialmanager.createflow.EnabledProviderInfo
41 import com.android.credentialmanager.createflow.RemoteInfo
42 import com.android.credentialmanager.createflow.RequestDisplayInfo
43 import com.android.credentialmanager.getflow.ActionEntryInfo
44 import com.android.credentialmanager.getflow.AuthenticationEntryInfo
45 import com.android.credentialmanager.getflow.CredentialEntryInfo
46 import com.android.credentialmanager.getflow.ProviderInfo
47 import com.android.credentialmanager.getflow.RemoteEntryInfo
48 import com.android.credentialmanager.getflow.TopBrandingContent
49 import androidx.credentials.CreateCredentialRequest
50 import androidx.credentials.CreateCustomCredentialRequest
51 import androidx.credentials.CreatePasswordRequest
52 import androidx.credentials.CreatePublicKeyCredentialRequest
53 import androidx.credentials.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
54 import androidx.credentials.provider.Action
55 import androidx.credentials.provider.AuthenticationAction
56 import androidx.credentials.provider.CreateEntry
57 import androidx.credentials.provider.CredentialEntry
58 import androidx.credentials.provider.CustomCredentialEntry
59 import androidx.credentials.provider.PasswordCredentialEntry
60 import androidx.credentials.provider.PublicKeyCredentialEntry
61 import androidx.credentials.provider.RemoteEntry
62 import org.json.JSONObject
63 import java.time.Instant
64
65 fun getAppLabel(
66 pm: PackageManager,
67 appPackageName: String
68 ): String? {
69 return try {
70 val pkgInfo = pm.getPackageInfo(appPackageName, PackageManager.PackageInfoFlags.of(0))
71 pkgInfo.applicationInfo.loadSafeLabel(
72 pm, 0f,
73 TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM
74 ).toString()
75 } catch (e: PackageManager.NameNotFoundException) {
76 Log.e(Constants.LOG_TAG, "Caller app not found", e)
77 null
78 }
79 }
80
getServiceLabelAndIconnull81 private fun getServiceLabelAndIcon(
82 pm: PackageManager,
83 providerFlattenedComponentName: String
84 ): Pair<String, Drawable>? {
85 var providerLabel: String? = null
86 var providerIcon: Drawable? = null
87 val component = ComponentName.unflattenFromString(providerFlattenedComponentName)
88 if (component == null) {
89 // Test data has only package name not component name.
90 // For test data usage only.
91 try {
92 val pkgInfo = pm.getPackageInfo(
93 providerFlattenedComponentName,
94 PackageManager.PackageInfoFlags.of(0)
95 )
96 providerLabel =
97 pkgInfo.applicationInfo.loadSafeLabel(
98 pm, 0f,
99 TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM
100 ).toString()
101 providerIcon = pkgInfo.applicationInfo.loadIcon(pm)
102 } catch (e: PackageManager.NameNotFoundException) {
103 Log.e(Constants.LOG_TAG, "Provider package info not found", e)
104 }
105 } else {
106 try {
107 val si = pm.getServiceInfo(component, PackageManager.ComponentInfoFlags.of(0))
108 providerLabel = si.loadSafeLabel(
109 pm, 0f,
110 TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM
111 ).toString()
112 providerIcon = si.loadIcon(pm)
113 } catch (e: PackageManager.NameNotFoundException) {
114 Log.e(Constants.LOG_TAG, "Provider service info not found", e)
115 // Added for mdoc use case where the provider may not need to register a service and
116 // instead only relies on the registration api.
117 try {
118 val pkgInfo = pm.getPackageInfo(
119 component.packageName,
120 PackageManager.PackageInfoFlags.of(0)
121 )
122 providerLabel =
123 pkgInfo.applicationInfo.loadSafeLabel(
124 pm, 0f,
125 TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM
126 ).toString()
127 providerIcon = pkgInfo.applicationInfo.loadIcon(pm)
128 } catch (e: PackageManager.NameNotFoundException) {
129 Log.e(Constants.LOG_TAG, "Provider package info not found", e)
130 }
131 }
132 }
133 return if (providerLabel == null || providerIcon == null) {
134 Log.d(
135 Constants.LOG_TAG,
136 "Failed to load provider label/icon for provider $providerFlattenedComponentName"
137 )
138 null
139 } else {
140 Pair(providerLabel, providerIcon)
141 }
142 }
143
144 /** Utility functions for converting CredentialManager data structures to or from UI formats. */
145 class GetFlowUtils {
146 companion object {
147 // Returns the list (potentially empty) of enabled provider.
toProviderListnull148 fun toProviderList(
149 providerDataList: List<GetCredentialProviderData>,
150 context: Context,
151 ): List<ProviderInfo> {
152 val providerList: MutableList<ProviderInfo> = mutableListOf()
153 providerDataList.forEach {
154 val providerLabelAndIcon = getServiceLabelAndIcon(
155 context.packageManager,
156 it.providerFlattenedComponentName
157 ) ?: return@forEach
158 val (providerLabel, providerIcon) = providerLabelAndIcon
159 providerList.add(
160 ProviderInfo(
161 id = it.providerFlattenedComponentName,
162 icon = providerIcon,
163 displayName = providerLabel,
164 credentialEntryList = getCredentialOptionInfoList(
165 providerId = it.providerFlattenedComponentName,
166 providerLabel = providerLabel,
167 credentialEntries = it.credentialEntries,
168 context = context
169 ),
170 authenticationEntryList = getAuthenticationEntryList(
171 it.providerFlattenedComponentName,
172 providerLabel,
173 providerIcon,
174 it.authenticationEntries),
175 remoteEntry = getRemoteEntry(
176 it.providerFlattenedComponentName,
177 it.remoteEntry
178 ),
179 actionEntryList = getActionEntryList(
180 it.providerFlattenedComponentName, it.actionChips, providerIcon
181 ),
182 )
183 )
184 }
185 return providerList
186 }
187
toRequestDisplayInfonull188 fun toRequestDisplayInfo(
189 requestInfo: RequestInfo?,
190 context: Context,
191 originName: String?,
192 ): com.android.credentialmanager.getflow.RequestDisplayInfo? {
193 val getCredentialRequest = requestInfo?.getCredentialRequest ?: return null
194 val preferImmediatelyAvailableCredentials = getCredentialRequest.data.getBoolean(
195 "androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS")
196 val preferUiBrandingComponentName =
197 getCredentialRequest.data.getParcelable(
198 "androidx.credentials.BUNDLE_KEY_PREFER_UI_BRANDING_COMPONENT_NAME",
199 ComponentName::class.java
200 )
201 val preferTopBrandingContent: TopBrandingContent? =
202 if (!requestInfo.hasPermissionToOverrideDefault() ||
203 preferUiBrandingComponentName == null) null
204 else {
205 val (displayName, icon) = getServiceLabelAndIcon(
206 context.packageManager, preferUiBrandingComponentName.flattenToString())
207 ?: Pair(null, null)
208 if (displayName != null && icon != null) {
209 TopBrandingContent(icon, displayName)
210 } else {
211 null
212 }
213 }
214 return com.android.credentialmanager.getflow.RequestDisplayInfo(
215 appName = originName?.ifEmpty { null }
216 ?: getAppLabel(context.packageManager, requestInfo.appPackageName)
217 ?: return null,
218 preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials,
219 preferIdentityDocUi = getCredentialRequest.data.getBoolean(
220 // TODO(b/276777444): replace with direct library constant reference once
221 // exposed.
222 "androidx.credentials.BUNDLE_KEY_PREFER_IDENTITY_DOC_UI"),
223 preferTopBrandingContent = preferTopBrandingContent,
224 )
225 }
226
227
228 /**
229 * Note: caller required handle empty list due to parsing error.
230 */
getCredentialOptionInfoListnull231 private fun getCredentialOptionInfoList(
232 providerId: String,
233 providerLabel: String,
234 credentialEntries: List<Entry>,
235 context: Context,
236 ): List<CredentialEntryInfo> {
237 val result: MutableList<CredentialEntryInfo> = mutableListOf()
238 credentialEntries.forEach {
239 val credentialEntry = parseCredentialEntryFromSlice(it.slice)
240 when (credentialEntry) {
241 is PasswordCredentialEntry -> {
242 result.add(CredentialEntryInfo(
243 providerId = providerId,
244 providerDisplayName = providerLabel,
245 entryKey = it.key,
246 entrySubkey = it.subkey,
247 pendingIntent = credentialEntry.pendingIntent,
248 fillInIntent = it.frameworkExtrasIntent,
249 credentialType = CredentialType.PASSWORD,
250 credentialTypeDisplayName = credentialEntry.typeDisplayName.toString(),
251 userName = credentialEntry.username.toString(),
252 displayName = credentialEntry.displayName?.toString(),
253 icon = credentialEntry.icon.loadDrawable(context),
254 shouldTintIcon = credentialEntry.isDefaultIcon,
255 lastUsedTimeMillis = credentialEntry.lastUsedTime,
256 isAutoSelectable = credentialEntry.isAutoSelectAllowed &&
257 credentialEntry.autoSelectAllowedFromOption,
258 ))
259 }
260 is PublicKeyCredentialEntry -> {
261 result.add(CredentialEntryInfo(
262 providerId = providerId,
263 providerDisplayName = providerLabel,
264 entryKey = it.key,
265 entrySubkey = it.subkey,
266 pendingIntent = credentialEntry.pendingIntent,
267 fillInIntent = it.frameworkExtrasIntent,
268 credentialType = CredentialType.PASSKEY,
269 credentialTypeDisplayName = credentialEntry.typeDisplayName.toString(),
270 userName = credentialEntry.username.toString(),
271 displayName = credentialEntry.displayName?.toString(),
272 icon = credentialEntry.icon.loadDrawable(context),
273 shouldTintIcon = credentialEntry.isDefaultIcon,
274 lastUsedTimeMillis = credentialEntry.lastUsedTime,
275 isAutoSelectable = credentialEntry.isAutoSelectAllowed &&
276 credentialEntry.autoSelectAllowedFromOption,
277 ))
278 }
279 is CustomCredentialEntry -> {
280 result.add(CredentialEntryInfo(
281 providerId = providerId,
282 providerDisplayName = providerLabel,
283 entryKey = it.key,
284 entrySubkey = it.subkey,
285 pendingIntent = credentialEntry.pendingIntent,
286 fillInIntent = it.frameworkExtrasIntent,
287 credentialType = CredentialType.UNKNOWN,
288 credentialTypeDisplayName =
289 credentialEntry.typeDisplayName?.toString().orEmpty(),
290 userName = credentialEntry.title.toString(),
291 displayName = credentialEntry.subtitle?.toString(),
292 icon = credentialEntry.icon.loadDrawable(context),
293 shouldTintIcon = credentialEntry.isDefaultIcon,
294 lastUsedTimeMillis = credentialEntry.lastUsedTime,
295 isAutoSelectable = credentialEntry.isAutoSelectAllowed &&
296 credentialEntry.autoSelectAllowedFromOption,
297 ))
298 }
299 else -> Log.d(
300 Constants.LOG_TAG,
301 "Encountered unrecognized credential entry ${it.slice.spec?.type}"
302 )
303 }
304 }
305 return result
306 }
307
parseCredentialEntryFromSlicenull308 private fun parseCredentialEntryFromSlice(slice: Slice): CredentialEntry? {
309 try {
310 when (slice.spec?.type) {
311 TYPE_PASSWORD_CREDENTIAL -> return PasswordCredentialEntry.fromSlice(slice)!!
312 TYPE_PUBLIC_KEY_CREDENTIAL -> return PublicKeyCredentialEntry.fromSlice(slice)!!
313 else -> return CustomCredentialEntry.fromSlice(slice)!!
314 }
315 } catch (e: Exception) {
316 // Try CustomCredentialEntry.fromSlice one last time in case the cause was a failed
317 // password / passkey parsing attempt.
318 return CustomCredentialEntry.fromSlice(slice)
319 }
320 }
321
322 /**
323 * Note: caller required handle empty list due to parsing error.
324 */
getAuthenticationEntryListnull325 private fun getAuthenticationEntryList(
326 providerId: String,
327 providerDisplayName: String,
328 providerIcon: Drawable,
329 authEntryList: List<AuthenticationEntry>,
330 ): List<AuthenticationEntryInfo> {
331 val result: MutableList<AuthenticationEntryInfo> = mutableListOf()
332 authEntryList.forEach { entry ->
333 val structuredAuthEntry =
334 AuthenticationAction.fromSlice(entry.slice) ?: return@forEach
335
336 val title: String =
337 structuredAuthEntry.title.toString().ifEmpty { providerDisplayName }
338
339 result.add(AuthenticationEntryInfo(
340 providerId = providerId,
341 entryKey = entry.key,
342 entrySubkey = entry.subkey,
343 pendingIntent = structuredAuthEntry.pendingIntent,
344 fillInIntent = entry.frameworkExtrasIntent,
345 title = title,
346 providerDisplayName = providerDisplayName,
347 icon = providerIcon,
348 isUnlockedAndEmpty = entry.status != AuthenticationEntry.STATUS_LOCKED,
349 isLastUnlocked =
350 entry.status == AuthenticationEntry.STATUS_UNLOCKED_BUT_EMPTY_MOST_RECENT
351 ))
352 }
353 return result
354 }
355
getRemoteEntrynull356 private fun getRemoteEntry(providerId: String, remoteEntry: Entry?): RemoteEntryInfo? {
357 if (remoteEntry == null) {
358 return null
359 }
360 val structuredRemoteEntry = RemoteEntry.fromSlice(remoteEntry.slice)
361 ?: return null
362 return RemoteEntryInfo(
363 providerId = providerId,
364 entryKey = remoteEntry.key,
365 entrySubkey = remoteEntry.subkey,
366 pendingIntent = structuredRemoteEntry.pendingIntent,
367 fillInIntent = remoteEntry.frameworkExtrasIntent,
368 )
369 }
370
371 /**
372 * Note: caller required handle empty list due to parsing error.
373 */
getActionEntryListnull374 private fun getActionEntryList(
375 providerId: String,
376 actionEntries: List<Entry>,
377 providerIcon: Drawable,
378 ): List<ActionEntryInfo> {
379 val result: MutableList<ActionEntryInfo> = mutableListOf()
380 actionEntries.forEach {
381 val actionEntryUi = Action.fromSlice(it.slice) ?: return@forEach
382 result.add(ActionEntryInfo(
383 providerId = providerId,
384 entryKey = it.key,
385 entrySubkey = it.subkey,
386 pendingIntent = actionEntryUi.pendingIntent,
387 fillInIntent = it.frameworkExtrasIntent,
388 title = actionEntryUi.title.toString(),
389 icon = providerIcon,
390 subTitle = actionEntryUi.subtitle?.toString(),
391 ))
392 }
393 return result
394 }
395 }
396 }
397
398 class CreateFlowUtils {
399 companion object {
400 /**
401 * Note: caller required handle empty list due to parsing error.
402 */
toEnabledProviderListnull403 fun toEnabledProviderList(
404 providerDataList: List<CreateCredentialProviderData>,
405 context: Context,
406 ): List<EnabledProviderInfo> {
407 val providerList: MutableList<EnabledProviderInfo> = mutableListOf()
408 providerDataList.forEach {
409 val providerLabelAndIcon = getServiceLabelAndIcon(
410 context.packageManager,
411 it.providerFlattenedComponentName
412 ) ?: return@forEach
413 val (providerLabel, providerIcon) = providerLabelAndIcon
414 providerList.add(EnabledProviderInfo(
415 id = it.providerFlattenedComponentName,
416 displayName = providerLabel,
417 icon = providerIcon,
418 sortedCreateOptions = toSortedCreationOptionInfoList(
419 it.providerFlattenedComponentName, it.saveEntries, context
420 ),
421 remoteEntry = toRemoteInfo(it.providerFlattenedComponentName, it.remoteEntry),
422 ))
423 }
424 return providerList
425 }
426
427 /**
428 * Note: caller required handle empty list due to parsing error.
429 */
toDisabledProviderListnull430 fun toDisabledProviderList(
431 providerDataList: List<DisabledProviderData>?,
432 context: Context,
433 ): List<DisabledProviderInfo> {
434 val providerList: MutableList<DisabledProviderInfo> = mutableListOf()
435 providerDataList?.forEach {
436 val providerLabelAndIcon = getServiceLabelAndIcon(
437 context.packageManager,
438 it.providerFlattenedComponentName
439 ) ?: return@forEach
440 val (providerLabel, providerIcon) = providerLabelAndIcon
441 providerList.add(DisabledProviderInfo(
442 icon = providerIcon,
443 id = it.providerFlattenedComponentName,
444 displayName = providerLabel,
445 ))
446 }
447 return providerList
448 }
449
toRequestDisplayInfonull450 fun toRequestDisplayInfo(
451 requestInfo: RequestInfo?,
452 context: Context,
453 originName: String?,
454 ): RequestDisplayInfo? {
455 if (requestInfo == null) {
456 return null
457 }
458 val appLabel = originName?.ifEmpty { null }
459 ?: getAppLabel(context.packageManager, requestInfo.appPackageName)
460 ?: return null
461 val createCredentialRequest = requestInfo.createCredentialRequest ?: return null
462 val createCredentialRequestJetpack = CreateCredentialRequest.createFrom(
463 createCredentialRequest.type,
464 createCredentialRequest.credentialData,
465 createCredentialRequest.candidateQueryData,
466 createCredentialRequest.isSystemProviderRequired,
467 createCredentialRequest.origin,
468 )
469 val appPreferredDefaultProviderId: String? =
470 if (!requestInfo.hasPermissionToOverrideDefault()) null
471 else createCredentialRequestJetpack?.displayInfo?.preferDefaultProvider
472 return when (createCredentialRequestJetpack) {
473 is CreatePasswordRequest -> RequestDisplayInfo(
474 createCredentialRequestJetpack.id,
475 createCredentialRequestJetpack.password,
476 CredentialType.PASSWORD,
477 appLabel,
478 context.getDrawable(R.drawable.ic_password_24) ?: return null,
479 preferImmediatelyAvailableCredentials =
480 createCredentialRequestJetpack.preferImmediatelyAvailableCredentials,
481 appPreferredDefaultProviderId = appPreferredDefaultProviderId,
482 userSetDefaultProviderIds = requestInfo.defaultProviderIds.toSet(),
483 // The jetpack library requires a fix to parse this value correctly for
484 // the password type. For now, directly parse it ourselves.
485 isAutoSelectRequest = createCredentialRequest.credentialData.getBoolean(
486 Constants.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS, false),
487 )
488 is CreatePublicKeyCredentialRequest -> {
489 newRequestDisplayInfoFromPasskeyJson(
490 requestJson = createCredentialRequestJetpack.requestJson,
491 appLabel = appLabel,
492 context = context,
493 preferImmediatelyAvailableCredentials =
494 createCredentialRequestJetpack.preferImmediatelyAvailableCredentials,
495 appPreferredDefaultProviderId = appPreferredDefaultProviderId,
496 userSetDefaultProviderIds = requestInfo.defaultProviderIds.toSet(),
497 // The jetpack library requires a fix to parse this value correctly for
498 // the passkey type. For now, directly parse it ourselves.
499 isAutoSelectRequest = createCredentialRequest.credentialData.getBoolean(
500 Constants.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS, false),
501 )
502 }
503 is CreateCustomCredentialRequest -> {
504 // TODO: directly use the display info once made public
505 val displayInfo = CreateCredentialRequest.DisplayInfo
506 .parseFromCredentialDataBundle(createCredentialRequest.credentialData)
507 ?: return null
508 RequestDisplayInfo(
509 title = displayInfo.userId.toString(),
510 subtitle = displayInfo.userDisplayName?.toString(),
511 type = CredentialType.UNKNOWN,
512 appName = appLabel,
513 typeIcon = displayInfo.credentialTypeIcon?.loadDrawable(context)
514 ?: context.getDrawable(R.drawable.ic_other_sign_in_24) ?: return null,
515 preferImmediatelyAvailableCredentials =
516 createCredentialRequestJetpack.preferImmediatelyAvailableCredentials,
517 appPreferredDefaultProviderId = appPreferredDefaultProviderId,
518 userSetDefaultProviderIds = requestInfo.defaultProviderIds.toSet(),
519 isAutoSelectRequest = createCredentialRequestJetpack.isAutoSelectAllowed,
520 )
521 }
522 else -> null
523 }
524 }
525
toCreateCredentialUiStatenull526 fun toCreateCredentialUiState(
527 enabledProviders: List<EnabledProviderInfo>,
528 disabledProviders: List<DisabledProviderInfo>?,
529 defaultProviderIdPreferredByApp: String?,
530 defaultProviderIdsSetByUser: Set<String>,
531 requestDisplayInfo: RequestDisplayInfo,
532 isOnPasskeyIntroStateAlready: Boolean,
533 isPasskeyFirstUse: Boolean,
534 ): CreateCredentialUiState? {
535 var remoteEntry: RemoteInfo? = null
536 var remoteEntryProvider: EnabledProviderInfo? = null
537 var defaultProviderPreferredByApp: EnabledProviderInfo? = null
538 var defaultProviderSetByUser: EnabledProviderInfo? = null
539 var createOptionsPairs:
540 MutableList<Pair<CreateOptionInfo, EnabledProviderInfo>> = mutableListOf()
541 enabledProviders.forEach { enabledProvider ->
542 if (defaultProviderIdPreferredByApp != null) {
543 if (enabledProvider.id == defaultProviderIdPreferredByApp) {
544 defaultProviderPreferredByApp = enabledProvider
545 }
546 }
547 if (enabledProvider.sortedCreateOptions.isNotEmpty() &&
548 defaultProviderIdsSetByUser.contains(enabledProvider.id)) {
549 if (defaultProviderSetByUser == null) {
550 defaultProviderSetByUser = enabledProvider
551 } else {
552 val newLastUsedTime = enabledProvider.sortedCreateOptions.firstOrNull()
553 ?.lastUsedTime
554 val curLastUsedTime = defaultProviderSetByUser?.sortedCreateOptions
555 ?.firstOrNull()?.lastUsedTime ?: Instant.MIN
556 if (newLastUsedTime != null) {
557 if (curLastUsedTime == null || newLastUsedTime > curLastUsedTime) {
558 defaultProviderSetByUser = enabledProvider
559 }
560 }
561 }
562 }
563 if (enabledProvider.sortedCreateOptions.isNotEmpty()) {
564 enabledProvider.sortedCreateOptions.forEach {
565 createOptionsPairs.add(Pair(it, enabledProvider))
566 }
567 }
568 val currRemoteEntry = enabledProvider.remoteEntry
569 if (currRemoteEntry != null) {
570 if (remoteEntry != null) {
571 // There can only be at most one remote entry
572 Log.d(Constants.LOG_TAG, "Found more than one remote entry.")
573 return null
574 }
575 remoteEntry = currRemoteEntry
576 remoteEntryProvider = enabledProvider
577 }
578 }
579 val defaultProvider = defaultProviderPreferredByApp ?: defaultProviderSetByUser
580 val initialScreenState = toCreateScreenState(
581 createOptionSize = createOptionsPairs.size,
582 isOnPasskeyIntroStateAlready = isOnPasskeyIntroStateAlready,
583 requestDisplayInfo = requestDisplayInfo,
584 remoteEntry = remoteEntry,
585 isPasskeyFirstUse = isPasskeyFirstUse
586 ) ?: return null
587 val sortedCreateOptionsPairs = createOptionsPairs.sortedWith(
588 compareByDescending { it.first.lastUsedTime }
589 )
590 return CreateCredentialUiState(
591 enabledProviders = enabledProviders,
592 disabledProviders = disabledProviders,
593 currentScreenState = initialScreenState,
594 requestDisplayInfo = requestDisplayInfo,
595 sortedCreateOptionsPairs = sortedCreateOptionsPairs,
596 activeEntry = toActiveEntry(
597 defaultProvider = defaultProvider,
598 sortedCreateOptionsPairs = sortedCreateOptionsPairs,
599 remoteEntry = remoteEntry,
600 remoteEntryProvider = remoteEntryProvider,
601 ),
602 remoteEntry = remoteEntry,
603 foundCandidateFromUserDefaultProvider = defaultProviderSetByUser != null,
604 )
605 }
606
toCreateScreenStatenull607 fun toCreateScreenState(
608 createOptionSize: Int,
609 isOnPasskeyIntroStateAlready: Boolean,
610 requestDisplayInfo: RequestDisplayInfo,
611 remoteEntry: RemoteInfo?,
612 isPasskeyFirstUse: Boolean,
613 ): CreateScreenState? {
614 return if (isPasskeyFirstUse && requestDisplayInfo.type == CredentialType.PASSKEY &&
615 !isOnPasskeyIntroStateAlready) {
616 CreateScreenState.PASSKEY_INTRO
617 } else if (createOptionSize == 0 && remoteEntry != null) {
618 CreateScreenState.EXTERNAL_ONLY_SELECTION
619 } else {
620 CreateScreenState.CREATION_OPTION_SELECTION
621 }
622 }
623
toActiveEntrynull624 private fun toActiveEntry(
625 defaultProvider: EnabledProviderInfo?,
626 sortedCreateOptionsPairs: List<Pair<CreateOptionInfo, EnabledProviderInfo>>,
627 remoteEntry: RemoteInfo?,
628 remoteEntryProvider: EnabledProviderInfo?,
629 ): ActiveEntry? {
630 return if (
631 sortedCreateOptionsPairs.isEmpty() && remoteEntry != null &&
632 remoteEntryProvider != null
633 ) {
634 ActiveEntry(remoteEntryProvider, remoteEntry)
635 } else if (defaultProvider != null &&
636 defaultProvider.sortedCreateOptions.isNotEmpty()) {
637 ActiveEntry(defaultProvider, defaultProvider.sortedCreateOptions.first())
638 } else if (sortedCreateOptionsPairs.isNotEmpty()) {
639 val (topEntry, topEntryProvider) = sortedCreateOptionsPairs.first()
640 ActiveEntry(topEntryProvider, topEntry)
641 } else null
642 }
643
644 /**
645 * Note: caller required handle empty list due to parsing error.
646 */
toSortedCreationOptionInfoListnull647 private fun toSortedCreationOptionInfoList(
648 providerId: String,
649 creationEntries: List<Entry>,
650 context: Context,
651 ): List<CreateOptionInfo> {
652 val result: MutableList<CreateOptionInfo> = mutableListOf()
653 creationEntries.forEach {
654 val createEntry = CreateEntry.fromSlice(it.slice) ?: return@forEach
655 result.add(CreateOptionInfo(
656 providerId = providerId,
657 entryKey = it.key,
658 entrySubkey = it.subkey,
659 pendingIntent = createEntry.pendingIntent,
660 fillInIntent = it.frameworkExtrasIntent,
661 userProviderDisplayName = createEntry.accountName.toString(),
662 profileIcon = createEntry.icon?.loadDrawable(context),
663 passwordCount = createEntry.getPasswordCredentialCount(),
664 passkeyCount = createEntry.getPublicKeyCredentialCount(),
665 totalCredentialCount = createEntry.getTotalCredentialCount(),
666 lastUsedTime = createEntry.lastUsedTime ?: Instant.MIN,
667 footerDescription = createEntry.description?.toString(),
668 // TODO(b/281065680): replace with official library constant once available
669 allowAutoSelect =
670 it.slice.items.firstOrNull {
671 it.hasHint("androidx.credentials.provider.createEntry.SLICE_HINT_AUTO_" +
672 "SELECT_ALLOWED")
673 }?.text == "true",
674 ))
675 }
676 return result.sortedWith(
677 compareByDescending { it.lastUsedTime }
678 )
679 }
680
toRemoteInfonull681 private fun toRemoteInfo(
682 providerId: String,
683 remoteEntry: Entry?,
684 ): RemoteInfo? {
685 return if (remoteEntry != null) {
686 val structuredRemoteEntry = RemoteEntry.fromSlice(remoteEntry.slice)
687 ?: return null
688 RemoteInfo(
689 providerId = providerId,
690 entryKey = remoteEntry.key,
691 entrySubkey = remoteEntry.subkey,
692 pendingIntent = structuredRemoteEntry.pendingIntent,
693 fillInIntent = remoteEntry.frameworkExtrasIntent,
694 )
695 } else null
696 }
697
newRequestDisplayInfoFromPasskeyJsonnull698 private fun newRequestDisplayInfoFromPasskeyJson(
699 requestJson: String,
700 appLabel: String,
701 context: Context,
702 preferImmediatelyAvailableCredentials: Boolean,
703 appPreferredDefaultProviderId: String?,
704 userSetDefaultProviderIds: Set<String>,
705 isAutoSelectRequest: Boolean
706 ): RequestDisplayInfo? {
707 val json = JSONObject(requestJson)
708 var passkeyUsername = ""
709 var passkeyDisplayName = ""
710 if (json.has("user")) {
711 val user: JSONObject = json.getJSONObject("user")
712 passkeyUsername = user.getString("name")
713 passkeyDisplayName = user.getString("displayName")
714 }
715 val (username, displayname) = userAndDisplayNameForPasskey(
716 passkeyUsername = passkeyUsername,
717 passkeyDisplayName = passkeyDisplayName,
718 )
719 return RequestDisplayInfo(
720 username,
721 displayname,
722 CredentialType.PASSKEY,
723 appLabel,
724 context.getDrawable(R.drawable.ic_passkey_24) ?: return null,
725 preferImmediatelyAvailableCredentials,
726 appPreferredDefaultProviderId,
727 userSetDefaultProviderIds,
728 isAutoSelectRequest,
729 )
730 }
731 }
732 }
733
734 /**
735 * Returns the actual username and display name for the UI display purpose for the passkey use case.
736 *
737 * Passkey has some special requirements:
738 * 1) display-name on top (turned into UI username) if one is available, username on second line.
739 * 2) username on top if display-name is not available.
740 * 3) don't show username on second line if username == display-name
741 */
userAndDisplayNameForPasskeynull742 fun userAndDisplayNameForPasskey(
743 passkeyUsername: String,
744 passkeyDisplayName: String,
745 ): Pair<String, String> {
746 if (!TextUtils.isEmpty(passkeyUsername) && !TextUtils.isEmpty(passkeyDisplayName)) {
747 if (passkeyUsername == passkeyDisplayName) {
748 return Pair(passkeyUsername, "")
749 } else {
750 return Pair(passkeyDisplayName, passkeyUsername)
751 }
752 } else if (!TextUtils.isEmpty(passkeyUsername)) {
753 return Pair(passkeyUsername, passkeyDisplayName)
754 } else if (!TextUtils.isEmpty(passkeyDisplayName)) {
755 return Pair(passkeyDisplayName, passkeyUsername)
756 } else {
757 return Pair(passkeyDisplayName, passkeyUsername)
758 }
759 }
760