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