diff --git a/core/api/current.txt b/core/api/current.txt index 12d579ee08f35..9cfc161fb7e43 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -9820,6 +9820,7 @@ package android.content { field public static final int CONTEXT_IGNORE_SECURITY = 2; // 0x2 field public static final int CONTEXT_INCLUDE_CODE = 1; // 0x1 field public static final int CONTEXT_RESTRICTED = 4; // 0x4 + field public static final String CREDENTIAL_SERVICE = "credential"; field public static final String CROSS_PROFILE_APPS_SERVICE = "crossprofileapps"; field public static final String DEVICE_POLICY_SERVICE = "device_policy"; field public static final String DISPLAY_HASH_SERVICE = "display_hash"; @@ -12879,6 +12880,82 @@ package android.content.res.loader { } +package android.credentials { + + public final class CreateCredentialRequest implements android.os.Parcelable { + ctor public CreateCredentialRequest(@NonNull String, @NonNull android.os.Bundle); + method public int describeContents(); + method @NonNull public android.os.Bundle getData(); + method @NonNull public String getType(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + + public final class CreateCredentialResponse implements android.os.Parcelable { + ctor public CreateCredentialResponse(@NonNull android.os.Bundle); + method public int describeContents(); + method @NonNull public android.os.Bundle getData(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + + public final class Credential implements android.os.Parcelable { + ctor public Credential(@NonNull String, @NonNull android.os.Bundle); + method public int describeContents(); + method @NonNull public android.os.Bundle getData(); + method @NonNull public String getType(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + + public final class CredentialManager { + method public void executeCreateCredential(@NonNull android.credentials.CreateCredentialRequest, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver); + method public void executeGetCredential(@NonNull android.credentials.GetCredentialRequest, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver); + } + + public class CredentialManagerException extends java.lang.Exception { + ctor public CredentialManagerException(int, @Nullable String); + ctor public CredentialManagerException(int, @Nullable String, @Nullable Throwable); + ctor public CredentialManagerException(int, @Nullable Throwable); + ctor public CredentialManagerException(int); + field public static final int ERROR_UNKNOWN = 0; // 0x0 + field public final int errorCode; + } + + public final class GetCredentialOption implements android.os.Parcelable { + ctor public GetCredentialOption(@NonNull String, @NonNull android.os.Bundle); + method public int describeContents(); + method @NonNull public android.os.Bundle getData(); + method @NonNull public String getType(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + + public final class GetCredentialRequest implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public java.util.List getGetCredentialOptions(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + + public static final class GetCredentialRequest.Builder { + ctor public GetCredentialRequest.Builder(); + method @NonNull public android.credentials.GetCredentialRequest.Builder addGetCredentialOption(@NonNull android.credentials.GetCredentialOption); + method @NonNull public android.credentials.GetCredentialRequest build(); + method @NonNull public android.credentials.GetCredentialRequest.Builder setGetCredentialOptions(@NonNull java.util.List); + } + + public final class GetCredentialResponse implements android.os.Parcelable { + ctor public GetCredentialResponse(@NonNull android.credentials.Credential); + ctor public GetCredentialResponse(); + method public int describeContents(); + method @Nullable public android.credentials.Credential getCredential(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + +} + package android.database { public abstract class AbstractCursor implements android.database.CrossProcessCursor { diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index c658b3e11c477..0d73c1375b031 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -57,6 +57,7 @@ import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; +import android.credentials.CredentialManager; import android.database.DatabaseErrorHandler; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; @@ -3933,6 +3934,7 @@ public abstract class Context { //@hide: ATTESTATION_VERIFICATION_SERVICE, //@hide: SAFETY_CENTER_SERVICE, DISPLAY_HASH_SERVICE, + CREDENTIAL_SERVICE, }) @Retention(RetentionPolicy.SOURCE) public @interface ServiceName {} @@ -6058,6 +6060,15 @@ public abstract class Context { */ public static final String HEALTHCONNECT_SERVICE = "healthconnect"; + /** + * Use with {@link #getSystemService(String)} to retrieve a + * {@link android.credentials.CredentialManager} to authenticate a user to your app. + * + * @see #getSystemService(String) + * @see CredentialManager + */ + public static final String CREDENTIAL_SERVICE = "credential"; + /** * Determine whether the given permission is allowed for a particular * process and user ID running in the system. diff --git a/core/java/android/credentials/CreateCredentialRequest.aidl b/core/java/android/credentials/CreateCredentialRequest.aidl new file mode 100644 index 0000000000000..5ab0b489fd48d --- /dev/null +++ b/core/java/android/credentials/CreateCredentialRequest.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials; + +parcelable CreateCredentialRequest; \ No newline at end of file diff --git a/core/java/android/credentials/CreateCredentialRequest.java b/core/java/android/credentials/CreateCredentialRequest.java new file mode 100644 index 0000000000000..333c837ff22c8 --- /dev/null +++ b/core/java/android/credentials/CreateCredentialRequest.java @@ -0,0 +1,114 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials; + +import static java.util.Objects.requireNonNull; + +import android.annotation.NonNull; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.AnnotationValidations; +import com.android.internal.util.Preconditions; + +/** + * A request to register a specific type of user credential, potentially launching UI flows to + * collect user consent and any other operation needed. + */ +public final class CreateCredentialRequest implements Parcelable { + + /** + * The requested credential type. + */ + @NonNull + private final String mType; + + /** + * The request data. + */ + @NonNull + private final Bundle mData; + + /** + * Returns the requested credential type. + */ + @NonNull + public String getType() { + return mType; + } + + /** + * Returns the request data. + */ + @NonNull + public Bundle getData() { + return mData; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString8(mType); + dest.writeBundle(mData); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "CreateCredentialRequest {" + "type=" + mType + ", data=" + mData + "}"; + } + + /** + * Constructs a {@link CreateCredentialRequest}. + * + * @param type the requested credential type. + * @param data the request data. + * + * @throws IllegalArgumentException If type is empty. + */ + public CreateCredentialRequest(@NonNull String type, @NonNull Bundle data) { + mType = Preconditions.checkStringNotEmpty(type, "type must not be empty"); + mData = requireNonNull(data, "data must not be null"); + } + + private CreateCredentialRequest(@NonNull Parcel in) { + String type = in.readString8(); + Bundle data = in.readBundle(); + + mType = type; + AnnotationValidations.validate(NonNull.class, null, mType); + mData = data; + AnnotationValidations.validate(NonNull.class, null, mData); + } + + public static final @NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public CreateCredentialRequest[] newArray(int size) { + return new CreateCredentialRequest[size]; + } + + @Override + public CreateCredentialRequest createFromParcel(@NonNull Parcel in) { + return new CreateCredentialRequest(in); + } + }; +} diff --git a/core/java/android/credentials/CreateCredentialResponse.aidl b/core/java/android/credentials/CreateCredentialResponse.aidl new file mode 100644 index 0000000000000..83e1bc425bea4 --- /dev/null +++ b/core/java/android/credentials/CreateCredentialResponse.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials; + +parcelable CreateCredentialResponse; \ No newline at end of file diff --git a/core/java/android/credentials/CreateCredentialResponse.java b/core/java/android/credentials/CreateCredentialResponse.java new file mode 100644 index 0000000000000..a3ee6772852ec --- /dev/null +++ b/core/java/android/credentials/CreateCredentialResponse.java @@ -0,0 +1,89 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials; + +import static java.util.Objects.requireNonNull; + +import android.annotation.NonNull; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.AnnotationValidations; + +/** + * A response object that encapsulates the result of a successful credential creation execution. + */ +public final class CreateCredentialResponse implements Parcelable { + + /** + * The response data. + */ + @NonNull + private final Bundle mData; + + /** + * Returns the response data. + */ + @NonNull + public Bundle getData() { + return mData; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeBundle(mData); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "CreateCredentialResponse {data=" + mData + "}"; + } + + /** + * Constructs a {@link CreateCredentialResponse}. + * + * @param data the data associated with the credential created. + */ + public CreateCredentialResponse(@NonNull Bundle data) { + mData = requireNonNull(data, "data must not be null"); + } + + private CreateCredentialResponse(@NonNull Parcel in) { + Bundle data = in.readBundle(); + mData = data; + AnnotationValidations.validate(NonNull.class, null, mData); + } + + public static final @NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public CreateCredentialResponse[] newArray(int size) { + return new CreateCredentialResponse[size]; + } + + @Override + public CreateCredentialResponse createFromParcel(@NonNull Parcel in) { + return new CreateCredentialResponse(in); + } + }; +} diff --git a/core/java/android/credentials/Credential.java b/core/java/android/credentials/Credential.java new file mode 100644 index 0000000000000..a247d16b3138a --- /dev/null +++ b/core/java/android/credentials/Credential.java @@ -0,0 +1,113 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials; + +import static java.util.Objects.requireNonNull; + +import android.annotation.NonNull; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.AnnotationValidations; +import com.android.internal.util.Preconditions; + +/** + * Represents a user credential that can be used to authenticate to your app. + */ +public final class Credential implements Parcelable { + + /** + * The credential type. + */ + @NonNull + private final String mType; + + /** + * The credential data. + */ + @NonNull + private final Bundle mData; + + /** + * Returns the credential type. + */ + @NonNull + public String getType() { + return mType; + } + + /** + * Returns the credential data. + */ + @NonNull + public Bundle getData() { + return mData; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString8(mType); + dest.writeBundle(mData); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "Credential {" + "type=" + mType + ", data=" + mData + "}"; + } + + /** + * Constructs a {@link Credential}. + * + * @param type the type of the credential returned. + * @param data the data associated with the credential returned. + * + * @throws IllegalArgumentException If type is empty. + */ + public Credential(@NonNull String type, @NonNull Bundle data) { + mType = Preconditions.checkStringNotEmpty(type, "type must not be empty"); + mData = requireNonNull(data, "data must not be null"); + } + + private Credential(@NonNull Parcel in) { + String type = in.readString8(); + Bundle data = in.readBundle(); + + mType = type; + AnnotationValidations.validate(NonNull.class, null, mType); + mData = data; + AnnotationValidations.validate(NonNull.class, null, mData); + } + + public static final @NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public Credential[] newArray(int size) { + return new Credential[size]; + } + + @Override + public Credential createFromParcel(@NonNull Parcel in) { + return new Credential(in); + } + }; +} diff --git a/core/java/android/credentials/CredentialManager.java b/core/java/android/credentials/CredentialManager.java new file mode 100644 index 0000000000000..b9cef0fccddf5 --- /dev/null +++ b/core/java/android/credentials/CredentialManager.java @@ -0,0 +1,186 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials; + +import static java.util.Objects.requireNonNull; + +import android.annotation.CallbackExecutor; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemService; +import android.content.Context; +import android.os.CancellationSignal; +import android.os.ICancellationSignal; +import android.os.OutcomeReceiver; +import android.os.RemoteException; +import android.util.Log; + +import java.util.concurrent.Executor; + +/** + * Manages user authentication flows. + * + *

Note that an application should call the Jetpack CredentialManager apis instead of directly + * calling these framework apis. + * + *

The CredentialManager apis launch framework UI flows for a user to + * register a new credential or to consent to a saved credential from supported credential + * providers, which can then be used to authenticate to the app. + */ +@SystemService(Context.CREDENTIAL_SERVICE) +public final class CredentialManager { + private static final String TAG = "CredentialManager"; + + private final Context mContext; + private final ICredentialManager mService; + + /** + * @hide instantiated by ContextImpl. + */ + public CredentialManager(Context context, ICredentialManager service) { + mContext = context; + mService = service; + } + + /** + * Launches the necessary flows to retrieve an app credential from the user. + * + *

The execution can potentially launch UI flows to collect user consent to using a + * credential, display a picker when multiple credentials exist, etc. + * + * @param request the request specifying type(s) of credentials to get from the user. + * @param cancellationSignal an optional signal that allows for cancelling this call. + * @param executor the callback will take place on this {@link Executor}. + * @param callback the callback invoked when the request succeeds or fails. + */ + public void executeGetCredential( + @NonNull GetCredentialRequest request, + @Nullable CancellationSignal cancellationSignal, + @CallbackExecutor @NonNull Executor executor, + @NonNull OutcomeReceiver< + GetCredentialResponse, CredentialManagerException> callback) { + requireNonNull(request, "request must not be null"); + requireNonNull(executor, "executor must not be null"); + requireNonNull(callback, "callback must not be null"); + + if (cancellationSignal != null && cancellationSignal.isCanceled()) { + Log.w(TAG, "executeGetCredential already canceled"); + return; + } + + ICancellationSignal cancelRemote = null; + try { + cancelRemote = mService.executeGetCredential(request, + new GetCredentialTransport(executor, callback)); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + + if (cancellationSignal != null && cancelRemote != null) { + cancellationSignal.setRemote(cancelRemote); + } + } + + /** + * Launches the necessary flows to register an app credential for the user. + * + *

The execution can potentially launch UI flows to collect user consent to creating + * or storing the new credential, etc. + * + * @param request the request specifying type(s) of credentials to get from the user. + * @param cancellationSignal an optional signal that allows for cancelling this call. + * @param executor the callback will take place on this {@link Executor}. + * @param callback the callback invoked when the request succeeds or fails. + */ + public void executeCreateCredential( + @NonNull CreateCredentialRequest request, + @Nullable CancellationSignal cancellationSignal, + @CallbackExecutor @NonNull Executor executor, + @NonNull OutcomeReceiver< + CreateCredentialResponse, CredentialManagerException> callback) { + requireNonNull(request, "request must not be null"); + requireNonNull(executor, "executor must not be null"); + requireNonNull(callback, "callback must not be null"); + + if (cancellationSignal != null && cancellationSignal.isCanceled()) { + Log.w(TAG, "executeCreateCredential already canceled"); + return; + } + + ICancellationSignal cancelRemote = null; + try { + cancelRemote = mService.executeCreateCredential(request, + new CreateCredentialTransport(executor, callback)); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + + if (cancellationSignal != null && cancelRemote != null) { + cancellationSignal.setRemote(cancelRemote); + } + } + + private static class GetCredentialTransport extends IGetCredentialCallback.Stub { + // TODO: listen for cancellation to release callback. + + private final Executor mExecutor; + private final OutcomeReceiver< + GetCredentialResponse, CredentialManagerException> mCallback; + + private GetCredentialTransport(Executor executor, + OutcomeReceiver callback) { + mExecutor = executor; + mCallback = callback; + } + + @Override + public void onResponse(GetCredentialResponse response) { + mExecutor.execute(() -> mCallback.onResult(response)); + } + + @Override + public void onError(int errorCode, String message) { + mExecutor.execute( + () -> mCallback.onError(new CredentialManagerException(errorCode, message))); + } + } + + private static class CreateCredentialTransport extends ICreateCredentialCallback.Stub { + // TODO: listen for cancellation to release callback. + + private final Executor mExecutor; + private final OutcomeReceiver< + CreateCredentialResponse, CredentialManagerException> mCallback; + + private CreateCredentialTransport(Executor executor, + OutcomeReceiver callback) { + mExecutor = executor; + mCallback = callback; + } + + @Override + public void onResponse(CreateCredentialResponse response) { + mExecutor.execute(() -> mCallback.onResult(response)); + } + + @Override + public void onError(int errorCode, String message) { + mExecutor.execute( + () -> mCallback.onError(new CredentialManagerException(errorCode, message))); + } + } +} diff --git a/core/java/android/credentials/CredentialManagerException.java b/core/java/android/credentials/CredentialManagerException.java new file mode 100644 index 0000000000000..116395c77253a --- /dev/null +++ b/core/java/android/credentials/CredentialManagerException.java @@ -0,0 +1,48 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials; + +import android.annotation.Nullable; + +/** Exception class for CredentialManager operations. */ +public class CredentialManagerException extends Exception { + /** Indicates that an unknown error was encountered. */ + public static final int ERROR_UNKNOWN = 0; + + public final int errorCode; + + public CredentialManagerException(int errorCode, @Nullable String message) { + super(message); + this.errorCode = errorCode; + } + + public CredentialManagerException( + int errorCode, @Nullable String message, @Nullable Throwable cause) { + super(message, cause); + this.errorCode = errorCode; + } + + public CredentialManagerException(int errorCode, @Nullable Throwable cause) { + super(cause); + this.errorCode = errorCode; + } + + public CredentialManagerException(int errorCode) { + super(); + this.errorCode = errorCode; + } +} diff --git a/core/java/android/credentials/GetCredentialOption.java b/core/java/android/credentials/GetCredentialOption.java new file mode 100644 index 0000000000000..af58b08290698 --- /dev/null +++ b/core/java/android/credentials/GetCredentialOption.java @@ -0,0 +1,113 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials; + +import static java.util.Objects.requireNonNull; + +import android.annotation.NonNull; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.AnnotationValidations; +import com.android.internal.util.Preconditions; + +/** + * A specific type of credential request. + */ +public final class GetCredentialOption implements Parcelable { + + /** + * The requested credential type. + */ + @NonNull + private final String mType; + + /** + * The request data. + */ + @NonNull + private final Bundle mData; + + /** + * Returns the requested credential type. + */ + @NonNull + public String getType() { + return mType; + } + + /** + * Returns the request data. + */ + @NonNull + public Bundle getData() { + return mData; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString8(mType); + dest.writeBundle(mData); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "GetCredentialOption {" + "type=" + mType + ", data=" + mData + "}"; + } + + /** + * Constructs a {@link GetCredentialOption}. + * + * @param type the requested credential type. + * @param data the request data. + * + * @throws IllegalArgumentException If type is empty. + */ + public GetCredentialOption(@NonNull String type, @NonNull Bundle data) { + mType = Preconditions.checkStringNotEmpty(type, "type must not be empty"); + mData = requireNonNull(data, "data must not be null"); + } + + private GetCredentialOption(@NonNull Parcel in) { + String type = in.readString8(); + Bundle data = in.readBundle(); + + mType = type; + AnnotationValidations.validate(NonNull.class, null, mType); + mData = data; + AnnotationValidations.validate(NonNull.class, null, mData); + } + + public static final @NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public GetCredentialOption[] newArray(int size) { + return new GetCredentialOption[size]; + } + + @Override + public GetCredentialOption createFromParcel(@NonNull Parcel in) { + return new GetCredentialOption(in); + } + }; +} diff --git a/core/java/android/credentials/GetCredentialRequest.aidl b/core/java/android/credentials/GetCredentialRequest.aidl new file mode 100644 index 0000000000000..2632a60717498 --- /dev/null +++ b/core/java/android/credentials/GetCredentialRequest.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials; + +parcelable GetCredentialRequest; \ No newline at end of file diff --git a/core/java/android/credentials/GetCredentialRequest.java b/core/java/android/credentials/GetCredentialRequest.java new file mode 100644 index 0000000000000..96a0ce18d919d --- /dev/null +++ b/core/java/android/credentials/GetCredentialRequest.java @@ -0,0 +1,138 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials; + +import static java.util.Objects.requireNonNull; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.AnnotationValidations; +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; +import java.util.List; + +/** + * A request to retrieve the user credential, potentially launching UI flows to let the user pick + * from different credential sources. + */ +public final class GetCredentialRequest implements Parcelable { + + /** + * The list of credential requests. + */ + @NonNull + private final List mGetCredentialOptions; + + /** + * Returns the list of credential options to be requested. + */ + @NonNull + public List getGetCredentialOptions() { + return mGetCredentialOptions; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeTypedList(mGetCredentialOptions, flags); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "GetCredentialRequest {getCredentialOption=" + mGetCredentialOptions + "}"; + } + + private GetCredentialRequest(@NonNull List getCredentialOptions) { + Preconditions.checkCollectionNotEmpty( + getCredentialOptions, + /*valueName=*/ "getCredentialOptions"); + Preconditions.checkCollectionElementsNotNull( + getCredentialOptions, + /*valueName=*/ "getCredentialOptions"); + mGetCredentialOptions = getCredentialOptions; + } + + private GetCredentialRequest(@NonNull Parcel in) { + List getCredentialOptions = new ArrayList(); + in.readTypedList(getCredentialOptions, GetCredentialOption.CREATOR); + mGetCredentialOptions = getCredentialOptions; + AnnotationValidations.validate(NonNull.class, null, mGetCredentialOptions); + } + + public static final @NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public GetCredentialRequest[] newArray(int size) { + return new GetCredentialRequest[size]; + } + + @Override + public GetCredentialRequest createFromParcel(@NonNull Parcel in) { + return new GetCredentialRequest(in); + } + }; + + /** A builder for {@link GetCredentialRequest}. */ + public static final class Builder { + + private @NonNull List mGetCredentialOptions = new ArrayList<>(); + + /** + * Adds a specific type of {@link GetCredentialOption}. + */ + public @NonNull Builder addGetCredentialOption( + @NonNull GetCredentialOption getCredentialOption) { + mGetCredentialOptions.add(requireNonNull( + getCredentialOption, "getCredentialOption must not be null")); + return this; + } + + /** + * Sets the list of {@link GetCredentialOption}. + */ + public @NonNull Builder setGetCredentialOptions( + @NonNull List getCredentialOptions) { + Preconditions.checkCollectionElementsNotNull( + getCredentialOptions, + /*valueName=*/ "getCredentialOptions"); + mGetCredentialOptions = new ArrayList<>(getCredentialOptions); + return this; + } + + /** + * Builds a {@link GetCredentialRequest}. + * + * @throws IllegalArgumentException If getCredentialOptions is empty. + */ + public @NonNull GetCredentialRequest build() { + Preconditions.checkCollectionNotEmpty( + mGetCredentialOptions, + /*valueName=*/ "getCredentialOptions"); + Preconditions.checkCollectionElementsNotNull( + mGetCredentialOptions, + /*valueName=*/ "getCredentialOptions"); + return new GetCredentialRequest(mGetCredentialOptions); + } + } +} diff --git a/core/java/android/credentials/GetCredentialResponse.aidl b/core/java/android/credentials/GetCredentialResponse.aidl new file mode 100644 index 0000000000000..71ebb12e44f94 --- /dev/null +++ b/core/java/android/credentials/GetCredentialResponse.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials; + +parcelable GetCredentialResponse; \ No newline at end of file diff --git a/core/java/android/credentials/GetCredentialResponse.java b/core/java/android/credentials/GetCredentialResponse.java new file mode 100644 index 0000000000000..576da8b6184ae --- /dev/null +++ b/core/java/android/credentials/GetCredentialResponse.java @@ -0,0 +1,97 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials; + +import static java.util.Objects.requireNonNull; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.AnnotationValidations; + +/** + * A response object that encapsulates the credential successfully retrieved from the user. + */ +public final class GetCredentialResponse implements Parcelable { + + /** + * The credential that can be used to authenticate the user. + */ + @Nullable + private final Credential mCredential; + + /** + * Returns the credential that can be used to authenticate the user, or {@code null} if no + * credential is available. + */ + @Nullable + public Credential getCredential() { + return mCredential; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeTypedObject(mCredential, flags); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "GetCredentialResponse {" + "credential=" + mCredential + "}"; + } + + /** + * Constructs a {@link GetCredentialResponse}. + * + * @param credential the credential successfully retrieved from the user. + */ + public GetCredentialResponse(@NonNull Credential credential) { + mCredential = requireNonNull(credential, "credential must not be null"); + } + + /** + * Constructs a {@link GetCredentialResponse}. + */ + public GetCredentialResponse() { + mCredential = null; + } + + private GetCredentialResponse(@NonNull Parcel in) { + Credential credential = in.readTypedObject(Credential.CREATOR); + mCredential = credential; + AnnotationValidations.validate(NonNull.class, null, mCredential); + } + + public static final @NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public GetCredentialResponse[] newArray(int size) { + return new GetCredentialResponse[size]; + } + + @Override + public GetCredentialResponse createFromParcel(@NonNull Parcel in) { + return new GetCredentialResponse(in); + } + }; +} diff --git a/core/java/android/credentials/ICreateCredentialCallback.aidl b/core/java/android/credentials/ICreateCredentialCallback.aidl new file mode 100644 index 0000000000000..75620faf04fdf --- /dev/null +++ b/core/java/android/credentials/ICreateCredentialCallback.aidl @@ -0,0 +1,29 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials; + +import android.credentials.CreateCredentialResponse; + +/** + * Listener for an executeCreateCredential request. + * + * @hide + */ +interface ICreateCredentialCallback { + oneway void onResponse(in CreateCredentialResponse response); + oneway void onError(int errorCode, String message); +} \ No newline at end of file diff --git a/core/java/android/credentials/ICredentialManager.aidl b/core/java/android/credentials/ICredentialManager.aidl index a281cd51d59f6..dcf7106d15eb8 100644 --- a/core/java/android/credentials/ICredentialManager.aidl +++ b/core/java/android/credentials/ICredentialManager.aidl @@ -1,10 +1,35 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package android.credentials; +import android.credentials.CreateCredentialRequest; +import android.credentials.GetCredentialRequest; +import android.credentials.ICreateCredentialCallback; +import android.credentials.IGetCredentialCallback; +import android.os.ICancellationSignal; + /** - * Mediator between apps and credential manager service implementations. + * System private interface for talking to the credential manager service. * - * {@hide} + * @hide */ -oneway interface ICredentialManager { - void getCredential(); -} \ No newline at end of file +interface ICredentialManager { + + @nullable ICancellationSignal executeGetCredential(in GetCredentialRequest request, in IGetCredentialCallback callback); + + @nullable ICancellationSignal executeCreateCredential(in CreateCredentialRequest request, in ICreateCredentialCallback callback); +} diff --git a/core/java/android/credentials/IGetCredentialCallback.aidl b/core/java/android/credentials/IGetCredentialCallback.aidl new file mode 100644 index 0000000000000..92e585142a468 --- /dev/null +++ b/core/java/android/credentials/IGetCredentialCallback.aidl @@ -0,0 +1,29 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials; + +import android.credentials.GetCredentialResponse; + +/** + * Listener for an executeGetCredential request. + * + * @hide + */ +interface IGetCredentialCallback { + oneway void onResponse(in GetCredentialResponse response); + oneway void onError(int errorCode, String message); +} \ No newline at end of file diff --git a/services/Android.bp b/services/Android.bp index 672b3450dc1b3..637c4eea7bb31 100644 --- a/services/Android.bp +++ b/services/Android.bp @@ -93,6 +93,7 @@ filegroup { ":services.contentcapture-sources", ":services.contentsuggestions-sources", ":services.coverage-sources", + ":services.credentials-sources", ":services.devicepolicy-sources", ":services.midi-sources", ":services.musicsearch-sources", @@ -146,6 +147,7 @@ java_library { "services.contentcapture", "services.contentsuggestions", "services.coverage", + "services.credentials", "services.devicepolicy", "services.midi", "services.musicsearch", diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java index 76ed2b345c338..91f5c69994270 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java @@ -16,10 +16,18 @@ package com.android.server.credentials; +import static android.content.Context.CREDENTIAL_SERVICE; + import android.annotation.NonNull; import android.annotation.UserIdInt; import android.content.Context; +import android.credentials.CreateCredentialRequest; +import android.credentials.GetCredentialRequest; +import android.credentials.ICreateCredentialCallback; import android.credentials.ICredentialManager; +import android.credentials.IGetCredentialCallback; +import android.os.CancellationSignal; +import android.os.ICancellationSignal; import android.os.UserHandle; import android.provider.Settings; import android.util.Log; @@ -59,11 +67,17 @@ public final class CredentialManagerService extends @Override public void onStart() { Log.i(TAG, "onStart"); + publishBinderService(CREDENTIAL_SERVICE, new CredentialManagerServiceStub()); } final class CredentialManagerServiceStub extends ICredentialManager.Stub { @Override - public void getCredential() { + public ICancellationSignal executeGetCredential( + GetCredentialRequest request, + IGetCredentialCallback callback) { + // TODO: implement. + Log.i(TAG, "executeGetCredential"); + final int userId = UserHandle.getCallingUserId(); synchronized (mLock) { final CredentialManagerServiceImpl service = peekServiceForUserLocked(userId); @@ -72,6 +86,19 @@ public final class CredentialManagerService extends service.getCredential(); } } + + ICancellationSignal cancelTransport = CancellationSignal.createTransport(); + return cancelTransport; + } + + @Override + public ICancellationSignal executeCreateCredential( + CreateCredentialRequest request, + ICreateCredentialCallback callback) { + // TODO: implement. + Log.i(TAG, "executeCreateCredential"); + ICancellationSignal cancelTransport = CancellationSignal.createTransport(); + return cancelTransport; } } }