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 package com.android.server.credentials;
17 
18 import android.annotation.NonNull;
19 import android.app.PendingIntent;
20 import android.content.ComponentName;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.credentials.CredentialManager;
24 import android.credentials.CredentialProviderInfo;
25 import android.credentials.selection.DisabledProviderData;
26 import android.credentials.selection.IntentCreationResult;
27 import android.credentials.selection.IntentFactory;
28 import android.credentials.selection.ProviderData;
29 import android.credentials.selection.RequestInfo;
30 import android.credentials.selection.UserSelectionDialogResult;
31 import android.os.Bundle;
32 import android.os.Handler;
33 import android.os.IBinder;
34 import android.os.Looper;
35 import android.os.ResultReceiver;
36 import android.os.UserHandle;
37 import android.service.credentials.CredentialProviderInfoFactory;
38 
39 import com.android.server.credentials.metrics.RequestSessionMetric;
40 
41 import java.util.ArrayList;
42 import java.util.HashSet;
43 import java.util.List;
44 import java.util.Set;
45 import java.util.UUID;
46 
47 /** Initiates the Credential Manager UI and receives results. */
48 public class CredentialManagerUi {
49     @NonNull
50     private final CredentialManagerUiCallback mCallbacks;
51     @NonNull
52     private final Context mContext;
53 
54     private final int mUserId;
55 
56     private UiStatus mStatus;
57 
58     private final Set<ComponentName> mEnabledProviders;
59 
60     enum UiStatus {
61         IN_PROGRESS,
62         USER_INTERACTION,
63         NOT_STARTED, TERMINATED
64     }
65 
66     @NonNull
67     private final ResultReceiver mResultReceiver = new ResultReceiver(
68             new Handler(Looper.getMainLooper())) {
69         @Override
70         protected void onReceiveResult(int resultCode, Bundle resultData) {
71             handleUiResult(resultCode, resultData);
72         }
73     };
74 
handleUiResult(int resultCode, Bundle resultData)75     private void handleUiResult(int resultCode, Bundle resultData) {
76 
77         switch (resultCode) {
78             case UserSelectionDialogResult.RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION:
79                 mStatus = UiStatus.IN_PROGRESS;
80                 UserSelectionDialogResult selection = UserSelectionDialogResult
81                         .fromResultData(resultData);
82                 if (selection != null) {
83                     mCallbacks.onUiSelection(selection);
84                 }
85                 break;
86             case UserSelectionDialogResult.RESULT_CODE_DIALOG_USER_CANCELED:
87 
88                 mStatus = UiStatus.TERMINATED;
89                 mCallbacks.onUiCancellation(/* isUserCancellation= */ true);
90                 break;
91             case UserSelectionDialogResult.RESULT_CODE_CANCELED_AND_LAUNCHED_SETTINGS:
92 
93                 mStatus = UiStatus.TERMINATED;
94                 mCallbacks.onUiCancellation(/* isUserCancellation= */ false);
95                 break;
96             case UserSelectionDialogResult.RESULT_CODE_DATA_PARSING_FAILURE:
97                 mStatus = UiStatus.TERMINATED;
98                 mCallbacks.onUiSelectorInvocationFailure();
99                 break;
100             default:
101                 mStatus = UiStatus.IN_PROGRESS;
102                 mCallbacks.onUiSelectorInvocationFailure();
103                 break;
104         }
105     }
106 
107     /** Creates intent that is ot be invoked to cancel an in-progress UI session. */
createCancelIntent(IBinder requestId, String packageName)108     public Intent createCancelIntent(IBinder requestId, String packageName) {
109         return IntentFactory.createCancelUiIntent(mContext, requestId,
110                 /*shouldShowCancellationUi=*/ true, packageName);
111     }
112 
113     /**
114      * Interface to be implemented by any class that wishes to get callbacks from the UI.
115      */
116     public interface CredentialManagerUiCallback {
117         /** Called when the user makes a selection. */
onUiSelection(UserSelectionDialogResult selection)118         void onUiSelection(UserSelectionDialogResult selection);
119 
120         /** Called when the UI is canceled without a successful provider result. */
onUiCancellation(boolean isUserCancellation)121         void onUiCancellation(boolean isUserCancellation);
122 
123         /** Called when the selector UI fails to come up (mostly due to parsing issue today). */
onUiSelectorInvocationFailure()124         void onUiSelectorInvocationFailure();
125     }
126 
CredentialManagerUi(Context context, int userId, CredentialManagerUiCallback callbacks, Set<ComponentName> enabledProviders)127     public CredentialManagerUi(Context context, int userId,
128             CredentialManagerUiCallback callbacks, Set<ComponentName> enabledProviders) {
129         mContext = context;
130         mUserId = userId;
131         mCallbacks = callbacks;
132         mEnabledProviders = enabledProviders;
133         mStatus = UiStatus.IN_PROGRESS;
134     }
135 
136     /** Set status for credential manager UI */
setStatus(UiStatus status)137     public void setStatus(UiStatus status) {
138         mStatus = status;
139     }
140 
141     /** Returns status for credential manager UI */
getStatus()142     public UiStatus getStatus() {
143         return mStatus;
144     }
145 
146     /**
147      * Creates a {@link PendingIntent} to be used to invoke the credential manager selector UI,
148      * by the calling app process. The bottom-sheet navigates to the default page when the intent
149      * is invoked.
150      *
151      * @param requestInfo            the information about the request
152      * @param providerDataList       the list of provider data from remote providers
153      */
createPendingIntent( RequestInfo requestInfo, ArrayList<ProviderData> providerDataList, RequestSessionMetric requestSessionMetric)154     public PendingIntent createPendingIntent(
155             RequestInfo requestInfo, ArrayList<ProviderData> providerDataList,
156             RequestSessionMetric requestSessionMetric) {
157         List<CredentialProviderInfo> allProviders =
158                 CredentialProviderInfoFactory.getCredentialProviderServices(
159                         mContext,
160                         mUserId,
161                         CredentialManager.PROVIDER_FILTER_USER_PROVIDERS_ONLY,
162                         mEnabledProviders,
163                         // Don't need primary providers here.
164                         new HashSet<ComponentName>());
165 
166         List<DisabledProviderData> disabledProviderDataList = allProviders.stream()
167                 .filter(provider -> !provider.isEnabled())
168                 .map(disabledProvider -> new DisabledProviderData(
169                         disabledProvider.getComponentName().flattenToString())).toList();
170 
171         IntentCreationResult intentCreationResult = IntentFactory
172                 .createCredentialSelectorIntentForCredMan(mContext, requestInfo, providerDataList,
173                         new ArrayList<>(disabledProviderDataList), mResultReceiver);
174         requestSessionMetric.collectUiConfigurationResults(
175                 mContext, intentCreationResult, mUserId);
176         Intent intent = intentCreationResult.getIntent();
177         intent.setAction(UUID.randomUUID().toString());
178         //TODO: Create unique pending intent using request code and cancel any pre-existing pending
179         // intents
180         return PendingIntent.getActivityAsUser(
181                 mContext, /*requestCode=*/0, intent,
182                 PendingIntent.FLAG_MUTABLE, /*options=*/null,
183                 UserHandle.of(mUserId));
184     }
185 
186     /**
187      * Creates an {@link Intent} to be used to invoke the credential manager selector UI,
188      * by the calling app process. This intent is invoked from the Autofill flow, when the user
189      * requests to bring up the 'All Options' page of the credential bottom-sheet. When the user
190      * clicks on the pinned entry, the intent will bring up the 'All Options' page of the
191      * bottom-sheet. The provider data list is processed by the credential autofill service for
192      * each autofill id and passed in as extras in the pending intent set as authentication
193      * of the pinned entry.
194      *
195      * @param requestInfo            the information about the request
196      * @param requestSessionMetric   the metric object for logging
197      */
createIntentForAutofill(RequestInfo requestInfo, RequestSessionMetric requestSessionMetric)198     public Intent createIntentForAutofill(RequestInfo requestInfo,
199             RequestSessionMetric requestSessionMetric) {
200         IntentCreationResult intentCreationResult = IntentFactory
201                 .createCredentialSelectorIntentForAutofill(mContext, requestInfo, new ArrayList<>(),
202                         mResultReceiver);
203         requestSessionMetric.collectUiConfigurationResults(
204                 mContext, intentCreationResult, mUserId);
205         return intentCreationResult.getIntent();
206     }
207 }
208