1 /*
<lambda>null2  * Copyright (C) 2023 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.autofill
18 
19 import android.app.PendingIntent
20 import android.app.assist.AssistStructure
21 import android.content.ComponentName
22 import android.content.Context
23 import android.credentials.CredentialManager
24 import android.credentials.GetCredentialRequest
25 import android.credentials.GetCandidateCredentialsResponse
26 import android.credentials.GetCandidateCredentialsException
27 import android.credentials.CredentialOption
28 import android.credentials.selection.Entry
29 import android.credentials.selection.GetCredentialProviderData
30 import android.credentials.selection.ProviderData
31 import android.graphics.BlendMode
32 import android.graphics.drawable.Icon
33 import android.os.Bundle
34 import android.os.CancellationSignal
35 import android.os.OutcomeReceiver
36 import android.os.ResultReceiver
37 import android.service.autofill.AutofillService
38 import com.android.credentialmanager.model.get.ProviderInfo
39 import androidx.core.graphics.drawable.toBitmap
40 import com.android.credentialmanager.model.get.ActionEntryInfo
41 import com.android.credentialmanager.model.EntryInfo
42 import android.service.autofill.Dataset
43 import android.service.autofill.Field
44 import android.service.autofill.FillCallback
45 import android.service.autofill.FillRequest
46 import android.service.autofill.FillResponse
47 import android.service.autofill.Flags
48 import android.service.autofill.InlinePresentation
49 import android.service.autofill.Presentations
50 import android.service.autofill.SaveCallback
51 import android.service.autofill.SaveRequest
52 import android.service.credentials.CredentialProviderService
53 import android.util.Log
54 import android.content.Intent
55 import android.os.IBinder
56 import android.view.autofill.AutofillId
57 import android.view.autofill.AutofillManager
58 import android.widget.RemoteViews
59 import android.widget.inline.InlinePresentationSpec
60 import androidx.autofill.inline.v1.InlineSuggestionUi
61 import androidx.credentials.provider.CustomCredentialEntry
62 import androidx.credentials.provider.PasswordCredentialEntry
63 import androidx.credentials.provider.PublicKeyCredentialEntry
64 import com.android.credentialmanager.GetFlowUtils
65 import com.android.credentialmanager.common.ui.InlinePresentationsFactory
66 import com.android.credentialmanager.common.ui.RemoteViewsFactory
67 import com.android.credentialmanager.getflow.ProviderDisplayInfo
68 import com.android.credentialmanager.getflow.toProviderDisplayInfo
69 import com.android.credentialmanager.ktx.credentialEntry
70 import com.android.credentialmanager.model.CredentialType
71 import java.util.ArrayList
72 import java.util.Objects
73 import java.util.concurrent.Executors
74 
75 class CredentialAutofillService : AutofillService() {
76 
77     companion object {
78         private const val TAG = "CredAutofill"
79 
80         private const val SESSION_ID_KEY = "autofill_session_id"
81         private const val REQUEST_ID_KEY = "autofill_request_id"
82     }
83 
84     override fun onFillRequest(
85             request: FillRequest,
86             cancellationSignal: CancellationSignal,
87             callback: FillCallback
88     ) {
89     }
90 
91     override fun onFillCredentialRequest(
92             request: FillRequest,
93             cancellationSignal: CancellationSignal,
94             callback: FillCallback,
95             autofillCallback: IBinder
96     ) {
97         val context = request.fillContexts
98         val structure = context[context.size - 1].structure
99         val callingPackage = structure.activityComponent.packageName
100         Log.i(TAG, "onFillCredentialRequest called for $callingPackage")
101 
102         val clientState = request.clientState
103         if (clientState == null) {
104             Log.i(TAG, "Client state not found")
105             callback.onFailure("Client state not found")
106             return
107         }
108         val sessionId = clientState.getInt(SESSION_ID_KEY)
109         val requestId = clientState.getInt(REQUEST_ID_KEY)
110         val resultReceiver = clientState.getParcelable(
111                 CredentialManager.EXTRA_AUTOFILL_RESULT_RECEIVER, ResultReceiver::class.java)
112         Log.i(TAG, "Autofill sessionId: $sessionId, autofill requestId: $requestId")
113         if (sessionId == 0 || requestId == 0 || resultReceiver == null) {
114             Log.i(TAG, "Session Id or request Id or resultReceiver not found")
115             callback.onFailure("Session Id or request Id or resultReceiver not found")
116             return
117         }
118 
119         val responseClientState = Bundle()
120         responseClientState.putBoolean(WEBVIEW_REQUESTED_CREDENTIAL_KEY, false)
121         val uniqueAutofillIdsForRequest: MutableSet<AutofillId> = mutableSetOf()
122         val getCredRequest: GetCredentialRequest? = getCredManRequest(
123             structure, sessionId,
124             requestId, resultReceiver, responseClientState, uniqueAutofillIdsForRequest
125         )
126         // TODO(b/324635774): Use callback for validating. If the request is coming
127         // directly from the view, there should be a corresponding callback, otherwise
128         // we should fail fast,
129         if (getCredRequest == null) {
130             Log.i(TAG, "No credential manager request found")
131             callback.onFailure("No credential manager request found")
132             return
133         }
134         val credentialManager: CredentialManager =
135             getSystemService(Context.CREDENTIAL_SERVICE) as CredentialManager
136 
137         val outcome = object : OutcomeReceiver<GetCandidateCredentialsResponse,
138                 GetCandidateCredentialsException> {
139             override fun onResult(result: GetCandidateCredentialsResponse) {
140                 Log.i(TAG, "getCandidateCredentials onResult")
141                 val fillResponse = convertToFillResponse(
142                     result, request,
143                     responseClientState, GetFlowUtils.extractTypePriorityMap(getCredRequest),
144                     uniqueAutofillIdsForRequest
145                 )
146                 if (fillResponse != null) {
147                     callback.onSuccess(fillResponse)
148                 } else {
149                     Log.e(TAG, "Failed to create a FillResponse from the CredentialResponse.")
150                     callback.onFailure("No dataset was created from the CredentialResponse")
151                 }
152             }
153 
154             override fun onError(error: GetCandidateCredentialsException) {
155                 Log.i(TAG, "getCandidateCredentials onError")
156                 callback.onFailure("error received from credential manager ${error.message}")
157             }
158         }
159 
160         credentialManager.getCandidateCredentials(
161                 getCredRequest,
162                 callingPackage,
163                 CancellationSignal(),
164                 Executors.newSingleThreadExecutor(),
165                 outcome,
166                 autofillCallback
167         )
168     }
169 
170     private fun getEntryToIconMap(
171             candidateProviderDataList: List<GetCredentialProviderData>
172     ): Map<String, Icon> {
173         val entryIconMap: MutableMap<String, Icon> = mutableMapOf()
174         candidateProviderDataList.forEach { provider ->
175             provider.credentialEntries.forEach { entry ->
176                 when (val credentialEntry = entry.slice.credentialEntry) {
177                     is PasswordCredentialEntry -> {
178                         entryIconMap[entry.key + entry.subkey] = credentialEntry.icon
179                     }
180 
181                     is PublicKeyCredentialEntry -> {
182                         entryIconMap[entry.key + entry.subkey] = credentialEntry.icon
183                     }
184 
185                     is CustomCredentialEntry -> {
186                         entryIconMap[entry.key + entry.subkey] = credentialEntry.icon
187                     }
188                 }
189             }
190         }
191         return entryIconMap
192     }
193 
194     private fun getDefaultIcon(): Icon {
195         return Icon.createWithResource(
196                 this, com.android.credentialmanager.R.drawable.ic_other_sign_in_24)
197     }
198 
199     private fun convertToFillResponse(
200             getCredResponse: GetCandidateCredentialsResponse,
201             fillRequest: FillRequest,
202             responseClientState: Bundle,
203             typePriorityMap: Map<String, Int>,
204             uniqueAutofillIdsForRequest: MutableSet<AutofillId>
205     ): FillResponse? {
206         val candidateProviders = getCredResponse.candidateProviderDataList
207         if (candidateProviders.isEmpty()) {
208             return null
209         }
210         val primaryProviderComponentName = getCredResponse.primaryProviderComponentName
211         val entryIconMap: Map<String, Icon> = getEntryToIconMap(candidateProviders)
212         val autofillIdToProvidersMap: Map<AutofillId, ArrayList<GetCredentialProviderData>> =
213             mapAutofillIdToProviders(
214                 uniqueAutofillIdsForRequest,
215                 candidateProviders,
216                 primaryProviderComponentName
217             )
218         val fillResponseBuilder = FillResponse.Builder()
219         fillResponseBuilder.setFlags(FillResponse.FLAG_CREDENTIAL_MANAGER_RESPONSE)
220         autofillIdToProvidersMap.forEach { (autofillId, providers) ->
221             var credentialDatasetAdded = addCredentialDatasetsForAutofillId(fillRequest,
222                 autofillId, providers, entryIconMap, fillResponseBuilder, typePriorityMap)
223             if (!credentialDatasetAdded && primaryProviderComponentName != null) {
224                 val providerList = GetFlowUtils.toProviderList(
225                     providers,
226                     this@CredentialAutofillService
227                 )
228                 val primaryProviderInfo =
229                     providerList.find { provider -> primaryProviderComponentName
230                         .flattenToString().equals(provider.id) }
231                 if (primaryProviderInfo != null) {
232                     addActionDatasetsForAutofillId(
233                         fillRequest,
234                         autofillId,
235                         primaryProviderInfo,
236                         fillResponseBuilder
237                     )
238                 }
239             }
240         }
241         for (autofillId in uniqueAutofillIdsForRequest) {
242             addMoreOptionsDataset(
243                 fillRequest,
244                 autofillId,
245                 fillResponseBuilder,
246                 getCredResponse.intent.putExtra(
247                     ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, ArrayList(candidateProviders)
248                 )
249             )
250         }
251         fillResponseBuilder.setClientState(responseClientState)
252         return fillResponseBuilder.build()
253     }
254 
255     private fun addActionDatasetsForAutofillId(
256         fillRequest: FillRequest,
257         autofillId: AutofillId,
258         primaryProvider: ProviderInfo,
259         fillResponseBuilder: FillResponse.Builder,
260     ): Boolean {
261         var index = 0
262         var datasetAdded = false
263         primaryProvider.actionEntryList.forEach { actionEntry ->
264             if (index >= maxDatasetDisplayLimit(primaryProvider.actionEntryList.size)) {
265                 return@forEach
266             }
267             val pendingIntent = actionEntry.pendingIntent
268             if (pendingIntent == null) {
269                 Log.e(TAG, "Pending intent for action chip is null")
270                 return@forEach
271             }
272 
273             val icon: Icon? = Icon.createWithBitmap(actionEntry.icon.toBitmap())
274             if (icon == null) {
275                 Log.e(TAG, "Icon for action chip is null")
276                 return@forEach
277             }
278 
279             val presentations = constructPresentations(
280                 fillRequest,
281                 index,
282                 actionEntry,
283                 pendingIntent,
284                 icon,
285                 actionEntry.title,
286                 actionEntry.subTitle,
287                 primaryProvider.actionEntryList.size
288             )
289 
290             fillResponseBuilder.addDataset(
291                 Dataset.Builder()
292                     .setField(
293                         autofillId,
294                         Field.Builder().setPresentations(presentations).build()
295                     )
296                     .setAuthentication(pendingIntent.intentSender)
297                     .build()
298             )
299             datasetAdded = true
300 
301             index++
302         }
303 
304         return datasetAdded
305     }
306 
307     private fun addCredentialDatasetsForAutofillId(
308         fillRequest: FillRequest,
309         autofillId: AutofillId,
310         providerDataList: List<GetCredentialProviderData>,
311         entryIconMap: Map<String, Icon>,
312         fillResponseBuilder: FillResponse.Builder,
313         typePriorityMap: Map<String, Int>,
314     ): Boolean {
315         val providerList = GetFlowUtils.toProviderList(
316             providerDataList,
317             this@CredentialAutofillService
318         )
319         if (providerList.isEmpty()) {
320             return false
321         }
322         val providerDisplayInfo: ProviderDisplayInfo =
323             toProviderDisplayInfo(providerList, typePriorityMap)
324         var totalEntryCount = providerDisplayInfo.sortedUserNameToCredentialEntryList.size
325 
326         var i = 0
327         var datasetAdded = false
328 
329         val duplicateDisplayNamesForPasskeys: MutableMap<String, Boolean> = mutableMapOf()
330         providerDisplayInfo.sortedUserNameToCredentialEntryList.forEach {
331             val credentialEntry = it.sortedCredentialEntryList.first()
332             if (credentialEntry.credentialType == CredentialType.PASSKEY) {
333                 credentialEntry.displayName?.let { displayName ->
334                     val duplicateEntry = duplicateDisplayNamesForPasskeys.contains(displayName)
335                     duplicateDisplayNamesForPasskeys[displayName] = duplicateEntry
336                 }
337             }
338         }
339         providerDisplayInfo.sortedUserNameToCredentialEntryList.forEach usernameLoop@{
340             val primaryEntry = it.sortedCredentialEntryList.first()
341             val pendingIntent = primaryEntry.pendingIntent
342             val fillInIntent = primaryEntry.fillInIntent
343             if (pendingIntent == null || fillInIntent == null) {
344                 // FillInIntent will not be null because autofillId was retrieved from it.
345                 Log.e(TAG, "PendingIntent was missing from the entry.")
346                 return@usernameLoop
347             }
348             if (i >= maxDatasetDisplayLimit(totalEntryCount)) {
349                 return@usernameLoop
350             }
351             val icon: Icon = if (primaryEntry.icon == null) {
352                 // The empty entry icon has non-null icon reference but null drawable reference.
353                 // If the drawable reference is null, then use the default icon.
354                 getDefaultIcon()
355             } else {
356                 entryIconMap[primaryEntry.entryKey + primaryEntry.entrySubkey]
357                     ?: getDefaultIcon()
358             }
359             val displayName = primaryEntry.displayName
360             val title: String = if (primaryEntry.credentialType == CredentialType.PASSKEY &&
361                 displayName != null
362             ) {
363                 displayName
364             } else {
365                 primaryEntry.userName
366             }
367             val subtitle = if (primaryEntry.credentialType ==
368                 CredentialType.PASSKEY && duplicateDisplayNamesForPasskeys[title] == true
369             ) {
370                 primaryEntry.userName
371             } else {
372                 null
373             }
374             val presentations =
375                 constructPresentations(
376                     fillRequest, i, primaryEntry, pendingIntent,
377                     icon, title, subtitle, totalEntryCount
378                 )
379             fillResponseBuilder.addDataset(
380                 Dataset.Builder()
381                     .setField(
382                         autofillId,
383                         Field.Builder().setPresentations(
384                             presentations
385                         )
386                             .build()
387                     )
388                     .setAuthentication(pendingIntent.intentSender)
389                     .setCredentialFillInIntent(fillInIntent)
390                     .build()
391             )
392             datasetAdded = true
393             i++
394         }
395         return datasetAdded
396     }
397 
398     private fun addMoreOptionsDataset(
399         fillRequest: FillRequest,
400         autofillId: AutofillId,
401         fillResponseBuilder: FillResponse.Builder,
402         bottomSheetIntent: Intent
403     ) {
404         val inlineSuggestionsRequest = fillRequest.inlineSuggestionsRequest
405         val inlinePresentationSpecs = inlineSuggestionsRequest?.inlinePresentationSpecs
406         val inlinePresentationSpecsCount = inlinePresentationSpecs?.size ?: 0
407         val pinnedSpec = getLastInlinePresentationSpec(
408             inlinePresentationSpecs,
409             inlinePresentationSpecsCount
410         )
411         addDropdownMoreOptionsPresentation(bottomSheetIntent, autofillId, fillResponseBuilder)
412         if (pinnedSpec != null) {
413             addPinnedInlineSuggestion(
414                 pinnedSpec, autofillId,
415                 fillResponseBuilder, bottomSheetIntent
416             )
417         }
418     }
419 
420     private fun constructPresentations(
421         fillRequest: FillRequest,
422         index: Int,
423         entry: EntryInfo,
424         pendingIntent: PendingIntent,
425         icon: Icon,
426         title: String,
427         subtitle: String?,
428         totalEntryCount: Int
429     ): Presentations {
430         val inlineSuggestionsRequest = fillRequest.inlineSuggestionsRequest
431         val inlinePresentationSpecs = inlineSuggestionsRequest?.inlinePresentationSpecs
432         val inlinePresentationSpecsCount = inlinePresentationSpecs?.size ?: 0
433 
434         // Create inline presentation
435         var inlinePresentation: InlinePresentation? = null
436         if (inlinePresentationSpecs != null && index < maxDatasetDisplayLimit(totalEntryCount)) {
437             val spec: InlinePresentationSpec? = if (index < inlinePresentationSpecsCount) {
438                 inlinePresentationSpecs[index]
439             } else {
440                 inlinePresentationSpecs[inlinePresentationSpecsCount - 1]
441             }
442             if (spec != null) {
443                 inlinePresentation = createInlinePresentation(
444                     pendingIntent, icon,
445                     InlinePresentationsFactory.modifyInlinePresentationSpec
446                         (this@CredentialAutofillService, spec),
447                     title, subtitle, entry is ActionEntryInfo
448                 )
449             }
450         }
451         var dropdownPresentation: RemoteViews? = null
452         if (index < maxDatasetDisplayLimit(totalEntryCount)) {
453             dropdownPresentation = RemoteViewsFactory.createDropdownPresentation(
454                 this, icon, entry, /*isFirstEntry= */ index == 0,
455                 /*isLastEntry= */ (totalEntryCount - index == 1)
456             )
457         }
458 
459         val presentationBuilder = Presentations.Builder()
460         if (dropdownPresentation != null) {
461             presentationBuilder.setMenuPresentation(dropdownPresentation)
462         }
463         if (inlinePresentation != null) {
464             presentationBuilder.setInlinePresentation(inlinePresentation)
465         }
466         return presentationBuilder.build()
467     }
468 
469     private fun maxDatasetDisplayLimit(totalEntryCount: Int) = this.resources.getInteger(
470         com.android.credentialmanager.R.integer.autofill_max_visible_datasets
471     ).coerceAtMost(totalEntryCount)
472 
473     private fun createInlinePresentation(
474         pendingIntent: PendingIntent,
475         icon: Icon,
476         spec: InlinePresentationSpec,
477         title: String,
478         subtitle: String?,
479         isActionEntry: Boolean
480     ): InlinePresentation {
481         val sliceBuilder = InlineSuggestionUi
482             .newContentBuilder(pendingIntent)
483             .setTitle(title)
484         icon.setTintBlendMode(BlendMode.DST)
485         sliceBuilder.setStartIcon(icon)
486         if (subtitle != null && !isActionEntry) {
487             sliceBuilder.setSubtitle(subtitle)
488         }
489         return InlinePresentation(
490             sliceBuilder.build().slice, spec, /* pinned= */ false
491         )
492     }
493 
494     private fun addDropdownMoreOptionsPresentation(
495         bottomSheetIntent: Intent,
496         autofillId: AutofillId,
497         fillResponseBuilder: FillResponse.Builder
498     ) {
499         val presentationBuilder = Presentations.Builder()
500             .setMenuPresentation(
501                 RemoteViewsFactory.createMoreSignInOptionsPresentation(this)
502             )
503         val pendingIntent = setUpBottomSheetPendingIntent(bottomSheetIntent)
504 
505         fillResponseBuilder.addDataset(
506             Dataset.Builder()
507                 .setId(AutofillManager.PINNED_DATASET_ID)
508                 .setField(
509                     autofillId,
510                     Field.Builder().setPresentations(
511                         presentationBuilder.build()
512                     )
513                         .build()
514                 )
515                 .setAuthentication(pendingIntent.intentSender)
516                 .build()
517         )
518     }
519 
520     private fun getLastInlinePresentationSpec(
521         inlinePresentationSpecs: List<InlinePresentationSpec>?,
522         inlinePresentationSpecsCount: Int
523     ): InlinePresentationSpec? {
524         if (inlinePresentationSpecs != null) {
525             return inlinePresentationSpecs[inlinePresentationSpecsCount - 1]
526         }
527         return null
528     }
529 
530     private fun addPinnedInlineSuggestion(
531         spec: InlinePresentationSpec,
532         autofillId: AutofillId,
533         fillResponseBuilder: FillResponse.Builder,
534         bottomSheetIntent: Intent
535     ) {
536         val pendingIntent = setUpBottomSheetPendingIntent(bottomSheetIntent)
537 
538         val dataSetBuilder = Dataset.Builder()
539         val sliceBuilder = InlineSuggestionUi
540             .newContentBuilder(pendingIntent)
541             .setStartIcon(
542                 Icon.createWithResource(
543                     this,
544                     com.android.credentialmanager.R.drawable.more_horiz_24px
545                 )
546             )
547         val presentationBuilder = Presentations.Builder()
548             .setInlinePresentation(
549                 InlinePresentation(
550                     sliceBuilder.build().slice, spec, /* pinned= */ true
551                 )
552             )
553 
554         fillResponseBuilder.addDataset(
555             dataSetBuilder
556                 .setId(AutofillManager.PINNED_DATASET_ID)
557                 .setField(
558                     autofillId,
559                     Field.Builder().setPresentations(
560                         presentationBuilder.build()
561                     ).build()
562                 )
563                 .setAuthentication(pendingIntent.intentSender)
564                 .build()
565         )
566     }
567 
568     private fun setUpBottomSheetPendingIntent(intent: Intent): PendingIntent {
569         intent.setAction(java.util.UUID.randomUUID().toString())
570         return PendingIntent.getActivity(this, /*requestCode=*/0, intent,
571             PendingIntent.FLAG_MUTABLE, /*options=*/null)
572     }
573 
574     /**
575      *  Maps Autofill Id to provider list. For example, passing in a provider info
576      *
577      *     ProviderInfo {
578      *       id1,
579      *       displayName1
580      *       [entry1(autofillId1), entry2(autofillId2), entry3(autofillId3)],
581      *       ...
582      *     }
583      *
584      *     will result in
585      *
586      *     { autofillId1: ProviderInfo {
587      *         id1,
588      *         displayName1,
589      *         [entry1(autofillId1)],
590      *         ...
591      *       }, autofillId2: ProviderInfo {
592      *         id1,
593      *         displayName1,
594      *         [entry2(autofillId2)],
595      *         ...
596      *       }, autofillId3: ProviderInfo {
597      *         id1,
598      *         displayName1,
599      *         [entry3(autofillId3)],
600      *         ...
601      *       }
602      *     }
603      */
604     private fun mapAutofillIdToProviders(
605         uniqueAutofillIdsForRequest: Set<AutofillId>,
606         providerList: List<GetCredentialProviderData>,
607         primaryProviderComponentName: ComponentName?
608     ): Map<AutofillId, ArrayList<GetCredentialProviderData>> {
609         val autofillIdToProviders: MutableMap<AutofillId, ArrayList<GetCredentialProviderData>> =
610             mutableMapOf()
611         var primaryProvider: GetCredentialProviderData? = null
612         providerList.forEach { provider ->
613             if (primaryProviderComponentName != null && Objects.equals(ComponentName
614                 .unflattenFromString(provider
615                     .providerFlattenedComponentName), primaryProviderComponentName)) {
616                 primaryProvider = provider
617             }
618             val autofillIdToCredentialEntries:
619                     MutableMap<AutofillId, ArrayList<Entry>> =
620                 mapAutofillIdToCredentialEntries(provider.credentialEntries)
621             autofillIdToCredentialEntries.forEach { (autofillId, entries) ->
622                 autofillIdToProviders.getOrPut(autofillId) { ArrayList() }
623                     .add(copyProviderInfo(provider, entries))
624             }
625         }
626         // adds primary provider action entries for autofill IDs without credential entries
627         uniqueAutofillIdsForRequest.forEach { autofillId ->
628             if (!autofillIdToProviders.containsKey(autofillId) && primaryProvider != null) {
629                 autofillIdToProviders.put(
630                     autofillId,
631                     ArrayList(listOf(copyProviderInfoForActionsOnly(primaryProvider!!))))
632             }
633         }
634         return autofillIdToProviders
635     }
636 
637     private fun mapAutofillIdToCredentialEntries(
638             credentialEntryList: List<Entry>
639     ): MutableMap<AutofillId, ArrayList<Entry>> {
640         val autofillIdToCredentialEntries:
641                 MutableMap<AutofillId, ArrayList<Entry>> = mutableMapOf()
642         credentialEntryList.forEach entryLoop@{ credentialEntry ->
643             val intent = credentialEntry.frameworkExtrasIntent
644             intent?.getParcelableExtra(
645                         CredentialProviderService.EXTRA_GET_CREDENTIAL_REQUEST,
646                         android.service.credentials.GetCredentialRequest::class.java)
647                     ?.credentialOptions
648                     ?.forEach { credentialOption ->
649                         credentialOption.candidateQueryData.getParcelableArrayList(
650                             CredentialProviderService.EXTRA_AUTOFILL_ID, AutofillId::class.java)
651                                 ?.forEach { autofillId ->
652                                     intent.putExtra(
653                                         CredentialProviderService.EXTRA_AUTOFILL_ID,
654                                         autofillId)
655                                     val entry = Entry(
656                                         credentialEntry.key,
657                                         credentialEntry.subkey,
658                                         credentialEntry.slice,
659                                         intent)
660                                     autofillIdToCredentialEntries
661                                             .getOrPut(autofillId) { ArrayList() }
662                                             .add(entry)
663                                 }
664                     }
665         }
666         return autofillIdToCredentialEntries
667     }
668 
669     private fun copyProviderInfo(
670             providerInfo: GetCredentialProviderData,
671             credentialList: List<Entry>
672     ): GetCredentialProviderData {
673         return GetCredentialProviderData(
674             providerInfo.providerFlattenedComponentName,
675             credentialList,
676             providerInfo.actionChips,
677             providerInfo.authenticationEntries,
678             providerInfo.remoteEntry
679         )
680     }
681 
682     private fun copyProviderInfoForActionsOnly(
683         providerInfo: GetCredentialProviderData,
684     ): GetCredentialProviderData {
685         return GetCredentialProviderData(
686             providerInfo.providerFlattenedComponentName,
687             emptyList(),
688             providerInfo.actionChips,
689             emptyList(),
690             null
691         )
692     }
693 
694     override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
695         TODO("Not yet implemented")
696     }
697 
698     private fun getCredManRequest(
699         structure: AssistStructure,
700         sessionId: Int,
701         requestId: Int,
702         resultReceiver: ResultReceiver,
703         responseClientState: Bundle,
704         uniqueAutofillIdsForRequest: MutableSet<AutofillId>
705     ): GetCredentialRequest? {
706         val credentialOptions: MutableList<CredentialOption> = mutableListOf()
707         traverseStructureForRequest(
708             structure, credentialOptions, responseClientState,
709             sessionId, uniqueAutofillIdsForRequest
710         )
711 
712         if (credentialOptions.isNotEmpty()) {
713             val dataBundle = Bundle()
714             dataBundle.putInt(SESSION_ID_KEY, sessionId)
715             dataBundle.putInt(REQUEST_ID_KEY, requestId)
716             dataBundle.putParcelable(CredentialManager.EXTRA_AUTOFILL_RESULT_RECEIVER,
717                     resultReceiver)
718 
719             return GetCredentialRequest.Builder(dataBundle)
720                     .setCredentialOptions(credentialOptions)
721                     .build()
722         }
723         return null
724     }
725 
726     private fun traverseStructureForRequest(
727             structure: AssistStructure,
728             cmRequests: MutableList<CredentialOption>,
729             responseClientState: Bundle,
730             sessionId: Int,
731             uniqueAutofillIdsForRequest: MutableSet<AutofillId>
732     ) {
733         val traversedViewNodes: MutableSet<AutofillId> = mutableSetOf()
734         val windowNodes: List<AssistStructure.WindowNode> =
735                 structure.run {
736                     (0 until windowNodeCount).map { getWindowNodeAt(it) }
737                 }
738 
739         windowNodes.forEach { windowNode: AssistStructure.WindowNode ->
740             traverseNodeForRequest(
741                 windowNode.rootViewNode, cmRequests, responseClientState, traversedViewNodes,
742                 sessionId, uniqueAutofillIdsForRequest)
743         }
744     }
745 
746     private fun traverseNodeForRequest(
747             viewNode: AssistStructure.ViewNode,
748             cmRequests: MutableList<CredentialOption>,
749             responseClientState: Bundle,
750             traversedViewNodes: MutableSet<AutofillId>,
751             sessionId: Int,
752             uniqueAutofillIdsForRequest: MutableSet<AutofillId>
753     ) {
754         viewNode.autofillId?.let {
755             val domain = viewNode.webDomain
756             val request = viewNode.pendingCredentialRequest
757             if (domain != null && request != null) {
758                 responseClientState.putBoolean(
759                     WEBVIEW_REQUESTED_CREDENTIAL_KEY, true)
760             }
761             cmRequests.addAll(getCredentialOptionsFromViewNode(viewNode, traversedViewNodes,
762                     sessionId, uniqueAutofillIdsForRequest)
763             )
764             traversedViewNodes.add(it)
765         }
766 
767         val children: List<AssistStructure.ViewNode> =
768                 viewNode.run {
769                     (0 until childCount).map { getChildAt(it) }
770                 }
771 
772         children.forEach { childNode: AssistStructure.ViewNode ->
773             traverseNodeForRequest(
774                 childNode, cmRequests, responseClientState, traversedViewNodes, sessionId,
775                     uniqueAutofillIdsForRequest
776             )
777         }
778     }
779 
780     private fun getCredentialOptionsFromViewNode(
781             viewNode: AssistStructure.ViewNode,
782             traversedViewNodes: MutableSet<AutofillId>,
783             sessionId: Int,
784             uniqueAutofillIdsForRequest: MutableSet<AutofillId>
785     ): MutableList<CredentialOption> {
786         val credentialOptions: MutableList<CredentialOption> = mutableListOf()
787         if (Flags.autofillCredmanDevIntegration() && viewNode.pendingCredentialRequest != null) {
788             viewNode.pendingCredentialRequest
789                     ?.getCredentialOptions()
790                     ?.forEach { credentialOption ->
791                 credentialOption.candidateQueryData
792                         .getParcelableArrayList(
793                             CredentialProviderService.EXTRA_AUTOFILL_ID, AutofillId::class.java)
794                         ?.let { associatedAutofillIds ->
795                             // Set sessionId in autofillIds. The autofillIds stored in Credential
796                             // Options do not have associated session id and will result in
797                             // different hashes than the ones in assistStructure.
798                             associatedAutofillIds.forEach { associatedAutofillId ->
799                                 associatedAutofillId.sessionId = sessionId
800                             }
801 
802                             // Check whether any of the associated autofill ids have already been
803                             // traversed. If so, skip, to dedupe on duplicate credential options.
804                             if ((traversedViewNodes intersect associatedAutofillIds.toSet())
805                                         .isEmpty()) {
806                                 credentialOptions.add(credentialOption)
807                             }
808 
809                             // Set the autofillIds with session id back to credential option.
810                             credentialOption.candidateQueryData.putParcelableArrayList(
811                                 CredentialProviderService.EXTRA_AUTOFILL_ID,
812                                 associatedAutofillIds
813                             )
814                             uniqueAutofillIdsForRequest.addAll(associatedAutofillIds)
815                         }
816                 }
817         }
818         return credentialOptions
819     }
820 }