blob: f447c1fd277e96f8e8d0756de04d83c0a61bacbe [file] [log] [blame]
/*
* Copyright (C) 2023 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.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.credentials.CredentialOption;
import android.credentials.GetCredentialRequest;
import android.credentials.IGetCredentialCallback;
import android.credentials.IPrepareGetCredentialCallback;
import android.credentials.PrepareGetCredentialResponseInternal;
import android.credentials.ui.GetCredentialProviderData;
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.service.credentials.PermissionUtils;
import android.util.Slog;
import java.util.ArrayList;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Central session for a single pendingGetCredential request. This class listens to the
* responses from providers, and the UX app, and updates the provider(s) state.
*/
public class PrepareGetRequestSession extends GetRequestSession {
private static final String TAG = "PrepareGetRequestSession";
private final IPrepareGetCredentialCallback mPrepareGetCredentialCallback;
public PrepareGetRequestSession(Context context,
RequestSession.SessionLifetime sessionCallback, Object lock, int userId,
int callingUid, IGetCredentialCallback getCredCallback, GetCredentialRequest request,
CallingAppInfo callingAppInfo, Set<ComponentName> enabledProviders,
CancellationSignal cancellationSignal, long startedTimestamp,
IPrepareGetCredentialCallback prepareGetCredentialCallback) {
super(context, sessionCallback, lock, userId, callingUid, getCredCallback, request,
callingAppInfo, enabledProviders, cancellationSignal, startedTimestamp);
int numTypes = (request.getCredentialOptions().stream()
.map(CredentialOption::getType).collect(
Collectors.toSet())).size(); // Dedupe type strings
mRequestSessionMetric.collectGetFlowInitialMetricInfo(request);
mPrepareGetCredentialCallback = prepareGetCredentialCallback;
}
@Override
public void onProviderStatusChanged(ProviderSession.Status status, ComponentName componentName,
ProviderSession.CredentialsSource source) {
Slog.i(TAG, "Provider Status changed with status: " + status + ", and "
+ "source: " + source);
switch (source) {
case REMOTE_PROVIDER:
// Remote provider's status changed. We should check if all providers are done, and
// if UI invocation is needed.
if (isAnyProviderPending()) {
// Waiting for a remote provider response
return;
}
boolean hasQueryCandidatePermission = PermissionUtils.hasPermission(
mContext,
mClientAppInfo.getPackageName(),
Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS);
if (isUiInvocationNeeded()) {
// To avoid extra computation, we only prepare the data at this point when we
// know that UI invocation is needed
ArrayList<ProviderData> providerData = getProviderDataForUi();
if (!providerData.isEmpty()) {
constructPendingResponseAndInvokeCallback(hasQueryCandidatePermission,
getCredentialResultTypes(hasQueryCandidatePermission),
hasAuthenticationResults(providerData,
hasQueryCandidatePermission),
hasRemoteResults(providerData, hasQueryCandidatePermission),
getUiIntent());
return;
}
}
// We reach here if Ui invocation is not needed, or provider data is empty
constructEmptyPendingResponseAndInvokeCallback(
hasQueryCandidatePermission);
break;
case AUTH_ENTRY:
// Status updated through a selected authentication entry. We don't need to
// check on any other credential source and can process this result directly.
if (status == ProviderSession.Status.NO_CREDENTIALS_FROM_AUTH_ENTRY) {
// Update entry subtitle and re-invoke UI
super.handleEmptyAuthenticationSelection(componentName);
} else if (status == ProviderSession.Status.CREDENTIALS_RECEIVED) {
getProviderDataAndInitiateUi();
}
break;
default:
Slog.w(TAG, "Unexpected source");
break;
}
}
private void constructPendingResponseAndInvokeCallback(boolean hasPermission,
Set<String> credentialTypes,
boolean hasAuthenticationResults, boolean hasRemoteResults, PendingIntent uiIntent) {
try {
mPrepareGetCredentialCallback.onResponse(
new PrepareGetCredentialResponseInternal(
hasPermission,
credentialTypes, hasAuthenticationResults, hasRemoteResults, uiIntent));
} catch (RemoteException e) {
Slog.e(TAG, "EXCEPTION while mPendingCallback.onResponse", e);
}
}
private void constructEmptyPendingResponseAndInvokeCallback(
boolean hasQueryCandidatePermission) {
try {
mPrepareGetCredentialCallback.onResponse(
new PrepareGetCredentialResponseInternal(
hasQueryCandidatePermission,
/*credentialResultTypes=*/ null,
/*hasAuthenticationResults=*/false,
/*hasRemoteResults=*/ false,
/*pendingIntent=*/ null));
} catch (RemoteException e) {
Slog.e(TAG, "EXCEPTION while mPendingCallback.onResponse", e);
}
}
private boolean hasRemoteResults(ArrayList<ProviderData> providerData,
boolean hasQueryCandidatePermission) {
if (!hasQueryCandidatePermission) {
return false;
}
return providerData.stream()
.map(data -> (GetCredentialProviderData) data)
.anyMatch(getCredentialProviderData ->
getCredentialProviderData.getRemoteEntry() != null);
}
private boolean hasAuthenticationResults(ArrayList<ProviderData> providerData,
boolean hasQueryCandidatePermission) {
if (!hasQueryCandidatePermission) {
return false;
}
return providerData.stream()
.map(data -> (GetCredentialProviderData) data)
.anyMatch(getCredentialProviderData ->
!getCredentialProviderData.getAuthenticationEntries().isEmpty());
}
@Nullable
private Set<String> getCredentialResultTypes(boolean hasQueryCandidatePermission) {
if (!hasQueryCandidatePermission) {
return null;
}
return mProviders.values().stream()
.map(session -> (ProviderGetSession) session)
.flatMap(providerGetSession -> providerGetSession
.getCredentialEntryTypes().stream())
.collect(Collectors.toSet());
}
private PendingIntent getUiIntent() {
ArrayList<ProviderData> providerDataList = new ArrayList<>();
for (ProviderSession session : mProviders.values()) {
ProviderData providerData = session.prepareUiData();
if (providerData != null) {
providerDataList.add(providerData);
}
}
if (!providerDataList.isEmpty()) {
return mCredentialManagerUi.createPendingIntent(
RequestInfo.newGetRequestInfo(
mRequestId, mClientRequest, mClientAppInfo.getPackageName(),
PermissionUtils.hasPermission(mContext, mClientAppInfo.getPackageName(),
Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS)),
providerDataList);
} else {
return null;
}
}
}