Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 534cfba8 authored by Reema Bajwa's avatar Reema Bajwa Committed by Android (Google) Code Review
Browse files

Merge "Refactor prepareGetCredential API" into udc-dev

parents 166086e1 69179a43
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -72,7 +72,7 @@ public final class ClearRequestSession extends RequestSession<ClearCredentialSta

    @Override // from provider session
    public void onProviderStatusChanged(ProviderSession.Status status,
            ComponentName componentName) {
            ComponentName componentName, ProviderSession.CredentialsSource source) {
        Log.i(TAG, "in onStatusChanged with status: " + status);
        if (ProviderSession.isTerminatingStatus(status)) {
            Log.i(TAG, "in onStatusChanged terminating status");
+1 −1
Original line number Diff line number Diff line
@@ -156,7 +156,7 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR

    @Override
    public void onProviderStatusChanged(ProviderSession.Status status,
            ComponentName componentName) {
            ComponentName componentName, ProviderSession.CredentialsSource source) {
        Log.i(TAG, "in onProviderStatusChanged with status: " + status);
        // 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
+43 −85
Original line number Diff line number Diff line
@@ -438,16 +438,12 @@ public final class CredentialManagerService
                    + callingPackage);
            ICancellationSignal cancelTransport = CancellationSignal.createTransport();

            if (request.getOrigin() != null) {
                // Check privileged permissions
                mContext.enforceCallingPermission(CREDENTIAL_MANAGER_SET_ORIGIN, null);
            }
            enforcePermissionForAllowedProviders(request);

            final int userId = UserHandle.getCallingUserId();
            final int callingUid = Binder.getCallingUid();
            enforceCallingPackage(callingPackage, callingUid);

            validateGetCredentialRequest(request);

            // New request session, scoped for this request only.
            final GetRequestSession session =
                    new GetRequestSession(
@@ -460,7 +456,24 @@ public final class CredentialManagerService
                            CancellationSignal.fromTransport(cancelTransport),
                            timestampBegan);

            processGetCredential(request, callback, session);
            List<ProviderSession> providerSessions =
                    prepareProviderSessions(request, session);

            if (providerSessions.isEmpty()) {
                try {
                    callback.onError(
                            GetCredentialException.TYPE_NO_CREDENTIAL,
                            "No credentials available on this device.");
                } catch (RemoteException e) {
                    Log.i(
                            TAG,
                            "Issue invoking onError on IGetCredentialCallback "
                                    + "callback: "
                                    + e.getMessage());
                }
            }

            invokeProviderSessions(providerSessions);
            return cancelTransport;
        }

@@ -488,75 +501,22 @@ public final class CredentialManagerService
                            getContext(),
                            userId,
                            callingUid,
                            prepareGetCredentialCallback,
                            getCredentialCallback,
                            request,
                            constructCallingAppInfo(callingPackage, userId, request.getOrigin()),
                            CancellationSignal.fromTransport(cancelTransport),
                            timestampBegan);
                            timestampBegan,
                            prepareGetCredentialCallback);

            processGetCredential(request, prepareGetCredentialCallback, session);

            return cancelTransport;
        }

        private void processGetCredential(
                GetCredentialRequest request,
                IPrepareGetCredentialCallback callback,
                PrepareGetRequestSession session) {
            List<ProviderSession> providerSessions;

            if (isCredentialDescriptionApiEnabled()) {
                List<CredentialOption> optionsThatRequireActiveCredentials =
                        request.getCredentialOptions().stream()
                                .filter(credentialOption -> credentialOption
                                        .getCredentialRetrievalData()
                                        .getStringArrayList(
                                                CredentialOption
                                                        .SUPPORTED_ELEMENT_KEYS) != null)
                                .toList();

                List<CredentialOption> optionsThatDoNotRequireActiveCredentials =
                        request.getCredentialOptions().stream()
                                .filter(credentialOption -> credentialOption
                                        .getCredentialRetrievalData()
                                        .getStringArrayList(
                                                CredentialOption
                                                        .SUPPORTED_ELEMENT_KEYS) == null)
                                .toList();

                List<ProviderSession> sessionsWithoutRemoteService =
                        initiateProviderSessionsWithActiveContainers(
                                session,
                                getFilteredResultFromRegistry(optionsThatRequireActiveCredentials));

                List<ProviderSession> sessionsWithRemoteService =
                        initiateProviderSessions(
                                session,
                                optionsThatDoNotRequireActiveCredentials.stream()
                                        .map(CredentialOption::getType)
                                        .collect(Collectors.toList()));

                Set<ProviderSession> all = new LinkedHashSet<>();
                all.addAll(sessionsWithRemoteService);
                all.addAll(sessionsWithoutRemoteService);

                providerSessions = new ArrayList<>(all);
            } else {
                // Initiate all provider sessions
                providerSessions =
                        initiateProviderSessions(
                                session,
                                request.getCredentialOptions().stream()
                                        .map(CredentialOption::getType)
                                        .collect(Collectors.toList()));
            }
            List<ProviderSession> providerSessions = prepareProviderSessions(request, session);

            if (providerSessions.isEmpty()) {
                try {
                    // TODO: fix
                    callback.onResponse(new PrepareGetCredentialResponseInternal(
                            false, null, false, false, null));
                    prepareGetCredentialCallback.onResponse(
                            new PrepareGetCredentialResponseInternal(
                            false, null,
                            false, false, null));
                } catch (RemoteException e) {
                    Log.i(
                            TAG,
@@ -566,14 +526,13 @@ public final class CredentialManagerService
                }
            }

            finalizeAndEmitInitialPhaseMetric(session);
            // TODO(b/271135048) - May still be worth emitting in the empty cases above.
            providerSessions.forEach(ProviderSession::invokeSession);
            invokeProviderSessions(providerSessions);

            return cancelTransport;
        }

        private void processGetCredential(
        private List<ProviderSession> prepareProviderSessions(
                GetCredentialRequest request,
                IGetCredentialCallback callback,
                GetRequestSession session) {
            List<ProviderSession> providerSessions;

@@ -623,21 +582,12 @@ public final class CredentialManagerService
                                        .collect(Collectors.toList()));
            }

            if (providerSessions.isEmpty()) {
                try {
                    callback.onError(
                            GetCredentialException.TYPE_NO_CREDENTIAL,
                            "No credentials available on this device.");
                } catch (RemoteException e) {
                    Slog.e(
                            TAG,
                            "Issue invoking onError on IGetCredentialCallback "
                                    + "callback: ", e);
                }
            }

            finalizeAndEmitInitialPhaseMetric(session);
            // TODO(b/271135048) - May still be worth emitting in the empty cases above.
            return providerSessions;
        }

        private void invokeProviderSessions(List<ProviderSession> providerSessions) {
            providerSessions.forEach(ProviderSession::invokeSession);
        }

