blob: c9e691e199c71945cafd7a6ce0cb860f27d0a4e6 [file] [log] [blame]
/*
* 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 android.Manifest;
import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
import android.credentials.CredentialOption;
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.Binder;
import android.os.CancellationSignal;
import android.os.RemoteException;
import android.service.credentials.CallingAppInfo;
import android.service.credentials.PermissionUtils;
import android.util.Slog;
import com.android.server.credentials.metrics.ProviderStatusForMetrics;
import java.util.ArrayList;
import java.util.Set;
/**
* 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 class GetRequestSession extends RequestSession<GetCredentialRequest,
IGetCredentialCallback, GetCredentialResponse>
implements ProviderSession.ProviderInternalCallback<GetCredentialResponse> {
private static final String TAG = "GetRequestSession";
public 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) {
super(context, sessionCallback, lock, userId, callingUid, request, callback,
getRequestInfoFromRequest(request), callingAppInfo, enabledProviders,
cancellationSignal, startedTimestamp);
mRequestSessionMetric.collectGetFlowInitialMetricInfo(request);
}
private static String getRequestInfoFromRequest(GetCredentialRequest request) {
for (CredentialOption option : request.getCredentialOptions()) {
if (option.getCredentialRetrievalData().getStringArrayList(
CredentialOption
.SUPPORTED_ELEMENT_KEYS) != null) {
return RequestInfo.TYPE_GET_VIA_REGISTRY;
}
}
return RequestInfo.TYPE_GET;
}
/**
* 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) {
Slog.i(TAG, "Provider session created and "
+ "being added for: " + providerInfo.getComponentName());
mProviders.put(providerGetSession.getComponentName().flattenToString(),
providerGetSession);
}
return providerGetSession;
}
@Override
protected void launchUiWithProviderData(ArrayList<ProviderData> providerDataList) {
mRequestSessionMetric.collectUiCallStartTime(System.nanoTime());
mCredentialManagerUi.setStatus(CredentialManagerUi.UiStatus.USER_INTERACTION);
Binder.withCleanCallingIdentity(()-> {
try {
cancelExistingPendingIntent();
mPendingIntent = mCredentialManagerUi.createPendingIntent(
RequestInfo.newGetRequestInfo(
mRequestId, mClientRequest, mClientAppInfo.getPackageName(),
PermissionUtils.hasPermission(mContext, mClientAppInfo.getPackageName(),
Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS)),
providerDataList);
mClientCallback.onPendingIntent(mPendingIntent);
} catch (RemoteException e) {
mRequestSessionMetric.collectUiReturnedFinalPhase(/*uiReturned=*/ false);
mCredentialManagerUi.setStatus(CredentialManagerUi.UiStatus.TERMINATED);
String exception = GetCredentialException.TYPE_UNKNOWN;
mRequestSessionMetric.collectFrameworkException(exception);
respondToClientWithErrorAndFinish(exception, "Unable to instantiate selector");
}
});
}
@Override
protected void invokeClientCallbackSuccess(GetCredentialResponse response)
throws RemoteException {
mClientCallback.onResponse(response);
}
@Override
protected void invokeClientCallbackError(String errorType, String errorMsg)
throws RemoteException {
mClientCallback.onError(errorType, errorMsg);
}
@Override
public void onFinalResponseReceived(ComponentName componentName,
@Nullable GetCredentialResponse response) {
Slog.i(TAG, "onFinalResponseReceived from: " + componentName.flattenToString());
mRequestSessionMetric.collectUiResponseData(/*uiReturned=*/ true, System.nanoTime());
mRequestSessionMetric.updateMetricsOnResponseReceived(mProviders, componentName,
isPrimaryProviderViaProviderInfo(componentName));
if (response != null) {
mRequestSessionMetric.collectChosenProviderStatus(
ProviderStatusForMetrics.FINAL_SUCCESS.getMetricCode());
respondToClientWithResponseAndFinish(response);
} else {
mRequestSessionMetric.collectChosenProviderStatus(
ProviderStatusForMetrics.FINAL_FAILURE.getMetricCode());
String exception = GetCredentialException.TYPE_NO_CREDENTIAL;
mRequestSessionMetric.collectFrameworkException(exception);
respondToClientWithErrorAndFinish(exception,
"Invalid response from provider");
}
}
//TODO(b/274954697): Further shorten the three below to completely migrate to superclass
@Override
public void onFinalErrorReceived(ComponentName componentName, String errorType,
String message) {
respondToClientWithErrorAndFinish(errorType, message);
}
@Override
public void onUiCancellation(boolean isUserCancellation) {
String exception = GetCredentialException.TYPE_USER_CANCELED;
String message = "User cancelled the selector";
if (!isUserCancellation) {
exception = GetCredentialException.TYPE_INTERRUPTED;
message = "The UI was interrupted - please try again.";
}
mRequestSessionMetric.collectFrameworkException(exception);
respondToClientWithErrorAndFinish(exception, message);
}
@Override
public void onUiSelectorInvocationFailure() {
String exception = GetCredentialException.TYPE_NO_CREDENTIAL;
mRequestSessionMetric.collectFrameworkException(exception);
respondToClientWithErrorAndFinish(exception,
"No credentials available.");
}
@Override
public void onProviderStatusChanged(ProviderSession.Status status,
ComponentName componentName, ProviderSession.CredentialsSource source) {
Slog.i(TAG, "Status changed for: " + componentName + ", with status: "
+ status + ", and source: " + source);
// 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()) {
Slog.i(TAG, "Provider status changed - ui invocation is needed");
getProviderDataAndInitiateUi();
} else {
String exception = GetCredentialException.TYPE_NO_CREDENTIAL;
mRequestSessionMetric.collectFrameworkException(exception);
respondToClientWithErrorAndFinish(exception,
"No credentials available");
}
}
}
protected 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()) {
String exception = GetCredentialException.TYPE_NO_CREDENTIAL;
mRequestSessionMetric.collectFrameworkException(exception);
respondToClientWithErrorAndFinish(exception,
"No credentials available");
}
}
private boolean providerDataContainsEmptyAuthEntriesOnly() {
for (String key : mProviders.keySet()) {
ProviderGetSession session = (ProviderGetSession) mProviders.get(key);
if (!session.containsEmptyAuthEntriesOnly()) {
return false;
}
}
return true;
}
}