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.Manifest; 20 import android.annotation.Nullable; 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.credentials.CredentialManager; 24 import android.credentials.CredentialOption; 25 import android.credentials.CredentialProviderInfo; 26 import android.credentials.GetCredentialException; 27 import android.credentials.GetCredentialRequest; 28 import android.credentials.GetCredentialResponse; 29 import android.credentials.IGetCredentialCallback; 30 import android.credentials.selection.ProviderData; 31 import android.credentials.selection.RequestInfo; 32 import android.os.Binder; 33 import android.os.CancellationSignal; 34 import android.os.RemoteException; 35 import android.service.credentials.CallingAppInfo; 36 import android.service.credentials.PermissionUtils; 37 import android.util.Slog; 38 39 import com.android.server.credentials.metrics.ProviderStatusForMetrics; 40 41 import java.util.ArrayList; 42 import java.util.Set; 43 44 /** 45 * Central session for a single getCredentials request. This class listens to the 46 * responses from providers, and the UX app, and updates the provider(S) state. 47 */ 48 public class GetRequestSession extends RequestSession<GetCredentialRequest, 49 IGetCredentialCallback, GetCredentialResponse> 50 implements ProviderSession.ProviderInternalCallback<GetCredentialResponse> { 51 private static final String TAG = CredentialManager.TAG; 52 GetRequestSession(Context context, RequestSession.SessionLifetime sessionCallback, Object lock, int userId, int callingUid, IGetCredentialCallback callback, GetCredentialRequest request, CallingAppInfo callingAppInfo, Set<ComponentName> enabledProviders, CancellationSignal cancellationSignal, long startedTimestamp)53 public GetRequestSession(Context context, RequestSession.SessionLifetime sessionCallback, 54 Object lock, int userId, int callingUid, 55 IGetCredentialCallback callback, GetCredentialRequest request, 56 CallingAppInfo callingAppInfo, Set<ComponentName> enabledProviders, 57 CancellationSignal cancellationSignal, 58 long startedTimestamp) { 59 super(context, sessionCallback, lock, userId, callingUid, request, callback, 60 getRequestInfoFromRequest(request), callingAppInfo, enabledProviders, 61 cancellationSignal, startedTimestamp, /*shouldBindClientToDeath=*/ true); 62 mRequestSessionMetric.collectGetFlowInitialMetricInfo(request); 63 } 64 getRequestInfoFromRequest(GetCredentialRequest request)65 private static String getRequestInfoFromRequest(GetCredentialRequest request) { 66 for (CredentialOption option : request.getCredentialOptions()) { 67 if (option.getCredentialRetrievalData().getStringArrayList( 68 CredentialOption 69 .SUPPORTED_ELEMENT_KEYS) != null) { 70 return RequestInfo.TYPE_GET_VIA_REGISTRY; 71 } 72 } 73 return RequestInfo.TYPE_GET; 74 } 75 76 /** 77 * Creates a new provider session, and adds it list of providers that are contributing to 78 * this session. 79 * 80 * @return the provider session created within this request session, for the given provider 81 * info. 82 */ 83 @Override 84 @Nullable initiateProviderSession(CredentialProviderInfo providerInfo, RemoteCredentialService remoteCredentialService)85 public ProviderSession initiateProviderSession(CredentialProviderInfo providerInfo, 86 RemoteCredentialService remoteCredentialService) { 87 ProviderGetSession providerGetSession = ProviderGetSession 88 .createNewSession(mContext, mUserId, providerInfo, 89 this, remoteCredentialService); 90 if (providerGetSession != null) { 91 Slog.i(TAG, "Provider session created and " 92 + "being added for: " + providerInfo.getComponentName()); 93 mProviders.put(providerGetSession.getComponentName().flattenToString(), 94 providerGetSession); 95 } 96 return providerGetSession; 97 } 98 99 @Override launchUiWithProviderData(ArrayList<ProviderData> providerDataList)100 protected void launchUiWithProviderData(ArrayList<ProviderData> providerDataList) { 101 mRequestSessionMetric.collectUiCallStartTime(System.nanoTime()); 102 mCredentialManagerUi.setStatus(CredentialManagerUi.UiStatus.USER_INTERACTION); 103 Binder.withCleanCallingIdentity(() -> { 104 try { 105 cancelExistingPendingIntent(); 106 mPendingIntent = mCredentialManagerUi.createPendingIntent( 107 RequestInfo.newGetRequestInfo( 108 mRequestId, mClientRequest, mClientAppInfo.getPackageName(), 109 PermissionUtils.hasPermission(mContext, 110 mClientAppInfo.getPackageName(), 111 Manifest.permission 112 .CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS), 113 /*isShowAllOptionsRequested=*/ false), 114 providerDataList, 115 mRequestSessionMetric); 116 mClientCallback.onPendingIntent(mPendingIntent); 117 } catch (RemoteException e) { 118 mRequestSessionMetric.collectUiReturnedFinalPhase(/*uiReturned=*/ false); 119 mCredentialManagerUi.setStatus(CredentialManagerUi.UiStatus.TERMINATED); 120 String exception = GetCredentialException.TYPE_UNKNOWN; 121 mRequestSessionMetric.collectFrameworkException(exception); 122 respondToClientWithErrorAndFinish(exception, "Unable to instantiate selector"); 123 } 124 }); 125 } 126 127 @Override invokeClientCallbackSuccess(GetCredentialResponse response)128 protected void invokeClientCallbackSuccess(GetCredentialResponse response) 129 throws RemoteException { 130 mClientCallback.onResponse(response); 131 } 132 133 @Override invokeClientCallbackError(String errorType, String errorMsg)134 protected void invokeClientCallbackError(String errorType, String errorMsg) 135 throws RemoteException { 136 mClientCallback.onError(errorType, errorMsg); 137 } 138 139 @Override onFinalResponseReceived(ComponentName componentName, @Nullable GetCredentialResponse response)140 public void onFinalResponseReceived(ComponentName componentName, 141 @Nullable GetCredentialResponse response) { 142 Slog.i(TAG, "onFinalResponseReceived from: " + componentName.flattenToString()); 143 mRequestSessionMetric.collectUiResponseData(/*uiReturned=*/ true, System.nanoTime()); 144 mRequestSessionMetric.updateMetricsOnResponseReceived(mProviders, componentName, 145 isPrimaryProviderViaProviderInfo(componentName)); 146 if (response != null) { 147 mRequestSessionMetric.collectChosenProviderStatus( 148 ProviderStatusForMetrics.FINAL_SUCCESS.getMetricCode()); 149 respondToClientWithResponseAndFinish(response); 150 } else { 151 mRequestSessionMetric.collectChosenProviderStatus( 152 ProviderStatusForMetrics.FINAL_FAILURE.getMetricCode()); 153 String exception = GetCredentialException.TYPE_NO_CREDENTIAL; 154 mRequestSessionMetric.collectFrameworkException(exception); 155 respondToClientWithErrorAndFinish(exception, 156 "Invalid response from provider"); 157 } 158 } 159 160 //TODO(b/274954697): Further shorten the three below to completely migrate to superclass 161 @Override onFinalErrorReceived(ComponentName componentName, String errorType, String message)162 public void onFinalErrorReceived(ComponentName componentName, String errorType, 163 String message) { 164 respondToClientWithErrorAndFinish(errorType, message); 165 } 166 167 @Override onUiCancellation(boolean isUserCancellation)168 public void onUiCancellation(boolean isUserCancellation) { 169 String exception = GetCredentialException.TYPE_USER_CANCELED; 170 String message = "User cancelled the selector"; 171 if (!isUserCancellation) { 172 exception = GetCredentialException.TYPE_INTERRUPTED; 173 message = "The UI was interrupted - please try again."; 174 } 175 mRequestSessionMetric.collectFrameworkException(exception); 176 respondToClientWithErrorAndFinish(exception, message); 177 } 178 179 @Override onUiSelectorInvocationFailure()180 public void onUiSelectorInvocationFailure() { 181 String exception = GetCredentialException.TYPE_NO_CREDENTIAL; 182 mRequestSessionMetric.collectFrameworkException(exception); 183 respondToClientWithErrorAndFinish(exception, 184 "No credentials available."); 185 } 186 187 @Override onProviderStatusChanged(ProviderSession.Status status, ComponentName componentName, ProviderSession.CredentialsSource source)188 public void onProviderStatusChanged(ProviderSession.Status status, 189 ComponentName componentName, ProviderSession.CredentialsSource source) { 190 Slog.i(TAG, "Status changed for: " + componentName + ", with status: " 191 + status + ", and source: " + source); 192 193 // Auth entry was selected, and it did not have any underlying credentials 194 if (status == ProviderSession.Status.NO_CREDENTIALS_FROM_AUTH_ENTRY) { 195 handleEmptyAuthenticationSelection(componentName); 196 return; 197 } 198 // For any other status, we check if all providers are done and then invoke UI if needed 199 if (!isAnyProviderPending()) { 200 // If all provider responses have been received, we can either need the UI, 201 // or we need to respond with error. The only other case is the entry being 202 // selected after the UI has been invoked which has a separate code path. 203 if (isUiInvocationNeeded()) { 204 Slog.i(TAG, "Provider status changed - ui invocation is needed"); 205 getProviderDataAndInitiateUi(); 206 } else { 207 String exception = GetCredentialException.TYPE_NO_CREDENTIAL; 208 mRequestSessionMetric.collectFrameworkException(exception); 209 respondToClientWithErrorAndFinish(exception, 210 "No credentials available"); 211 } 212 } 213 } 214 handleEmptyAuthenticationSelection(ComponentName componentName)215 protected void handleEmptyAuthenticationSelection(ComponentName componentName) { 216 // Update auth entry statuses across different provider sessions 217 mProviders.keySet().forEach(key -> { 218 ProviderGetSession session = (ProviderGetSession) mProviders.get(key); 219 if (!session.mComponentName.equals(componentName)) { 220 session.updateAuthEntriesStatusFromAnotherSession(); 221 } 222 }); 223 224 // Invoke UI since it needs to show a snackbar if last auth entry, or a status on each 225 // auth entries along with other valid entries 226 getProviderDataAndInitiateUi(); 227 228 // Respond to client if all auth entries are empty and nothing else to show on the UI 229 if (providerDataContainsEmptyAuthEntriesOnly()) { 230 String exception = GetCredentialException.TYPE_NO_CREDENTIAL; 231 mRequestSessionMetric.collectFrameworkException(exception); 232 respondToClientWithErrorAndFinish(exception, 233 "No credentials available"); 234 } 235 } 236 providerDataContainsEmptyAuthEntriesOnly()237 private boolean providerDataContainsEmptyAuthEntriesOnly() { 238 for (String key : mProviders.keySet()) { 239 ProviderGetSession session = (ProviderGetSession) mProviders.get(key); 240 if (!session.containsEmptyAuthEntriesOnly()) { 241 return false; 242 } 243 } 244 return true; 245 } 246 } 247