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 }