• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.CredentialManager;
26 import android.credentials.CredentialProviderInfo;
27 import android.credentials.flags.Flags;
28 import android.credentials.selection.ProviderData;
29 import android.credentials.selection.UserSelectionDialogResult;
30 import android.os.Binder;
31 import android.os.CancellationSignal;
32 import android.os.Handler;
33 import android.os.IBinder;
34 import android.os.IInterface;
35 import android.os.Looper;
36 import android.os.RemoteException;
37 import android.os.UserHandle;
38 import android.service.credentials.CallingAppInfo;
39 import android.util.Slog;
40 
41 import com.android.internal.R;
42 import com.android.server.credentials.metrics.ApiName;
43 import com.android.server.credentials.metrics.ApiStatus;
44 import com.android.server.credentials.metrics.ProviderSessionMetric;
45 import com.android.server.credentials.metrics.ProviderStatusForMetrics;
46 import com.android.server.credentials.metrics.RequestSessionMetric;
47 
48 import java.util.ArrayList;
49 import java.util.Map;
50 import java.util.Set;
51 import java.util.concurrent.ConcurrentHashMap;
52 
53 /**
54  * Base class of a request session, that listens to UI events. This class must be extended
55  * every time a new response type is expected from the providers.
56  */
57 abstract class RequestSession<T, U, V> implements CredentialManagerUi.CredentialManagerUiCallback {
58     private static final String TAG = CredentialManager.TAG;
59 
60     public interface SessionLifetime {
61         /** Called when the user makes a selection. */
onFinishRequestSession(@serIdInt int userId, IBinder token)62         void onFinishRequestSession(@UserIdInt int userId, IBinder token);
63     }
64 
65     // TODO: Revise access levels of attributes
66     @NonNull
67     protected final T mClientRequest;
68     @NonNull
69     protected final U mClientCallback;
70     @NonNull
71     protected final IBinder mRequestId;
72     @NonNull
73     protected final Context mContext;
74     @NonNull
75     protected final CredentialManagerUi mCredentialManagerUi;
76     @NonNull
77     protected final String mRequestType;
78     @NonNull
79     protected final Handler mHandler;
80     @UserIdInt
81     protected final int mUserId;
82 
83     protected final int mUniqueSessionInteger;
84     private final int mCallingUid;
85     @NonNull
86     protected final CallingAppInfo mClientAppInfo;
87     @NonNull
88     protected final CancellationSignal mCancellationSignal;
89 
90     protected final Map<String, ProviderSession> mProviders = new ConcurrentHashMap<>();
91     protected final RequestSessionMetric mRequestSessionMetric;
92     protected final String mHybridService;
93 
94     protected final Object mLock;
95 
96     protected final SessionLifetime mSessionCallback;
97 
98     private final Set<ComponentName> mEnabledProviders;
99 
100     private final RequestSessionDeathRecipient mDeathRecipient =
101             new RequestSessionDeathRecipient();
102 
103     protected PendingIntent mPendingIntent;
104 
105     @NonNull
106     protected RequestSessionStatus mRequestSessionStatus =
107             RequestSessionStatus.IN_PROGRESS;
108 
109     /** The status in which a given request session is. */
110     enum RequestSessionStatus {
111         /** Request is in progress. This is the status a request session is instantiated with. */
112         IN_PROGRESS,
113         /** Request has been cancelled by the developer. */
114         CANCELLED,
115         /** Request is complete. */
116         COMPLETE
117     }
118 
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, boolean shouldBindClientToDeath)119     protected RequestSession(@NonNull Context context,
120             RequestSession.SessionLifetime sessionCallback,
121             Object lock, @UserIdInt int userId, int callingUid,
122             @NonNull T clientRequest, U clientCallback,
123             @NonNull String requestType,
124             CallingAppInfo callingAppInfo,
125             Set<ComponentName> enabledProviders,
126             CancellationSignal cancellationSignal, long timestampStarted,
127             boolean shouldBindClientToDeath) {
128         mContext = context;
129         mLock = lock;
130         mSessionCallback = sessionCallback;
131         mUserId = userId;
132         mCallingUid = callingUid;
133         mClientRequest = clientRequest;
134         mClientCallback = clientCallback;
135         mRequestType = requestType;
136         mClientAppInfo = callingAppInfo;
137         mEnabledProviders = enabledProviders;
138         mCancellationSignal = cancellationSignal;
139         mHandler = new Handler(Looper.getMainLooper(), null, true);
140         mRequestId = new Binder();
141         mCredentialManagerUi = new CredentialManagerUi(mContext,
142                 mUserId, this, mEnabledProviders);
143         mHybridService = context.getResources().getString(
144                 R.string.config_defaultCredentialManagerHybridService);
145         mUniqueSessionInteger = MetricUtilities.getHighlyUniqueInteger();
146         mRequestSessionMetric = new RequestSessionMetric(mUniqueSessionInteger,
147                 MetricUtilities.getHighlyUniqueInteger());
148         mRequestSessionMetric.collectInitialPhaseMetricInfo(timestampStarted,
149                 mCallingUid, ApiName.getMetricCodeFromRequestInfo(mRequestType));
150         setCancellationListener();
151         if (shouldBindClientToDeath && Flags.clearSessionEnabled()) {
152             if (mClientCallback != null && mClientCallback instanceof IInterface) {
153                 setUpClientCallbackListener(((IInterface) mClientCallback).asBinder());
154             }
155         }
156     }
157 
setUpClientCallbackListener(IBinder clientBinder)158     protected void setUpClientCallbackListener(IBinder clientBinder) {
159         if (mClientCallback != null && mClientCallback instanceof IInterface) {
160             IInterface callback = (IInterface) mClientCallback;
161             try {
162                 clientBinder.linkToDeath(mDeathRecipient, 0);
163             } catch (RemoteException e) {
164                 Slog.e(TAG, e.getMessage());
165             }
166         }
167     }
168 
setCancellationListener()169     private void setCancellationListener() {
170         mCancellationSignal.setOnCancelListener(
171                 () -> {
172                     Slog.d(TAG, "Cancellation invoked from the client - clearing session");
173                     boolean isUiActive = maybeCancelUi();
174                     finishSession(!isUiActive, ApiStatus.CLIENT_CANCELED.getMetricCode());
175                 }
176         );
177     }
178 
maybeCancelUi()179     private boolean maybeCancelUi() {
180         if (mCredentialManagerUi.getStatus()
181                 == CredentialManagerUi.UiStatus.USER_INTERACTION) {
182             final long originalCallingUidToken = Binder.clearCallingIdentity();
183             try {
184                 mContext.startActivityAsUser(mCredentialManagerUi.createCancelIntent(
185                                 mRequestId, mClientAppInfo.getPackageName())
186                         .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), UserHandle.of(mUserId));
187                 return true;
188             } finally {
189                 Binder.restoreCallingIdentity(originalCallingUidToken);
190             }
191         }
192         return false;
193     }
194 
isUiWaitingForData()195     private boolean isUiWaitingForData() {
196         // Technically, the status can also be IN_PROGRESS when the user has made a selection
197         // so this an over estimation, but safe to do so as it is used for cancellation
198         // propagation to the provider in a very narrow time frame. If provider has
199         // already responded, cancellation is not an issue as the cancellation listener
200         // is independent of the service binding.
201         // TODO(b/313512500): Do not propagate cancelation if provider has responded in
202         // query phase.
203         return mCredentialManagerUi.getStatus() == CredentialManagerUi.UiStatus.IN_PROGRESS;
204     }
205 
initiateProviderSession(CredentialProviderInfo providerInfo, RemoteCredentialService remoteCredentialService)206     public abstract ProviderSession initiateProviderSession(CredentialProviderInfo providerInfo,
207             RemoteCredentialService remoteCredentialService);
208 
launchUiWithProviderData(ArrayList<ProviderData> providerDataList)209     protected abstract void launchUiWithProviderData(ArrayList<ProviderData> providerDataList);
210 
invokeClientCallbackSuccess(V response)211     protected abstract void invokeClientCallbackSuccess(V response) throws RemoteException;
212 
invokeClientCallbackError(String errorType, String errorMsg)213     protected abstract void invokeClientCallbackError(String errorType, String errorMsg) throws
214             RemoteException;
215 
addProviderSession(ComponentName componentName, ProviderSession providerSession)216     public void addProviderSession(ComponentName componentName, ProviderSession providerSession) {
217         mProviders.put(componentName.flattenToString(), providerSession);
218     }
219 
220     // UI callbacks
221 
222     @Override // from CredentialManagerUiCallbacks
onUiSelection(UserSelectionDialogResult selection)223     public void onUiSelection(UserSelectionDialogResult selection) {
224         if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) {
225             Slog.w(TAG, "Request has already been completed. This is strange.");
226             return;
227         }
228         if (isSessionCancelled()) {
229             finishSession(/*propagateCancellation=*/true,
230                     ApiStatus.CLIENT_CANCELED.getMetricCode());
231             return;
232         }
233         String providerId = selection.getProviderId();
234         ProviderSession providerSession = mProviders.get(providerId);
235         if (providerSession == null) {
236             Slog.w(TAG, "providerSession not found in onUiSelection. This is strange.");
237             return;
238         }
239 
240         ProviderSessionMetric providerSessionMetric = providerSession.mProviderSessionMetric;
241         int initialAuthMetricsProvider = providerSessionMetric.getBrowsedAuthenticationMetric()
242                 .size();
243         mRequestSessionMetric.collectMetricPerBrowsingSelect(selection,
244                 providerSession.mProviderSessionMetric.getCandidatePhasePerProviderMetric());
245         providerSession.onUiEntrySelected(selection.getEntryKey(),
246                 selection.getEntrySubkey(), selection.getPendingIntentProviderResponse());
247         int numAuthPerProvider = providerSessionMetric.getBrowsedAuthenticationMetric().size();
248         boolean authMetricLogged = (numAuthPerProvider - initialAuthMetricsProvider) == 1;
249         if (authMetricLogged) {
250             mRequestSessionMetric.logAuthEntry(
251                     providerSession.mProviderSessionMetric.getBrowsedAuthenticationMetric()
252                             .get(numAuthPerProvider - 1));
253         }
254     }
255 
finishSession(boolean propagateCancellation, int apiStatus)256     protected void finishSession(boolean propagateCancellation, int apiStatus) {
257         Slog.i(TAG, "finishing session with propagateCancellation " + propagateCancellation);
258         if (propagateCancellation) {
259             mProviders.values().forEach(ProviderSession::cancelProviderRemoteSession);
260         }
261         mRequestSessionStatus = RequestSessionStatus.COMPLETE;
262         if (Flags.fixMetricDuplicationEmits()) {
263             logTrackOneCandidatesAndPrepareFinalPhaseLogs(apiStatus);
264         }
265         mRequestSessionMetric.logApiCalledAtFinish(apiStatus);
266         mProviders.clear();
267         clearRequestSessionLocked();
268     }
269 
270     /**
271      * Ensures all logging done in final phase methods only occur within the 'finishSession'.
272      */
logTrackOneCandidatesAndPrepareFinalPhaseLogs(int apiStatus)273     private void logTrackOneCandidatesAndPrepareFinalPhaseLogs(int apiStatus) {
274         mRequestSessionMetric.logCandidateAggregateMetrics(mProviders);
275         if (isRespondingWithError(apiStatus)) {
276             mRequestSessionMetric.collectFinalPhaseProviderMetricStatus(
277                     /*hasException=*/ true, ProviderStatusForMetrics.FINAL_FAILURE);
278         } else if (isRespondingWithUserCanceledError(apiStatus)) {
279             mRequestSessionMetric.collectFinalPhaseProviderMetricStatus(
280                     /*hasException=*/false, ProviderStatusForMetrics.FINAL_FAILURE
281             );
282         } else if (isRespondingWithSuccess(apiStatus)) {
283             mRequestSessionMetric.collectFinalPhaseProviderMetricStatus(/*hasException=*/ false,
284                     ProviderStatusForMetrics.FINAL_SUCCESS);
285         }
286     }
287 
cancelExistingPendingIntent()288     void cancelExistingPendingIntent() {
289         if (mPendingIntent != null) {
290             try {
291                 mPendingIntent.cancel();
292                 mPendingIntent = null;
293             } catch (Exception e) {
294                 Slog.e(TAG, "Unable to cancel existing pending intent", e);
295             }
296         }
297     }
298 
clearRequestSessionLocked()299     private void clearRequestSessionLocked() {
300         synchronized (mLock) {
301             mSessionCallback.onFinishRequestSession(mUserId, mRequestId);
302         }
303     }
304 
isAnyProviderPending()305     protected boolean isAnyProviderPending() {
306         for (ProviderSession session : mProviders.values()) {
307             if (ProviderSession.isStatusWaitingForRemoteResponse(session.getStatus())) {
308                 return true;
309             }
310         }
311         return false;
312     }
313 
isSessionCancelled()314     protected boolean isSessionCancelled() {
315         return mCancellationSignal.isCanceled();
316     }
317 
318     /**
319      * Returns true if at least one provider is ready for UI invocation, and no
320      * provider is pending a response.
321      */
isUiInvocationNeeded()322     protected boolean isUiInvocationNeeded() {
323         for (ProviderSession session : mProviders.values()) {
324             if (ProviderSession.isUiInvokingStatus(session.getStatus())) {
325                 return true;
326             } else if (ProviderSession.isStatusWaitingForRemoteResponse(session.getStatus())) {
327                 return false;
328             }
329         }
330         return false;
331     }
332 
getProviderDataAndInitiateUi()333     void getProviderDataAndInitiateUi() {
334         ArrayList<ProviderData> providerDataList = getProviderDataForUi();
335         if (!providerDataList.isEmpty()) {
336             launchUiWithProviderData(providerDataList);
337         }
338     }
339 
340     @NonNull
getProviderDataForUi()341     protected ArrayList<ProviderData> getProviderDataForUi() {
342         Slog.i(TAG, "For ui, provider data size: " + mProviders.size());
343         ArrayList<ProviderData> providerDataList = new ArrayList<>();
344         mRequestSessionMetric.logCandidatePhaseMetrics(mProviders);
345 
346         if (isSessionCancelled()) {
347             finishSession(/*propagateCancellation=*/true,
348                     ApiStatus.CLIENT_CANCELED.getMetricCode());
349             return providerDataList;
350         }
351 
352         for (ProviderSession session : mProviders.values()) {
353             ProviderData providerData = session.prepareUiData();
354             if (providerData != null) {
355                 providerDataList.add(providerData);
356             }
357         }
358         return providerDataList;
359     }
360 
361     /**
362      * Allows subclasses to directly finalize the call and set closing metrics on response.
363      *
364      * @param response the response associated with the API call that just completed
365      */
respondToClientWithResponseAndFinish(V response)366     protected void respondToClientWithResponseAndFinish(V response) {
367         if (!Flags.fixMetricDuplicationEmits()) {
368             mRequestSessionMetric.logCandidateAggregateMetrics(mProviders);
369             mRequestSessionMetric.collectFinalPhaseProviderMetricStatus(/*hasException=*/ false,
370                     ProviderStatusForMetrics.FINAL_SUCCESS);
371         }
372         if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) {
373             Slog.w(TAG, "Request has already been completed. This is strange.");
374             return;
375         }
376         if (isSessionCancelled()) {
377             finishSession(/*propagateCancellation=*/true,
378                     ApiStatus.CLIENT_CANCELED.getMetricCode());
379             return;
380         }
381         try {
382             invokeClientCallbackSuccess(response);
383             finishSession(/*propagateCancellation=*/false,
384                     ApiStatus.SUCCESS.getMetricCode());
385         } catch (RemoteException e) {
386             if (!Flags.fixMetricDuplicationEmits()) {
387                 mRequestSessionMetric.collectFinalPhaseProviderMetricStatus(
388                         /*hasException=*/ true, ProviderStatusForMetrics.FINAL_FAILURE);
389             }
390             Slog.e(TAG, "Issue while responding to client with a response : " + e);
391             finishSession(/*propagateCancellation=*/false, ApiStatus.FAILURE.getMetricCode());
392         }
393     }
394 
395     /**
396      * Allows subclasses to directly finalize the call and set closing metrics on error completion.
397      *
398      * @param errorType the type of error given back in the flow
399      * @param errorMsg  the error message given back in the flow
400      */
respondToClientWithErrorAndFinish(String errorType, String errorMsg)401     protected void respondToClientWithErrorAndFinish(String errorType, String errorMsg) {
402         if (!Flags.fixMetricDuplicationEmits()) {
403             mRequestSessionMetric.logCandidateAggregateMetrics(mProviders);
404             mRequestSessionMetric.collectFinalPhaseProviderMetricStatus(
405                     /*hasException=*/ true, ProviderStatusForMetrics.FINAL_FAILURE);
406         }
407         if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) {
408             Slog.w(TAG, "Request has already been completed. This is strange.");
409             return;
410         }
411         if (isSessionCancelled()) {
412             finishSession(/*propagateCancellation=*/true, ApiStatus.CLIENT_CANCELED.getMetricCode());
413             return;
414         }
415         try {
416             invokeClientCallbackError(errorType, errorMsg);
417         } catch (RemoteException e) {
418             Slog.e(TAG, "Issue while responding to client with error : " + e);
419         }
420         boolean isUserCanceled = errorType.contains(MetricUtilities.USER_CANCELED_SUBSTRING);
421         if (isUserCanceled) {
422             if (!Flags.fixMetricDuplicationEmits()) {
423                 mRequestSessionMetric.setHasExceptionFinalPhase(/* hasException */ false);
424             }
425             finishSession(/*propagateCancellation=*/false,
426                     ApiStatus.USER_CANCELED.getMetricCode());
427         } else {
428             finishSession(/*propagateCancellation=*/false,
429                     ApiStatus.FAILURE.getMetricCode());
430         }
431     }
432 
433     /**
434      * Reveals if a certain provider is primary after ensuring it exists at all in the designated
435      * provider info.
436      *
437      * @param componentName used to identify the provider we want to check primary status for
438      */
isPrimaryProviderViaProviderInfo(ComponentName componentName)439     protected boolean isPrimaryProviderViaProviderInfo(ComponentName componentName) {
440         var chosenProviderSession = mProviders.get(componentName.flattenToString());
441         return chosenProviderSession != null && chosenProviderSession.mProviderInfo != null
442                 && chosenProviderSession.mProviderInfo.isPrimary();
443     }
444 
445     private class RequestSessionDeathRecipient implements IBinder.DeathRecipient {
446         @Override
binderDied()447         public void binderDied() {
448             Slog.d(TAG, "Client binder died - clearing session");
449             finishSession(isUiWaitingForData(), ApiStatus.BINDER_DIED.getMetricCode());
450         }
451     }
452 
453     /**
454      * This captures the final state of the apiStatus as presented in 'finishSession'.
455      */
isRespondingWithError(int apiStatus)456     private boolean isRespondingWithError(int apiStatus) {
457         return apiStatus == ApiStatus.FAILURE.getMetricCode()
458                 || apiStatus == ApiStatus.CLIENT_CANCELED.getMetricCode();
459     }
460 
461     /**
462      * A unique failure case, where we do not set the exception bit to be true.
463      */
isRespondingWithUserCanceledError(int apiStatus)464     private boolean isRespondingWithUserCanceledError(int apiStatus) {
465         return apiStatus == ApiStatus.USER_CANCELED.getMetricCode();
466     }
467 
468     /**
469      * This captures the final state of the apiStatus as presented in 'finishSession'.
470      */
isRespondingWithSuccess(int apiStatus)471     private boolean isRespondingWithSuccess(int apiStatus) {
472         return apiStatus == ApiStatus.SUCCESS.getMetricCode();
473     }
474 }
475