| /* |
| * Copyright (C) 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.credentials; |
| |
| import static com.android.server.credentials.MetricUtilities.METRICS_PROVIDER_STATUS_FINAL_FAILURE; |
| import static com.android.server.credentials.MetricUtilities.METRICS_PROVIDER_STATUS_FINAL_SUCCESS; |
| |
| import android.annotation.Nullable; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.credentials.CredentialProviderInfo; |
| import android.credentials.GetCredentialException; |
| import android.credentials.GetCredentialRequest; |
| import android.credentials.GetCredentialResponse; |
| import android.credentials.IGetCredentialCallback; |
| import android.credentials.ui.ProviderData; |
| import android.credentials.ui.RequestInfo; |
| import android.os.CancellationSignal; |
| import android.os.RemoteException; |
| import android.service.credentials.CallingAppInfo; |
| import android.util.Log; |
| |
| import com.android.server.credentials.metrics.ApiName; |
| import com.android.server.credentials.metrics.ApiStatus; |
| |
| import java.util.ArrayList; |
| |
| /** |
| * Central session for a single getCredentials request. This class listens to the |
| * responses from providers, and the UX app, and updates the provider(S) state. |
| */ |
| public final class GetRequestSession extends RequestSession<GetCredentialRequest, |
| IGetCredentialCallback> |
| implements ProviderSession.ProviderInternalCallback<GetCredentialResponse> { |
| private static final String TAG = "GetRequestSession"; |
| public GetRequestSession(Context context, int userId, int callingUid, |
| IGetCredentialCallback callback, GetCredentialRequest request, |
| CallingAppInfo callingAppInfo, CancellationSignal cancellationSignal) { |
| super(context, userId, callingUid, request, callback, RequestInfo.TYPE_GET, |
| callingAppInfo, cancellationSignal); |
| } |
| |
| /** |
| * Creates a new provider session, and adds it list of providers that are contributing to |
| * this session. |
| * @return the provider session created within this request session, for the given provider |
| * info. |
| */ |
| @Override |
| @Nullable |
| public ProviderSession initiateProviderSession(CredentialProviderInfo providerInfo, |
| RemoteCredentialService remoteCredentialService) { |
| ProviderGetSession providerGetSession = ProviderGetSession |
| .createNewSession(mContext, mUserId, providerInfo, |
| this, remoteCredentialService); |
| if (providerGetSession != null) { |
| Log.i(TAG, "In startProviderSession - provider session created and being added"); |
| mProviders.put(providerGetSession.getComponentName().flattenToString(), |
| providerGetSession); |
| } |
| return providerGetSession; |
| } |
| |
| @Override |
| protected void launchUiWithProviderData(ArrayList<ProviderData> providerDataList) { |
| try { |
| mClientCallback.onPendingIntent(mCredentialManagerUi.createPendingIntent( |
| RequestInfo.newGetRequestInfo( |
| mRequestId, mClientRequest, mClientAppInfo.getPackageName()), |
| providerDataList)); |
| } catch (RemoteException e) { |
| respondToClientWithErrorAndFinish( |
| GetCredentialException.TYPE_UNKNOWN, "Unable to instantiate selector"); |
| } |
| } |
| |
| @Override |
| public void onFinalResponseReceived(ComponentName componentName, |
| @Nullable GetCredentialResponse response) { |
| Log.i(TAG, "onFinalCredentialReceived from: " + componentName.flattenToString()); |
| setChosenMetric(componentName); |
| if (response != null) { |
| mChosenProviderMetric.setChosenProviderStatus( |
| METRICS_PROVIDER_STATUS_FINAL_SUCCESS); |
| respondToClientWithResponseAndFinish(response); |
| } else { |
| mChosenProviderMetric.setChosenProviderStatus( |
| METRICS_PROVIDER_STATUS_FINAL_FAILURE); |
| respondToClientWithErrorAndFinish(GetCredentialException.TYPE_NO_CREDENTIAL, |
| "Invalid response from provider"); |
| } |
| } |
| |
| //TODO: Try moving the three error & response methods below to RequestSession to be shared |
| // between get & create. |
| @Override |
| public void onFinalErrorReceived(ComponentName componentName, String errorType, |
| String message) { |
| respondToClientWithErrorAndFinish(errorType, message); |
| } |
| |
| private void respondToClientWithResponseAndFinish(GetCredentialResponse response) { |
| if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) { |
| Log.i(TAG, "Request has already been completed. This is strange."); |
| return; |
| } |
| if (isSessionCancelled()) { |
| logApiCall(ApiName.GET_CREDENTIAL, /* apiStatus */ |
| ApiStatus.METRICS_API_STATUS_CLIENT_CANCELED); |
| finishSession(/*propagateCancellation=*/true); |
| return; |
| } |
| try { |
| mClientCallback.onResponse(response); |
| logApiCall(ApiName.GET_CREDENTIAL, /* apiStatus */ |
| ApiStatus.METRICS_API_STATUS_SUCCESS); |
| } catch (RemoteException e) { |
| Log.i(TAG, "Issue while responding to client with a response : " + e.getMessage()); |
| logApiCall(ApiName.GET_CREDENTIAL, /* apiStatus */ |
| ApiStatus.METRICS_API_STATUS_FAILURE); |
| } |
| finishSession(/*propagateCancellation=*/false); |
| } |
| |
| private void respondToClientWithErrorAndFinish(String errorType, String errorMsg) { |
| if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) { |
| Log.i(TAG, "Request has already been completed. This is strange."); |
| return; |
| } |
| if (isSessionCancelled()) { |
| logApiCall(ApiName.GET_CREDENTIAL, /* apiStatus */ |
| ApiStatus.METRICS_API_STATUS_CLIENT_CANCELED); |
| finishSession(/*propagateCancellation=*/true); |
| return; |
| } |
| |
| try { |
| mClientCallback.onError(errorType, errorMsg); |
| } catch (RemoteException e) { |
| Log.i(TAG, "Issue while responding to client with error : " + e.getMessage()); |
| } |
| logFailureOrUserCancel(errorType); |
| finishSession(/*propagateCancellation=*/false); |
| } |
| |
| private void logFailureOrUserCancel(String errorType) { |
| if (GetCredentialException.TYPE_USER_CANCELED.equals(errorType)) { |
| logApiCall(ApiName.GET_CREDENTIAL, |
| /* apiStatus */ ApiStatus.METRICS_API_STATUS_USER_CANCELED); |
| } else { |
| logApiCall(ApiName.GET_CREDENTIAL, |
| /* apiStatus */ ApiStatus.METRICS_API_STATUS_FAILURE); |
| } |
| } |
| |
| @Override |
| public void onUiCancellation(boolean isUserCancellation) { |
| if (isUserCancellation) { |
| respondToClientWithErrorAndFinish(GetCredentialException.TYPE_USER_CANCELED, |
| "User cancelled the selector"); |
| } else { |
| respondToClientWithErrorAndFinish(GetCredentialException.TYPE_INTERRUPTED, |
| "The UI was interrupted - please try again."); |
| } |
| } |
| |
| @Override |
| public void onUiSelectorInvocationFailure() { |
| respondToClientWithErrorAndFinish(GetCredentialException.TYPE_NO_CREDENTIAL, |
| "No credentials available."); |
| } |
| |
| @Override |
| public void onProviderStatusChanged(ProviderSession.Status status, |
| ComponentName componentName) { |
| Log.i(TAG, "in onStatusChanged with status: " + status); |
| // Auth entry was selected, and it did not have any underlying credentials |
| if (status == ProviderSession.Status.NO_CREDENTIALS_FROM_AUTH_ENTRY) { |
| handleEmptyAuthenticationSelection(componentName); |
| return; |
| } |
| // For any other status, we check if all providers are done and then invoke UI if needed |
| if (!isAnyProviderPending()) { |
| // If all provider responses have been received, we can either need the UI, |
| // or we need to respond with error. The only other case is the entry being |
| // selected after the UI has been invoked which has a separate code path. |
| if (isUiInvocationNeeded()) { |
| Log.i(TAG, "in onProviderStatusChanged - isUiInvocationNeeded"); |
| getProviderDataAndInitiateUi(); |
| } else { |
| respondToClientWithErrorAndFinish(GetCredentialException.TYPE_NO_CREDENTIAL, |
| "No credentials available"); |
| } |
| } |
| } |
| |
| private void handleEmptyAuthenticationSelection(ComponentName componentName) { |
| // Update auth entry statuses across different provider sessions |
| mProviders.keySet().forEach(key -> { |
| ProviderGetSession session = (ProviderGetSession) mProviders.get(key); |
| if (!session.mComponentName.equals(componentName)) { |
| session.updateAuthEntriesStatusFromAnotherSession(); |
| } |
| }); |
| |
| // Invoke UI since it needs to show a snackbar if last auth entry, or a status on each |
| // auth entries along with other valid entries |
| getProviderDataAndInitiateUi(); |
| |
| // Respond to client if all auth entries are empty and nothing else to show on the UI |
| if (providerDataContainsEmptyAuthEntriesOnly()) { |
| respondToClientWithErrorAndFinish(GetCredentialException.TYPE_NO_CREDENTIAL, |
| "No credentials available"); |
| } |
| } |
| |
| private boolean providerDataContainsEmptyAuthEntriesOnly() { |
| for (String key : mProviders.keySet()) { |
| ProviderGetSession session = (ProviderGetSession) mProviders.get(key); |
| if (!session.containsEmptyAuthEntriesOnly()) { |
| return false; |
| } |
| } |
| return true; |
| } |
| } |