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

Commit b1d1033b authored by Helen Qin's avatar Helen Qin
Browse files

[CredMan] Unhide the getPendingCredential api.

Per partner feedback, expose a version of getCredential api that splits
prefetching candidates and selection UI into two separate calls. This
allows performing prefetch work and an early time and delaying the UI to
a later time which will be significantly faster to surface upon request.

Bug: 273308895
Test: CTS & local tested
Change-Id: I2f08a4333ac90bb9fdc77cb8f522b415cd694336
parent e5e601c6
Loading
Loading
Loading
Loading
+13 −0
Original line number Diff line number Diff line
@@ -83,6 +83,7 @@ package android {
    field public static final String CLEAR_APP_CACHE = "android.permission.CLEAR_APP_CACHE";
    field public static final String CONFIGURE_WIFI_DISPLAY = "android.permission.CONFIGURE_WIFI_DISPLAY";
    field public static final String CONTROL_LOCATION_UPDATES = "android.permission.CONTROL_LOCATION_UPDATES";
    field public static final String CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS = "android.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS";
    field public static final String CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS = "android.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS";
    field public static final String CREDENTIAL_MANAGER_SET_ORIGIN = "android.permission.CREDENTIAL_MANAGER_SET_ORIGIN";
    field public static final String DELETE_CACHE_FILES = "android.permission.DELETE_CACHE_FILES";
@@ -13669,7 +13670,9 @@ package android.credentials {
    method public void clearCredentialState(@NonNull android.credentials.ClearCredentialStateRequest, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.credentials.ClearCredentialStateException>);
    method public void createCredential(@NonNull android.credentials.CreateCredentialRequest, @NonNull android.app.Activity, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.credentials.CreateCredentialResponse,android.credentials.CreateCredentialException>);
    method public void getCredential(@NonNull android.credentials.GetCredentialRequest, @NonNull android.app.Activity, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.credentials.GetCredentialResponse,android.credentials.GetCredentialException>);
    method public void getCredential(@NonNull android.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle, @NonNull android.app.Activity, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.credentials.GetCredentialResponse,android.credentials.GetCredentialException>);
    method public boolean isEnabledCredentialProviderService(@NonNull android.content.ComponentName);
    method public void prepareGetCredential(@NonNull android.credentials.GetCredentialRequest, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.credentials.PrepareGetCredentialResponse,android.credentials.GetCredentialException>);
    method public void registerCredentialDescription(@NonNull android.credentials.RegisterCredentialDescriptionRequest);
    method public void unregisterCredentialDescription(@NonNull android.credentials.UnregisterCredentialDescriptionRequest);
  }
@@ -13734,6 +13737,16 @@ package android.credentials {
    field @NonNull public static final android.os.Parcelable.Creator<android.credentials.GetCredentialResponse> CREATOR;
  }
  public final class PrepareGetCredentialResponse {
    method @NonNull public android.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle getPendingGetCredentialHandle();
    method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasAuthenticationResults();
    method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasCredentialResults(@NonNull String);
    method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasRemoteResults();
  }
  public static final class PrepareGetCredentialResponse.PendingGetCredentialHandle {
  }
  public final class RegisterCredentialDescriptionRequest implements android.os.Parcelable {
    ctor public RegisterCredentialDescriptionRequest(@NonNull android.credentials.CredentialDescription);
    ctor public RegisterCredentialDescriptionRequest(@NonNull java.util.Set<android.credentials.CredentialDescription>);
+114 −19
Original line number Diff line number Diff line
@@ -16,8 +16,6 @@

package android.credentials;

import static android.Manifest.permission.CREDENTIAL_MANAGER_SET_ORIGIN;

import static java.util.Objects.requireNonNull;

import android.annotation.CallbackExecutor;
@@ -167,37 +165,83 @@ public final class CredentialManager {
    }

    /**
     * Gets a {@link GetPendingCredentialResponse} that can launch the credential retrieval UI flow
     * to request a user credential for your app.
     * Launches the remaining flows to retrieve an app credential from the user, after the
     * completed prefetch work corresponding to the given {@code pendingGetCredentialHandle}.
     *
     * @param request            the request specifying type(s) of credentials to get from the user
     * <p>The execution can potentially launch UI flows to collect user consent to using a
     * credential, display a picker when multiple credentials exist, etc.
     *
     * <p>Use this API to complete the full credential retrieval operation after you initiated a
     * request through the {@link #prepareGetCredential(
     * GetCredentialRequest, CancellationSignal, Executor, OutcomeReceiver)} API.
     *
     * @param pendingGetCredentialHandle the handle representing the pending operation to resume
     * @param activity the activity used to launch any UI needed
     * @param cancellationSignal an optional signal that allows for cancelling this call
     * @param executor the callback will take place on this {@link Executor}
     * @param callback the callback invoked when the request succeeds or fails
     */
    public void getCredential(
            @NonNull PrepareGetCredentialResponse.PendingGetCredentialHandle
                    pendingGetCredentialHandle,
            @NonNull Activity activity,
            @Nullable CancellationSignal cancellationSignal,
            @CallbackExecutor @NonNull Executor executor,
            @NonNull OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback) {
        requireNonNull(pendingGetCredentialHandle, "pendingGetCredentialHandle must not be null");
        requireNonNull(activity, "activity must not be null");
        requireNonNull(executor, "executor must not be null");
        requireNonNull(callback, "callback must not be null");

        if (cancellationSignal != null && cancellationSignal.isCanceled()) {
            Log.w(TAG, "getCredential already canceled");
            return;
        }

        pendingGetCredentialHandle.show(activity, cancellationSignal, executor, callback);
    }

    /**
     * Prepare for a get-credential operation. Returns a {@link PrepareGetCredentialResponse} that
     * can launch the credential retrieval UI flow to request a user credential for your app.
     *
     * @hide
     * <p>This API doesn't invoke any UI. It only performs the preparation work so that you can
     * later launch the remaining get-credential operation (involves UIs) through the {@link
     * #getCredential(PrepareGetCredentialResponse.PendingGetCredentialHandle, Activity,
     * CancellationSignal, Executor, OutcomeReceiver)} API which incurs less latency compared to
     * the {@link #getCredential(GetCredentialRequest, Activity, CancellationSignal, Executor,
     * OutcomeReceiver)} API that executes the whole operation in one call.
     *
     * @param request            the request specifying type(s) of credentials to get from the user
     * @param cancellationSignal an optional signal that allows for cancelling this call
     * @param executor           the callback will take place on this {@link Executor}
     * @param callback           the callback invoked when the request succeeds or fails
     */
    public void getPendingCredential(
    public void prepareGetCredential(
            @NonNull GetCredentialRequest request,
            @Nullable CancellationSignal cancellationSignal,
            @CallbackExecutor @NonNull Executor executor,
            @NonNull OutcomeReceiver<
                    GetPendingCredentialResponse, GetCredentialException> callback) {
                    PrepareGetCredentialResponse, GetCredentialException> callback) {
        requireNonNull(request, "request must not be null");
        requireNonNull(executor, "executor must not be null");
        requireNonNull(callback, "callback must not be null");

        if (cancellationSignal != null && cancellationSignal.isCanceled()) {
            Log.w(TAG, "getPendingCredential already canceled");
            Log.w(TAG, "prepareGetCredential already canceled");
            return;
        }

        ICancellationSignal cancelRemote = null;
        GetCredentialTransportPendingUseCase getCredentialTransport =
                new GetCredentialTransportPendingUseCase();
        try {
            cancelRemote =
                    mService.executeGetPendingCredential(
                    mService.executePrepareGetCredential(
                            request,
                            new GetPendingCredentialTransport(executor, callback),
                            new PrepareGetCredentialTransport(
                                    executor, callback, getCredentialTransport),
                            getCredentialTransport,
                            mContext.getOpPackageName());
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
@@ -484,23 +528,27 @@ public final class CredentialManager {
        }
    }

    private static class GetPendingCredentialTransport extends IGetPendingCredentialCallback.Stub {
    private static class PrepareGetCredentialTransport extends IPrepareGetCredentialCallback.Stub {
        // TODO: listen for cancellation to release callback.

        private final Executor mExecutor;
        private final OutcomeReceiver<
                GetPendingCredentialResponse, GetCredentialException> mCallback;
                PrepareGetCredentialResponse, GetCredentialException> mCallback;
        private final GetCredentialTransportPendingUseCase mGetCredentialTransport;

        private GetPendingCredentialTransport(
        private PrepareGetCredentialTransport(
                Executor executor,
                OutcomeReceiver<GetPendingCredentialResponse, GetCredentialException> callback) {
                OutcomeReceiver<PrepareGetCredentialResponse, GetCredentialException> callback,
                GetCredentialTransportPendingUseCase getCredentialTransport) {
            mExecutor = executor;
            mCallback = callback;
            mGetCredentialTransport = getCredentialTransport;
        }

        @Override
        public void onResponse(GetPendingCredentialResponse response) {
            mExecutor.execute(() -> mCallback.onResult(response));
        public void onResponse(PrepareGetCredentialResponseInternal response) {
            mExecutor.execute(() -> mCallback.onResult(
                    new PrepareGetCredentialResponse(response, mGetCredentialTransport)));
        }

        @Override
@@ -510,6 +558,51 @@ public final class CredentialManager {
        }
    }

    /** @hide */
    protected static class GetCredentialTransportPendingUseCase
            extends IGetCredentialCallback.Stub {
        @Nullable private PrepareGetCredentialResponse.GetPendingCredentialInternalCallback
                mCallback = null;

        private GetCredentialTransportPendingUseCase() {}

        public void setCallback(
                PrepareGetCredentialResponse.GetPendingCredentialInternalCallback callback) {
            if (mCallback == null) {
                mCallback = callback;
            } else {
                throw new IllegalStateException("callback has already been set once");
            }
        }

        @Override
        public void onPendingIntent(PendingIntent pendingIntent) {
            if (mCallback != null) {
                mCallback.onPendingIntent(pendingIntent);
            } else {
                Log.d(TAG, "Unexpected onPendingIntent call before the show invocation");
            }
        }

        @Override
        public void onResponse(GetCredentialResponse response) {
            if (mCallback != null) {
                mCallback.onResponse(response);
            } else {
                Log.d(TAG, "Unexpected onResponse call before the show invocation");
            }
        }

        @Override
        public void onError(String errorType, String message) {
            if (mCallback != null) {
                mCallback.onError(errorType, message);
            } else {
                Log.d(TAG, "Unexpected onError call before the show invocation");
            }
        }
    }

    private static class GetCredentialTransport extends IGetCredentialCallback.Stub {
        // TODO: listen for cancellation to release callback.

@@ -535,7 +628,8 @@ public final class CredentialManager {
                        TAG,
                        "startIntentSender() failed for intent:" + pendingIntent.getIntentSender(),
                        e);
                // TODO: propagate the error.
                mExecutor.execute(() -> mCallback.onError(
                        new GetCredentialException(GetCredentialException.TYPE_UNKNOWN)));
            }
        }

@@ -577,7 +671,8 @@ public final class CredentialManager {
                        TAG,
                        "startIntentSender() failed for intent:" + pendingIntent.getIntentSender(),
                        e);
                // TODO: propagate the error.
                mExecutor.execute(() -> mCallback.onError(
                        new CreateCredentialException(CreateCredentialException.TYPE_UNKNOWN)));
            }
        }

+2 −2
Original line number Diff line number Diff line
@@ -27,7 +27,7 @@ import android.credentials.UnregisterCredentialDescriptionRequest;
import android.credentials.IClearCredentialStateCallback;
import android.credentials.ICreateCredentialCallback;
import android.credentials.IGetCredentialCallback;
import android.credentials.IGetPendingCredentialCallback;
import android.credentials.IPrepareGetCredentialCallback;
import android.credentials.ISetEnabledProvidersCallback;
import android.content.ComponentName;
import android.os.ICancellationSignal;
@@ -41,7 +41,7 @@ interface ICredentialManager {

    @nullable ICancellationSignal executeGetCredential(in GetCredentialRequest request, in IGetCredentialCallback callback, String callingPackage);

    @nullable ICancellationSignal executeGetPendingCredential(in GetCredentialRequest request, in IGetPendingCredentialCallback callback, String callingPackage);
    @nullable ICancellationSignal executePrepareGetCredential(in GetCredentialRequest request, in IPrepareGetCredentialCallback prepareGetCredentialCallback, in IGetCredentialCallback getCredentialCallback, String callingPackage);

    @nullable ICancellationSignal executeCreateCredential(in CreateCredentialRequest request, in ICreateCredentialCallback callback, String callingPackage);

+4 −4
Original line number Diff line number Diff line
@@ -17,14 +17,14 @@
package android.credentials;

import android.app.PendingIntent;
import android.credentials.GetPendingCredentialResponse;
import android.credentials.PrepareGetCredentialResponseInternal;

/**
 * Listener for a executeGetPendingCredential request.
 * Listener for a executePrepareGetCredential request.
 *
 * @hide
 */
interface IGetPendingCredentialCallback {
    oneway void onResponse(in GetPendingCredentialResponse response);
interface IPrepareGetCredentialCallback {
    oneway void onResponse(in PrepareGetCredentialResponseInternal response);
    oneway void onError(String errorType, String message);
}
 No newline at end of file
+180 −0
Original line number Diff line number Diff line
/*
 * Copyright 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 android.credentials;

import static android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS;

import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.IntentSender;
import android.os.CancellationSignal;
import android.os.OutcomeReceiver;
import android.util.Log;

import java.util.concurrent.Executor;


/**
 * A response object that prefetches user app credentials and provides metadata about them. It can
 * then be used to issue the full credential retrieval flow via the
 * {@link CredentialManager#getCredential(PendingGetCredentialHandle, Activity, CancellationSignal,
 * Executor, OutcomeReceiver)} method to perform the remaining flows such as consent collection
 * and credential selection, to officially retrieve a credential.
 */
public final class PrepareGetCredentialResponse {

    /**
     * A handle that represents a pending get-credential operation. Pass this handle to {@link
     * CredentialManager#getCredential(PendingGetCredentialHandle, Activity, CancellationSignal,
     * Executor, OutcomeReceiver)} to perform the remaining flows to officially retrieve a
     * credential.
     */
    public static final class PendingGetCredentialHandle {
        @NonNull
        private final CredentialManager.GetCredentialTransportPendingUseCase
                mGetCredentialTransport;
        /**
         * The pending intent to be launched to finalize the user credential. If null, the callback
         * will fail with {@link GetCredentialException#TYPE_NO_CREDENTIAL}.
         */
        @Nullable
        private final PendingIntent mPendingIntent;

        /** @hide */
        PendingGetCredentialHandle(
                @NonNull CredentialManager.GetCredentialTransportPendingUseCase transport,
                @Nullable PendingIntent pendingIntent) {
            mGetCredentialTransport = transport;
            mPendingIntent = pendingIntent;
        }

        /** @hide */
        void show(@NonNull Activity activity, @Nullable CancellationSignal cancellationSignal,
                @CallbackExecutor @NonNull Executor executor,
                @NonNull OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback) {
            if (mPendingIntent == null) {
                executor.execute(() -> callback.onError(
                        new GetCredentialException(GetCredentialException.TYPE_NO_CREDENTIAL)));
                return;
            }

            mGetCredentialTransport.setCallback(new GetPendingCredentialInternalCallback() {
                @Override
                public void onPendingIntent(PendingIntent pendingIntent) {
                    try {
                        activity.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0);
                    } catch (IntentSender.SendIntentException e) {
                        Log.e(TAG, "startIntentSender() failed for intent for show()", e);
                        executor.execute(() -> callback.onError(
                                new GetCredentialException(GetCredentialException.TYPE_UNKNOWN)));
                    }
                }

                @Override
                public void onResponse(GetCredentialResponse response) {
                    executor.execute(() -> callback.onResult(response));
                }

                @Override
                public void onError(String errorType, String message) {
                    executor.execute(
                            () -> callback.onError(new GetCredentialException(errorType, message)));
                }
            });

            try {
                activity.startIntentSender(mPendingIntent.getIntentSender(), null, 0, 0, 0);
            } catch (IntentSender.SendIntentException e) {
                Log.e(TAG, "startIntentSender() failed for intent for show()", e);
                executor.execute(() -> callback.onError(
                        new GetCredentialException(GetCredentialException.TYPE_UNKNOWN)));
            }
        }
    }
    private static final String TAG = "CredentialManager";

    @NonNull private final PrepareGetCredentialResponseInternal mResponseInternal;

    @NonNull private final PendingGetCredentialHandle mPendingGetCredentialHandle;

    /**
     * Returns true if the user has any candidate credentials for the given {@code credentialType},
     * and false otherwise.
     */
    @RequiresPermission(CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS)
    public boolean hasCredentialResults(@NonNull String credentialType) {
        return mResponseInternal.hasCredentialResults(credentialType);
    }

    /**
     * Returns true if the user has any candidate authentication actions (locked credential
     * supplier), and false otherwise.
     */
    @RequiresPermission(CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS)
    public boolean hasAuthenticationResults() {
        return mResponseInternal.hasAuthenticationResults();
    }

    /**
     * Returns true if the user has any candidate remote credential results, and false otherwise.
     */
    @RequiresPermission(CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS)
    public boolean hasRemoteResults() {
        return mResponseInternal.hasRemoteResults();
    }

    /**
     * Returns a handle that represents this pending get-credential operation. Pass this handle to
     * {@link CredentialManager#getCredential(PendingGetCredentialHandle, Activity,
     * CancellationSignal, Executor, OutcomeReceiver)} to perform the remaining flows to officially
     * retrieve a credential.
     */
    @NonNull
    public PendingGetCredentialHandle getPendingGetCredentialHandle() {
        return mPendingGetCredentialHandle;
    }

    /**
     * Constructs a {@link PrepareGetCredentialResponse}.
     *
     * @param responseInternal       whether caller has the permission to query the credential
     *                               result metadata
     * @param getCredentialTransport the transport for the operation to finalaze a credential
     * @hide
     */
    protected PrepareGetCredentialResponse(
            @NonNull PrepareGetCredentialResponseInternal responseInternal,
            @NonNull CredentialManager.GetCredentialTransportPendingUseCase
                    getCredentialTransport) {
        mResponseInternal = responseInternal;
        mPendingGetCredentialHandle = new PendingGetCredentialHandle(
                getCredentialTransport, responseInternal.getPendingIntent());
    }

    /** @hide */
    protected interface GetPendingCredentialInternalCallback {
        void onPendingIntent(@NonNull PendingIntent pendingIntent);

        void onResponse(@NonNull GetCredentialResponse response);

        void onError(@NonNull String errorType, @Nullable String message);
    }
}
Loading