diff options
17 files changed, 1097 insertions, 100 deletions
diff --git a/core/java/android/credentials/CredentialManager.java b/core/java/android/credentials/CredentialManager.java index b9cef0fccddf..30ee118d3b59 100644 --- a/core/java/android/credentials/CredentialManager.java +++ b/core/java/android/credentials/CredentialManager.java @@ -85,7 +85,7 @@ public final class CredentialManager { ICancellationSignal cancelRemote = null; try { cancelRemote = mService.executeGetCredential(request, - new GetCredentialTransport(executor, callback)); + new GetCredentialTransport(executor, callback), mContext.getOpPackageName()); } catch (RemoteException e) { e.rethrowFromSystemServer(); } @@ -124,7 +124,8 @@ public final class CredentialManager { ICancellationSignal cancelRemote = null; try { cancelRemote = mService.executeCreateCredential(request, - new CreateCredentialTransport(executor, callback)); + new CreateCredentialTransport(executor, callback), + mContext.getOpPackageName()); } catch (RemoteException e) { e.rethrowFromSystemServer(); } diff --git a/core/java/android/credentials/ICredentialManager.aidl b/core/java/android/credentials/ICredentialManager.aidl index dcf7106d15eb..b0f27f9164f3 100644 --- a/core/java/android/credentials/ICredentialManager.aidl +++ b/core/java/android/credentials/ICredentialManager.aidl @@ -29,7 +29,7 @@ import android.os.ICancellationSignal; */ interface ICredentialManager { - @nullable ICancellationSignal executeGetCredential(in GetCredentialRequest request, in IGetCredentialCallback callback); + @nullable ICancellationSignal executeGetCredential(in GetCredentialRequest request, in IGetCredentialCallback callback, String callingPackage); - @nullable ICancellationSignal executeCreateCredential(in CreateCredentialRequest request, in ICreateCredentialCallback callback); + @nullable ICancellationSignal executeCreateCredential(in CreateCredentialRequest request, in ICreateCredentialCallback callback, String callingPackage); } diff --git a/core/java/android/credentials/ui/ProviderData.java b/core/java/android/credentials/ui/ProviderData.java index 35e12fa43b28..3728469d723d 100644 --- a/core/java/android/credentials/ui/ProviderData.java +++ b/core/java/android/credentials/ui/ProviderData.java @@ -43,10 +43,10 @@ public class ProviderData implements Parcelable { "android.credentials.ui.extra.PROVIDER_DATA_LIST"; @NonNull - private final String mProviderId; + private final String mProviderFlattenedComponentName; @NonNull private final String mProviderDisplayName; - @NonNull + @Nullable private final Icon mIcon; @NonNull private final List<Entry> mCredentialEntries; @@ -58,11 +58,11 @@ public class ProviderData implements Parcelable { private final @CurrentTimeMillisLong long mLastUsedTimeMillis; public ProviderData( - @NonNull String providerId, @NonNull String providerDisplayName, - @NonNull Icon icon, @NonNull List<Entry> credentialEntries, + @NonNull String providerFlattenedComponentName, @NonNull String providerDisplayName, + @Nullable Icon icon, @NonNull List<Entry> credentialEntries, @NonNull List<Entry> actionChips, @Nullable Entry authenticationEntry, @CurrentTimeMillisLong long lastUsedTimeMillis) { - mProviderId = providerId; + mProviderFlattenedComponentName = providerFlattenedComponentName; mProviderDisplayName = providerDisplayName; mIcon = icon; mCredentialEntries = credentialEntries; @@ -73,8 +73,8 @@ public class ProviderData implements Parcelable { /** Returns the unique provider id. */ @NonNull - public String getProviderId() { - return mProviderId; + public String getProviderFlattenedComponentName() { + return mProviderFlattenedComponentName; } @NonNull @@ -82,7 +82,7 @@ public class ProviderData implements Parcelable { return mProviderDisplayName; } - @NonNull + @Nullable public Icon getIcon() { return mIcon; } @@ -108,9 +108,9 @@ public class ProviderData implements Parcelable { } protected ProviderData(@NonNull Parcel in) { - String providerId = in.readString8(); - mProviderId = providerId; - AnnotationValidations.validate(NonNull.class, null, mProviderId); + String providerFlattenedComponentName = in.readString8(); + mProviderFlattenedComponentName = providerFlattenedComponentName; + AnnotationValidations.validate(NonNull.class, null, mProviderFlattenedComponentName); String providerDisplayName = in.readString8(); mProviderDisplayName = providerDisplayName; @@ -118,7 +118,6 @@ public class ProviderData implements Parcelable { Icon icon = in.readTypedObject(Icon.CREATOR); mIcon = icon; - AnnotationValidations.validate(NonNull.class, null, mIcon); List<Entry> credentialEntries = new ArrayList<>(); in.readTypedList(credentialEntries, Entry.CREATOR); @@ -139,7 +138,7 @@ public class ProviderData implements Parcelable { @Override public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeString8(mProviderId); + dest.writeString8(mProviderFlattenedComponentName); dest.writeString8(mProviderDisplayName); dest.writeTypedObject(mIcon, flags); dest.writeTypedList(mCredentialEntries); @@ -171,26 +170,27 @@ public class ProviderData implements Parcelable { * @hide */ public static class Builder { - private @NonNull String mProviderId; + private @NonNull String mProviderFlattenedComponentName; private @NonNull String mProviderDisplayName; - private @NonNull Icon mIcon; + private @Nullable Icon mIcon; private @NonNull List<Entry> mCredentialEntries = new ArrayList<>(); private @NonNull List<Entry> mActionChips = new ArrayList<>(); private @Nullable Entry mAuthenticationEntry = null; private @CurrentTimeMillisLong long mLastUsedTimeMillis = 0L; /** Constructor with required properties. */ - public Builder(@NonNull String providerId, @NonNull String providerDisplayName, - @NonNull Icon icon) { - mProviderId = providerId; + public Builder(@NonNull String providerFlattenedComponentName, + @NonNull String providerDisplayName, + @Nullable Icon icon) { + mProviderFlattenedComponentName = providerFlattenedComponentName; mProviderDisplayName = providerDisplayName; mIcon = icon; } /** Sets the unique provider id. */ @NonNull - public Builder setProviderId(@NonNull String providerId) { - mProviderId = providerId; + public Builder setProviderFlattenedComponentName(@NonNull String providerFlattenedComponentName) { + mProviderFlattenedComponentName = providerFlattenedComponentName; return this; } @@ -239,7 +239,8 @@ public class ProviderData implements Parcelable { /** Builds a {@link ProviderData}. */ @NonNull public ProviderData build() { - return new ProviderData(mProviderId, mProviderDisplayName, mIcon, mCredentialEntries, + return new ProviderData(mProviderFlattenedComponentName, mProviderDisplayName, + mIcon, mCredentialEntries, mActionChips, mAuthenticationEntry, mLastUsedTimeMillis); } } diff --git a/core/java/android/service/credentials/CredentialEntry.java b/core/java/android/service/credentials/CredentialEntry.java index 4cc43a10e88f..a3fa979a6188 100644 --- a/core/java/android/service/credentials/CredentialEntry.java +++ b/core/java/android/service/credentials/CredentialEntry.java @@ -140,8 +140,8 @@ public final class CredentialEntry implements Parcelable { public static final class Builder { private String mType; private Slice mSlice; - private PendingIntent mPendingIntent; - private Credential mCredential; + private PendingIntent mPendingIntent = null; + private Credential mCredential = null; private boolean mAutoSelectAllowed = false; /** diff --git a/core/java/android/service/credentials/CredentialProviderException.java b/core/java/android/service/credentials/CredentialProviderException.java index b39b4a0cc180..06f0052a29a9 100644 --- a/core/java/android/service/credentials/CredentialProviderException.java +++ b/core/java/android/service/credentials/CredentialProviderException.java @@ -30,6 +30,22 @@ import java.lang.annotation.RetentionPolicy; public class CredentialProviderException extends Exception { public static final int ERROR_UNKNOWN = 0; + /** + * For internal use only. + * Error code to be used when the provider request times out. + * + * @hide + */ + public static final int ERROR_TIMEOUT = 1; + + /** + * For internal use only. + * Error code to be used when the async task is canceled internally. + * + * @hide + */ + public static final int ERROR_TASK_CANCELED = 2; + private final int mErrorCode; /** @@ -37,6 +53,8 @@ public class CredentialProviderException extends Exception { */ @IntDef(prefix = {"ERROR_"}, value = { ERROR_UNKNOWN, + ERROR_TIMEOUT, + ERROR_TASK_CANCELED }) @Retention(RetentionPolicy.SOURCE) public @interface CredentialProviderError { } diff --git a/core/java/android/service/credentials/CredentialProviderInfo.java b/core/java/android/service/credentials/CredentialProviderInfo.java index e3f8cb7bb23e..2c7a983826f6 100644 --- a/core/java/android/service/credentials/CredentialProviderInfo.java +++ b/core/java/android/service/credentials/CredentialProviderInfo.java @@ -18,16 +18,21 @@ package android.service.credentials; import android.Manifest; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.UserIdInt; 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; import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.os.Bundle; import android.os.RemoteException; +import android.text.TextUtils; import android.util.Log; import android.util.Slog; @@ -48,19 +53,21 @@ public final class CredentialProviderInfo { @NonNull private final List<String> mCapabilities; - // TODO: Move the two strings below to CredentialProviderService when ready. - private static final String CAPABILITY_META_DATA_KEY = "android.credentials.capabilities"; - private static final String SERVICE_INTERFACE = - "android.service.credentials.CredentialProviderService"; - + @NonNull + private final Context mContext; + @Nullable + private final Drawable mIcon; + @Nullable + private final CharSequence mLabel; /** * Constructs an information instance of the credential provider. * - * @param context The context object - * @param serviceComponent The serviceComponent of the provider service - * @param userId The android userId for which the current process is running + * @param context the context object + * @param serviceComponent the serviceComponent of the provider service + * @param userId the android userId for which the current process is running * @throws PackageManager.NameNotFoundException If provider service is not found + * @throws SecurityException If provider does not require the relevant permission */ public CredentialProviderInfo(@NonNull Context context, @NonNull ComponentName serviceComponent, int userId) @@ -68,7 +75,13 @@ public final class CredentialProviderInfo { this(context, getServiceInfoOrThrow(serviceComponent, userId)); } - private CredentialProviderInfo(@NonNull Context context, @NonNull ServiceInfo serviceInfo) { + /** + * Constructs an information instance of the credential provider. + * @param context the context object + * @param serviceInfo the service info for the provider app. This must be retrieved from the + * {@code PackageManager} + */ + public CredentialProviderInfo(@NonNull Context context, @NonNull ServiceInfo serviceInfo) { if (!Manifest.permission.BIND_CREDENTIAL_PROVIDER_SERVICE.equals(serviceInfo.permission)) { Log.i(TAG, "Credential Provider Service from : " + serviceInfo.packageName + "does not require permission" @@ -76,32 +89,43 @@ public final class CredentialProviderInfo { throw new SecurityException("Service does not require the expected permission : " + Manifest.permission.BIND_CREDENTIAL_PROVIDER_SERVICE); } + mContext = context; mServiceInfo = serviceInfo; mCapabilities = new ArrayList<>(); - populateProviderCapabilities(context); + mIcon = mServiceInfo.loadIcon(mContext.getPackageManager()); + mLabel = mServiceInfo.loadSafeLabel( + mContext.getPackageManager(), 0 /* do not ellipsize */, + TextUtils.SAFE_STRING_FLAG_FIRST_LINE | TextUtils.SAFE_STRING_FLAG_TRIM); + populateProviderCapabilities(context, serviceInfo); } - private void populateProviderCapabilities(@NonNull Context context) { - if (mServiceInfo.applicationInfo.metaData == null) { - return; - } + private void populateProviderCapabilities(@NonNull Context context, ServiceInfo serviceInfo) { + final PackageManager pm = context.getPackageManager(); try { - final int resourceId = mServiceInfo.applicationInfo.metaData.getInt( - CAPABILITY_META_DATA_KEY); - String[] capabilities = context.getResources().getStringArray(resourceId); - if (capabilities == null) { - Log.w(TAG, "No capabilities found for provider: " + mServiceInfo.packageName); + Bundle metadata = serviceInfo.metaData; + Resources resources = pm.getResourcesForApplication(serviceInfo.applicationInfo); + if (metadata == null || resources == null) { + Log.i(TAG, "populateProviderCapabilities - metadata or resources is null"); + return; + } + + String[] capabilities = resources.getStringArray(metadata.getInt( + CredentialProviderService.CAPABILITY_META_DATA_KEY)); + if (capabilities == null || capabilities.length == 0) { + Slog.i(TAG, "No capabilities found for provider:" + serviceInfo.packageName); return; } + for (String capability : capabilities) { if (capability.isEmpty()) { - Log.w(TAG, "Skipping empty capability"); + Slog.i(TAG, "Skipping empty capability"); continue; } + Slog.i(TAG, "Capabilities found for provider: " + capability); mCapabilities.add(capability); } - } catch (Resources.NotFoundException e) { - Log.w(TAG, "Exception while populating provider capabilities: " + e.getMessage()); + } catch (PackageManager.NameNotFoundException e) { + Slog.i(TAG, e.getMessage()); } } @@ -135,6 +159,18 @@ public final class CredentialProviderInfo { return mServiceInfo; } + /** Returns the service icon. */ + @Nullable + public Drawable getServiceIcon() { + return mIcon; + } + + /** Returns the service label. */ + @Nullable + public CharSequence getServiceLabel() { + return mLabel; + } + /** Returns an immutable list of capabilities this provider service can support. */ @NonNull public List<String> getCapabilities() { @@ -145,14 +181,15 @@ public final class CredentialProviderInfo { * Returns the valid credential provider services available for the user with the * given {@code userId}. */ + @NonNull public static List<CredentialProviderInfo> getAvailableServices(@NonNull Context context, @UserIdInt int userId) { final List<CredentialProviderInfo> services = new ArrayList<>(); final List<ResolveInfo> resolveInfos = context.getPackageManager().queryIntentServicesAsUser( - new Intent(SERVICE_INTERFACE), - PackageManager.GET_META_DATA, + new Intent(CredentialProviderService.SERVICE_INTERFACE), + PackageManager.ResolveInfoFlags.of(PackageManager.GET_META_DATA), userId); for (ResolveInfo resolveInfo : resolveInfos) { final ServiceInfo serviceInfo = resolveInfo.serviceInfo; @@ -169,8 +206,9 @@ public final class CredentialProviderInfo { * Returns the valid credential provider services available for the user, that can * support the given {@code credentialType}. */ + @NonNull public static List<CredentialProviderInfo> getAvailableServicesForCapability( - Context context, @UserIdInt int userId, String credentialType) { + @NonNull Context context, @UserIdInt int userId, @NonNull String credentialType) { List<CredentialProviderInfo> servicesForCapability = new ArrayList<>(); final List<CredentialProviderInfo> services = getAvailableServices(context, userId); diff --git a/core/java/android/service/credentials/CredentialProviderService.java b/core/java/android/service/credentials/CredentialProviderService.java index 1cdf186d898c..b1b08f466622 100644 --- a/core/java/android/service/credentials/CredentialProviderService.java +++ b/core/java/android/service/credentials/CredentialProviderService.java @@ -42,6 +42,9 @@ import java.util.Objects; */ public abstract class CredentialProviderService extends Service { private static final String TAG = "CredProviderService"; + + public static final String CAPABILITY_META_DATA_KEY = "android.credentials.capabilities"; + private Handler mHandler; /** @@ -71,12 +74,13 @@ public abstract class CredentialProviderService extends Service { private final ICredentialProviderService mInterface = new ICredentialProviderService.Stub() { @Override - public void onGetCredentials(GetCredentialsRequest request, ICancellationSignal transport, + public ICancellationSignal onGetCredentials(GetCredentialsRequest request, IGetCredentialsCallback callback) { Objects.requireNonNull(request); - Objects.requireNonNull(transport); Objects.requireNonNull(callback); + ICancellationSignal transport = CancellationSignal.createTransport(); + mHandler.sendMessage(obtainMessage( CredentialProviderService::onGetCredentials, CredentialProviderService.this, request, @@ -100,15 +104,17 @@ public abstract class CredentialProviderService extends Service { } } )); + return transport; } @Override - public void onCreateCredential(CreateCredentialRequest request, - ICancellationSignal transport, ICreateCredentialCallback callback) { + public ICancellationSignal onCreateCredential(CreateCredentialRequest request, + ICreateCredentialCallback callback) { Objects.requireNonNull(request); - Objects.requireNonNull(transport); Objects.requireNonNull(callback); + ICancellationSignal transport = CancellationSignal.createTransport(); + mHandler.sendMessage(obtainMessage( CredentialProviderService::onCreateCredential, CredentialProviderService.this, request, @@ -132,6 +138,7 @@ public abstract class CredentialProviderService extends Service { } } )); + return transport; } }; diff --git a/core/java/android/service/credentials/ICredentialProviderService.aidl b/core/java/android/service/credentials/ICredentialProviderService.aidl index c68430ce752e..c21cefab701a 100644 --- a/core/java/android/service/credentials/ICredentialProviderService.aidl +++ b/core/java/android/service/credentials/ICredentialProviderService.aidl @@ -21,13 +21,14 @@ import android.service.credentials.GetCredentialsRequest; import android.service.credentials.CreateCredentialRequest; import android.service.credentials.IGetCredentialsCallback; import android.service.credentials.ICreateCredentialCallback; +import android.os.ICancellationSignal; /** * Interface from the system to a credential provider service. * * @hide */ -oneway interface ICredentialProviderService { - void onGetCredentials(in GetCredentialsRequest request, in ICancellationSignal transport, in IGetCredentialsCallback callback); - void onCreateCredential(in CreateCredentialRequest request, in ICancellationSignal transport, in ICreateCredentialCallback callback); +interface ICredentialProviderService { + ICancellationSignal onGetCredentials(in GetCredentialsRequest request, in IGetCredentialsCallback callback); + ICancellationSignal onCreateCredential(in CreateCredentialRequest request, in ICreateCredentialCallback callback); } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt index e037db7aa0e2..2ba8748c16b7 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -35,7 +35,7 @@ class GetFlowUtils { ProviderInfo( // TODO: replace to extract from the service data structure when available icon = context.getDrawable(R.drawable.ic_passkey)!!, - name = it.providerId, + name = it.providerFlattenedComponentName, displayName = it.providerDisplayName, credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!, credentialOptions = toCredentialOptionInfoList(it.credentialEntries, context) @@ -79,7 +79,7 @@ class CreateFlowUtils { com.android.credentialmanager.createflow.ProviderInfo( // TODO: replace to extract from the service data structure when available icon = context.getDrawable(R.drawable.ic_passkey)!!, - name = it.providerId, + name = it.providerFlattenedComponentName, displayName = it.providerDisplayName, credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!, createOptions = toCreationOptionInfoList(it.credentialEntries, context), diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java index 91f5c6999427..40412db2ed5e 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java @@ -21,20 +21,28 @@ import static android.content.Context.CREDENTIAL_SERVICE; import android.annotation.NonNull; import android.annotation.UserIdInt; import android.content.Context; +import android.content.pm.PackageManager; import android.credentials.CreateCredentialRequest; import android.credentials.GetCredentialRequest; import android.credentials.ICreateCredentialCallback; import android.credentials.ICredentialManager; import android.credentials.IGetCredentialCallback; +import android.os.Binder; import android.os.CancellationSignal; import android.os.ICancellationSignal; import android.os.UserHandle; import android.provider.Settings; +import android.text.TextUtils; import android.util.Log; +import android.util.Slog; import com.android.server.infra.AbstractMasterSystemService; import com.android.server.infra.SecureSettingsServiceNameResolver; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + /** * Entry point service for credential management. * @@ -49,52 +57,99 @@ public final class CredentialManagerService extends public CredentialManagerService(@NonNull Context context) { super(context, - new SecureSettingsServiceNameResolver(context, Settings.Secure.AUTOFILL_SERVICE), + new SecureSettingsServiceNameResolver(context, Settings.Secure.CREDENTIAL_SERVICE, + /*isMultipleMode=*/true), null, PACKAGE_UPDATE_POLICY_REFRESH_EAGER); } @Override protected String getServiceSettingsProperty() { - return Settings.Secure.AUTOFILL_SERVICE; + return Settings.Secure.CREDENTIAL_SERVICE; } @Override // from AbstractMasterSystemService protected CredentialManagerServiceImpl newServiceLocked(@UserIdInt int resolvedUserId, boolean disabled) { - return new CredentialManagerServiceImpl(this, mLock, resolvedUserId); + // This method should not be called for CredentialManagerService as it is configured to use + // multiple services. + Slog.w(TAG, "Should not be here - CredentialManagerService is configured to use " + + "multiple services"); + return null; } - @Override + @Override // from SystemService public void onStart() { - Log.i(TAG, "onStart"); publishBinderService(CREDENTIAL_SERVICE, new CredentialManagerServiceStub()); } + @Override // from AbstractMasterSystemService + protected List<CredentialManagerServiceImpl> newServiceListLocked(int resolvedUserId, + boolean disabled, String[] serviceNames) { + if (serviceNames == null || serviceNames.length == 0) { + Slog.i(TAG, "serviceNames sent in newServiceListLocked is null, or empty"); + return new ArrayList<>(); + } + List<CredentialManagerServiceImpl> 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])) { + 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) { + Log.i(TAG, "Unable to add serviceInfo : " + e.getMessage()); + } + } + return serviceList; + } + + private void runForUser(@NonNull final Consumer<CredentialManagerServiceImpl> c) { + final int userId = UserHandle.getCallingUserId(); + final long origId = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + final List<CredentialManagerServiceImpl> services = + getServiceListForUserLocked(userId); + services.forEach(s -> { + c.accept(s); + }); + } + } finally { + Binder.restoreCallingIdentity(origId); + } + } + final class CredentialManagerServiceStub extends ICredentialManager.Stub { @Override public ICancellationSignal executeGetCredential( GetCredentialRequest request, - IGetCredentialCallback callback) { - // TODO: implement. - Log.i(TAG, "executeGetCredential"); + IGetCredentialCallback callback, + final String callingPackage) { + Log.i(TAG, "starting executeGetCredential with callingPackage: " + callingPackage); + // TODO : Implement cancellation + ICancellationSignal cancelTransport = CancellationSignal.createTransport(); - final int userId = UserHandle.getCallingUserId(); - synchronized (mLock) { - final CredentialManagerServiceImpl service = peekServiceForUserLocked(userId); - if (service != null) { - Log.i(TAG, "Got service for : " + userId); - service.getCredential(); - } - } + // New request session, scoped for this request only. + final GetRequestSession session = new GetRequestSession(getContext(), + UserHandle.getCallingUserId(), + callback); - ICancellationSignal cancelTransport = CancellationSignal.createTransport(); + // Invoke all services of a user + runForUser((service) -> { + service.getCredential(request, session, callingPackage); + }); return cancelTransport; } @Override public ICancellationSignal executeCreateCredential( CreateCredentialRequest request, - ICreateCredentialCallback callback) { + ICreateCredentialCallback callback, + String callingPackage) { // TODO: implement. Log.i(TAG, "executeCreateCredential"); ICancellationSignal cancelTransport = CancellationSignal.createTransport(); diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java index aa19241e77dd..cc03f9b89119 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java @@ -17,47 +17,94 @@ package com.android.server.credentials; import android.annotation.NonNull; -import android.app.AppGlobals; +import android.annotation.Nullable; import android.content.ComponentName; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; -import android.os.RemoteException; -import android.util.Log; +import android.credentials.GetCredentialRequest; +import android.service.credentials.CredentialProviderInfo; +import android.service.credentials.GetCredentialsRequest; +import android.util.Slog; import com.android.server.infra.AbstractPerUserSystemService; + /** - * Per-user implementation of {@link CredentialManagerService} + * Per-user, per remote service implementation of {@link CredentialManagerService} */ public final class CredentialManagerServiceImpl extends AbstractPerUserSystemService<CredentialManagerServiceImpl, CredentialManagerService> { private static final String TAG = "CredManSysServiceImpl"; - protected CredentialManagerServiceImpl( + // TODO(b/210531) : Make final when update flow is fixed + private ComponentName mRemoteServiceComponentName; + private CredentialProviderInfo mInfo; + + public CredentialManagerServiceImpl( @NonNull CredentialManagerService master, - @NonNull Object lock, int userId) { + @NonNull Object lock, int userId, String serviceName) + throws PackageManager.NameNotFoundException { super(master, lock, userId); + Slog.i(TAG, "in CredentialManagerServiceImpl cons"); + // TODO : Replace with newServiceInfoLocked after confirming behavior + mRemoteServiceComponentName = ComponentName.unflattenFromString(serviceName); + mInfo = new CredentialProviderInfo(getContext(), mRemoteServiceComponentName, mUserId); } @Override // from PerUserSystemService protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent) throws PackageManager.NameNotFoundException { - ServiceInfo si; - try { - si = AppGlobals.getPackageManager().getServiceInfo(serviceComponent, - PackageManager.GET_META_DATA, mUserId); - } catch (RemoteException e) { - throw new PackageManager.NameNotFoundException( - "Could not get service for " + serviceComponent); + // TODO : Test update flows with multiple providers + Slog.i(TAG , "newServiceInfoLocked with : " + serviceComponent.getPackageName()); + mRemoteServiceComponentName = serviceComponent; + mInfo = new CredentialProviderInfo(getContext(), serviceComponent, mUserId); + return mInfo.getServiceInfo(); + } + + public void getCredential(GetCredentialRequest request, GetRequestSession requestSession, + String callingPackage) { + Slog.i(TAG, "in getCredential in CredManServiceImpl"); + if (mInfo == null) { + Slog.i(TAG, "in getCredential in CredManServiceImpl, but mInfo is null"); + return; + } + + // 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); } - return si; } - /** - * Unimplemented getCredentials - */ - public void getCredential() { - Log.i(TAG, "getCredential not implemented"); - // TODO : Implement logic + @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 null; } } diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java new file mode 100644 index 000000000000..69fb1eaa7543 --- /dev/null +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java @@ -0,0 +1,92 @@ +/* + * 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.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.credentials.ui.IntentFactory; +import android.credentials.ui.ProviderData; +import android.credentials.ui.RequestInfo; +import android.credentials.ui.UserSelectionDialogResult; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.ResultReceiver; +import android.util.Log; +import android.util.Slog; + +import java.util.ArrayList; + +/** Initiates the Credential Manager UI and receives results. */ +public class CredentialManagerUi { + private static final String TAG = "CredentialManagerUi"; + @NonNull + private final CredentialManagerUiCallback mCallbacks; + @NonNull private final Context mContext; + private final int mUserId; + @NonNull private final ResultReceiver mResultReceiver = new ResultReceiver( + new Handler(Looper.getMainLooper())) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + handleUiResult(resultCode, resultData); + } + }; + + private void handleUiResult(int resultCode, Bundle resultData) { + if (resultCode == Activity.RESULT_OK) { + UserSelectionDialogResult selection = UserSelectionDialogResult + .fromResultData(resultData); + if (selection != null) { + mCallbacks.onUiSelection(selection); + } else { + Slog.i(TAG, "No selection found in UI result"); + } + } else if (resultCode == Activity.RESULT_CANCELED) { + mCallbacks.onUiCancelation(); + } + } + + /** + * Interface to be implemented by any class that wishes to get callbacks from the UI. + */ + public interface CredentialManagerUiCallback { + /** Called when the user makes a selection. */ + void onUiSelection(UserSelectionDialogResult selection); + /** Called when the user cancels the UI. */ + void onUiCancelation(); + } + public CredentialManagerUi(Context context, int userId, + CredentialManagerUiCallback callbacks) { + Log.i(TAG, "In CredentialManagerUi constructor"); + mContext = context; + mUserId = userId; + mCallbacks = callbacks; + } + + /** + * Surfaces the Credential Manager bottom sheet UI. + * @param providerDataList the list of provider data from remote providers + */ + public void show(RequestInfo requestInfo, ArrayList<ProviderData> providerDataList) { + Log.i(TAG, "In show"); + Intent intent = IntentFactory.newIntent(requestInfo, providerDataList, + 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 new file mode 100644 index 000000000000..80f0fec06825 --- /dev/null +++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java @@ -0,0 +1,164 @@ +/* + * 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.content.ComponentName; +import android.content.Context; +import android.credentials.Credential; +import android.credentials.GetCredentialResponse; +import android.credentials.IGetCredentialCallback; +import android.credentials.ui.ProviderData; +import android.credentials.ui.RequestInfo; +import android.credentials.ui.UserSelectionDialogResult; +import android.os.RemoteException; +import android.service.credentials.CredentialEntry; +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 { + private static final String TAG = "GetRequestSession"; + + private final IGetCredentialCallback mClientCallback; + private final Map<String, ProviderGetSession> mProviders; + + public GetRequestSession(Context context, int userId, + IGetCredentialCallback callback) { + super(context, userId, RequestInfo.TYPE_GET); + mClientCallback = callback; + mProviders = new HashMap<>(); + } + + /** + * Adds a new provider to the list of providers that are contributing to this session. + */ + 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(); + } + } + + @Override + public void onUiSelection(UserSelectionDialogResult selection) { + String providerId = selection.getProviderId(); + ProviderGetSession providerSession = mProviders.get(providerId); + if (providerSession != null) { + CredentialEntry credentialEntry = providerSession.getCredentialEntry( + selection.getEntrySubkey()); + if (credentialEntry != null && credentialEntry.getCredential() != null) { + 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<ProviderData> 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<ProviderData> 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)); + } catch (RemoteException e) { + e.printStackTrace(); + } + 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/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java new file mode 100644 index 000000000000..24610df7ad79 --- /dev/null +++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java @@ -0,0 +1,197 @@ +/* + * 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.app.slice.Slice; +import android.credentials.ui.Entry; +import android.credentials.ui.ProviderData; +import android.service.credentials.Action; +import android.service.credentials.CredentialEntry; +import android.service.credentials.CredentialProviderInfo; +import android.service.credentials.CredentialsDisplayContent; +import android.service.credentials.GetCredentialsResponse; +import android.util.Log; +import android.util.Slog; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +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. + */ +public final class ProviderGetSession extends ProviderSession<GetCredentialsResponse> + implements RemoteCredentialService.ProviderCallbacks<GetCredentialsResponse> { + 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<String, CredentialEntry> mUiCredentials = new HashMap<>(); + + @NonNull + private final Map<String, Action> mUiActions = new HashMap<>(); + + 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); + } + } + + /** Returns the response being maintained in this provider session. */ + @Override + @Nullable + public GetCredentialsResponse getResponse() { + return mResponse; + } + + /** 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); + } + + /** Called when the provider response has been updated by an external source. */ + @Override + public void onProviderResponseSuccess(@Nullable GetCredentialsResponse response) { + Log.i(TAG, "in onProviderResponseSuccess"); + updateResponse(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"); + } + } + + @Override + protected final ProviderData prepareUiData() throws IllegalArgumentException { + Log.i(TAG, "In prepareUiData"); + if (!ProviderSession.isCompletionStatus(getStatus())) { + Log.i(TAG, "In prepareUiData not complete"); + + throw new IllegalStateException("Status must be in completion mode"); + } + GetCredentialsResponse response = getResponse(); + if (response == null) { + Log.i(TAG, "In prepareUiData response null"); + + throw new IllegalStateException("Response must be in completion mode"); + } + if (response.getAuthenticationAction() != null) { + Log.i(TAG, "In prepareUiData auth not null"); + + return prepareUiProviderDataWithAuthentication(response.getAuthenticationAction()); + } + if (response.getCredentialsDisplayContent() != null){ + Log.i(TAG, "In prepareUiData credentials not null"); + + return prepareUiProviderDataWithCredentials(response.getCredentialsDisplayContent()); + } + return null; + } + + /** + * To be called by {@link ProviderGetSession} when the UI is to be invoked. + */ + @Nullable + private ProviderData prepareUiProviderDataWithCredentials(@NonNull + CredentialsDisplayContent content) { + Log.i(TAG, "in prepareUiProviderData"); + List<Entry> credentialEntries = new ArrayList<>(); + List<Entry> actionChips = new ArrayList<>(); + Entry authenticationEntry = null; + + // Populate the credential entries + for (CredentialEntry credentialEntry : content.getCredentialEntries()) { + String entryId = UUID.randomUUID().toString(); + mUiCredentials.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, + 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())); + } + + // TODO : Set the correct last used time + return new ProviderData.Builder(mComponentName.flattenToString(), + mProviderInfo.getServiceLabel() == null ? "" : + mProviderInfo.getServiceLabel().toString(), + /*icon=*/null) + .setCredentialEntries(credentialEntries) + .setActionChips(actionChips) + .setAuthenticationEntry(authenticationEntry) + .setLastUsedTimeMillis(0) + .build(); + } + + /** + * To be called by {@link ProviderGetSession} when the UI is to be invoked. + */ + @Nullable + private ProviderData prepareUiProviderDataWithAuthentication(@NonNull + Action authenticationEntry) { + // TODO : Implement authentication flow + return null; + } +} diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java new file mode 100644 index 000000000000..3a9f96432d8c --- /dev/null +++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java @@ -0,0 +1,124 @@ +/* + * 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.content.ComponentName; +import android.credentials.ui.ProviderData; +import android.service.credentials.CredentialProviderException; +import android.service.credentials.CredentialProviderInfo; + +/** + * Provider session storing the state of provider response and ui entries. + * @param <T> The request type expected from the remote provider, for a given request session. + */ +public abstract class ProviderSession<T> implements RemoteCredentialService.ProviderCallbacks<T> { + // Key to be used as the entry key for an action entry + protected static final String ACTION_ENTRY_KEY = "action_key"; + + @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; + + /** + * Interface to be implemented by any class that wishes to get a callback when a particular + * provider session's status changes. Typically, implemented by the {@link RequestSession} + * class. + */ + public interface ProviderInternalCallback { + /** + * Called when status changes. + */ + void onProviderStatusChanged(Status status, ComponentName componentName); + } + + protected ProviderSession(@NonNull CredentialProviderInfo info, + @NonNull ProviderInternalCallback callbacks, + @NonNull int userId, + @NonNull RemoteCredentialService remoteCredentialService) { + mProviderInfo = info; + mCallbacks = callbacks; + mUserId = userId; + mComponentName = info.getServiceInfo().getComponentName(); + mRemoteCredentialService = remoteCredentialService; + } + + /** 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. */ + enum Status { + NOT_STARTED, + PENDING, + REQUIRES_AUTHENTICATION, + COMPLETE, + SERVICE_DEAD, + CANCELED + } + + protected void setStatus(@NonNull Status status) { + mStatus = status; + } + + @NonNull + protected Status getStatus() { + return mStatus; + } + + @NonNull + protected ComponentName getComponentName() { + return mComponentName; + } + + /** 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; + } + + /** + * 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 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; + } +} diff --git a/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java b/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java new file mode 100644 index 000000000000..d0b6e7d6238c --- /dev/null +++ b/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java @@ -0,0 +1,185 @@ +/* + * 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.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.ICancellationSignal; +import android.os.RemoteException; +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.ICredentialProviderService; +import android.service.credentials.IGetCredentialsCallback; +import android.text.format.DateUtils; +import android.util.Log; +import android.util.Slog; + +import com.android.internal.infra.ServiceConnector; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Handles connections with the remote credential provider + * + * @hide + */ +public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialProviderService>{ + + private static final String TAG = "RemoteCredentialService"; + /** Timeout for a single request. */ + private static final long TIMEOUT_REQUEST_MILLIS = 5 * DateUtils.SECOND_IN_MILLIS; + /** Timeout to unbind after the task queue is empty. */ + private static final long TIMEOUT_IDLE_SERVICE_CONNECTION_MILLIS = + 5 * DateUtils.SECOND_IN_MILLIS; + + private final ComponentName mComponentName; + + /** + * Callbacks to be invoked when the provider remote service responds with a + * success or failure. + * @param <T> the type of response expected from the provider + */ + public interface ProviderCallbacks<T> { + /** Called when a successful response is received from the remote provider. */ + void onProviderResponseSuccess(@Nullable T response); + /** Called when a failure response is received from the remote provider. */ + void onProviderResponseFailure(int errorCode, @Nullable CharSequence message); + /** Called when the remote provider service dies. */ + void onProviderServiceDied(RemoteCredentialService service); + } + + public RemoteCredentialService(@NonNull Context context, + @NonNull ComponentName componentName, int userId) { + super(context, new Intent(CredentialProviderService.SERVICE_INTERFACE) + .setComponent(componentName), Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS, + userId, ICredentialProviderService.Stub::asInterface); + mComponentName = componentName; + } + + /** Unbinds automatically after this amount of time. */ + @Override + protected long getAutoDisconnectTimeoutMs() { + return TIMEOUT_IDLE_SERVICE_CONNECTION_MILLIS; + } + + /** Return the componentName of the service to be connected. */ + @NonNull public ComponentName getComponentName() { + return mComponentName; + } + + /** Destroys this remote service by unbinding the connection. */ + public void destroy() { + unbind(); + } + + /** Main entry point to be called for executing a getCredential 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 ProviderSession} class that maintains provider state + */ + public void onGetCredentials(@NonNull GetCredentialsRequest request, + ProviderCallbacks<GetCredentialsResponse> callback) { + Log.i(TAG, "In onGetCredentials in RemoteCredentialService"); + AtomicReference<ICancellationSignal> cancellationSink = new AtomicReference<>(); + AtomicReference<CompletableFuture<GetCredentialsResponse>> futureRef = + new AtomicReference<>(); + + CompletableFuture<GetCredentialsResponse> connectThenExecute = postAsync(service -> { + CompletableFuture<GetCredentialsResponse> 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 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<GetCredentialsResponse> future = futureRef.get(); + if (future != null && future.isCancelled()) { + dispatchCancellationSignal(cancellationSignal); + } else { + cancellationSink.set(cancellationSignal); + } + return getCredentials; + }).orTimeout(TIMEOUT_REQUEST_MILLIS, TimeUnit.MILLISECONDS); + futureRef.set(connectThenExecute); + + connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() -> { + 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) { + if (signal == null) { + Slog.e(TAG, "Error dispatching a cancellation - Signal is null"); + return; + } + try { + signal.cancel(); + } catch (RemoteException e) { + Slog.e(TAG, "Error dispatching a cancellation", e); + } + } +} diff --git a/services/credentials/java/com/android/server/credentials/RequestSession.java b/services/credentials/java/com/android/server/credentials/RequestSession.java new file mode 100644 index 000000000000..1bacbb342edb --- /dev/null +++ b/services/credentials/java/com/android/server/credentials/RequestSession.java @@ -0,0 +1,67 @@ +/* + * 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.UserIdInt; +import android.content.ComponentName; +import android.content.Context; +import android.credentials.ui.UserSelectionDialogResult; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; + +/** + * 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, + ProviderSession.ProviderInternalCallback { + @NonNull protected final IBinder mRequestId; + @NonNull protected final Context mContext; + @NonNull protected final CredentialManagerUi mCredentialManagerUi; + @NonNull protected final String mRequestType; + @NonNull protected final Handler mHandler; + @NonNull protected boolean mIsFirstUiTurn = true; + @UserIdInt protected final int mUserId; + + protected RequestSession(@NonNull Context context, + @UserIdInt int userId, @NonNull String requestType) { + mContext = context; + mUserId = userId; + mRequestType = requestType; + 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; + } + + @Override // from CredentialManagerUiCallback + public abstract void onUiSelection(UserSelectionDialogResult selection); + + @Override // from CredentialManagerUiCallback + public abstract void onUiCancelation(); + + @Override // from ProviderInternalCallback + public abstract void onProviderStatusChanged(ProviderSession.Status status, ComponentName componentName); +} |