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