diff --git a/core/java/android/service/credentials/CredentialEntry.java b/core/java/android/service/credentials/CredentialEntry.java index 1d4ac25eb2842..98c537a6d6ef1 100644 --- a/core/java/android/service/credentials/CredentialEntry.java +++ b/core/java/android/service/credentials/CredentialEntry.java @@ -173,7 +173,7 @@ public final class CredentialEntry implements Parcelable { */ public @NonNull Builder setPendingIntent(@Nullable PendingIntent pendingIntent) { if (pendingIntent != null) { - Preconditions.checkState(mCredential != null, + Preconditions.checkState(mCredential == null, "credential is already set. Cannot set both the pendingIntent " + "and the credential"); } @@ -189,7 +189,7 @@ public final class CredentialEntry implements Parcelable { */ public @NonNull Builder setCredential(@Nullable Credential credential) { if (credential != null) { - Preconditions.checkState(mPendingIntent != null, + Preconditions.checkState(mPendingIntent == null, "pendingIntent is already set. Cannot set both the " + "pendingIntent and the credential"); } @@ -215,10 +215,10 @@ public final class CredentialEntry implements Parcelable { * is set, or if both are set. */ public @NonNull CredentialEntry build() { - Preconditions.checkState(mPendingIntent == null && mCredential == null, - "Either pendingIntent or credential must be set"); - Preconditions.checkState(mPendingIntent != null && mCredential != null, - "Cannot set both the pendingIntent and credential"); + Preconditions.checkState(((mPendingIntent != null && mCredential == null) + || (mPendingIntent == null && mCredential != null)), + "Either pendingIntent or credential must be set, and both cannot" + + "be set at the same time"); return new CredentialEntry(mType, mSlice, mPendingIntent, mCredential, mAutoSelectAllowed); } diff --git a/core/java/android/service/credentials/CredentialProviderInfo.java b/core/java/android/service/credentials/CredentialProviderInfo.java index 2c7a983826f6a..f89ad8e6e4290 100644 --- a/core/java/android/service/credentials/CredentialProviderInfo.java +++ b/core/java/android/service/credentials/CredentialProviderInfo.java @@ -24,7 +24,6 @@ import android.app.AppGlobals; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; @@ -96,6 +95,8 @@ public final class CredentialProviderInfo { mLabel = mServiceInfo.loadSafeLabel( mContext.getPackageManager(), 0 /* do not ellipsize */, TextUtils.SAFE_STRING_FLAG_FIRST_LINE | TextUtils.SAFE_STRING_FLAG_TRIM); + Log.i(TAG, "mLabel is : " + mLabel + ", for: " + mServiceInfo.getComponentName() + .flattenToString()); populateProviderCapabilities(context, serviceInfo); } diff --git a/core/java/android/service/credentials/CredentialProviderService.java b/core/java/android/service/credentials/CredentialProviderService.java index b1b08f4666220..6f3e786ffc4e8 100644 --- a/core/java/android/service/credentials/CredentialProviderService.java +++ b/core/java/android/service/credentials/CredentialProviderService.java @@ -41,6 +41,18 @@ import java.util.Objects; * @hide */ public abstract class CredentialProviderService extends Service { + /** Extra to be used by provider to populate the credential when ending the activity started + * through the {@code pendingIntent} on the selected {@link SaveEntry}. **/ + public static final String EXTRA_SAVE_CREDENTIAL = + "android.service.credentials.extra.SAVE_CREDENTIAL"; + + /** + * Provider must read the value against this extra to receive the complete create credential + * request parameters, when a pending intent is launched. + */ + public static final String EXTRA_CREATE_CREDENTIAL_REQUEST_PARAMS = + "android.service.credentials.extra.CREATE_CREDENTIAL_REQUEST_PARAMS"; + private static final String TAG = "CredProviderService"; public static final String CAPABILITY_META_DATA_KEY = "android.credentials.capabilities"; @@ -64,7 +76,7 @@ public abstract class CredentialProviderService extends Service { } @Override - public final @NonNull IBinder onBind(@NonNull Intent intent) { + @NonNull public final IBinder onBind(@NonNull Intent intent) { if (SERVICE_INTERFACE.equals(intent.getAction())) { return mInterface.asBinder(); } diff --git a/core/java/android/service/credentials/GetCredentialsRequest.java b/core/java/android/service/credentials/GetCredentialsRequest.java index e06be44330621..03ba20e1df276 100644 --- a/core/java/android/service/credentials/GetCredentialsRequest.java +++ b/core/java/android/service/credentials/GetCredentialsRequest.java @@ -119,9 +119,9 @@ public final class GetCredentialsRequest implements Parcelable { */ public @NonNull Builder setGetCredentialOptions( @NonNull List getCredentialOptions) { - Preconditions.checkCollectionNotEmpty(mGetCredentialOptions, + Preconditions.checkCollectionNotEmpty(getCredentialOptions, "getCredentialOptions"); - Preconditions.checkCollectionElementsNotNull(mGetCredentialOptions, + Preconditions.checkCollectionElementsNotNull(getCredentialOptions, "getCredentialOptions"); mGetCredentialOptions = getCredentialOptions; return this; diff --git a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java new file mode 100644 index 0000000000000..e07bc7761156b --- /dev/null +++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java @@ -0,0 +1,75 @@ +/* + * 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.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.credentials.CreateCredentialRequest; +import android.credentials.CredentialManager; +import android.credentials.ICreateCredentialCallback; +import android.credentials.ui.ProviderData; +import android.credentials.ui.RequestInfo; +import android.service.credentials.CredentialProviderInfo; +import android.util.Log; + +import java.util.ArrayList; + +/** + * Central session for a single {@link CredentialManager#executeCreateCredential} request. + * This class listens to the responses from providers, and the UX app, and updates the + * provider(s) state maintained in {@link ProviderCreateSession}. + */ +public final class CreateRequestSession extends RequestSession { + private static final String TAG = "CreateRequestSession"; + + CreateRequestSession(@NonNull Context context, int userId, + CreateCredentialRequest request, + ICreateCredentialCallback callback, + String callingPackage) { + super(context, userId, request, callback, RequestInfo.TYPE_CREATE, callingPackage); + } + + /** + * Creates a new provider session, and adds it to list of providers that are contributing to + * this request session. + * + * @return the provider session that was started + */ + @Override + @Nullable + public ProviderSession initiateProviderSession(CredentialProviderInfo providerInfo, + RemoteCredentialService remoteCredentialService) { + ProviderCreateSession providerCreateSession = ProviderCreateSession + .createNewSession(mContext, mUserId, providerInfo, + this, remoteCredentialService); + if (providerCreateSession != null) { + Log.i(TAG, "In startProviderSession - provider session created and being added"); + mProviders.put(providerCreateSession.getComponentName().flattenToString(), + providerCreateSession); + } + return providerCreateSession; + } + + @Override + protected void launchUiWithProviderData(ArrayList providerDataList) { + mHandler.post(() -> mCredentialManagerUi.show(RequestInfo.newCreateRequestInfo( + mRequestId, mClientRequest, mIsFirstUiTurn, mClientCallingPackage), + providerDataList)); + } +} diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java index 321f022f526f4..374da1c8e7e3c 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java @@ -23,6 +23,7 @@ import android.annotation.UserIdInt; import android.content.Context; import android.content.pm.PackageManager; import android.credentials.CreateCredentialRequest; +import android.credentials.GetCredentialOption; import android.credentials.GetCredentialRequest; import android.credentials.IClearCredentialSessionCallback; import android.credentials.ICreateCredentialCallback; @@ -33,6 +34,7 @@ import android.os.CancellationSignal; import android.os.ICancellationSignal; import android.os.UserHandle; import android.provider.Settings; +import android.service.credentials.GetCredentialsRequest; import android.text.TextUtils; import android.util.Log; import android.util.Slog; @@ -43,6 +45,7 @@ import com.android.server.infra.SecureSettingsServiceNameResolver; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; +import java.util.stream.Collectors; /** * Entry point service for credential management. @@ -91,17 +94,15 @@ public final class CredentialManagerService extends return new ArrayList<>(); } List serviceList = new ArrayList<>(serviceNames.length); - for (int i = 0; i < serviceNames.length; i++) { - Log.i(TAG, "in newServiceListLocked, service: " + serviceNames[i]); - if (TextUtils.isEmpty(serviceNames[i])) { + for (String serviceName : serviceNames) { + Log.i(TAG, "in newServiceListLocked, service: " + serviceName); + if (TextUtils.isEmpty(serviceName)) { continue; } try { serviceList.add(new CredentialManagerServiceImpl(this, mLock, resolvedUserId, - serviceNames[i])); - } catch (PackageManager.NameNotFoundException e) { - Log.i(TAG, "Unable to add serviceInfo : " + e.getMessage()); - } catch (SecurityException e) { + serviceName)); + } catch (PackageManager.NameNotFoundException | SecurityException e) { Log.i(TAG, "Unable to add serviceInfo : " + e.getMessage()); } } @@ -115,15 +116,31 @@ public final class CredentialManagerService extends synchronized (mLock) { final List services = getServiceListForUserLocked(userId); - services.forEach(s -> { + for (CredentialManagerServiceImpl s : services) { c.accept(s); - }); + } } } finally { Binder.restoreCallingIdentity(origId); } } + private List initiateProviderSessions(RequestSession session, + List requestOptions) { + List providerSessions = new ArrayList<>(); + // Invoke all services of a user to initiate a provider session + runForUser((service) -> { + if (service.isServiceCapable(requestOptions)) { + ProviderSession providerSession = service + .initiateProviderSessionForRequest(session); + if (providerSession != null) { + providerSessions.add(providerSession); + } + } + }); + return providerSessions; + } + final class CredentialManagerServiceStub extends ICredentialManager.Stub { @Override public ICancellationSignal executeGetCredential( @@ -137,11 +154,22 @@ public final class CredentialManagerService extends // New request session, scoped for this request only. final GetRequestSession session = new GetRequestSession(getContext(), UserHandle.getCallingUserId(), - callback); + callback, + request, + callingPackage); - // Invoke all services of a user - runForUser((service) -> { - service.getCredential(request, session, callingPackage); + // Initiate all provider sessions + List providerSessions = + initiateProviderSessions(session, request.getGetCredentialOptions() + .stream().map(GetCredentialOption::getType) + .collect(Collectors.toList())); + // TODO : Return error when no providers available + + // Iterate over all provider sessions and invoke the request + providerSessions.forEach(providerGetSession -> { + providerGetSession.getRemoteCredentialService().onGetCredentials( + (GetCredentialsRequest) providerGetSession.getProviderRequest(), + /*callback=*/providerGetSession); }); return cancelTransport; } @@ -151,9 +179,29 @@ public final class CredentialManagerService extends CreateCredentialRequest request, ICreateCredentialCallback callback, String callingPackage) { - // TODO: implement. - Log.i(TAG, "executeCreateCredential"); + Log.i(TAG, "starting executeCreateCredential with callingPackage: " + callingPackage); + // TODO : Implement cancellation ICancellationSignal cancelTransport = CancellationSignal.createTransport(); + + // New request session, scoped for this request only. + final CreateRequestSession session = new CreateRequestSession(getContext(), + UserHandle.getCallingUserId(), + request, + callback, + callingPackage); + + // Initiate all provider sessions + List providerSessions = + initiateProviderSessions(session, List.of(request.getType())); + // TODO : Return error when no providers available + + // Iterate over all provider sessions and invoke the request + providerSessions.forEach(providerCreateSession -> { + providerCreateSession.getRemoteCredentialService().onCreateCredential( + (android.service.credentials.CreateCredentialRequest) + providerCreateSession.getProviderRequest(), + /*callback=*/providerCreateSession); + }); 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 cc03f9b891194..0c323043a7a3b 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java @@ -21,13 +21,13 @@ import android.annotation.Nullable; import android.content.ComponentName; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; -import android.credentials.GetCredentialRequest; import android.service.credentials.CredentialProviderInfo; -import android.service.credentials.GetCredentialsRequest; import android.util.Slog; import com.android.server.infra.AbstractPerUserSystemService; +import java.util.List; + /** * Per-user, per remote service implementation of {@link CredentialManagerService} @@ -61,50 +61,38 @@ public final class CredentialManagerServiceImpl extends return mInfo.getServiceInfo(); } - public void getCredential(GetCredentialRequest request, GetRequestSession requestSession, - String callingPackage) { - Slog.i(TAG, "in getCredential in CredManServiceImpl"); + /** + * Starts a provider session and associates it with the given request session. */ + @Nullable + public ProviderSession initiateProviderSessionForRequest( + RequestSession requestSession) { + Slog.i(TAG, "in initiateProviderSessionForRequest in CredManServiceImpl"); if (mInfo == null) { - Slog.i(TAG, "in getCredential in CredManServiceImpl, but mInfo is null"); - return; + Slog.i(TAG, "in initiateProviderSessionForRequest in CredManServiceImpl, " + + "but mInfo is null. This shouldn't happen"); + return null; } - - // TODO : Determine if remoteService instance can be reused across requests final RemoteCredentialService remoteService = new RemoteCredentialService( getContext(), mInfo.getServiceInfo().getComponentName(), mUserId); - ProviderGetSession providerSession = new ProviderGetSession(mInfo, - requestSession, mUserId, remoteService); - // Set the provider info to the session when the request is initiated. This happens here - // because there is one serviceImpl per remote provider, and so we can only retrieve - // the provider information in the scope of this instance, whereas the session is for the - // entire request. - requestSession.addProviderSession(providerSession); - GetCredentialsRequest filteredRequest = getRequestWithValidType(request, callingPackage); - if (filteredRequest != null) { - remoteService.onGetCredentials(getRequestWithValidType(request, callingPackage), - providerSession); - } + ProviderSession providerSession = + requestSession.initiateProviderSession(mInfo, remoteService); + return providerSession; } - @Nullable - private GetCredentialsRequest getRequestWithValidType(GetCredentialRequest request, - String callingPackage) { - GetCredentialsRequest.Builder builder = - new GetCredentialsRequest.Builder(callingPackage); - request.getGetCredentialOptions().forEach( option -> { - if (mInfo.hasCapability(option.getType())) { - Slog.i(TAG, "Provider can handle: " + option.getType()); - builder.addGetCredentialOption(option); - } else { - Slog.i(TAG, "Skipping request as provider cannot handle it"); - } - }); - - try { - return builder.build(); - } catch (IllegalArgumentException | NullPointerException e) { - Slog.i(TAG, "issue with request build: " + e.getMessage()); + /** Return true if at least one capability found. */ + boolean isServiceCapable(List requestedOptions) { + if (mInfo == null) { + Slog.i(TAG, "in isServiceCapable, mInfo is null"); + return false; } - return null; + for (String capability : requestedOptions) { + if (mInfo.hasCapability(capability)) { + Slog.i(TAG, "Provider can handle: " + capability); + return true; + } else { + Slog.i(TAG, "Provider cannot handle: " + capability); + } + } + return false; } } diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java index dcf094f99aae7..e889594ff8576 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java @@ -37,6 +37,7 @@ public class CredentialManagerUi { @NonNull private final CredentialManagerUiCallback mCallbacks; @NonNull private final Context mContext; + // TODO : Use for starting the activity for this user private final int mUserId; @NonNull private final ResultReceiver mResultReceiver = new ResultReceiver( new Handler(Looper.getMainLooper())) { @@ -56,7 +57,7 @@ public class CredentialManagerUi { Slog.i(TAG, "No selection found in UI result"); } } else if (resultCode == UserSelectionDialogResult.RESULT_CODE_DIALOG_CANCELED) { - mCallbacks.onUiCancelation(); + mCallbacks.onUiCancellation(); } } @@ -67,7 +68,7 @@ public class CredentialManagerUi { /** Called when the user makes a selection. */ void onUiSelection(UserSelectionDialogResult selection); /** Called when the user cancels the UI. */ - void onUiCancelation(); + void onUiCancellation(); } public CredentialManagerUi(Context context, int userId, CredentialManagerUiCallback callbacks) { @@ -83,9 +84,8 @@ public class CredentialManagerUi { */ public void show(RequestInfo requestInfo, ArrayList providerDataList) { Log.i(TAG, "In show"); - Intent intent = IntentFactory.newIntent( - requestInfo, providerDataList, - new ArrayList<>(), mResultReceiver); + Intent intent = IntentFactory.newIntent(requestInfo, providerDataList, new ArrayList<>(), + mResultReceiver); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mContext.startActivity(intent); } diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java index 80f0fec068259..8238632fc6c2f 100644 --- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java @@ -16,9 +16,10 @@ package com.android.server.credentials; -import android.content.ComponentName; +import android.annotation.Nullable; import android.content.Context; import android.credentials.Credential; +import android.credentials.GetCredentialRequest; import android.credentials.GetCredentialResponse; import android.credentials.IGetCredentialCallback; import android.credentials.ui.ProviderData; @@ -26,62 +27,52 @@ import android.credentials.ui.RequestInfo; import android.credentials.ui.UserSelectionDialogResult; import android.os.RemoteException; import android.service.credentials.CredentialEntry; +import android.service.credentials.CredentialProviderInfo; import android.util.Log; -import android.util.Slog; import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; /** * 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 final class GetRequestSession extends RequestSession { +public final class GetRequestSession extends RequestSession { private static final String TAG = "GetRequestSession"; - private final IGetCredentialCallback mClientCallback; - private final Map mProviders; - public GetRequestSession(Context context, int userId, - IGetCredentialCallback callback) { - super(context, userId, RequestInfo.TYPE_GET); - mClientCallback = callback; - mProviders = new HashMap<>(); + IGetCredentialCallback callback, GetCredentialRequest request, + String callingPackage) { + super(context, userId, request, callback, RequestInfo.TYPE_GET, callingPackage); } /** - * Adds a new provider to the list of providers that are contributing to this session. + * 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. */ - public void addProviderSession(ProviderGetSession providerSession) { - mProviders.put(providerSession.getComponentName().flattenToString(), - providerSession); - } - @Override - public void onProviderStatusChanged(ProviderSession.Status status, - ComponentName componentName) { - Log.i(TAG, "in onStatusChanged"); - if (ProviderSession.isTerminatingStatus(status)) { - Log.i(TAG, "in onStatusChanged terminating status"); - - ProviderGetSession session = mProviders.remove(componentName.flattenToString()); - if (session != null) { - Slog.i(TAG, "Provider session removed."); - } else { - Slog.i(TAG, "Provider session null, did not exist."); - } - } else if (ProviderSession.isCompletionStatus(status)) { - Log.i(TAG, "in onStatusChanged isCompletionStatus status"); - onProviderResponseComplete(); + @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; } + // TODO: Override for this method not needed once get selection logic is + // moved to ProviderGetSession @Override public void onUiSelection(UserSelectionDialogResult selection) { String providerId = selection.getProviderId(); - ProviderGetSession providerSession = mProviders.get(providerId); + ProviderGetSession providerSession = (ProviderGetSession) mProviders.get(providerId); if (providerSession != null) { CredentialEntry credentialEntry = providerSession.getCredentialEntry( selection.getEntrySubkey()); @@ -89,57 +80,17 @@ public final class GetRequestSession extends RequestSession { respondToClientAndFinish(credentialEntry.getCredential()); } // TODO : Handle action chips and authentication selection - return; } // TODO : finish session and respond to client if provider not found } @Override - public void onUiCancelation() { - // User canceled the activity - // TODO : Send error code to client - finishSession(); - } - - private void onProviderResponseComplete() { - Log.i(TAG, "in onProviderResponseComplete"); - if (isResponseCompleteAcrossProviders()) { - Log.i(TAG, "in onProviderResponseComplete - isResponseCompleteAcrossProviders"); - getProviderDataAndInitiateUi(); - } - } - - private void getProviderDataAndInitiateUi() { - ArrayList providerDataList = new ArrayList<>(); - for (ProviderGetSession session : mProviders.values()) { - Log.i(TAG, "preparing data for : " + session.getComponentName()); - providerDataList.add(session.prepareUiData()); - } - if (!providerDataList.isEmpty()) { - Log.i(TAG, "provider list not empty about to initiate ui"); - initiateUi(providerDataList); - } - } - - private void initiateUi(ArrayList providerDataList) { + protected void launchUiWithProviderData(ArrayList providerDataList) { mHandler.post(() -> mCredentialManagerUi.show(RequestInfo.newGetRequestInfo( mRequestId, null, mIsFirstUiTurn, ""), providerDataList)); } - /** - * Iterates over all provider sessions and returns true if all have responded. - */ - private boolean isResponseCompleteAcrossProviders() { - AtomicBoolean isRequestComplete = new AtomicBoolean(true); - mProviders.forEach( (packageName, session) -> { - if (session.getStatus() != ProviderSession.Status.COMPLETE) { - isRequestComplete.set(false); - } - }); - return isRequestComplete.get(); - } - private void respondToClientAndFinish(Credential credential) { try { mClientCallback.onResponse(new GetCredentialResponse(credential)); @@ -148,17 +99,4 @@ public final class GetRequestSession extends RequestSession { } finishSession(); } - - private void finishSession() { - clearProviderSessions(); - } - - private void clearProviderSessions() { - for (ProviderGetSession session : mProviders.values()) { - // TODO : Evaluate if we should unbind remote services here or wait for them - // to automatically unbind when idle. Re-binding frequently also has a cost. - //session.destroy(); - } - mProviders.clear(); - } } diff --git a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java new file mode 100644 index 0000000000000..49c416f943c16 --- /dev/null +++ b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java @@ -0,0 +1,231 @@ +/* + * 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.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.content.Context; +import android.credentials.Credential; +import android.credentials.ui.CreateCredentialProviderData; +import android.credentials.ui.Entry; +import android.os.Bundle; +import android.service.credentials.CreateCredentialRequest; +import android.service.credentials.CreateCredentialResponse; +import android.service.credentials.CredentialProviderInfo; +import android.service.credentials.CredentialProviderService; +import android.service.credentials.SaveEntry; +import android.util.Log; +import android.util.Slog; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Central provider session that listens for provider callbacks, and maintains provider state. + * Will likely split this into remote response state and UI state. + */ +public final class ProviderCreateSession extends ProviderSession< + CreateCredentialRequest, CreateCredentialResponse> { + private static final String TAG = "ProviderCreateSession"; + + // Key to be used as an entry key for a save entry + private static final String SAVE_ENTRY_KEY = "save_entry_key"; + + @NonNull + private final Map mUiSaveEntries = new HashMap<>(); + /** The complete request to be used in the second round. */ + private final CreateCredentialRequest mCompleteRequest; + + /** Creates a new provider session to be used by the request session. */ + @Nullable public static ProviderCreateSession createNewSession( + Context context, + @UserIdInt int userId, + CredentialProviderInfo providerInfo, + CreateRequestSession createRequestSession, + RemoteCredentialService remoteCredentialService) { + CreateCredentialRequest providerRequest = + createProviderRequest(providerInfo.getCapabilities(), + createRequestSession.mClientRequest, + createRequestSession.mClientCallingPackage); + if (providerRequest != null) { + return new ProviderCreateSession(context, providerInfo, createRequestSession, userId, + remoteCredentialService, providerRequest); + } + Log.i(TAG, "Unable to create provider session"); + return null; + } + + @Nullable + private static CreateCredentialRequest createProviderRequest(List providerCapabilities, + android.credentials.CreateCredentialRequest clientRequest, + String clientCallingPackage) { + String capability = clientRequest.getType(); + if (providerCapabilities.contains(capability)) { + return new CreateCredentialRequest(clientCallingPackage, capability, + clientRequest.getData()); + } + Log.i(TAG, "Unable to create provider request - capabilities do not match"); + return null; + } + + private static CreateCredentialRequest getFirstRoundRequest(CreateCredentialRequest request) { + // TODO: Replace with first round bundle from request when ready + return new CreateCredentialRequest( + request.getCallingPackage(), + request.getType(), + new Bundle()); + } + + private ProviderCreateSession( + @NonNull Context context, + @NonNull CredentialProviderInfo info, + @NonNull ProviderInternalCallback callbacks, + @UserIdInt int userId, + @NonNull RemoteCredentialService remoteCredentialService, + @NonNull CreateCredentialRequest request) { + super(context, info, getFirstRoundRequest(request), callbacks, userId, + remoteCredentialService); + // TODO : Replace with proper splitting of request + mCompleteRequest = request; + setStatus(Status.PENDING); + } + + /** Returns the save entry maintained in state by this provider session. */ + public SaveEntry getUiSaveEntry(String entryId) { + return mUiSaveEntries.get(entryId); + } + + @Override + public void onProviderResponseSuccess( + @Nullable CreateCredentialResponse response) { + Log.i(TAG, "in onProviderResponseSuccess"); + onUpdateResponse(response); + } + + /** Called when the provider response resulted in a failure. */ + @Override + public void onProviderResponseFailure(int errorCode, @Nullable CharSequence message) { + updateStatusAndInvokeCallback(toStatus(errorCode)); + } + + /** Called when provider service dies. */ + @Override + 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"); + } + } + + private void onUpdateResponse(CreateCredentialResponse response) { + Log.i(TAG, "updateResponse with save entries"); + mProviderResponse = response; + updateStatusAndInvokeCallback(Status.SAVE_ENTRIES_RECEIVED); + } + + @Override + @Nullable protected CreateCredentialProviderData prepareUiData() + throws IllegalArgumentException { + Log.i(TAG, "In prepareUiData"); + if (!ProviderSession.isUiInvokingStatus(getStatus())) { + Log.i(TAG, "In prepareUiData not in uiInvokingStatus"); + return null; + } + final CreateCredentialResponse response = getProviderResponse(); + if (response == null) { + Log.i(TAG, "In prepareUiData response null"); + throw new IllegalStateException("Response must be in completion mode"); + } + if (response.getSaveEntries() != null) { + Log.i(TAG, "In prepareUiData save entries not null"); + return prepareUiProviderData( + prepareUiSaveEntries(response.getSaveEntries()), + null, + /*isDefaultProvider=*/false); + } + return null; + } + + @Override + public void onProviderIntentResult(Bundle resultData) { + Credential credential = resultData.getParcelable( + CredentialProviderService.EXTRA_SAVE_CREDENTIAL, + Credential.class); + if (credential == null) { + Log.i(TAG, "Credential returned from intent is null"); + return; + } + updateFinalCredentialResponse(credential); + } + + @Override + public void onUiEntrySelected(String entryType, String entryKey) { + if (entryType.equals(SAVE_ENTRY_KEY)) { + SaveEntry saveEntry = mUiSaveEntries.get(entryKey); + if (saveEntry == null) { + Log.i(TAG, "Save entry not found"); + return; + } + // TODO: Uncomment when pending intent works + // onSaveEntrySelected(saveEntry); + } + } + + @Override + public void onProviderIntentCancelled() { + //TODO (Implement) + } + + private List prepareUiSaveEntries(@NonNull List saveEntries) { + Log.i(TAG, "in populateUiSaveEntries"); + List uiSaveEntries = new ArrayList<>(); + + // Populate the save entries + for (SaveEntry saveEntry : saveEntries) { + String entryId = generateEntryId(); + mUiSaveEntries.put(entryId, saveEntry); + Log.i(TAG, "in prepareUiProviderData creating ui entry with id " + entryId); + uiSaveEntries.add(new Entry(SAVE_ENTRY_KEY, entryId, saveEntry.getSlice())); + } + return uiSaveEntries; + } + + private void updateFinalCredentialResponse(@NonNull Credential credential) { + mFinalCredentialResponse = credential; + updateStatusAndInvokeCallback(Status.CREDENTIAL_RECEIVED_FROM_INTENT); + } + + private CreateCredentialProviderData prepareUiProviderData(List saveEntries, + Entry remoteEntry, boolean isDefaultProvider) { + return new CreateCredentialProviderData.Builder( + mComponentName.flattenToString()) + .setSaveEntries(saveEntries) + .setIsDefaultProvider(isDefaultProvider) + .build(); + } + + private void onSaveEntrySelected(SaveEntry saveEntry) { + mProviderIntentController.setupAndInvokePendingIntent(saveEntry.getPendingIntent(), + mProviderRequest); + setStatus(Status.PENDING_INTENT_INVOKED); + } +} diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java index ff2107a95d252..362d98167462e 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java @@ -18,13 +18,16 @@ package com.android.server.credentials; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.slice.Slice; +import android.annotation.UserIdInt; +import android.content.Context; +import android.credentials.GetCredentialOption; import android.credentials.ui.Entry; import android.credentials.ui.GetCredentialProviderData; +import android.os.Bundle; import android.service.credentials.Action; import android.service.credentials.CredentialEntry; import android.service.credentials.CredentialProviderInfo; -import android.service.credentials.CredentialsDisplayContent; +import android.service.credentials.GetCredentialsRequest; import android.service.credentials.GetCredentialsResponse; import android.util.Log; import android.util.Slog; @@ -38,75 +41,95 @@ import java.util.UUID; /** * Central provider session that listens for provider callbacks, and maintains provider state. * Will likely split this into remote response state and UI state. + * + * @hide */ -public final class ProviderGetSession extends ProviderSession - implements RemoteCredentialService.ProviderCallbacks { +public final class ProviderGetSession extends ProviderSession + implements + RemoteCredentialService.ProviderCallbacks { private static final String TAG = "ProviderGetSession"; // Key to be used as an entry key for a credential entry private static final String CREDENTIAL_ENTRY_KEY = "credential_key"; - private GetCredentialsResponse mResponse; - @NonNull - private final Map mUiCredentials = new HashMap<>(); - + private final Map mUiCredentialEntries = new HashMap<>(); @NonNull - private final Map mUiActions = new HashMap<>(); + private final Map mUiActionsEntries = new HashMap<>(); + private Action mAuthenticationAction = null; - public ProviderGetSession(CredentialProviderInfo info, - ProviderInternalCallback callbacks, - int userId, RemoteCredentialService remoteCredentialService) { - super(info, callbacks, userId, remoteCredentialService); - setStatus(Status.PENDING); - } - - /** Updates the response being maintained in state by this provider session. */ - @Override - public void updateResponse(GetCredentialsResponse response) { - if (response.getAuthenticationAction() != null) { - // TODO : Implement authentication logic - } else if (response.getCredentialsDisplayContent() != null) { - Log.i(TAG , "updateResponse with credentialEntries"); - mResponse = response; - updateStatusAndInvokeCallback(Status.COMPLETE); + /** Creates a new provider session to be used by the request session. */ + @Nullable public static ProviderGetSession createNewSession( + Context context, + @UserIdInt int userId, + CredentialProviderInfo providerInfo, + GetRequestSession getRequestSession, + RemoteCredentialService remoteCredentialService) { + GetCredentialsRequest providerRequest = + createProviderRequest(providerInfo.getCapabilities(), + getRequestSession.mClientRequest, + getRequestSession.mClientCallingPackage); + if (providerRequest != null) { + return new ProviderGetSession(context, providerInfo, getRequestSession, userId, + remoteCredentialService, providerRequest); } + Log.i(TAG, "Unable to create provider session"); + return null; } - /** Returns the response being maintained in this provider session. */ - @Override @Nullable - public GetCredentialsResponse getResponse() { - return mResponse; + private static GetCredentialsRequest createProviderRequest(List providerCapabilities, + android.credentials.GetCredentialRequest clientRequest, + String clientCallingPackage) { + List filteredOptions = new ArrayList<>(); + for (GetCredentialOption option : clientRequest.getGetCredentialOptions()) { + if (providerCapabilities.contains(option.getType())) { + Log.i(TAG, "In createProviderRequest - capability found : " + option.getType()); + filteredOptions.add(option); + } else { + Log.i(TAG, "In createProviderRequest - capability not " + + "found : " + option.getType()); + } + } + if (!filteredOptions.isEmpty()) { + return new GetCredentialsRequest.Builder(clientCallingPackage).setGetCredentialOptions( + filteredOptions).build(); + } + Log.i(TAG, "In createProviderRequest - returning null"); + return null; + } + + public ProviderGetSession(Context context, + CredentialProviderInfo info, + ProviderInternalCallback callbacks, + int userId, RemoteCredentialService remoteCredentialService, + GetCredentialsRequest request) { + super(context, info, request, callbacks, userId, remoteCredentialService); + setStatus(Status.PENDING); } /** Returns the credential entry maintained in state by this provider session. */ @Nullable public CredentialEntry getCredentialEntry(@NonNull String entryId) { - return mUiCredentials.get(entryId); - } - - /** Returns the action entry maintained in state by this provider session. */ - @Nullable - public Action getAction(@NonNull String entryId) { - return mUiActions.get(entryId); + return mUiCredentialEntries.get(entryId); } /** Called when the provider response has been updated by an external source. */ - @Override + @Override // Callback from the remote provider public void onProviderResponseSuccess(@Nullable GetCredentialsResponse response) { Log.i(TAG, "in onProviderResponseSuccess"); - updateResponse(response); + onUpdateResponse(response); } /** Called when the provider response resulted in a failure. */ - @Override + @Override // Callback from the remote provider public void onProviderResponseFailure(int errorCode, @Nullable CharSequence message) { updateStatusAndInvokeCallback(toStatus(errorCode)); } /** Called when provider service dies. */ - @Override + @Override // Callback from the remote provider public void onProviderServiceDied(RemoteCredentialService service) { if (service.getComponentName().equals(mProviderInfo.getServiceInfo().getComponentName())) { updateStatusAndInvokeCallback(Status.SERVICE_DEAD); @@ -116,77 +139,106 @@ public final class ProviderGetSession extends ProviderSession credentialEntries = new ArrayList<>(); - List actionChips = new ArrayList<>(); - Entry authenticationEntry = null; + private Entry prepareUiAuthenticationActionEntry(@NonNull Action authenticationAction) { + String entryId = generateEntryId(); + mUiActionsEntries.put(entryId, authenticationAction); + return new Entry(ACTION_ENTRY_KEY, entryId, authenticationAction.getSlice()); + } + + private List prepareUiCredentialEntries(@NonNull + List credentialEntries) { + Log.i(TAG, "in prepareUiProviderDataWithCredentials"); + List credentialUiEntries = new ArrayList<>(); // Populate the credential entries - for (CredentialEntry credentialEntry : content.getCredentialEntries()) { - String entryId = UUID.randomUUID().toString(); - mUiCredentials.put(entryId, credentialEntry); + for (CredentialEntry credentialEntry : credentialEntries) { + String entryId = generateEntryId(); + mUiCredentialEntries.put(entryId, credentialEntry); Log.i(TAG, "in prepareUiProviderData creating ui entry with id " + entryId); - Slice slice = credentialEntry.getSlice(); - // TODO : Remove conversion of string to int after change in Entry class - credentialEntries.add(new Entry(CREDENTIAL_ENTRY_KEY, entryId, + credentialUiEntries.add(new Entry(CREDENTIAL_ENTRY_KEY, entryId, credentialEntry.getSlice())); } - // populate the action chip - for (Action action : content.getActions()) { - String entryId = UUID.randomUUID().toString(); - mUiActions.put(entryId, action); - // TODO : Remove conversion of string to int after change in Entry class - actionChips.add(new Entry(ACTION_ENTRY_KEY, entryId, - action.getSlice())); - } + return credentialUiEntries; + } - return new GetCredentialProviderData.Builder(mComponentName.flattenToString()) + private List populateUiActionEntries(@Nullable List actions) { + List actionEntries = new ArrayList<>(); + for (Action action : actions) { + String entryId = UUID.randomUUID().toString(); + mUiActionsEntries.put(entryId, action); + // TODO : Remove conversion of string to int after change in Entry class + actionEntries.add(new Entry(ACTION_ENTRY_KEY, entryId, action.getSlice())); + } + return actionEntries; + } + + private GetCredentialProviderData prepareUiProviderData(List actionEntries, + List credentialEntries, Entry authenticationActionEntry, + Entry remoteEntry) { + return new GetCredentialProviderData.Builder( + mComponentName.flattenToString()).setActionChips(actionEntries) .setCredentialEntries(credentialEntries) - .setActionChips(actionChips) - .setAuthenticationEntry(authenticationEntry) + .setAuthenticationEntry(authenticationActionEntry) .build(); } - /** - * To be called by {@link ProviderGetSession} when the UI is to be invoked. - */ - @Nullable - private GetCredentialProviderData prepareUiProviderDataWithAuthentication(@NonNull - Action authenticationEntry) { - // TODO : Implement authentication flow - return null; + /** Updates the response being maintained in state by this provider session. */ + private void onUpdateResponse(GetCredentialsResponse response) { + mProviderResponse = response; + if (response.getAuthenticationAction() != null) { + Log.i(TAG , "updateResponse with authentication entry"); + // TODO validate authentication action + mAuthenticationAction = response.getAuthenticationAction(); + updateStatusAndInvokeCallback(Status.REQUIRES_AUTHENTICATION); + } else if (response.getCredentialsDisplayContent() != null) { + Log.i(TAG , "updateResponse with credentialEntries"); + // TODO validate response + updateStatusAndInvokeCallback(Status.CREDENTIALS_RECEIVED); + } } } diff --git a/services/credentials/java/com/android/server/credentials/ProviderIntentController.java b/services/credentials/java/com/android/server/credentials/ProviderIntentController.java new file mode 100644 index 0000000000000..0f2e8ecdbbc68 --- /dev/null +++ b/services/credentials/java/com/android/server/credentials/ProviderIntentController.java @@ -0,0 +1,117 @@ +/* + * 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.NonNull; +import android.annotation.SuppressLint; +import android.annotation.UserIdInt; +import android.app.Activity; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.IntentSender; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Parcel; +import android.os.ResultReceiver; +import android.service.credentials.CreateCredentialRequest; +import android.service.credentials.CredentialProviderService; +import android.util.Log; + +/** + * Class that invokes providers' pending intents and listens to the responses. + */ +@SuppressLint("LongLogTag") +public class ProviderIntentController { + private static final String TAG = "ProviderIntentController"; + /** + * Interface to be implemented by any class that wishes to get callbacks from the UI. + */ + public interface ProviderIntentControllerCallback { + /** Called when the user makes a selection. */ + void onProviderIntentResult(Bundle resultData); + /** Called when the user cancels the UI. */ + void onProviderIntentCancelled(); + } + + private final int mUserId; + private final Context mContext; + private final ProviderIntentControllerCallback mCallback; + private final ResultReceiver mResultReceiver = new ResultReceiver( + new Handler(Looper.getMainLooper())) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + Log.i(TAG, "onReceiveResult in providerIntentController"); + + if (resultCode == Activity.RESULT_OK) { + Log.i(TAG, "onReceiveResult - ACTIVITYOK"); + mCallback.onProviderIntentResult(resultData); + } else if (resultCode == Activity.RESULT_CANCELED) { + Log.i(TAG, "onReceiveResult - RESULTCANCELED"); + mCallback.onProviderIntentCancelled(); + } + // Drop unknown result + } + }; + + public ProviderIntentController(@UserIdInt int userId, + Context context, + ProviderIntentControllerCallback callback) { + mUserId = userId; + mContext = context; + mCallback = callback; + } + + /** Sets up the request data and invokes the given pending intent. */ + public void setupAndInvokePendingIntent(@NonNull PendingIntent pendingIntent, + CreateCredentialRequest request) { + Log.i(TAG, "in invokePendingIntent"); + setupIntent(pendingIntent, request); + Log.i(TAG, "in invokePendingIntent receiver set up"); + Log.i(TAG, "creator package: " + pendingIntent.getIntentSender() + .getCreatorPackage()); + + try { + mContext.startIntentSender(pendingIntent.getIntentSender(), + null, 0, 0, 0); + } catch (IntentSender.SendIntentException e) { + Log.i(TAG, "Error while invoking pending intent"); + } + + } + + private void setupIntent(PendingIntent pendingIntent, CreateCredentialRequest request) { + pendingIntent.getIntent().putExtra(Intent.EXTRA_RESULT_RECEIVER, + toIpcFriendlyResultReceiver(mResultReceiver)); + pendingIntent.getIntent().putExtra( + CredentialProviderService.EXTRA_CREATE_CREDENTIAL_REQUEST_PARAMS, + request.getData()); + } + + private ResultReceiver toIpcFriendlyResultReceiver( + T resultReceiver) { + final Parcel parcel = Parcel.obtain(); + resultReceiver.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + final ResultReceiver ipcFriendly = ResultReceiver.CREATOR.createFromParcel(parcel); + parcel.recycle(); + + return ipcFriendly; + } +} diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java index 3a9f96432d8c3..14a9157548631 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java @@ -17,25 +17,70 @@ package com.android.server.credentials; import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.ComponentName; +import android.content.Context; +import android.credentials.Credential; import android.credentials.ui.ProviderData; +import android.os.Bundle; import android.service.credentials.CredentialProviderException; import android.service.credentials.CredentialProviderInfo; +import java.util.UUID; + /** * Provider session storing the state of provider response and ui entries. - * @param The request type expected from the remote provider, for a given request session. + * @param The request to be sent to the provider + * @param The response to be expected from the provider */ -public abstract class ProviderSession implements RemoteCredentialService.ProviderCallbacks { +public abstract class ProviderSession implements RemoteCredentialService.ProviderCallbacks, + ProviderIntentController.ProviderIntentControllerCallback { // Key to be used as the entry key for an action entry protected static final String ACTION_ENTRY_KEY = "action_key"; + @NonNull protected final Context mContext; @NonNull protected final ComponentName mComponentName; @NonNull protected final CredentialProviderInfo mProviderInfo; @NonNull protected final RemoteCredentialService mRemoteCredentialService; @NonNull protected final int mUserId; @NonNull protected Status mStatus = Status.NOT_STARTED; @NonNull protected final ProviderInternalCallback mCallbacks; + @NonNull protected final ProviderIntentController mProviderIntentController; + @Nullable protected Credential mFinalCredentialResponse; + @NonNull protected final T mProviderRequest; + @Nullable protected R mProviderResponse; + + /** + * Returns true if the given status reflects that the provider state is ready to be shown + * on the credMan UI. + */ + public static boolean isUiInvokingStatus(Status status) { + return status == Status.CREDENTIALS_RECEIVED || status == Status.SAVE_ENTRIES_RECEIVED; + } + + /** + * Returns true if the given status reflects that the provider is waiting for a remote + * response. + */ + public static boolean isStatusWaitingForRemoteResponse(Status status) { + return status == Status.PENDING; + } + + /** + * Returns true if the given status means that the provider session must be terminated. + */ + public static boolean isTerminatingStatus(Status status) { + return status == Status.CANCELED || status == Status.SERVICE_DEAD; + } + + /** + * Returns true if the given status reflects that the provider is done getting the response, + * and is ready to return the final credential back to the user. + */ + public static boolean isCompletionStatus(Status status) { + return status == Status.CREDENTIAL_RECEIVED_FROM_INTENT + || status == Status.CREDENTIAL_RECEIVED_FROM_SELECTION; + } /** * Interface to be implemented by any class that wishes to get a callback when a particular @@ -49,35 +94,49 @@ public abstract class ProviderSession implements RemoteCredentialService.Prov void onProviderStatusChanged(Status status, ComponentName componentName); } - protected ProviderSession(@NonNull CredentialProviderInfo info, + protected ProviderSession(@NonNull Context context, @NonNull CredentialProviderInfo info, + @NonNull T providerRequest, @NonNull ProviderInternalCallback callbacks, @NonNull int userId, @NonNull RemoteCredentialService remoteCredentialService) { + mContext = context; mProviderInfo = info; + mProviderRequest = providerRequest; mCallbacks = callbacks; mUserId = userId; mComponentName = info.getServiceInfo().getComponentName(); mRemoteCredentialService = remoteCredentialService; + mProviderIntentController = new ProviderIntentController(userId, context, this); } - /** Update the response state stored with the provider session. */ - protected abstract void updateResponse (T response); - - /** Update the response state stored with the provider session. */ - protected abstract T getResponse (); - - /** Should be overridden to prepare, and stores state for {@link ProviderData} to be - * shown on the UI. */ - protected abstract ProviderData prepareUiData(); - /** Provider status at various states of the request session. */ + // TODO: Review status values, and adjust where needed enum Status { NOT_STARTED, PENDING, REQUIRES_AUTHENTICATION, - COMPLETE, + CREDENTIALS_RECEIVED, SERVICE_DEAD, - CANCELED + CREDENTIAL_RECEIVED_FROM_INTENT, + PENDING_INTENT_INVOKED, + CREDENTIAL_RECEIVED_FROM_SELECTION, + SAVE_ENTRIES_RECEIVED, CANCELED + } + + /** Converts exception to a provider session status. */ + @NonNull + public static Status toStatus( + @CredentialProviderException.CredentialProviderError int errorCode) { + // TODO : Add more mappings as more flows are supported + return Status.CANCELED; + } + + protected String generateEntryId() { + return UUID.randomUUID().toString(); + } + + public Credential getFinalCredentialResponse() { + return mFinalCredentialResponse; } protected void setStatus(@NonNull Status status) { @@ -94,31 +153,38 @@ public abstract class ProviderSession implements RemoteCredentialService.Prov return mComponentName; } + @NonNull + protected RemoteCredentialService getRemoteCredentialService() { + return mRemoteCredentialService; + } + /** Updates the status .*/ protected void updateStatusAndInvokeCallback(@NonNull Status status) { setStatus(status); mCallbacks.onProviderStatusChanged(status, mComponentName); } - @NonNull - public static Status toStatus( - @CredentialProviderException.CredentialProviderError int errorCode) { - // TODO : Add more mappings as more flows are supported - return Status.CANCELED; + /** Get the request to be sent to the provider. */ + protected T getProviderRequest() { + return mProviderRequest; } - /** - * Returns true if the given status means that the provider session must be terminated. - */ - public static boolean isTerminatingStatus(Status status) { - return status == Status.CANCELED || status == Status.SERVICE_DEAD; + /** Update the response state stored with the provider session. */ + @Nullable protected R getProviderResponse() { + return mProviderResponse; } - /** - * Returns true if the given status means that the provider is done getting the response, - * and is ready for user interaction. - */ - public static boolean isCompletionStatus(Status status) { - return status == Status.COMPLETE || status == Status.REQUIRES_AUTHENTICATION; - } + /** Should be overridden to prepare, and stores state for {@link ProviderData} to be + * shown on the UI. */ + @Nullable protected abstract ProviderData prepareUiData(); + + /** Should be overridden to handle the selected entry from the UI. */ + protected abstract void onUiEntrySelected(String entryType, String entryId); + + @Override + public abstract void onProviderIntentResult(Bundle resultData); + + @Override + public abstract void onProviderIntentCancelled(); + } diff --git a/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java b/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java index d0b6e7d6238c9..c2464b5d235eb 100644 --- a/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java +++ b/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java @@ -24,11 +24,14 @@ import android.content.Intent; import android.os.Handler; import android.os.ICancellationSignal; import android.os.RemoteException; +import android.service.credentials.CreateCredentialRequest; +import android.service.credentials.CreateCredentialResponse; import android.service.credentials.CredentialProviderException; import android.service.credentials.CredentialProviderException.CredentialProviderError; import android.service.credentials.CredentialProviderService; import android.service.credentials.GetCredentialsRequest; import android.service.credentials.GetCredentialsResponse; +import android.service.credentials.ICreateCredentialCallback; import android.service.credentials.ICredentialProviderService; import android.service.credentials.IGetCredentialsCallback; import android.text.format.DateUtils; @@ -76,7 +79,7 @@ public class RemoteCredentialService extends ServiceConnector.Impl callback) { @@ -114,21 +117,21 @@ public class RemoteCredentialService extends ServiceConnector.Impl getCredentials = new CompletableFuture<>(); ICancellationSignal cancellationSignal = service.onGetCredentials(request, new IGetCredentialsCallback.Stub() { - @Override - public void onSuccess(GetCredentialsResponse response) { - Log.i(TAG, "In onSuccess in RemoteCredentialService"); - getCredentials.complete(response); - } + @Override + public void onSuccess(GetCredentialsResponse response) { + Log.i(TAG, "In onSuccess in RemoteCredentialService"); + getCredentials.complete(response); + } - @Override - public void onFailure(@CredentialProviderError int errorCode, - CharSequence message) { - Log.i(TAG, "In onFailure in RemoteCredentialService"); - String errorMsg = message == null ? "" : String.valueOf(message); - getCredentials.completeExceptionally(new CredentialProviderException( - errorCode, errorMsg)); - } - }); + @Override + public void onFailure(@CredentialProviderError int errorCode, + CharSequence message) { + Log.i(TAG, "In onFailure in RemoteCredentialService"); + String errorMsg = message == null ? "" : String.valueOf(message); + getCredentials.completeExceptionally(new CredentialProviderException( + errorCode, errorMsg)); + } + }); CompletableFuture future = futureRef.get(); if (future != null && future.isCancelled()) { dispatchCancellationSignal(cancellationSignal); @@ -137,38 +140,91 @@ public class RemoteCredentialService extends ServiceConnector.Impl Handler.getMain().post(() -> { - if (error == null) { - Log.i(TAG, "In RemoteCredentialService execute error is null"); - callback.onProviderResponseSuccess(result); + futureRef.set(connectThenExecute); + connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() -> + handleExecutionResponse(result, error, cancellationSink, callback))); + } + + /** Main entry point to be called for executing a createCredential call on the remote + * provider service. + * @param request the request to be sent to the provider + * @param callback the callback to be used to send back the provider response to the + * {@link ProviderCreateSession} class that maintains provider state + */ + public void onCreateCredential(@NonNull CreateCredentialRequest request, + ProviderCallbacks callback) { + Log.i(TAG, "In onCreateCredential in RemoteCredentialService"); + AtomicReference cancellationSink = new AtomicReference<>(); + AtomicReference> futureRef = + new AtomicReference<>(); + + CompletableFuture connectThenExecute = postAsync(service -> { + CompletableFuture createCredentialFuture = + new CompletableFuture<>(); + ICancellationSignal cancellationSignal = service.onCreateCredential( + request, new ICreateCredentialCallback.Stub() { + @Override + public void onSuccess(CreateCredentialResponse response) { + Log.i(TAG, "In onSuccess onCreateCredential " + + "in RemoteCredentialService"); + createCredentialFuture.complete(response); + } + + @Override + public void onFailure(@CredentialProviderError int errorCode, + CharSequence message) { + Log.i(TAG, "In onFailure in RemoteCredentialService"); + String errorMsg = message == null ? "" : String.valueOf(message); + createCredentialFuture.completeExceptionally( + new CredentialProviderException(errorCode, errorMsg)); + }}); + CompletableFuture future = futureRef.get(); + if (future != null && future.isCancelled()) { + dispatchCancellationSignal(cancellationSignal); } else { - if (error instanceof TimeoutException) { - Log.i(TAG, "In RemoteCredentialService execute error is timeout"); - dispatchCancellationSignal(cancellationSink.get()); - callback.onProviderResponseFailure( - CredentialProviderException.ERROR_TIMEOUT, - error.getMessage()); - } else if (error instanceof CancellationException) { - Log.i(TAG, "In RemoteCredentialService execute error is cancellation"); - dispatchCancellationSignal(cancellationSink.get()); - callback.onProviderResponseFailure( - CredentialProviderException.ERROR_TASK_CANCELED, - error.getMessage()); - } else if (error instanceof CredentialProviderException) { - Log.i(TAG, "In RemoteCredentialService execute error is provider error"); - callback.onProviderResponseFailure(((CredentialProviderException) error) - .getErrorCode(), - error.getMessage()); - } else { - Log.i(TAG, "In RemoteCredentialService execute error is unknown"); - callback.onProviderResponseFailure( - CredentialProviderException.ERROR_UNKNOWN, - error.getMessage()); - } + cancellationSink.set(cancellationSignal); } - })); + return createCredentialFuture; + }).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, + ProviderCallbacks callback) { + if (error == null) { + Log.i(TAG, "In RemoteCredentialService execute error is null"); + callback.onProviderResponseSuccess(result); + } else { + if (error instanceof TimeoutException) { + Log.i(TAG, "In RemoteCredentialService execute error is timeout"); + dispatchCancellationSignal(cancellationSink.get()); + callback.onProviderResponseFailure( + CredentialProviderException.ERROR_TIMEOUT, + error.getMessage()); + } else if (error instanceof CancellationException) { + Log.i(TAG, "In RemoteCredentialService execute error is cancellation"); + dispatchCancellationSignal(cancellationSink.get()); + callback.onProviderResponseFailure( + CredentialProviderException.ERROR_TASK_CANCELED, + error.getMessage()); + } else if (error instanceof CredentialProviderException) { + Log.i(TAG, "In RemoteCredentialService execute error is provider error"); + callback.onProviderResponseFailure(((CredentialProviderException) error) + .getErrorCode(), + error.getMessage()); + } else { + Log.i(TAG, "In RemoteCredentialService execute error is unknown"); + callback.onProviderResponseFailure( + CredentialProviderException.ERROR_UNKNOWN, + error.getMessage()); + } + } } private void dispatchCancellationSignal(@Nullable ICancellationSignal signal) { diff --git a/services/credentials/java/com/android/server/credentials/RequestSession.java b/services/credentials/java/com/android/server/credentials/RequestSession.java index 1bacbb342edb0..056d0e8718be8 100644 --- a/services/credentials/java/com/android/server/credentials/RequestSession.java +++ b/services/credentials/java/com/android/server/credentials/RequestSession.java @@ -20,18 +20,30 @@ import android.annotation.NonNull; import android.annotation.UserIdInt; import android.content.ComponentName; import android.content.Context; +import android.credentials.ui.ProviderData; import android.credentials.ui.UserSelectionDialogResult; import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.Looper; +import android.service.credentials.CredentialProviderInfo; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; /** * Base class of a request session, that listens to UI events. This class must be extended * every time a new response type is expected from the providers. */ -abstract class RequestSession implements CredentialManagerUi.CredentialManagerUiCallback, +abstract class RequestSession implements CredentialManagerUi.CredentialManagerUiCallback, ProviderSession.ProviderInternalCallback { + private static final String TAG = "RequestSession"; + + // TODO: Revise access levels of attributes + @NonNull protected final T mClientRequest; + @NonNull protected final U mClientCallback; @NonNull protected final IBinder mRequestId; @NonNull protected final Context mContext; @NonNull protected final CredentialManagerUi mCredentialManagerUi; @@ -39,29 +51,120 @@ abstract class RequestSession implements CredentialManagerUi.CredentialManagerUi @NonNull protected final Handler mHandler; @NonNull protected boolean mIsFirstUiTurn = true; @UserIdInt protected final int mUserId; + @NonNull protected final String mClientCallingPackage; + + protected final Map mProviders = new HashMap<>(); protected RequestSession(@NonNull Context context, - @UserIdInt int userId, @NonNull String requestType) { + @UserIdInt int userId, @NonNull T clientRequest, U clientCallback, + @NonNull String requestType, + String clientCallingPackage) { mContext = context; mUserId = userId; + mClientRequest = clientRequest; + mClientCallback = clientCallback; mRequestType = requestType; + mClientCallingPackage = clientCallingPackage; mHandler = new Handler(Looper.getMainLooper(), null, true); mRequestId = new Binder(); mCredentialManagerUi = new CredentialManagerUi(mContext, mUserId, this); } - /** Returns the unique identifier of this request session. */ - public IBinder getRequestId() { - return mRequestId; + public abstract ProviderSession initiateProviderSession(CredentialProviderInfo providerInfo, + RemoteCredentialService remoteCredentialService); + + protected abstract void launchUiWithProviderData(ArrayList providerDataList); + + // UI callbacks + + @Override // from CredentialManagerUiCallbacks + public void onUiSelection(UserSelectionDialogResult selection) { + String providerId = selection.getProviderId(); + Log.i(TAG, "onUiSelection, providerId: " + providerId); + ProviderSession providerSession = mProviders.get(providerId); + if (providerSession == null) { + Log.i(TAG, "providerSession not found in onUiSelection"); + return; + } + Log.i(TAG, "Provider session found"); + providerSession.onUiEntrySelected(selection.getEntryKey(), + selection.getEntrySubkey()); } - @Override // from CredentialManagerUiCallback - public abstract void onUiSelection(UserSelectionDialogResult selection); + @Override // from CredentialManagerUiCallbacks + public void onUiCancellation() { + // User canceled the activity + finishSession(); + } - @Override // from CredentialManagerUiCallback - public abstract void onUiCancelation(); + @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); + //TODO: Check if this was the provider we were waiting for and can invoke the UI now + } else if (ProviderSession.isCompletionStatus(status)) { + Log.i(TAG, "in onStatusChanged isCompletionStatus status"); + onProviderResponseComplete(componentName); + } else if (ProviderSession.isUiInvokingStatus(status)) { + Log.i(TAG, "in onStatusChanged isUiInvokingStatus status"); + onProviderResponseRequiresUi(); + } + } - @Override // from ProviderInternalCallback - public abstract void onProviderStatusChanged(ProviderSession.Status status, ComponentName componentName); + protected void onProviderTerminated(ComponentName componentName) { + //TODO: Implement + } + + protected void onProviderResponseComplete(ComponentName componentName) { + //TODO: Implement + } + + protected void onProviderResponseRequiresUi() { + Log.i(TAG, "in onProviderResponseComplete"); + // TODO: Determine whether UI has already been invoked, and deal accordingly + if (!isAnyProviderPending()) { + Log.i(TAG, "in onProviderResponseComplete - isResponseCompleteAcrossProviders"); + getProviderDataAndInitiateUi(); + } else { + Log.i(TAG, "Can't invoke UI - waiting on some providers"); + } + } + + protected void finishSession() { + clearProviderSessions(); + } + + protected void clearProviderSessions() { + //TODO: Implement + mProviders.clear(); + } + + private boolean isAnyProviderPending() { + for (ProviderSession session : mProviders.values()) { + if (ProviderSession.isStatusWaitingForRemoteResponse(session.getStatus())) { + return true; + } + } + return false; + } + + private void getProviderDataAndInitiateUi() { + ArrayList providerDataList = new ArrayList<>(); + for (ProviderSession session : mProviders.values()) { + Log.i(TAG, "preparing data for : " + session.getComponentName()); + ProviderData providerData = session.prepareUiData(); + if (providerData != null) { + Log.i(TAG, "Provider data is not null"); + providerDataList.add(providerData); + } + } + if (!providerDataList.isEmpty()) { + Log.i(TAG, "provider list not empty about to initiate ui"); + launchUiWithProviderData(providerDataList); + } + } }