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.UserIdInt;
21 import android.app.PendingIntent;
22 import android.content.ComponentName;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.credentials.CredentialProviderInfo;
26 import android.credentials.ui.ProviderData;
27 import android.credentials.ui.UserSelectionDialogResult;
28 import android.os.Binder;
29 import android.os.CancellationSignal;
30 import android.os.Handler;
31 import android.os.IBinder;
32 import android.os.Looper;
33 import android.os.RemoteException;
34 import android.os.UserHandle;
35 import android.service.credentials.CallingAppInfo;
36 import android.util.Slog;
37 
38 import com.android.internal.R;
39 import com.android.server.credentials.metrics.ApiName;
40 import com.android.server.credentials.metrics.ApiStatus;
41 import com.android.server.credentials.metrics.ProviderSessionMetric;
42 import com.android.server.credentials.metrics.ProviderStatusForMetrics;
43 import com.android.server.credentials.metrics.RequestSessionMetric;
44 
45 import java.util.ArrayList;
46 import java.util.Map;
47 import java.util.Set;
48 import java.util.concurrent.ConcurrentHashMap;
49 
50 /**
51  * Base class of a request session, that listens to UI events. This class must be extended
52  * every time a new response type is expected from the providers.
53  */
54 abstract class RequestSession<T, U, V> implements CredentialManagerUi.CredentialManagerUiCallback {
55     private static final String TAG = "RequestSession";
56 
57     public interface SessionLifetime {
58         /** Called when the user makes a selection. */
onFinishRequestSession(@serIdInt int userId, IBinder token)59         void onFinishRequestSession(@UserIdInt int userId, IBinder token);
60     }
61 
62     // TODO: Revise access levels of attributes
63     @NonNull
64     protected final T mClientRequest;
65     @NonNull
66     protected final U mClientCallback;
67     @NonNull
68     protected final IBinder mRequestId;
69     @NonNull
70     protected final Context mContext;
71     @NonNull
72     protected final CredentialManagerUi mCredentialManagerUi;
73     @NonNull
74     protected final String mRequestType;
75     @NonNull
76     protected final Handler mHandler;
77     @UserIdInt
78     protected final int mUserId;
79 
80     protected final int mUniqueSessionInteger;
81     private final int mCallingUid;
82     @NonNull
83     protected final CallingAppInfo mClientAppInfo;
84     @NonNull
85     protected final CancellationSignal mCancellationSignal;
86 
87     protected final Map<String, ProviderSession> mProviders = new ConcurrentHashMap<>();
88     protected final RequestSessionMetric mRequestSessionMetric;
89     protected final String mHybridService;
90 
91     protected final Object mLock;
92 
93     protected final SessionLifetime mSessionCallback;
94 
95     private final Set<ComponentName> mEnabledProviders;
96 
97     protected PendingIntent mPendingIntent;
98 
99     @NonNull
100     protected RequestSessionStatus mRequestSessionStatus =
101             RequestSessionStatus.IN_PROGRESS;
102 
103     /** The status in which a given request session is. */
104     enum RequestSessionStatus {
105         /** Request is in progress. This is the status a request session is instantiated with. */
106         IN_PROGRESS,
107         /** Request has been cancelled by the developer. */
108         CANCELLED,
109         /** Request is complete. */
110         COMPLETE
111     }
112 
RequestSession(@onNull Context context, RequestSession.SessionLifetime sessionCallback, Object lock, @UserIdInt int userId, int callingUid, @NonNull T clientRequest, U clientCallback, @NonNull String requestType, CallingAppInfo callingAppInfo, Set<ComponentName> enabledProviders, CancellationSignal cancellationSignal, long timestampStarted)113     protected RequestSession(@NonNull Context context,
114             RequestSession.SessionLifetime sessionCallback,
115             Object lock, @UserIdInt int userId, int callingUid,
116             @NonNull T clientRequest, U clientCallback,
117             @NonNull String requestType,
118             CallingAppInfo callingAppInfo,
119             Set<ComponentName> enabledProviders,
120             CancellationSignal cancellationSignal, long timestampStarted) {
121         mContext = context;
122         mLock = lock;
123         mSessionCallback = sessionCallback;
124         mUserId = userId;
125         mCallingUid = callingUid;
126         mClientRequest = clientRequest;
127         mClientCallback = clientCallback;
128         mRequestType = requestType;
129         mClientAppInfo = callingAppInfo;
130         mEnabledProviders = enabledProviders;
131         mCancellationSignal = cancellationSignal;
132         mHandler = new Handler(Looper.getMainLooper(), null, true);
133         mRequestId = new Binder();
134         mCredentialManagerUi = new CredentialManagerUi(mContext,
135                 mUserId, this, mEnabledProviders);
136         mHybridService = context.getResources().getString(
137                 R.string.config_defaultCredentialManagerHybridService);
138         mUniqueSessionInteger = MetricUtilities.getHighlyUniqueInteger();
139         mRequestSessionMetric = new RequestSessionMetric(mUniqueSessionInteger,
140                 MetricUtilities.getHighlyUniqueInteger());
141         mRequestSessionMetric.collectInitialPhaseMetricInfo(timestampStarted,
142                 mCallingUid, ApiName.getMetricCodeFromRequestInfo(mRequestType));
143         setCancellationListener();
144     }
145 
setCancellationListener()146     private void setCancellationListener() {
147         mCancellationSignal.setOnCancelListener(
148                 () -> {
149                     boolean isUiActive = maybeCancelUi();
150                     finishSession(!isUiActive);
151                 }
152         );
153     }
154 
maybeCancelUi()155     private boolean maybeCancelUi() {
156         if (mCredentialManagerUi.getStatus()
157                 == CredentialManagerUi.UiStatus.USER_INTERACTION) {
158             final long originalCallingUidToken = Binder.clearCallingIdentity();
159             try {
160                 mContext.startActivityAsUser(mCredentialManagerUi.createCancelIntent(
161                                 mRequestId, mClientAppInfo.getPackageName())
162                         .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), UserHandle.of(mUserId));
163                 return true;
164             } finally {
165                 Binder.restoreCallingIdentity(originalCallingUidToken);
166             }
167         }
168         return false;
169     }
170 
initiateProviderSession(CredentialProviderInfo providerInfo, RemoteCredentialService remoteCredentialService)171     public abstract ProviderSession initiateProviderSession(CredentialProviderInfo providerInfo,
172             RemoteCredentialService remoteCredentialService);
173 
launchUiWithProviderData(ArrayList<ProviderData> providerDataList)174     protected abstract void launchUiWithProviderData(ArrayList<ProviderData> providerDataList);
175 
invokeClientCallbackSuccess(V response)176     protected abstract void invokeClientCallbackSuccess(V response) throws RemoteException;
177 
invokeClientCallbackError(String errorType, String errorMsg)178     protected abstract void invokeClientCallbackError(String errorType, String errorMsg) throws
179             RemoteException;
180 
addProviderSession(ComponentName componentName, ProviderSession providerSession)181     public void addProviderSession(ComponentName componentName, ProviderSession providerSession) {
182         mProviders.put(componentName.flattenToString(), providerSession);
183     }
184 
185     // UI callbacks
186 
187     @Override // from CredentialManagerUiCallbacks
onUiSelection(UserSelectionDialogResult selection)188     public void onUiSelection(UserSelectionDialogResult selection) {
189         if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) {
190             Slog.w(TAG, "Request has already been completed. This is strange.");
191             return;
192         }
193         if (isSessionCancelled()) {
194             finishSession(/*propagateCancellation=*/true);
195             return;
196         }
197         String providerId = selection.getProviderId();
198         ProviderSession providerSession = mProviders.get(providerId);
199         if (providerSession == null) {
200             Slog.w(TAG, "providerSession not found in onUiSelection. This is strange.");
201             return;
202         }
203         ProviderSessionMetric providerSessionMetric = providerSession.mProviderSessionMetric;
204         int initialAuthMetricsProvider = providerSessionMetric.getBrowsedAuthenticationMetric()
205                 .size();
206         mRequestSessionMetric.collectMetricPerBrowsingSelect(selection,
207                 providerSession.mProviderSessionMetric.getCandidatePhasePerProviderMetric());
208         providerSession.onUiEntrySelected(selection.getEntryKey(),
209                 selection.getEntrySubkey(), selection.getPendingIntentProviderResponse());
210         int numAuthPerProvider = providerSessionMetric.getBrowsedAuthenticationMetric().size();
211         boolean authMetricLogged = (numAuthPerProvider - initialAuthMetricsProvider) == 1;
212         if (authMetricLogged) {
213             mRequestSessionMetric.logAuthEntry(
214                     providerSession.mProviderSessionMetric.getBrowsedAuthenticationMetric()
215                             .get(numAuthPerProvider - 1));
216         }
217     }
218 
finishSession(boolean propagateCancellation)219     protected void finishSession(boolean propagateCancellation) {
220         Slog.i(TAG, "finishing session with propagateCancellation " + propagateCancellation);
221         if (propagateCancellation) {
222             mProviders.values().forEach(ProviderSession::cancelProviderRemoteSession);
223         }
224         mRequestSessionStatus = RequestSessionStatus.COMPLETE;
225         mProviders.clear();
226         clearRequestSessionLocked();
227     }
228 
cancelExistingPendingIntent()229     void cancelExistingPendingIntent() {
230         if (mPendingIntent != null) {
231             try {
232                 mPendingIntent.cancel();
233                 mPendingIntent = null;
234             } catch (Exception e) {
235                 Slog.e(TAG, "Unable to cancel existing pending intent", e);
236             }
237         }
238     }
239 
clearRequestSessionLocked()240     private void clearRequestSessionLocked() {
241         synchronized (mLock) {
242             mSessionCallback.onFinishRequestSession(mUserId, mRequestId);
243         }
244     }
245 
isAnyProviderPending()246     protected boolean isAnyProviderPending() {
247         for (ProviderSession session : mProviders.values()) {
248             if (ProviderSession.isStatusWaitingForRemoteResponse(session.getStatus())) {
249                 return true;
250             }
251         }
252         return false;
253     }
254 
isSessionCancelled()255     protected boolean isSessionCancelled() {
256         return mCancellationSignal.isCanceled();
257     }
258 
259     /**
260      * Returns true if at least one provider is ready for UI invocation, and no
261      * provider is pending a response.
262      */
isUiInvocationNeeded()263     protected boolean isUiInvocationNeeded() {
264         for (ProviderSession session : mProviders.values()) {
265             if (ProviderSession.isUiInvokingStatus(session.getStatus())) {
266                 return true;
267             } else if (ProviderSession.isStatusWaitingForRemoteResponse(session.getStatus())) {
268                 return false;
269             }
270         }
271         return false;
272     }
273 
getProviderDataAndInitiateUi()274     void getProviderDataAndInitiateUi() {
275         ArrayList<ProviderData> providerDataList = getProviderDataForUi();
276         if (!providerDataList.isEmpty()) {
277             launchUiWithProviderData(providerDataList);
278         }
279     }
280 
281     @NonNull
getProviderDataForUi()282     protected ArrayList<ProviderData> getProviderDataForUi() {
283         Slog.i(TAG, "For ui, provider data size: " + mProviders.size());
284         ArrayList<ProviderData> providerDataList = new ArrayList<>();
285         mRequestSessionMetric.logCandidatePhaseMetrics(mProviders);
286 
287         if (isSessionCancelled()) {
288             finishSession(/*propagateCancellation=*/true);
289             return providerDataList;
290         }
291 
292         for (ProviderSession session : mProviders.values()) {
293             ProviderData providerData = session.prepareUiData();
294             if (providerData != null) {
295                 providerDataList.add(providerData);
296             }
297         }
298         return providerDataList;
299     }
300 
301     /**
302      * Allows subclasses to directly finalize the call and set closing metrics on response.
303      *
304      * @param response the response associated with the API call that just completed
305      */
respondToClientWithResponseAndFinish(V response)306     protected void respondToClientWithResponseAndFinish(V response) {
307         mRequestSessionMetric.logCandidateAggregateMetrics(mProviders);
308         mRequestSessionMetric.collectFinalPhaseProviderMetricStatus(/*has_exception=*/ false,
309                 ProviderStatusForMetrics.FINAL_SUCCESS);
310         if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) {
311             Slog.w(TAG, "Request has already been completed. This is strange.");
312             return;
313         }
314         if (isSessionCancelled()) {
315             mRequestSessionMetric.logApiCalledAtFinish(
316                     /*apiStatus=*/ ApiStatus.CLIENT_CANCELED.getMetricCode());
317             finishSession(/*propagateCancellation=*/true);
318             return;
319         }
320         try {
321             invokeClientCallbackSuccess(response);
322             mRequestSessionMetric.logApiCalledAtFinish(
323                     /*apiStatus=*/ ApiStatus.SUCCESS.getMetricCode());
324         } catch (RemoteException e) {
325             mRequestSessionMetric.collectFinalPhaseProviderMetricStatus(
326                     /*has_exception=*/ true, ProviderStatusForMetrics.FINAL_FAILURE);
327             Slog.e(TAG, "Issue while responding to client with a response : " + e);
328             mRequestSessionMetric.logApiCalledAtFinish(
329                     /*apiStatus=*/ ApiStatus.FAILURE.getMetricCode());
330         }
331         finishSession(/*propagateCancellation=*/false);
332     }
333 
334     /**
335      * Allows subclasses to directly finalize the call and set closing metrics on error completion.
336      *
337      * @param errorType the type of error given back in the flow
338      * @param errorMsg  the error message given back in the flow
339      */
respondToClientWithErrorAndFinish(String errorType, String errorMsg)340     protected void respondToClientWithErrorAndFinish(String errorType, String errorMsg) {
341         mRequestSessionMetric.logCandidateAggregateMetrics(mProviders);
342         mRequestSessionMetric.collectFinalPhaseProviderMetricStatus(
343                 /*has_exception=*/ true, ProviderStatusForMetrics.FINAL_FAILURE);
344         if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) {
345             Slog.w(TAG, "Request has already been completed. This is strange.");
346             return;
347         }
348         if (isSessionCancelled()) {
349             mRequestSessionMetric.logApiCalledAtFinish(
350                     /*apiStatus=*/ ApiStatus.CLIENT_CANCELED.getMetricCode());
351             finishSession(/*propagateCancellation=*/true);
352             return;
353         }
354 
355         try {
356             invokeClientCallbackError(errorType, errorMsg);
357         } catch (RemoteException e) {
358             Slog.e(TAG, "Issue while responding to client with error : " + e);
359         }
360         boolean isUserCanceled = errorType.contains(MetricUtilities.USER_CANCELED_SUBSTRING);
361         mRequestSessionMetric.logFailureOrUserCancel(isUserCanceled);
362         finishSession(/*propagateCancellation=*/false);
363     }
364 
365     /**
366      * Reveals if a certain provider is primary after ensuring it exists at all in the designated
367      * provider info.
368      *
369      * @param componentName used to identify the provider we want to check primary status for
370      */
isPrimaryProviderViaProviderInfo(ComponentName componentName)371     protected boolean isPrimaryProviderViaProviderInfo(ComponentName componentName) {
372         var chosenProviderSession = mProviders.get(componentName.flattenToString());
373         return chosenProviderSession != null && chosenProviderSession.mProviderInfo != null
374                 && chosenProviderSession.mProviderInfo.isPrimary();
375     }
376 }
377