1 /* 2 * 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.server.credentials; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.annotation.UserIdInt; 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.credentials.CredentialManager; 26 import android.credentials.CredentialOption; 27 import android.credentials.CredentialProviderInfo; 28 import android.credentials.GetCredentialException; 29 import android.credentials.GetCredentialResponse; 30 import android.credentials.selection.AuthenticationEntry; 31 import android.credentials.selection.Entry; 32 import android.credentials.selection.GetCredentialProviderData; 33 import android.credentials.selection.ProviderPendingIntentResponse; 34 import android.os.ICancellationSignal; 35 import android.service.credentials.Action; 36 import android.service.credentials.BeginGetCredentialOption; 37 import android.service.credentials.BeginGetCredentialRequest; 38 import android.service.credentials.BeginGetCredentialResponse; 39 import android.service.credentials.CallingAppInfo; 40 import android.service.credentials.CredentialEntry; 41 import android.service.credentials.CredentialProviderService; 42 import android.service.credentials.GetCredentialRequest; 43 import android.service.credentials.RemoteEntry; 44 import android.util.Pair; 45 import android.util.Slog; 46 47 import java.util.ArrayList; 48 import java.util.HashMap; 49 import java.util.HashSet; 50 import java.util.List; 51 import java.util.Map; 52 import java.util.Optional; 53 import java.util.Set; 54 55 /** 56 * Central provider session that listens for provider callbacks, and maintains provider state. 57 * Will likely split this into remote response state and UI state. 58 * 59 * @hide 60 */ 61 public final class ProviderGetSession extends ProviderSession<BeginGetCredentialRequest, 62 BeginGetCredentialResponse> 63 implements 64 RemoteCredentialService.ProviderCallbacks<BeginGetCredentialResponse> { 65 private static final String TAG = CredentialManager.TAG; 66 // Key to be used as the entry key for an action entry 67 public static final String ACTION_ENTRY_KEY = "action_key"; 68 // Key to be used as the entry key for the authentication entry 69 public static final String AUTHENTICATION_ACTION_ENTRY_KEY = "authentication_action_key"; 70 // Key to be used as an entry key for a remote entry 71 public static final String REMOTE_ENTRY_KEY = "remote_entry_key"; 72 // Key to be used as an entry key for a credential entry 73 public static final String CREDENTIAL_ENTRY_KEY = "credential_key"; 74 75 @NonNull 76 private final Map<String, CredentialOption> mBeginGetOptionToCredentialOptionMap; 77 78 /** The complete request to be used in the second round. */ 79 private final android.credentials.GetCredentialRequest mCompleteRequest; 80 private final CallingAppInfo mCallingAppInfo; 81 82 private GetCredentialException mProviderException; 83 84 private final ProviderResponseDataHandler mProviderResponseDataHandler; 85 86 /** Creates a new provider session to be used by the request session. */ 87 @Nullable createNewSession( Context context, @UserIdInt int userId, CredentialProviderInfo providerInfo, GetRequestSession getRequestSession, RemoteCredentialService remoteCredentialService)88 public static ProviderGetSession createNewSession( 89 Context context, 90 @UserIdInt int userId, 91 CredentialProviderInfo providerInfo, 92 GetRequestSession getRequestSession, 93 RemoteCredentialService remoteCredentialService) { 94 android.credentials.GetCredentialRequest filteredRequest = 95 filterOptions(providerInfo.getCapabilities(), 96 getRequestSession.mClientRequest, 97 providerInfo, getRequestSession.mHybridService); 98 if (filteredRequest != null) { 99 Map<String, CredentialOption> beginGetOptionToCredentialOptionMap = 100 new HashMap<>(); 101 return new ProviderGetSession( 102 context, 103 providerInfo, 104 getRequestSession, 105 userId, 106 remoteCredentialService, 107 constructQueryPhaseRequest( 108 filteredRequest, getRequestSession.mClientAppInfo, 109 getRequestSession.mClientRequest.alwaysSendAppInfoToProvider(), 110 beginGetOptionToCredentialOptionMap), 111 filteredRequest, 112 getRequestSession.mClientAppInfo, 113 beginGetOptionToCredentialOptionMap, 114 getRequestSession.mHybridService 115 ); 116 } 117 Slog.i(TAG, "Unable to create provider session for: " 118 + providerInfo.getComponentName()); 119 return null; 120 } 121 122 /** Creates a new provider session to be used by the request session. */ 123 @Nullable createNewSession( Context context, @UserIdInt int userId, CredentialProviderInfo providerInfo, GetCandidateRequestSession getRequestSession, RemoteCredentialService remoteCredentialService)124 public static ProviderGetSession createNewSession( 125 Context context, 126 @UserIdInt int userId, 127 CredentialProviderInfo providerInfo, 128 GetCandidateRequestSession getRequestSession, 129 RemoteCredentialService remoteCredentialService) { 130 android.credentials.GetCredentialRequest filteredRequest = 131 filterOptions(providerInfo.getCapabilities(), 132 getRequestSession.mClientRequest, 133 providerInfo, getRequestSession.mHybridService); 134 if (filteredRequest != null) { 135 Map<String, CredentialOption> beginGetOptionToCredentialOptionMap = 136 new HashMap<>(); 137 return new ProviderGetSession( 138 context, 139 providerInfo, 140 getRequestSession, 141 userId, 142 remoteCredentialService, 143 constructQueryPhaseRequest( 144 filteredRequest, getRequestSession.mClientAppInfo, 145 getRequestSession.mClientRequest.alwaysSendAppInfoToProvider(), 146 beginGetOptionToCredentialOptionMap), 147 filteredRequest, 148 getRequestSession.mClientAppInfo, 149 beginGetOptionToCredentialOptionMap, 150 getRequestSession.mHybridService 151 ); 152 } 153 Slog.i(TAG, "Unable to create provider session for: " 154 + providerInfo.getComponentName()); 155 return null; 156 } 157 constructQueryPhaseRequest( android.credentials.GetCredentialRequest filteredRequest, CallingAppInfo callingAppInfo, boolean propagateToProvider, Map<String, CredentialOption> beginGetOptionToCredentialOptionMap )158 private static BeginGetCredentialRequest constructQueryPhaseRequest( 159 android.credentials.GetCredentialRequest filteredRequest, 160 CallingAppInfo callingAppInfo, 161 boolean propagateToProvider, 162 Map<String, CredentialOption> beginGetOptionToCredentialOptionMap 163 ) { 164 BeginGetCredentialRequest.Builder builder = new BeginGetCredentialRequest.Builder(); 165 filteredRequest.getCredentialOptions().forEach(option -> { 166 String id = generateUniqueId(); 167 builder.addBeginGetCredentialOption( 168 new BeginGetCredentialOption( 169 id, option.getType(), option.getCandidateQueryData()) 170 ); 171 beginGetOptionToCredentialOptionMap.put(id, option); 172 }); 173 if (propagateToProvider) { 174 builder.setCallingAppInfo(callingAppInfo); 175 } 176 return builder.build(); 177 } 178 179 @Nullable filterOptions( List<String> providerCapabilities, android.credentials.GetCredentialRequest clientRequest, CredentialProviderInfo info, String hybridService)180 private static android.credentials.GetCredentialRequest filterOptions( 181 List<String> providerCapabilities, 182 android.credentials.GetCredentialRequest clientRequest, 183 CredentialProviderInfo info, 184 String hybridService) { 185 Slog.i(TAG, "Filtering request options for: " + info.getComponentName()); 186 if (android.credentials.flags.Flags.hybridFilterOptFixEnabled()) { 187 ComponentName hybridComponentName = ComponentName.unflattenFromString(hybridService); 188 if (hybridComponentName != null && hybridComponentName 189 .equals(info.getComponentName())) { 190 Slog.i(TAG, "Skipping filtering of options for hybrid service"); 191 return clientRequest; 192 } 193 Slog.w(TAG, "Could not parse hybrid service while filtering options"); 194 } 195 196 List<CredentialOption> filteredOptions = new ArrayList<>(); 197 for (CredentialOption option : clientRequest.getCredentialOptions()) { 198 if (providerCapabilities.contains(option.getType()) 199 && isProviderAllowed(option, info) 200 && checkSystemProviderRequirement(option, info.isSystemProvider())) { 201 Slog.i(TAG, "Option of type: " + option.getType() + " meets all filtering" 202 + "conditions"); 203 filteredOptions.add(option); 204 } 205 } 206 if (!filteredOptions.isEmpty()) { 207 return new android.credentials.GetCredentialRequest 208 .Builder(clientRequest.getData()) 209 .setCredentialOptions( 210 filteredOptions).build(); 211 } 212 Slog.i(TAG, "No options filtered"); 213 return null; 214 } 215 isProviderAllowed(CredentialOption option, CredentialProviderInfo providerInfo)216 private static boolean isProviderAllowed(CredentialOption option, 217 CredentialProviderInfo providerInfo) { 218 if (providerInfo.isSystemProvider()) { 219 // Always allow system providers , including the remote provider 220 return true; 221 } 222 if (!option.getAllowedProviders().isEmpty() && !option.getAllowedProviders().contains( 223 providerInfo.getComponentName())) { 224 Slog.i(TAG, "Provider allow list specified but does not contain this provider"); 225 return false; 226 } 227 return true; 228 } 229 checkSystemProviderRequirement(CredentialOption option, boolean isSystemProvider)230 private static boolean checkSystemProviderRequirement(CredentialOption option, 231 boolean isSystemProvider) { 232 if (option.isSystemProviderRequired() && !isSystemProvider) { 233 Slog.i(TAG, "System provider required, but this service is not a system provider"); 234 return false; 235 } 236 return true; 237 } 238 ProviderGetSession(Context context, CredentialProviderInfo info, ProviderInternalCallback callbacks, int userId, RemoteCredentialService remoteCredentialService, BeginGetCredentialRequest beginGetRequest, android.credentials.GetCredentialRequest completeGetRequest, CallingAppInfo callingAppInfo, Map<String, CredentialOption> beginGetOptionToCredentialOptionMap, String hybridService)239 public ProviderGetSession(Context context, 240 CredentialProviderInfo info, 241 ProviderInternalCallback callbacks, 242 int userId, RemoteCredentialService remoteCredentialService, 243 BeginGetCredentialRequest beginGetRequest, 244 android.credentials.GetCredentialRequest completeGetRequest, 245 CallingAppInfo callingAppInfo, 246 Map<String, CredentialOption> beginGetOptionToCredentialOptionMap, 247 String hybridService) { 248 super(context, beginGetRequest, callbacks, info.getComponentName(), 249 userId, remoteCredentialService); 250 mCompleteRequest = completeGetRequest; 251 mCallingAppInfo = callingAppInfo; 252 setStatus(Status.PENDING); 253 mBeginGetOptionToCredentialOptionMap = new HashMap<>(beginGetOptionToCredentialOptionMap); 254 mProviderResponseDataHandler = new ProviderResponseDataHandler( 255 ComponentName.unflattenFromString(hybridService)); 256 } 257 258 /** Called when the provider response has been updated by an external source. */ 259 @Override // Callback from the remote provider onProviderResponseSuccess(@ullable BeginGetCredentialResponse response)260 public void onProviderResponseSuccess(@Nullable BeginGetCredentialResponse response) { 261 Slog.i(TAG, "Remote provider responded with a valid response: " + mComponentName); 262 onSetInitialRemoteResponse(response); 263 } 264 265 /** Called when the provider response resulted in a failure. */ 266 @Override // Callback from the remote provider onProviderResponseFailure(int errorCode, Exception exception)267 public void onProviderResponseFailure(int errorCode, Exception exception) { 268 if (exception instanceof GetCredentialException) { 269 mProviderException = (GetCredentialException) exception; 270 // TODO(b/271135048) : Decide on exception type length 271 mProviderSessionMetric.collectCandidateFrameworkException(mProviderException.getType()); 272 } 273 mProviderSessionMetric.collectCandidateExceptionStatus(/*hasException=*/true); 274 updateStatusAndInvokeCallback(Status.CANCELED, 275 /*source=*/ CredentialsSource.REMOTE_PROVIDER); 276 } 277 278 /** Called when provider service dies. */ 279 @Override // Callback from the remote provider onProviderServiceDied(RemoteCredentialService service)280 public void onProviderServiceDied(RemoteCredentialService service) { 281 if (service.getComponentName().equals(mComponentName)) { 282 updateStatusAndInvokeCallback(Status.SERVICE_DEAD, 283 /*source=*/ CredentialsSource.REMOTE_PROVIDER); 284 } else { 285 Slog.w(TAG, "Component names different in onProviderServiceDied - " 286 + "this should not happen"); 287 } 288 } 289 290 @Override onProviderCancellable(ICancellationSignal cancellation)291 public void onProviderCancellable(ICancellationSignal cancellation) { 292 mProviderCancellationSignal = cancellation; 293 } 294 295 @Override // Selection call from the request provider onUiEntrySelected(String entryType, String entryKey, ProviderPendingIntentResponse providerPendingIntentResponse)296 protected void onUiEntrySelected(String entryType, String entryKey, 297 ProviderPendingIntentResponse providerPendingIntentResponse) { 298 Slog.i(TAG, "onUiEntrySelected with entryType: " + entryType + ", and entryKey: " 299 + entryKey); 300 switch (entryType) { 301 case CREDENTIAL_ENTRY_KEY: 302 CredentialEntry credentialEntry = mProviderResponseDataHandler 303 .getCredentialEntry(entryKey); 304 if (credentialEntry == null) { 305 Slog.i(TAG, "Unexpected credential entry key"); 306 invokeCallbackOnInternalInvalidState(); 307 return; 308 } 309 onCredentialEntrySelected(providerPendingIntentResponse); 310 break; 311 case ACTION_ENTRY_KEY: 312 Action actionEntry = mProviderResponseDataHandler.getActionEntry(entryKey); 313 if (actionEntry == null) { 314 Slog.i(TAG, "Unexpected action entry key"); 315 invokeCallbackOnInternalInvalidState(); 316 return; 317 } 318 onActionEntrySelected(providerPendingIntentResponse); 319 break; 320 case AUTHENTICATION_ACTION_ENTRY_KEY: 321 Action authenticationEntry = mProviderResponseDataHandler 322 .getAuthenticationAction(entryKey); 323 mProviderSessionMetric.createAuthenticationBrowsingMetric(); 324 if (authenticationEntry == null) { 325 Slog.i(TAG, "Unexpected authenticationEntry key"); 326 invokeCallbackOnInternalInvalidState(); 327 return; 328 } 329 boolean additionalContentReceived = 330 onAuthenticationEntrySelected(providerPendingIntentResponse); 331 if (additionalContentReceived) { 332 Slog.i(TAG, "Additional content received - removing authentication entry"); 333 mProviderResponseDataHandler.removeAuthenticationAction(entryKey); 334 if (!mProviderResponseDataHandler.isEmptyResponse()) { 335 updateStatusAndInvokeCallback(Status.CREDENTIALS_RECEIVED, 336 /*source=*/ CredentialsSource.AUTH_ENTRY); 337 } 338 } else { 339 Slog.i(TAG, "Additional content not received from authentication entry"); 340 mProviderResponseDataHandler 341 .updateAuthEntryWithNoCredentialsReceived(entryKey); 342 updateStatusAndInvokeCallback(Status.NO_CREDENTIALS_FROM_AUTH_ENTRY, 343 /*source=*/ CredentialsSource.AUTH_ENTRY); 344 } 345 break; 346 case REMOTE_ENTRY_KEY: 347 if (mProviderResponseDataHandler.getRemoteEntry(entryKey) != null) { 348 onRemoteEntrySelected(providerPendingIntentResponse); 349 } else { 350 Slog.i(TAG, "Unexpected remote entry key"); 351 invokeCallbackOnInternalInvalidState(); 352 } 353 break; 354 default: 355 Slog.i(TAG, "Unsupported entry type selected"); 356 invokeCallbackOnInternalInvalidState(); 357 } 358 } 359 360 @Override invokeSession()361 protected void invokeSession() { 362 if (mRemoteCredentialService != null) { 363 startCandidateMetrics(); 364 mRemoteCredentialService.setCallback(this); 365 mRemoteCredentialService.onBeginGetCredential(mProviderRequest); 366 } 367 } 368 369 @NonNull getCredentialEntryTypes()370 protected Set<String> getCredentialEntryTypes() { 371 return mProviderResponseDataHandler.getCredentialEntryTypes(); 372 } 373 374 @Override // Call from request session to data to be shown on the UI 375 @Nullable prepareUiData()376 protected GetCredentialProviderData prepareUiData() throws IllegalArgumentException { 377 if (!ProviderSession.isUiInvokingStatus(getStatus())) { 378 Slog.i(TAG, "No data for UI from: " + mComponentName.flattenToString()); 379 return null; 380 } 381 if (mProviderResponse != null && !mProviderResponseDataHandler.isEmptyResponse()) { 382 return mProviderResponseDataHandler.toGetCredentialProviderData(); 383 } 384 return null; 385 } 386 setUpFillInIntentWithFinalRequest(@onNull String id)387 private Intent setUpFillInIntentWithFinalRequest(@NonNull String id) { 388 // TODO: Determine if we should skip this entry if entry id is not set, or is set 389 // but does not resolve to a valid option. For now, not skipping it because 390 // it may be possible that the provider adds their own extras and expects to receive 391 // those and complete the flow. 392 Intent intent = new Intent(); 393 CredentialOption credentialOption = mBeginGetOptionToCredentialOptionMap.get(id); 394 if (credentialOption == null) { 395 Slog.w(TAG, "Id from Credential Entry does not resolve to a valid option"); 396 return intent; 397 } 398 return intent.putExtra( 399 CredentialProviderService.EXTRA_GET_CREDENTIAL_REQUEST, 400 new GetCredentialRequest( 401 mCallingAppInfo, 402 List.of(credentialOption))); 403 } 404 setUpFillInIntentWithQueryRequest()405 private Intent setUpFillInIntentWithQueryRequest() { 406 Intent intent = new Intent(); 407 intent.putExtra(CredentialProviderService.EXTRA_BEGIN_GET_CREDENTIAL_REQUEST, 408 mProviderRequest); 409 return intent; 410 } 411 onRemoteEntrySelected( ProviderPendingIntentResponse providerPendingIntentResponse)412 private void onRemoteEntrySelected( 413 ProviderPendingIntentResponse providerPendingIntentResponse) { 414 onCredentialEntrySelected(providerPendingIntentResponse); 415 } 416 onCredentialEntrySelected( ProviderPendingIntentResponse providerPendingIntentResponse)417 private void onCredentialEntrySelected( 418 ProviderPendingIntentResponse providerPendingIntentResponse) { 419 if (providerPendingIntentResponse == null) { 420 invokeCallbackOnInternalInvalidState(); 421 return; 422 } 423 // Check if pending intent has an error 424 GetCredentialException exception = maybeGetPendingIntentException( 425 providerPendingIntentResponse); 426 if (exception != null) { 427 invokeCallbackWithError(exception.getType(), exception.getMessage()); 428 return; 429 } 430 431 // Check if pending intent has a credential response 432 GetCredentialResponse getCredentialResponse = PendingIntentResultHandler 433 .extractGetCredentialResponse( 434 providerPendingIntentResponse.getResultData()); 435 if (getCredentialResponse != null) { 436 mCallbacks.onFinalResponseReceived(mComponentName, 437 getCredentialResponse); 438 return; 439 } 440 Slog.i(TAG, "Pending intent response contains no credential, or error " 441 + "for a credential entry"); 442 invokeCallbackOnInternalInvalidState(); 443 } 444 445 @Nullable maybeGetPendingIntentException( ProviderPendingIntentResponse pendingIntentResponse)446 private GetCredentialException maybeGetPendingIntentException( 447 ProviderPendingIntentResponse pendingIntentResponse) { 448 if (pendingIntentResponse == null) { 449 return null; 450 } 451 if (PendingIntentResultHandler.isValidResponse(pendingIntentResponse)) { 452 GetCredentialException exception = PendingIntentResultHandler 453 .extractGetCredentialException(pendingIntentResponse.getResultData()); 454 if (exception != null) { 455 return exception; 456 } 457 } else if (PendingIntentResultHandler.isCancelledResponse(pendingIntentResponse)) { 458 return new GetCredentialException(GetCredentialException.TYPE_USER_CANCELED); 459 } else { 460 return new GetCredentialException(GetCredentialException.TYPE_NO_CREDENTIAL); 461 } 462 return null; 463 } 464 465 /** 466 * Returns true if either an exception or a response is retrieved from the result. 467 * Returns false if the response is not set at all, or set to null, or empty. 468 */ onAuthenticationEntrySelected( @ullable ProviderPendingIntentResponse providerPendingIntentResponse)469 private boolean onAuthenticationEntrySelected( 470 @Nullable ProviderPendingIntentResponse providerPendingIntentResponse) { 471 // Authentication entry is expected to have a BeginGetCredentialResponse instance. If it 472 // does not have it, we remove the authentication entry and do not add any more content. 473 if (providerPendingIntentResponse == null) { 474 // Nothing received. This is equivalent to no content received. 475 return false; 476 } 477 478 GetCredentialException exception = maybeGetPendingIntentException( 479 providerPendingIntentResponse); 480 if (exception != null) { 481 // TODO (b/271135048), for AuthenticationEntry callback selection, set error 482 mProviderSessionMetric.collectAuthenticationExceptionStatus(/*hasException*/true); 483 invokeCallbackWithError(exception.getType(), 484 exception.getMessage()); 485 // Additional content received is in the form of an exception which ends the flow. 486 return true; 487 } 488 // Check if pending intent has the response. If yes, remove this auth entry and 489 // replace it with the response content received. 490 BeginGetCredentialResponse response = PendingIntentResultHandler 491 .extractResponseContent(providerPendingIntentResponse 492 .getResultData()); 493 mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/true, null); 494 if (response != null && !mProviderResponseDataHandler.isEmptyResponse(response)) { 495 addToInitialRemoteResponse(response, /*isInitialResponse=*/ false); 496 // Additional content received is in the form of new response content. 497 return true; 498 } 499 // No response or exception found. 500 return false; 501 } 502 addToInitialRemoteResponse(BeginGetCredentialResponse content, boolean isInitialResponse)503 private void addToInitialRemoteResponse(BeginGetCredentialResponse content, 504 boolean isInitialResponse) { 505 if (content == null) { 506 return; 507 } 508 mProviderResponseDataHandler.addResponseContent( 509 content.getCredentialEntries(), 510 content.getActions(), 511 content.getAuthenticationActions(), 512 content.getRemoteCredentialEntry(), 513 isInitialResponse 514 ); 515 } 516 517 /** Returns true if either an exception or a response is found. */ onActionEntrySelected(ProviderPendingIntentResponse providerPendingIntentResponse)518 private void onActionEntrySelected(ProviderPendingIntentResponse 519 providerPendingIntentResponse) { 520 Slog.i(TAG, "onActionEntrySelected"); 521 onCredentialEntrySelected(providerPendingIntentResponse); 522 } 523 524 525 /** Updates the response being maintained in state by this provider session. */ onSetInitialRemoteResponse(BeginGetCredentialResponse response)526 private void onSetInitialRemoteResponse(BeginGetCredentialResponse response) { 527 mProviderResponse = response; 528 addToInitialRemoteResponse(response, /*isInitialResponse=*/true); 529 // Log the data. 530 if (mProviderResponseDataHandler.isEmptyResponse(response)) { 531 mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/false, 532 null); 533 updateStatusAndInvokeCallback(Status.EMPTY_RESPONSE, 534 /*source=*/ CredentialsSource.REMOTE_PROVIDER); 535 return; 536 } 537 mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/false, 538 null); 539 updateStatusAndInvokeCallback(Status.CREDENTIALS_RECEIVED, 540 /*source=*/ CredentialsSource.REMOTE_PROVIDER); 541 } 542 543 /** 544 * When an invalid state occurs, e.g. entry mismatch or no response from provider, 545 * we send back a TYPE_NO_CREDENTIAL error as to the developer. 546 */ invokeCallbackOnInternalInvalidState()547 private void invokeCallbackOnInternalInvalidState() { 548 mCallbacks.onFinalErrorReceived(mComponentName, 549 GetCredentialException.TYPE_NO_CREDENTIAL, null); 550 } 551 552 /** Update auth entries status based on an auth entry selected from a different session. */ updateAuthEntriesStatusFromAnotherSession()553 public void updateAuthEntriesStatusFromAnotherSession() { 554 // Pass null for entryKey if the auth entry selected belongs to a different session 555 mProviderResponseDataHandler.updateAuthEntryWithNoCredentialsReceived(/*entryKey=*/null); 556 } 557 558 /** Returns true if the provider response contains empty auth entries only, false otherwise. **/ containsEmptyAuthEntriesOnly()559 public boolean containsEmptyAuthEntriesOnly() { 560 // We do not consider action entries here because if actions are the only entries, 561 // we don't show the UI 562 return mProviderResponseDataHandler.mUiCredentialEntries.isEmpty() 563 && mProviderResponseDataHandler.mUiRemoteEntry == null 564 && mProviderResponseDataHandler.mUiAuthenticationEntries 565 .values().stream().allMatch( 566 e -> e.second.getStatus() == AuthenticationEntry 567 .STATUS_UNLOCKED_BUT_EMPTY_LESS_RECENT 568 || e.second.getStatus() 569 == AuthenticationEntry.STATUS_UNLOCKED_BUT_EMPTY_MOST_RECENT 570 ); 571 } 572 573 private class ProviderResponseDataHandler { 574 @Nullable 575 private final ComponentName mExpectedRemoteEntryProviderService; 576 @NonNull 577 private final Map<String, Pair<CredentialEntry, Entry>> mUiCredentialEntries = 578 new HashMap<>(); 579 @NonNull 580 private final Map<String, Pair<Action, Entry>> mUiActionsEntries = new HashMap<>(); 581 @Nullable 582 private final Map<String, Pair<Action, AuthenticationEntry>> mUiAuthenticationEntries = 583 new HashMap<>(); 584 585 @NonNull 586 private final Set<String> mCredentialEntryTypes = new HashSet<>(); 587 588 @Nullable 589 private Pair<String, Pair<RemoteEntry, Entry>> mUiRemoteEntry = null; 590 ProviderResponseDataHandler(@ullable ComponentName expectedRemoteEntryProviderService)591 ProviderResponseDataHandler(@Nullable ComponentName expectedRemoteEntryProviderService) { 592 mExpectedRemoteEntryProviderService = expectedRemoteEntryProviderService; 593 } 594 addResponseContent(List<CredentialEntry> credentialEntries, List<Action> actions, List<Action> authenticationActions, RemoteEntry remoteEntry, boolean isInitialResponse)595 public void addResponseContent(List<CredentialEntry> credentialEntries, 596 List<Action> actions, List<Action> authenticationActions, 597 RemoteEntry remoteEntry, boolean isInitialResponse) { 598 credentialEntries.forEach(this::addCredentialEntry); 599 actions.forEach(this::addAction); 600 authenticationActions.forEach( 601 authenticationAction -> addAuthenticationAction(authenticationAction, 602 AuthenticationEntry.STATUS_LOCKED)); 603 // In the query phase, it is likely most providers will return a null remote entry 604 // so no need to invoke the setter since it adds the overhead of checking for the 605 // hybrid permission, and then sets an already null value to null. 606 // If this is not the query phase, e.g. response after a locked entry is unlocked 607 // then it is valid for the provider to remove the remote entry, and so we allow 608 // them to set it to null. 609 if (remoteEntry != null || !isInitialResponse) { 610 setRemoteEntry(remoteEntry); 611 } 612 } 613 addCredentialEntry(CredentialEntry credentialEntry)614 public void addCredentialEntry(CredentialEntry credentialEntry) { 615 String id = generateUniqueId(); 616 Entry entry = new Entry(CREDENTIAL_ENTRY_KEY, 617 id, credentialEntry.getSlice(), 618 setUpFillInIntentWithFinalRequest(credentialEntry 619 .getBeginGetCredentialOptionId())); 620 mUiCredentialEntries.put(id, new Pair<>(credentialEntry, entry)); 621 mCredentialEntryTypes.add(credentialEntry.getType()); 622 } 623 addAction(Action action)624 public void addAction(Action action) { 625 String id = generateUniqueId(); 626 Entry entry = new Entry(ACTION_ENTRY_KEY, 627 id, action.getSlice(), 628 setUpFillInIntentWithQueryRequest()); 629 mUiActionsEntries.put(id, new Pair<>(action, entry)); 630 } 631 addAuthenticationAction(Action authenticationAction, @AuthenticationEntry.Status int status)632 public void addAuthenticationAction(Action authenticationAction, 633 @AuthenticationEntry.Status int status) { 634 String id = generateUniqueId(); 635 AuthenticationEntry entry = new AuthenticationEntry( 636 AUTHENTICATION_ACTION_ENTRY_KEY, 637 id, authenticationAction.getSlice(), 638 status, 639 setUpFillInIntentWithQueryRequest()); 640 mUiAuthenticationEntries.put(id, new Pair<>(authenticationAction, entry)); 641 } 642 removeAuthenticationAction(String id)643 public void removeAuthenticationAction(String id) { 644 mUiAuthenticationEntries.remove(id); 645 } 646 setRemoteEntry(@ullable RemoteEntry remoteEntry)647 public void setRemoteEntry(@Nullable RemoteEntry remoteEntry) { 648 if (!enforceRemoteEntryRestrictions(mExpectedRemoteEntryProviderService)) { 649 Slog.w(TAG, "Remote entry being dropped as it does not meet the restriction" 650 + " checks."); 651 return; 652 } 653 if (remoteEntry == null) { 654 mUiRemoteEntry = null; 655 return; 656 } 657 String id = generateUniqueId(); 658 Entry entry = new Entry(REMOTE_ENTRY_KEY, 659 id, remoteEntry.getSlice(), setUpFillInIntentForRemoteEntry()); 660 mUiRemoteEntry = new Pair<>(id, new Pair<>(remoteEntry, entry)); 661 } 662 663 toGetCredentialProviderData()664 public GetCredentialProviderData toGetCredentialProviderData() { 665 return new GetCredentialProviderData.Builder( 666 mComponentName.flattenToString()).setActionChips(prepareActionEntries()) 667 .setCredentialEntries(prepareCredentialEntries()) 668 .setAuthenticationEntries(prepareAuthenticationEntries()) 669 .setRemoteEntry(prepareRemoteEntry()) 670 .build(); 671 } 672 prepareActionEntries()673 private List<Entry> prepareActionEntries() { 674 List<Entry> actionEntries = new ArrayList<>(); 675 for (String key : mUiActionsEntries.keySet()) { 676 actionEntries.add(mUiActionsEntries.get(key).second); 677 } 678 return actionEntries; 679 } 680 prepareAuthenticationEntries()681 private List<AuthenticationEntry> prepareAuthenticationEntries() { 682 List<AuthenticationEntry> authEntries = new ArrayList<>(); 683 for (String key : mUiAuthenticationEntries.keySet()) { 684 authEntries.add(mUiAuthenticationEntries.get(key).second); 685 } 686 return authEntries; 687 } 688 prepareCredentialEntries()689 private List<Entry> prepareCredentialEntries() { 690 List<Entry> credEntries = new ArrayList<>(); 691 for (String key : mUiCredentialEntries.keySet()) { 692 credEntries.add(mUiCredentialEntries.get(key).second); 693 } 694 return credEntries; 695 } 696 prepareRemoteEntry()697 private Entry prepareRemoteEntry() { 698 if (mUiRemoteEntry == null || mUiRemoteEntry.first == null 699 || mUiRemoteEntry.second == null) { 700 return null; 701 } 702 return mUiRemoteEntry.second.second; 703 } 704 isEmptyResponse()705 private boolean isEmptyResponse() { 706 return mUiCredentialEntries.isEmpty() && mUiActionsEntries.isEmpty() 707 && mUiAuthenticationEntries.isEmpty() && mUiRemoteEntry == null; 708 } 709 isEmptyResponse(BeginGetCredentialResponse response)710 private boolean isEmptyResponse(BeginGetCredentialResponse response) { 711 return response.getCredentialEntries().isEmpty() && response.getActions().isEmpty() 712 && response.getAuthenticationActions().isEmpty() 713 && response.getRemoteCredentialEntry() == null; 714 } 715 716 @NonNull getCredentialEntryTypes()717 public Set<String> getCredentialEntryTypes() { 718 return mCredentialEntryTypes; 719 } 720 721 @Nullable getAuthenticationAction(String entryKey)722 public Action getAuthenticationAction(String entryKey) { 723 return mUiAuthenticationEntries.get(entryKey) == null ? null : 724 mUiAuthenticationEntries.get(entryKey).first; 725 } 726 727 @Nullable getActionEntry(String entryKey)728 public Action getActionEntry(String entryKey) { 729 return mUiActionsEntries.get(entryKey) == null 730 ? null : mUiActionsEntries.get(entryKey).first; 731 } 732 733 @Nullable getRemoteEntry(String entryKey)734 public RemoteEntry getRemoteEntry(String entryKey) { 735 return mUiRemoteEntry.first.equals(entryKey) && mUiRemoteEntry.second != null 736 ? mUiRemoteEntry.second.first : null; 737 } 738 739 @Nullable getCredentialEntry(String entryKey)740 public CredentialEntry getCredentialEntry(String entryKey) { 741 return mUiCredentialEntries.get(entryKey) == null 742 ? null : mUiCredentialEntries.get(entryKey).first; 743 } 744 updateAuthEntryWithNoCredentialsReceived(@ullable String entryKey)745 public void updateAuthEntryWithNoCredentialsReceived(@Nullable String entryKey) { 746 if (entryKey == null) { 747 // Auth entry from a different provider was selected by the user. 748 updatePreviousMostRecentAuthEntry(); 749 return; 750 } 751 updatePreviousMostRecentAuthEntry(); 752 updateMostRecentAuthEntry(entryKey); 753 } 754 updateMostRecentAuthEntry(String entryKey)755 private void updateMostRecentAuthEntry(String entryKey) { 756 AuthenticationEntry previousAuthenticationEntry = 757 mUiAuthenticationEntries.get(entryKey).second; 758 Action previousAuthenticationAction = mUiAuthenticationEntries.get(entryKey).first; 759 mUiAuthenticationEntries.put(entryKey, new Pair<>( 760 previousAuthenticationAction, 761 copyAuthEntryAndChangeStatus( 762 previousAuthenticationEntry, 763 AuthenticationEntry.STATUS_UNLOCKED_BUT_EMPTY_MOST_RECENT))); 764 } 765 updatePreviousMostRecentAuthEntry()766 private void updatePreviousMostRecentAuthEntry() { 767 Optional<Map.Entry<String, Pair<Action, AuthenticationEntry>>> 768 previousMostRecentAuthEntry = mUiAuthenticationEntries 769 .entrySet().stream().filter(e -> e.getValue().second.getStatus() 770 == AuthenticationEntry.STATUS_UNLOCKED_BUT_EMPTY_MOST_RECENT) 771 .findFirst(); 772 if (previousMostRecentAuthEntry.isEmpty()) { 773 return; 774 } 775 String id = previousMostRecentAuthEntry.get().getKey(); 776 mUiAuthenticationEntries.remove(id); 777 mUiAuthenticationEntries.put(id, new Pair<>( 778 previousMostRecentAuthEntry.get().getValue().first, 779 copyAuthEntryAndChangeStatus( 780 previousMostRecentAuthEntry.get().getValue().second, 781 AuthenticationEntry.STATUS_UNLOCKED_BUT_EMPTY_LESS_RECENT))); 782 } 783 copyAuthEntryAndChangeStatus( AuthenticationEntry from, Integer toStatus)784 private AuthenticationEntry copyAuthEntryAndChangeStatus( 785 AuthenticationEntry from, Integer toStatus) { 786 return new AuthenticationEntry(AUTHENTICATION_ACTION_ENTRY_KEY, from.getSubkey(), 787 from.getSlice(), toStatus, 788 from.getFrameworkExtrasIntent()); 789 } 790 } 791 setUpFillInIntentForRemoteEntry()792 private Intent setUpFillInIntentForRemoteEntry() { 793 return new Intent().putExtra(CredentialProviderService.EXTRA_GET_CREDENTIAL_REQUEST, 794 new GetCredentialRequest( 795 mCallingAppInfo, mCompleteRequest.getCredentialOptions())); 796 } 797 } 798