diff --git a/core/java/android/service/credentials/CreateCredentialCallback.java b/core/java/android/service/credentials/CreateCredentialCallback.java new file mode 100644 index 0000000000000..6108eea5bea19 --- /dev/null +++ b/core/java/android/service/credentials/CreateCredentialCallback.java @@ -0,0 +1,64 @@ +/* + * 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 android.service.credentials; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.RemoteException; +import android.util.Log; + +/** + * Callback to be invoked as a response to {@link CreateCredentialRequest}. + * + * @hide + */ +public final class CreateCredentialCallback { + private static final String TAG = "CreateCredentialCallback"; + + private final ICreateCredentialCallback mCallback; + + /** @hide */ + public CreateCredentialCallback(@NonNull ICreateCredentialCallback callback) { + mCallback = callback; + } + + /** + * Invoked on a successful response for {@link CreateCredentialRequest} + * @param response The response from the credential provider. + */ + public void onSuccess(@NonNull CreateCredentialResponse response) { + try { + mCallback.onSuccess(response); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + } + + /** + * Invoked on a failure response for {@link CreateCredentialRequest} + * @param errorCode The code defining the type of error. + * @param message The message corresponding to the failure. + */ + public void onFailure(int errorCode, @Nullable CharSequence message) { + Log.w(TAG, "onFailure: " + message); + try { + mCallback.onFailure(errorCode, message); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + } +} diff --git a/core/java/android/service/credentials/CreateCredentialRequest.aidl b/core/java/android/service/credentials/CreateCredentialRequest.aidl new file mode 100644 index 0000000000000..eb7fba9405f7b --- /dev/null +++ b/core/java/android/service/credentials/CreateCredentialRequest.aidl @@ -0,0 +1,3 @@ +package android.service.credentials; + +parcelable CreateCredentialRequest; \ No newline at end of file diff --git a/core/java/android/service/credentials/CreateCredentialRequest.java b/core/java/android/service/credentials/CreateCredentialRequest.java new file mode 100644 index 0000000000000..ac11e04bcb77d --- /dev/null +++ b/core/java/android/service/credentials/CreateCredentialRequest.java @@ -0,0 +1,103 @@ +/* + * 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 android.service.credentials; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import com.android.internal.util.Preconditions; + +import java.util.Objects; + +/** + * Request for creating a credential. + * + * @hide + */ +public final class CreateCredentialRequest implements Parcelable { + private final @NonNull String mCallingPackage; + private final @NonNull String mType; + private final @NonNull Bundle mData; + + /** + * Constructs a new instance. + * + * @throws IllegalArgumentException If {@code callingPackage}, or {@code type} string is + * null or empty. + * @throws NullPointerException If {@code data} is null. + */ + public CreateCredentialRequest(@NonNull String callingPackage, + @NonNull String type, @NonNull Bundle data) { + mCallingPackage = Preconditions.checkStringNotEmpty(callingPackage, + "callingPackage must not be null or empty"); + mType = Preconditions.checkStringNotEmpty(type, + "type must not be null or empty"); + mData = Objects.requireNonNull(data, "data must not be null"); + } + + private CreateCredentialRequest(@NonNull Parcel in) { + mCallingPackage = in.readString8(); + mType = in.readString8(); + mData = in.readBundle(); + } + + public static final @NonNull Creator CREATOR = + new Creator() { + @Override + public CreateCredentialRequest createFromParcel(@NonNull Parcel in) { + return new CreateCredentialRequest(in); + } + + @Override + public CreateCredentialRequest[] newArray(int size) { + return new CreateCredentialRequest[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString8(mCallingPackage); + dest.writeString8(mType); + dest.writeBundle(mData); + } + + /** Returns the calling package of the calling app. */ + @NonNull + public String getCallingPackage() { + return mCallingPackage; + } + + /** Returns the type of the credential to be created. */ + @NonNull + public String getType() { + return mType; + } + + /** Returns the data to be used while creating the credential. */ + @NonNull + public Bundle getData() { + return mData; + } +} diff --git a/core/java/android/service/credentials/CreateCredentialResponse.aidl b/core/java/android/service/credentials/CreateCredentialResponse.aidl new file mode 100644 index 0000000000000..73c9147b0ae42 --- /dev/null +++ b/core/java/android/service/credentials/CreateCredentialResponse.aidl @@ -0,0 +1,19 @@ +/* + * 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 android.service.credentials; + +parcelable CreateCredentialResponse; diff --git a/core/java/android/service/credentials/CreateCredentialResponse.java b/core/java/android/service/credentials/CreateCredentialResponse.java new file mode 100644 index 0000000000000..f2ad7272f2071 --- /dev/null +++ b/core/java/android/service/credentials/CreateCredentialResponse.java @@ -0,0 +1,140 @@ +/* + * 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 android.service.credentials; + +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Response to a {@link CreateCredentialRequest}. + * + * @hide + */ +public final class CreateCredentialResponse implements Parcelable { + private final @Nullable CharSequence mHeader; + private final @NonNull List mSaveEntries; + + private CreateCredentialResponse(@NonNull Parcel in) { + mHeader = in.readCharSequence(); + mSaveEntries = in.createTypedArrayList(SaveEntry.CREATOR); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeCharSequence(mHeader); + dest.writeTypedList(mSaveEntries); + } + + @Override + public int describeContents() { + return 0; + } + + public static final @NonNull Creator CREATOR = + new Creator() { + @Override + public CreateCredentialResponse createFromParcel(@NonNull Parcel in) { + return new CreateCredentialResponse(in); + } + + @Override + public CreateCredentialResponse[] newArray(int size) { + return new CreateCredentialResponse[size]; + } + }; + + /* package-private */ CreateCredentialResponse( + @Nullable CharSequence header, + @NonNull List saveEntries) { + this.mHeader = header; + this.mSaveEntries = saveEntries; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mSaveEntries); + } + + /** Returns the header to be displayed on the UI. */ + public @Nullable CharSequence getHeader() { + return mHeader; + } + + /** Returns the list of save entries to be displayed on the UI. */ + public @NonNull List getSaveEntries() { + return mSaveEntries; + } + + /** + * A builder for {@link CreateCredentialResponse} + */ + @SuppressWarnings("WeakerAccess") + public static final class Builder { + + private @Nullable CharSequence mHeader; + private @NonNull List mSaveEntries = new ArrayList<>(); + + /** Sets the header to be displayed on the UI. */ + public @NonNull Builder setHeader(@Nullable CharSequence header) { + mHeader = header; + return this; + } + + /** + * Sets the list of save entries to be shown on the UI. + * + * @throws IllegalArgumentException If {@code saveEntries} is empty. + * @throws NullPointerException If {@code saveEntries} is null, or any of its elements + * are null. + */ + public @NonNull Builder setSaveEntries(@NonNull List saveEntries) { + Preconditions.checkCollectionNotEmpty(saveEntries, "saveEntries"); + mSaveEntries = Preconditions.checkCollectionElementsNotNull( + saveEntries, "saveEntries"); + return this; + } + + /** + * Adds an entry to the list of save entries to be shown on the UI. + * + * @throws NullPointerException If {@code saveEntry} is null. + */ + public @NonNull Builder addSaveEntry(@NonNull SaveEntry saveEntry) { + mSaveEntries.add(Objects.requireNonNull(saveEntry)); + return this; + } + + /** + * Builds the instance. + * + * @throws IllegalArgumentException If {@code saveEntries} is empty. + */ + public @NonNull CreateCredentialResponse build() { + Preconditions.checkCollectionNotEmpty(mSaveEntries, "saveEntries must " + + "not be empty"); + return new CreateCredentialResponse( + mHeader, + mSaveEntries); + } + } +} diff --git a/core/java/android/service/credentials/CredentialProviderService.java b/core/java/android/service/credentials/CredentialProviderService.java index e2a495d209501..1fe89dffaa0f8 100644 --- a/core/java/android/service/credentials/CredentialProviderService.java +++ b/core/java/android/service/credentials/CredentialProviderService.java @@ -66,6 +66,7 @@ public abstract class CredentialProviderService extends Service { public void onGetCredentials(GetCredentialsRequest request, ICancellationSignal transport, IGetCredentialsCallback callback) throws RemoteException { Objects.requireNonNull(request); + Objects.requireNonNull(transport); Objects.requireNonNull(callback); mHandler.sendMessage(obtainMessage( @@ -75,6 +76,22 @@ public abstract class CredentialProviderService extends Service { new GetCredentialsCallback(callback) )); } + + @Override + public void onCreateCredential(CreateCredentialRequest request, + ICancellationSignal transport, ICreateCredentialCallback callback) + throws RemoteException { + Objects.requireNonNull(request); + Objects.requireNonNull(transport); + Objects.requireNonNull(callback); + + mHandler.sendMessage(obtainMessage( + CredentialProviderService::onCreateCredential, + CredentialProviderService.this, request, + CancellationSignal.fromTransport(transport), + new CreateCredentialCallback(callback) + )); + } }; /** @@ -88,4 +105,15 @@ public abstract class CredentialProviderService extends Service { public abstract void onGetCredentials(@NonNull GetCredentialsRequest request, @NonNull CancellationSignal cancellationSignal, @NonNull GetCredentialsCallback callback); + + /** + * Called by the android system to create a credential. + * @param request The credential creation request for the provider to handle. + * @param cancellationSignal Signal for providers to listen to any cancellation requests from + * the android system. + * @param callback Object used to relay the response of the credential creation request. + */ + public abstract void onCreateCredential(@NonNull CreateCredentialRequest request, + @NonNull CancellationSignal cancellationSignal, + @NonNull CreateCredentialCallback callback); } diff --git a/core/java/android/service/credentials/GetCredentialsCallback.java b/core/java/android/service/credentials/GetCredentialsCallback.java index 0e81b8f14fe48..42a73946b5cca 100644 --- a/core/java/android/service/credentials/GetCredentialsCallback.java +++ b/core/java/android/service/credentials/GetCredentialsCallback.java @@ -41,7 +41,7 @@ public final class GetCredentialsCallback { * Invoked on a successful response for {@link GetCredentialsRequest} * @param response The response from the credential provider. */ - public void onSuccess(@Nullable GetCredentialsResponse response) { + public void onSuccess(@NonNull GetCredentialsResponse response) { try { mCallback.onSuccess(response); } catch (RemoteException e) { diff --git a/core/java/android/service/credentials/ICreateCredentialCallback.aidl b/core/java/android/service/credentials/ICreateCredentialCallback.aidl new file mode 100644 index 0000000000000..4cc76a4a341b1 --- /dev/null +++ b/core/java/android/service/credentials/ICreateCredentialCallback.aidl @@ -0,0 +1,13 @@ +package android.service.credentials; + +import android.service.credentials.CreateCredentialResponse; + +/** + * Interface from the system to a credential provider service. + * + * @hide + */ +oneway interface ICreateCredentialCallback { + void onSuccess(in CreateCredentialResponse request); + void onFailure(int errorCode, in CharSequence message); +} \ No newline at end of file diff --git a/core/java/android/service/credentials/ICredentialProviderService.aidl b/core/java/android/service/credentials/ICredentialProviderService.aidl index 0b2a9d64174e7..c68430ce752eb 100644 --- a/core/java/android/service/credentials/ICredentialProviderService.aidl +++ b/core/java/android/service/credentials/ICredentialProviderService.aidl @@ -18,7 +18,9 @@ package android.service.credentials; import android.os.ICancellationSignal; import android.service.credentials.GetCredentialsRequest; +import android.service.credentials.CreateCredentialRequest; import android.service.credentials.IGetCredentialsCallback; +import android.service.credentials.ICreateCredentialCallback; /** * Interface from the system to a credential provider service. @@ -27,4 +29,5 @@ import android.service.credentials.IGetCredentialsCallback; */ oneway interface ICredentialProviderService { void onGetCredentials(in GetCredentialsRequest request, in ICancellationSignal transport, in IGetCredentialsCallback callback); -} \ No newline at end of file + void onCreateCredential(in CreateCredentialRequest request, in ICancellationSignal transport, in ICreateCredentialCallback callback); +} diff --git a/core/java/android/service/credentials/SaveEntry.java b/core/java/android/service/credentials/SaveEntry.java new file mode 100644 index 0000000000000..28fec30caa89f --- /dev/null +++ b/core/java/android/service/credentials/SaveEntry.java @@ -0,0 +1,162 @@ +/* + * 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 android.service.credentials; + +import android.annotation.Nullable; +import android.app.PendingIntent; +import android.app.slice.Slice; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import com.android.internal.util.Preconditions; + +import java.util.Objects; + +/** + * An entry to be shown on the UI. This entry represents where the credential to be created will + * be stored. Examples include user's account, family group etc. + * + * @hide + */ +public final class SaveEntry implements Parcelable { + private final @NonNull Slice mInfo; + private final @Nullable PendingIntent mPendingIntent; + private final @Nullable Credential mCredential; + + private SaveEntry(@NonNull Parcel in) { + mInfo = in.readParcelable(Slice.class.getClassLoader(), Slice.class); + mPendingIntent = in.readParcelable(PendingIntent.class.getClassLoader(), + PendingIntent.class); + mCredential = in.readParcelable(Credential.class.getClassLoader(), Credential.class); + } + + public static final @NonNull Creator CREATOR = new Creator() { + @Override + public SaveEntry createFromParcel(@NonNull Parcel in) { + return new SaveEntry(in); + } + + @Override + public SaveEntry[] newArray(int size) { + return new SaveEntry[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + mInfo.writeToParcel(dest, flags); + mPendingIntent.writeToParcel(dest, flags); + mCredential.writeToParcel(dest, flags); + } + + /* package-private */ SaveEntry( + @NonNull Slice info, + @Nullable PendingIntent pendingIntent, + @Nullable Credential credential) { + this.mInfo = info; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mInfo); + this.mPendingIntent = pendingIntent; + this.mCredential = credential; + } + + /** Returns the info to be displayed with this save entry on the UI. */ + public @NonNull Slice getInfo() { + return mInfo; + } + + /** Returns the pendingIntent to be invoked when this save entry on the UI is selectcd. */ + public @Nullable PendingIntent getPendingIntent() { + return mPendingIntent; + } + + /** Returns the credential produced by the {@link CreateCredentialRequest}. */ + public @Nullable Credential getCredential() { + return mCredential; + } + + /** + * A builder for {@link SaveEntry}. + */ + public static final class Builder { + + private @NonNull Slice mInfo; + private @Nullable PendingIntent mPendingIntent; + private @Nullable Credential mCredential; + + /** + * Builds the instance. + * @param info The info to be displayed with this save entry. + * + * @throws NullPointerException If {@code info} is null. + */ + public Builder(@NonNull Slice info) { + mInfo = Objects.requireNonNull(info, "info must not be null"); + } + + /** + * Sets the pendingIntent to be invoked when this entry is selected by the user. + * + * @throws IllegalStateException If {@code credential} is already set. Must only set either + * {@code credential}, or the {@code pendingIntent}. + */ + public @NonNull Builder setPendingIntent(@Nullable PendingIntent pendingIntent) { + Preconditions.checkState(pendingIntent != null + && mCredential != null, "credential is already set. Must only set " + + "either the pendingIntent or the credential"); + mPendingIntent = pendingIntent; + return this; + } + + /** + * Sets the credential to be returned when this entry is selected by the user. + * + * @throws IllegalStateException If {@code pendingIntent} is already set. Must only + * set either the {@code pendingIntent}, or {@code credential}. + */ + public @NonNull Builder setCredential(@Nullable Credential credential) { + Preconditions.checkState(credential != null && mPendingIntent != null, + "pendingIntent is already set. Must only set either the credential " + + "or the pendingIntent"); + mCredential = credential; + return this; + } + + /** + * Builds the instance. + * + * @throws IllegalStateException if both {@code pendingIntent} and {@code credential} + * are null. + */ + public @NonNull SaveEntry build() { + Preconditions.checkState(mPendingIntent == null && mCredential == null, + "pendingIntent and credential both must not be null. Must set " + + "either the pendingIntnet or the credential"); + return new SaveEntry( + mInfo, + mPendingIntent, + mCredential); + } + } +}