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