From b7ef16f59b517216e29dbc33cae14f2934b4a056 Mon Sep 17 00:00:00 2001 From: Reema Bajwa Date: Tue, 20 Dec 2022 06:44:48 +0000 Subject: [PATCH] Add clear cred implementaion Test: Built locally Change-Id: Iab1d87994b1f2199065f102f157d2f431eae832f --- .../android/credentials/ui/RequestInfo.java | 2 + .../credentials/ClearRequestSession.java | 142 ++++++++++++++++++ .../credentials/CredentialManagerService.java | 40 ++++- .../CredentialManagerServiceImpl.java | 6 +- .../credentials/ProviderClearSession.java | 119 +++++++++++++++ .../server/credentials/ProviderSession.java | 12 +- .../credentials/RemoteCredentialService.java | 50 ++++++ .../server/credentials/RequestSession.java | 2 +- 8 files changed, 364 insertions(+), 9 deletions(-) create mode 100644 services/credentials/java/com/android/server/credentials/ClearRequestSession.java create mode 100644 services/credentials/java/com/android/server/credentials/ProviderClearSession.java diff --git a/core/java/android/credentials/ui/RequestInfo.java b/core/java/android/credentials/ui/RequestInfo.java index c3937b6845ac1..5eaf2a0824027 100644 --- a/core/java/android/credentials/ui/RequestInfo.java +++ b/core/java/android/credentials/ui/RequestInfo.java @@ -44,6 +44,8 @@ public class RequestInfo implements Parcelable { public static final @NonNull String EXTRA_REQUEST_INFO = "android.credentials.ui.extra.REQUEST_INFO"; + /** Type value for any request that does not require UI. */ + public static final @NonNull String TYPE_UNDEFINED = "android.credentials.ui.TYPE_UNDEFINED"; /** Type value for an executeGetCredential request. */ public static final @NonNull String TYPE_GET = "android.credentials.ui.TYPE_GET"; /** Type value for an executeCreateCredential request. */ diff --git a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java new file mode 100644 index 0000000000000..6b254bf82cf61 --- /dev/null +++ b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java @@ -0,0 +1,142 @@ +/* + * 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.annotation.Nullable; +import android.content.ComponentName; +import android.content.Context; +import android.credentials.ClearCredentialStateRequest; +import android.credentials.IClearCredentialStateCallback; +import android.credentials.ui.ProviderData; +import android.credentials.ui.RequestInfo; +import android.os.RemoteException; +import android.service.credentials.CredentialProviderInfo; +import android.util.Log; + +import java.util.ArrayList; + +/** + * Central session for a single clearCredentialState request. This class listens to the + * responses from providers, and updates the provider(S) state. + */ +public final class ClearRequestSession extends RequestSession + implements ProviderSession.ProviderInternalCallback { + private static final String TAG = "GetRequestSession"; + + public ClearRequestSession(Context context, int userId, + IClearCredentialStateCallback callback, ClearCredentialStateRequest request, + String callingPackage) { + super(context, userId, request, callback, RequestInfo.TYPE_UNDEFINED, callingPackage); + } + + /** + * 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) { + ProviderClearSession providerClearSession = ProviderClearSession + .createNewSession(mContext, mUserId, providerInfo, + this, remoteCredentialService); + if (providerClearSession != null) { + Log.i(TAG, "In startProviderSession - provider session created and being added"); + mProviders.put(providerClearSession.getComponentName().flattenToString(), + providerClearSession); + } + return providerClearSession; + } + + @Override // from provider session + public void onProviderStatusChanged(ProviderSession.Status status, + ComponentName componentName) { + Log.i(TAG, "in onStatusChanged with status: " + status); + if (ProviderSession.isTerminatingStatus(status)) { + Log.i(TAG, "in onStatusChanged terminating status"); + onProviderTerminated(componentName); + } else if (ProviderSession.isCompletionStatus(status)) { + Log.i(TAG, "in onStatusChanged isCompletionStatus status"); + onProviderResponseComplete(componentName); + } + } + + @Override + public void onFinalResponseReceived(ComponentName componentName, Void response) { + respondToClientWithResponseAndFinish(); + } + + @Override + protected void onProviderResponseComplete(ComponentName componentName) { + if (!isAnyProviderPending()) { + onFinalResponseReceived(componentName, null); + } + } + + @Override + protected void onProviderTerminated(ComponentName componentName) { + if (!isAnyProviderPending()) { + processResponses(); + } + } + + @Override + protected void launchUiWithProviderData(ArrayList providerDataList) { + //Not applicable for clearCredential as UI is not needed + } + + @Override + public void onFinalErrorReceived(ComponentName componentName, String errorType, + String message) { + //Not applicable for clearCredential as response is not picked by the user + } + + private void respondToClientWithResponseAndFinish() { + Log.i(TAG, "respondToClientWithResponseAndFinish"); + try { + mClientCallback.onSuccess(); + } catch (RemoteException e) { + Log.i(TAG, "Issue while propagating the response to the client"); + } + finishSession(); + } + + private void respondToClientWithErrorAndFinish(String errorType, String errorMsg) { + Log.i(TAG, "respondToClientWithErrorAndFinish"); + try { + mClientCallback.onError(errorType, errorMsg); + } catch (RemoteException e) { + e.printStackTrace(); + } + finishSession(); + } + private void processResponses() { + for (ProviderSession session: mProviders.values()) { + if (session.isProviderResponseSet()) { + // If even one provider responded successfully, send back the response + // TODO: Aggregate other exceptions + respondToClientWithResponseAndFinish(); + return; + } + } + // TODO: Replace with properly defined error type + respondToClientWithErrorAndFinish("unknown", "All providers failed"); + } +} diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java index 1f74e932688a5..d29f86e572485 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java @@ -314,9 +314,45 @@ public final class CredentialManagerService @Override public ICancellationSignal clearCredentialState(ClearCredentialStateRequest request, IClearCredentialStateCallback callback, String callingPackage) { - // TODO: implement. - Log.i(TAG, "clearCredentialSession"); + Log.i(TAG, "starting clearCredentialState with callingPackage: " + callingPackage); + // TODO : Implement cancellation ICancellationSignal cancelTransport = CancellationSignal.createTransport(); + + // New request session, scoped for this request only. + final ClearRequestSession session = + new ClearRequestSession( + getContext(), + UserHandle.getCallingUserId(), + callback, + request, + callingPackage); + + // Initiate all provider sessions + // TODO: Determine if provider needs to have clear capability in their manifest + List providerSessions = + initiateProviderSessions(session, List.of()); + + if (providerSessions.isEmpty()) { + try { + // TODO("Replace with properly defined error type") + callback.onError("unknown_type", + "No providers available to fulfill request."); + } catch (RemoteException e) { + Log.i(TAG, "Issue invoking onError on IClearCredentialStateCallback " + + "callback: " + e.getMessage()); + } + } + + // Iterate over all provider sessions and invoke the request + providerSessions.forEach( + providerClearSession -> { + providerClearSession + .getRemoteCredentialService() + .onClearCredentialState( + (android.service.credentials.ClearCredentialStateRequest) + providerClearSession.getProviderRequest(), + /* callback= */ providerClearSession); + }); return cancelTransport; } } diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java index 08fdeedc45d25..c03d505bc90f6 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java @@ -76,7 +76,7 @@ public final class CredentialManagerServiceImpl extends @GuardedBy("mLock") public ProviderSession initiateProviderSessionForRequestLocked( RequestSession requestSession, List requestOptions) { - if (!isServiceCapableLocked(requestOptions)) { + if (!requestOptions.isEmpty() && !isServiceCapableLocked(requestOptions)) { Log.i(TAG, "Service is not capable"); return null; } @@ -88,9 +88,7 @@ public final class CredentialManagerServiceImpl extends } final RemoteCredentialService remoteService = new RemoteCredentialService( getContext(), mInfo.getServiceInfo().getComponentName(), mUserId); - ProviderSession providerSession = - requestSession.initiateProviderSession(mInfo, remoteService); - return providerSession; + return requestSession.initiateProviderSession(mInfo, remoteService); } /** Return true if at least one capability found. */ diff --git a/services/credentials/java/com/android/server/credentials/ProviderClearSession.java b/services/credentials/java/com/android/server/credentials/ProviderClearSession.java new file mode 100644 index 0000000000000..020552aa21b8a --- /dev/null +++ b/services/credentials/java/com/android/server/credentials/ProviderClearSession.java @@ -0,0 +1,119 @@ +/* + * 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.annotation.Nullable; +import android.annotation.UserIdInt; +import android.content.Context; +import android.credentials.ClearCredentialStateException; +import android.credentials.ui.ProviderData; +import android.credentials.ui.ProviderPendingIntentResponse; +import android.service.credentials.CallingAppInfo; +import android.service.credentials.ClearCredentialStateRequest; +import android.service.credentials.CredentialProviderInfo; +import android.util.ArraySet; +import android.util.Log; +import android.util.Slog; + +/** + * Central provider session that listens for provider callbacks, and maintains provider state. + * + * @hide + */ +public final class ProviderClearSession extends ProviderSession + implements + RemoteCredentialService.ProviderCallbacks { + private static final String TAG = "ProviderClearSession"; + + private ClearCredentialStateException mProviderException; + + /** Creates a new provider session to be used by the request session. */ + @Nullable public static ProviderClearSession createNewSession( + Context context, + @UserIdInt int userId, + CredentialProviderInfo providerInfo, + ClearRequestSession clearRequestSession, + RemoteCredentialService remoteCredentialService) { + ClearCredentialStateRequest providerRequest = + createProviderRequest( + clearRequestSession.mClientRequest, + clearRequestSession.mClientCallingPackage); + return new ProviderClearSession(context, providerInfo, clearRequestSession, userId, + remoteCredentialService, providerRequest); + } + + @Nullable + private static ClearCredentialStateRequest createProviderRequest( + android.credentials.ClearCredentialStateRequest clientRequest, + String clientCallingPackage + ) { + // TODO: Determine if provider needs to declare clear capability in manifest + return new ClearCredentialStateRequest( + new CallingAppInfo(clientCallingPackage, new ArraySet<>()), + clientRequest.getData()); + } + + public ProviderClearSession(Context context, + CredentialProviderInfo info, + ProviderInternalCallback callbacks, + int userId, RemoteCredentialService remoteCredentialService, + ClearCredentialStateRequest providerRequest) { + super(context, info, providerRequest, callbacks, userId, remoteCredentialService); + setStatus(Status.PENDING); + } + + @Override + public void onProviderResponseSuccess(@Nullable Void response) { + Log.i(TAG, "in onProviderResponseSuccess"); + mProviderResponseSet = true; + updateStatusAndInvokeCallback(Status.COMPLETE); + } + + /** Called when the provider response resulted in a failure. */ + @Override // Callback from the remote provider + public void onProviderResponseFailure(int errorCode, Exception exception) { + if (exception instanceof ClearCredentialStateException) { + mProviderException = (ClearCredentialStateException) exception; + } + updateStatusAndInvokeCallback(toStatus(errorCode)); + } + + /** Called when provider service dies. */ + @Override // Callback from the remote provider + public void onProviderServiceDied(RemoteCredentialService service) { + if (service.getComponentName().equals(mProviderInfo.getServiceInfo().getComponentName())) { + updateStatusAndInvokeCallback(Status.SERVICE_DEAD); + } else { + Slog.i(TAG, "Component names different in onProviderServiceDied - " + + "this should not happen"); + } + } + + @Nullable + @Override + protected ProviderData prepareUiData() { + //Not applicable for clearCredential as response is not picked by the user + return null; + } + + @Override + protected void onUiEntrySelected(String entryType, String entryId, + ProviderPendingIntentResponse providerPendingIntentResponse) { + //Not applicable for clearCredential as response is not picked by the user + } +} diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java index 7ecae9d3bec95..93e816a6e88bc 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java @@ -51,6 +51,7 @@ public abstract class ProviderSession @Nullable protected Credential mFinalCredentialResponse; @NonNull protected final T mProviderRequest; @Nullable protected R mProviderResponse; + @NonNull protected Boolean mProviderResponseSet = false; @Nullable protected Pair mUiRemoteEntry; @@ -84,7 +85,8 @@ public abstract class ProviderSession */ public static boolean isCompletionStatus(Status status) { return status == Status.CREDENTIAL_RECEIVED_FROM_INTENT - || status == Status.CREDENTIAL_RECEIVED_FROM_SELECTION; + || status == Status.CREDENTIAL_RECEIVED_FROM_SELECTION + || status == Status.COMPLETE; } /** @@ -130,7 +132,8 @@ public abstract class ProviderSession CREDENTIAL_RECEIVED_FROM_INTENT, PENDING_INTENT_INVOKED, CREDENTIAL_RECEIVED_FROM_SELECTION, - SAVE_ENTRIES_RECEIVED, CANCELED + SAVE_ENTRIES_RECEIVED, CANCELED, + COMPLETE } /** Converts exception to a provider session status. */ @@ -183,6 +186,11 @@ public abstract class ProviderSession return mProviderRequest; } + /** Returns whether the provider response is set. */ + protected Boolean isProviderResponseSet() { + return mProviderResponse != null || mProviderResponseSet; + } + /** Update the response state stored with the provider session. */ @Nullable protected R getProviderResponse() { return mProviderResponse; diff --git a/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java b/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java index 6049fd9a4a214..8cad6acebc4a1 100644 --- a/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java +++ b/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java @@ -21,6 +21,7 @@ import android.annotation.Nullable; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.credentials.ClearCredentialStateException; import android.credentials.CreateCredentialException; import android.credentials.GetCredentialException; import android.os.Handler; @@ -30,10 +31,12 @@ import android.service.credentials.BeginCreateCredentialRequest; import android.service.credentials.BeginCreateCredentialResponse; import android.service.credentials.BeginGetCredentialRequest; import android.service.credentials.BeginGetCredentialResponse; +import android.service.credentials.ClearCredentialStateRequest; import android.service.credentials.CredentialProviderErrors; import android.service.credentials.CredentialProviderService; import android.service.credentials.IBeginCreateCredentialCallback; import android.service.credentials.IBeginGetCredentialCallback; +import android.service.credentials.IClearCredentialStateCallback; import android.service.credentials.ICredentialProviderService; import android.text.format.DateUtils; import android.util.Log; @@ -195,6 +198,53 @@ public class RemoteCredentialService extends ServiceConnector.Impl callback) { + Log.i(TAG, "In onClearCredentialState in RemoteCredentialService"); + AtomicReference cancellationSink = new AtomicReference<>(); + AtomicReference> futureRef = new AtomicReference<>(); + + CompletableFuture connectThenExecute = + postAsync(service -> { + CompletableFuture clearCredentialFuture = + new CompletableFuture<>(); + ICancellationSignal cancellationSignal = service.onClearCredentialState( + request, new IClearCredentialStateCallback.Stub() { + @Override + public void onSuccess() { + Log.i(TAG, "In onSuccess onClearCredentialState " + + "in RemoteCredentialService"); + clearCredentialFuture.complete(null); + } + + @Override + public void onFailure(String errorType, CharSequence message) { + Log.i(TAG, "In onFailure in RemoteCredentialService"); + String errorMsg = message == null ? "" : + String.valueOf(message); + clearCredentialFuture.completeExceptionally( + new ClearCredentialStateException(errorType, errorMsg)); + }}); + CompletableFuture future = futureRef.get(); + if (future != null && future.isCancelled()) { + dispatchCancellationSignal(cancellationSignal); + } else { + cancellationSink.set(cancellationSignal); + } + return clearCredentialFuture; + }).orTimeout(TIMEOUT_REQUEST_MILLIS, TimeUnit.MILLISECONDS); + + futureRef.set(connectThenExecute); + connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() -> + handleExecutionResponse(result, error, cancellationSink, callback))); + } + private void handleExecutionResponse(T result, Throwable error, AtomicReference cancellationSink, diff --git a/services/credentials/java/com/android/server/credentials/RequestSession.java b/services/credentials/java/com/android/server/credentials/RequestSession.java index 937fac98a0b33..6d7cb4c1e8598 100644 --- a/services/credentials/java/com/android/server/credentials/RequestSession.java +++ b/services/credentials/java/com/android/server/credentials/RequestSession.java @@ -144,7 +144,7 @@ abstract class RequestSession implements CredentialManagerUi.CredentialMan mProviders.clear(); } - private boolean isAnyProviderPending() { + boolean isAnyProviderPending() { for (ProviderSession session : mProviders.values()) { if (ProviderSession.isStatusWaitingForRemoteResponse(session.getStatus())) { return true;