@@ -936,6 +886,14 @@ public final class CredentialManagerService
        }
    }

    private void validateGetCredentialRequest(GetCredentialRequest request) {
        if (request.getOrigin() != null) {
            // Check privileged permissions
            mContext.enforceCallingPermission(CREDENTIAL_MANAGER_SET_ORIGIN, null);
        }
        enforcePermissionForAllowedProviders(request);
    }

    private void enforcePermissionForAllowedProviders(GetCredentialRequest request) {
        boolean containsAllowedProviders = request.getCredentialOptions()
                .stream()
+4 −3
Original line number Diff line number Diff line
@@ -151,8 +151,9 @@ public class GetRequestSession extends RequestSession<GetCredentialRequest,

    @Override
    public void onProviderStatusChanged(ProviderSession.Status status,
            ComponentName componentName) {
        Log.i(TAG, "in onStatusChanged with status: " + status);
            ComponentName componentName, ProviderSession.CredentialsSource source) {
        Log.i(TAG, "in onStatusChanged 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);
@@ -173,7 +174,7 @@ public class GetRequestSession extends RequestSession<GetCredentialRequest,
        }
    }

    private void handleEmptyAuthenticationSelection(ComponentName componentName) {
    protected void handleEmptyAuthenticationSelection(ComponentName componentName) {
        // Update auth entry statuses across different provider sessions
        mProviders.keySet().forEach(key -> {
            ProviderGetSession session = (ProviderGetSession) mProviders.get(key);
+44 −173
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 The Android Open Source Project
 * 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.
@@ -22,10 +22,7 @@ import android.app.PendingIntent;
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.IPrepareGetCredentialCallback;
import android.credentials.PrepareGetCredentialResponseInternal;
@@ -37,32 +34,29 @@ import android.os.RemoteException;
import android.service.credentials.CallingAppInfo;
import android.service.credentials.PermissionUtils;
import android.util.Log;

import com.android.server.credentials.metrics.ProviderStatusForMetrics;
import android.util.Slog;

import java.util.ArrayList;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Central session for a single prepareGetCredentials request. This class listens to the
 * responses from providers, and the UX app, and updates the provider(S) state.
 * 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 RequestSession<GetCredentialRequest,
        IGetCredentialCallback, GetCredentialResponse>
        implements ProviderSession.ProviderInternalCallback<GetCredentialResponse> {
    private static final String TAG = "GetRequestSession";
public class PrepareGetRequestSession extends GetRequestSession {
    private static final String TAG = "PrepareGetRequestSession";

    private final IPrepareGetCredentialCallback mPrepareGetCredentialCallback;
    private boolean mIsInitialQuery = true;

    public PrepareGetRequestSession(Context context, int userId, int callingUid,
            IPrepareGetCredentialCallback prepareGetCredentialCallback,
            IGetCredentialCallback getCredCallback, GetCredentialRequest request,
            CallingAppInfo callingAppInfo, CancellationSignal cancellationSignal,
            long startedTimestamp) {
        super(context, userId, callingUid, request, getCredCallback, RequestInfo.TYPE_GET,
                callingAppInfo, cancellationSignal, startedTimestamp);
            IGetCredentialCallback callback,
            GetCredentialRequest request,
            CallingAppInfo callingAppInfo,
            CancellationSignal cancellationSignal, long startedTimestamp,
            IPrepareGetCredentialCallback prepareGetCredentialCallback) {
        super(context, userId, callingUid, callback, request, callingAppInfo, cancellationSignal,
                startedTimestamp);
        int numTypes = (request.getCredentialOptions().stream()
                .map(CredentialOption::getType).collect(
                        Collectors.toSet())).size(); // Dedupe type strings
@@ -70,146 +64,53 @@ public class PrepareGetRequestSession extends RequestSession<GetCredentialReques
        mPrepareGetCredentialCallback = prepareGetCredentialCallback;
    }

    /**
     * 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) {
        mRequestSessionMetric.collectUiCallStartTime(System.nanoTime());
        try {
            mClientCallback.onPendingIntent(mCredentialManagerUi.createPendingIntent(
                    RequestInfo.newGetRequestInfo(
                            mRequestId, mClientRequest, mClientAppInfo.getPackageName()),
                    providerDataList));
        } catch (RemoteException e) {
            mRequestSessionMetric.collectUiReturnedFinalPhase(/*uiReturned=*/ false);
            respondToClientWithErrorAndFinish(
                    GetCredentialException.TYPE_UNKNOWN, "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) {
        Log.i(TAG, "onFinalCredentialReceived from: " + componentName.flattenToString());
        mRequestSessionMetric.collectUiResponseData(/*uiReturned=*/ true, System.nanoTime());
        mRequestSessionMetric.collectChosenMetricViaCandidateTransfer(mProviders.get(
                componentName.flattenToString()).mProviderSessionMetric
                .getCandidatePhasePerProviderMetric());
        if (response != null) {
            mRequestSessionMetric.collectChosenProviderStatus(
                    ProviderStatusForMetrics.FINAL_SUCCESS.getMetricCode());
            respondToClientWithResponseAndFinish(response);
        } else {
            mRequestSessionMetric.collectChosenProviderStatus(
                    ProviderStatusForMetrics.FINAL_FAILURE.getMetricCode());
            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);
    }

    @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);
    public void onProviderStatusChanged(ProviderSession.Status status, ComponentName componentName,
            ProviderSession.CredentialsSource 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;
                }
        // 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 (mIsInitialQuery) {
                // First time in this state. UI shouldn't be invoked because developer wants to
                // punt it for later
                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),
                                hasAuthenticationResults(providerData,
                                        hasQueryCandidatePermission),
                                hasRemoteResults(providerData, hasQueryCandidatePermission),
                                getUiIntent());
                    } else {
                        constructEmptyPendingResponseAndInvokeCallback(hasQueryCandidatePermission);
                        return;
                    }
                } else {
                    constructEmptyPendingResponseAndInvokeCallback(hasQueryCandidatePermission);
                }
                mIsInitialQuery = false;
            } else {
                // Not the first time. This could be a result of a user selection leading to a UI
                // invocation again.
                if (isUiInvocationNeeded()) {
                // 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();
                } else {
                    respondToClientWithErrorAndFinish(GetCredentialException.TYPE_NO_CREDENTIAL,
                            "No credentials available");
                }
                }
                break;
            default:
                Slog.w(TAG, "Unexpected source");
                break;
        }
    }

@@ -294,34 +195,4 @@ public class PrepareGetRequestSession extends RequestSession<GetCredentialReques
            return null;
        }
    }

    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;
    }
}
Loading