diff options
| author | 2017-06-29 02:43:36 +0000 | |
|---|---|---|
| committer | 2017-06-29 02:43:36 +0000 | |
| commit | ee39d54a8d6ea1d6515191ed5fef549fecaaacee (patch) | |
| tree | 0c0224058d8f8dd48eb0dc63941ea9a0bd86b8ff | |
| parent | cabb638640b55b9f66a43279218a43d55199b0e9 (diff) | |
| parent | 979013d027d828f404e71f48b88403e562ccbc7b (diff) | |
Merge "Initial implementation of the new Save APIs."
23 files changed, 1659 insertions, 23 deletions
diff --git a/api/current.txt b/api/current.txt index 0eab1fd802ca..17dbcbf392b9 100644 --- a/api/current.txt +++ b/api/current.txt @@ -36908,6 +36908,30 @@ package android.service.autofill { field public static final java.lang.String SERVICE_META_DATA = "android.autofill"; } + public final class CharSequenceTransformation implements android.os.Parcelable { + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.CharSequenceTransformation> CREATOR; + } + + public static class CharSequenceTransformation.Builder { + ctor public CharSequenceTransformation.Builder(); + method public android.service.autofill.CharSequenceTransformation.Builder addField(android.view.autofill.AutofillId, java.lang.String, java.lang.String); + method public android.service.autofill.CharSequenceTransformation build(); + } + + public final class CustomDescription implements android.os.Parcelable { + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.CustomDescription> CREATOR; + } + + public static class CustomDescription.Builder { + ctor public CustomDescription.Builder(android.widget.RemoteViews); + method public android.service.autofill.CustomDescription.Builder addChild(int, android.service.autofill.Transformation); + method public android.service.autofill.CustomDescription build(); + } + public final class Dataset implements android.os.Parcelable { method public int describeContents(); method public void writeToParcel(android.os.Parcel, int); @@ -36981,6 +37005,25 @@ package android.service.autofill { method public android.service.autofill.FillResponse.Builder setSaveInfo(android.service.autofill.SaveInfo); } + public final class ImageTransformation implements android.os.Parcelable { + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.ImageTransformation> CREATOR; + } + + public static class ImageTransformation.Builder { + ctor public ImageTransformation.Builder(android.view.autofill.AutofillId); + method public android.service.autofill.ImageTransformation.Builder addOption(java.lang.String, int); + method public android.service.autofill.ImageTransformation build(); + } + + public final class LuhnChecksumValidator implements android.os.Parcelable { + ctor public LuhnChecksumValidator(android.view.autofill.AutofillId...); + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.LuhnChecksumValidator> CREATOR; + } + public final class SaveCallback { method public void onFailure(java.lang.CharSequence); method public void onSuccess(); @@ -37004,10 +37047,12 @@ package android.service.autofill { public static final class SaveInfo.Builder { ctor public SaveInfo.Builder(int, android.view.autofill.AutofillId[]); method public android.service.autofill.SaveInfo build(); + method public android.service.autofill.SaveInfo.Builder setCustomDescription(android.service.autofill.CustomDescription); method public android.service.autofill.SaveInfo.Builder setDescription(java.lang.CharSequence); method public android.service.autofill.SaveInfo.Builder setFlags(int); method public android.service.autofill.SaveInfo.Builder setNegativeAction(int, android.content.IntentSender); method public android.service.autofill.SaveInfo.Builder setOptionalIds(android.view.autofill.AutofillId[]); + method public android.service.autofill.SaveInfo.Builder setValidator(android.service.autofill.Validator); } public final class SaveRequest implements android.os.Parcelable { @@ -37018,6 +37063,24 @@ package android.service.autofill { field public static final android.os.Parcelable.Creator<android.service.autofill.SaveRequest> CREATOR; } + public final class SimpleRegexValidator implements android.os.Parcelable { + ctor public SimpleRegexValidator(android.view.autofill.AutofillId, java.lang.String); + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.SimpleRegexValidator> CREATOR; + } + + public abstract interface Transformation { + } + + public abstract interface Validator { + } + + public final class Validators { + method public static android.service.autofill.Validator and(android.service.autofill.Validator...); + method public static android.service.autofill.Validator or(android.service.autofill.Validator...); + } + } package android.service.carrier { diff --git a/api/system-current.txt b/api/system-current.txt index 833e1f6fc7c1..53e1b81d95d3 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -39985,6 +39985,30 @@ package android.service.autofill { field public static final java.lang.String SERVICE_META_DATA = "android.autofill"; } + public final class CharSequenceTransformation implements android.os.Parcelable { + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.CharSequenceTransformation> CREATOR; + } + + public static class CharSequenceTransformation.Builder { + ctor public CharSequenceTransformation.Builder(); + method public android.service.autofill.CharSequenceTransformation.Builder addField(android.view.autofill.AutofillId, java.lang.String, java.lang.String); + method public android.service.autofill.CharSequenceTransformation build(); + } + + public final class CustomDescription implements android.os.Parcelable { + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.CustomDescription> CREATOR; + } + + public static class CustomDescription.Builder { + ctor public CustomDescription.Builder(android.widget.RemoteViews); + method public android.service.autofill.CustomDescription.Builder addChild(int, android.service.autofill.Transformation); + method public android.service.autofill.CustomDescription build(); + } + public final class Dataset implements android.os.Parcelable { method public int describeContents(); method public void writeToParcel(android.os.Parcel, int); @@ -40058,6 +40082,25 @@ package android.service.autofill { method public android.service.autofill.FillResponse.Builder setSaveInfo(android.service.autofill.SaveInfo); } + public final class ImageTransformation implements android.os.Parcelable { + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.ImageTransformation> CREATOR; + } + + public static class ImageTransformation.Builder { + ctor public ImageTransformation.Builder(android.view.autofill.AutofillId); + method public android.service.autofill.ImageTransformation.Builder addOption(java.lang.String, int); + method public android.service.autofill.ImageTransformation build(); + } + + public final class LuhnChecksumValidator implements android.os.Parcelable { + ctor public LuhnChecksumValidator(android.view.autofill.AutofillId...); + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.LuhnChecksumValidator> CREATOR; + } + public final class SaveCallback { method public void onFailure(java.lang.CharSequence); method public void onSuccess(); @@ -40081,10 +40124,12 @@ package android.service.autofill { public static final class SaveInfo.Builder { ctor public SaveInfo.Builder(int, android.view.autofill.AutofillId[]); method public android.service.autofill.SaveInfo build(); + method public android.service.autofill.SaveInfo.Builder setCustomDescription(android.service.autofill.CustomDescription); method public android.service.autofill.SaveInfo.Builder setDescription(java.lang.CharSequence); method public android.service.autofill.SaveInfo.Builder setFlags(int); method public android.service.autofill.SaveInfo.Builder setNegativeAction(int, android.content.IntentSender); method public android.service.autofill.SaveInfo.Builder setOptionalIds(android.view.autofill.AutofillId[]); + method public android.service.autofill.SaveInfo.Builder setValidator(android.service.autofill.Validator); } public final class SaveRequest implements android.os.Parcelable { @@ -40095,6 +40140,24 @@ package android.service.autofill { field public static final android.os.Parcelable.Creator<android.service.autofill.SaveRequest> CREATOR; } + public final class SimpleRegexValidator implements android.os.Parcelable { + ctor public SimpleRegexValidator(android.view.autofill.AutofillId, java.lang.String); + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.SimpleRegexValidator> CREATOR; + } + + public abstract interface Transformation { + } + + public abstract interface Validator { + } + + public final class Validators { + method public static android.service.autofill.Validator and(android.service.autofill.Validator...); + method public static android.service.autofill.Validator or(android.service.autofill.Validator...); + } + } package android.service.carrier { diff --git a/api/test-current.txt b/api/test-current.txt index e6c22ae979f0..abfcaba3727e 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -37080,6 +37080,30 @@ package android.service.autofill { field public static final java.lang.String SERVICE_META_DATA = "android.autofill"; } + public final class CharSequenceTransformation implements android.os.Parcelable { + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.CharSequenceTransformation> CREATOR; + } + + public static class CharSequenceTransformation.Builder { + ctor public CharSequenceTransformation.Builder(); + method public android.service.autofill.CharSequenceTransformation.Builder addField(android.view.autofill.AutofillId, java.lang.String, java.lang.String); + method public android.service.autofill.CharSequenceTransformation build(); + } + + public final class CustomDescription implements android.os.Parcelable { + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.CustomDescription> CREATOR; + } + + public static class CustomDescription.Builder { + ctor public CustomDescription.Builder(android.widget.RemoteViews); + method public android.service.autofill.CustomDescription.Builder addChild(int, android.service.autofill.Transformation); + method public android.service.autofill.CustomDescription build(); + } + public final class Dataset implements android.os.Parcelable { method public int describeContents(); method public void writeToParcel(android.os.Parcel, int); @@ -37153,6 +37177,25 @@ package android.service.autofill { method public android.service.autofill.FillResponse.Builder setSaveInfo(android.service.autofill.SaveInfo); } + public final class ImageTransformation implements android.os.Parcelable { + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.ImageTransformation> CREATOR; + } + + public static class ImageTransformation.Builder { + ctor public ImageTransformation.Builder(android.view.autofill.AutofillId); + method public android.service.autofill.ImageTransformation.Builder addOption(java.lang.String, int); + method public android.service.autofill.ImageTransformation build(); + } + + public final class LuhnChecksumValidator implements android.os.Parcelable { + ctor public LuhnChecksumValidator(android.view.autofill.AutofillId...); + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.LuhnChecksumValidator> CREATOR; + } + public final class SaveCallback { method public void onFailure(java.lang.CharSequence); method public void onSuccess(); @@ -37176,10 +37219,12 @@ package android.service.autofill { public static final class SaveInfo.Builder { ctor public SaveInfo.Builder(int, android.view.autofill.AutofillId[]); method public android.service.autofill.SaveInfo build(); + method public android.service.autofill.SaveInfo.Builder setCustomDescription(android.service.autofill.CustomDescription); method public android.service.autofill.SaveInfo.Builder setDescription(java.lang.CharSequence); method public android.service.autofill.SaveInfo.Builder setFlags(int); method public android.service.autofill.SaveInfo.Builder setNegativeAction(int, android.content.IntentSender); method public android.service.autofill.SaveInfo.Builder setOptionalIds(android.view.autofill.AutofillId[]); + method public android.service.autofill.SaveInfo.Builder setValidator(android.service.autofill.Validator); } public final class SaveRequest implements android.os.Parcelable { @@ -37190,6 +37235,24 @@ package android.service.autofill { field public static final android.os.Parcelable.Creator<android.service.autofill.SaveRequest> CREATOR; } + public final class SimpleRegexValidator implements android.os.Parcelable { + ctor public SimpleRegexValidator(android.view.autofill.AutofillId, java.lang.String); + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.SimpleRegexValidator> CREATOR; + } + + public abstract interface Transformation { + } + + public abstract interface Validator { + } + + public final class Validators { + method public static android.service.autofill.Validator and(android.service.autofill.Validator...); + method public static android.service.autofill.Validator or(android.service.autofill.Validator...); + } + } package android.service.carrier { diff --git a/core/java/android/service/autofill/CharSequenceTransformation.java b/core/java/android/service/autofill/CharSequenceTransformation.java new file mode 100644 index 000000000000..7472aba99c21 --- /dev/null +++ b/core/java/android/service/autofill/CharSequenceTransformation.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2017 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.autofill; + +import static android.view.autofill.Helper.sDebug; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.ArrayMap; +import android.util.Log; +import android.util.Pair; +import android.view.autofill.AutofillId; +import android.widget.RemoteViews; +import android.widget.TextView; + +import com.android.internal.util.Preconditions; + +/** + * Replaces a {@link TextView} child of a {@link CustomDescription} with the contents of one or + * more regular expressions (regexs). + * + * <p>When it contains more than one field, the fields that match their regex are added to the + * overall transformation result. + * + * <p>For example, a transformation to mask a credit card number contained in just one field would + * be: + * + * <pre class="prettyprint"> + * new CharSequenceTransformation.Builder() + * .addField(ccNumberId, "^.*(\\d\\d\\d\\d)$", "...$1") + * .build(); + * </pre> + * + * <p>But a tranformation that generates a {@code Exp: MM / YYYY} credit expiration date from two + * fields (month and year) would be: + * + * <pre class="prettyprint"> + * new CharSequenceTransformation.Builder() + * .addField(ccExpMonthId, "^(\\d\\d)$", "Exp: $1") + * .addField(ccExpYearId, "^(\\d\\d\\d\\d)$", " / $1"); + * </pre> + */ +//TODO(b/62534917): add unit tests +public final class CharSequenceTransformation extends InternalTransformation implements Parcelable { + private static final String TAG = "CharSequenceTransformation"; + private final ArrayMap<AutofillId, Pair<String, String>> mFields; + + private CharSequenceTransformation(Builder builder) { + mFields = builder.mFields; + } + + /** @hide */ + @Override + public void apply(@NonNull ValueFinder finder, @NonNull RemoteViews parentTemplate, + int childViewId) { + final StringBuilder converted = new StringBuilder(); + final int size = mFields.size(); + if (sDebug) Log.d(TAG, size + " multiple fields on id " + childViewId); + for (int i = 0; i < size; i++) { + final AutofillId id = mFields.keyAt(i); + final Pair<String, String> regex = mFields.valueAt(i); + final String value = finder.findByAutofillId(id); + if (value == null) { + Log.w(TAG, "No value for id " + id); + return; + } + final String convertedValue = value.replaceAll(regex.first, regex.second); + converted.append(convertedValue); + } + parentTemplate.setCharSequence(childViewId, "setText", converted); + } + + /** + * Builder for {@link CharSequenceTransformation} objects. + */ + public static class Builder { + private ArrayMap<AutofillId, Pair<String, String>> mFields; + private boolean mDestroyed; + + //TODO(b/62534917): add constructor that takes a field so we force it to have at least one + // (and then remove the check for empty from build()) + + /** + * Adds the transformed contents of a field to the overall result of this transformation. + * + * @param id id of the screen field. + * @param regex regular expression with groups (delimited by {@code (} and {@code (}) that + * are used to substitute parts of the value. + * @param subst the string that substitutes the matched regex, using {@code $} for + * group substitution ({@code $1} for 1st group match, {@code $2} for 2nd, etc). + * + * @return this builder. + */ + public Builder addField(@NonNull AutofillId id, @NonNull String regex, + @NonNull String subst) { + //TODO(b/62534917): throw exception if regex /subts are invalid + throwIfDestroyed(); + Preconditions.checkNotNull(id); + Preconditions.checkNotNull(regex); + Preconditions.checkNotNull(subst); + if (mFields == null) { + mFields = new ArrayMap<>(); + } + mFields.put(id, new Pair<>(regex, subst)); + return this; + } + + /** + * Creates a new {@link CharSequenceTransformation} instance. + * + * @throws IllegalStateException if no call to {@link #addField(AutofillId, String, String)} + * was made. + */ + public CharSequenceTransformation build() { + throwIfDestroyed(); + Preconditions.checkState(mFields != null && !mFields.isEmpty(), + "Must add at least one field"); + mDestroyed = true; + return new CharSequenceTransformation(this); + } + + private void throwIfDestroyed() { + Preconditions.checkState(!mDestroyed, "Already called build()"); + } + } + + ///////////////////////////////////// + // Object "contract" methods. // + ///////////////////////////////////// + @Override + public String toString() { + if (!sDebug) return super.toString(); + + return "MultipleViewsCharSequenceTransformation: [fields=" + mFields + "]"; + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + final int size = mFields.size(); + final AutofillId[] ids = new AutofillId[size]; + final String[] regexs = new String[size]; + final String[] substs = new String[size]; + Pair<String, String> pair; + for (int i = 0; i < size; i++) { + ids[i] = mFields.keyAt(i); + pair = mFields.valueAt(i); + regexs[i] = pair.first; + substs[i] = pair.second; + } + parcel.writeParcelableArray(ids, flags); + parcel.writeStringArray(regexs); + parcel.writeStringArray(substs); + } + + public static final Parcelable.Creator<CharSequenceTransformation> CREATOR = + new Parcelable.Creator<CharSequenceTransformation>() { + @Override + public CharSequenceTransformation createFromParcel(Parcel parcel) { + // Always go through the builder to ensure the data ingested by + // the system obeys the contract of the builder to avoid attacks + // using specially crafted parcels. + final CharSequenceTransformation.Builder builder = + new CharSequenceTransformation.Builder(); + final AutofillId[] ids = parcel.readParcelableArray(null, AutofillId.class); + final String[] regexs = parcel.createStringArray(); + final String[] substs = parcel.createStringArray(); + final int size = ids.length; + for (int i = 0; i < size; i++) { + builder.addField(ids[i], regexs[i], substs[i]); + } + return builder.build(); + } + + @Override + public CharSequenceTransformation[] newArray(int size) { + return new CharSequenceTransformation[size]; + } + }; +} diff --git a/core/java/android/service/autofill/CustomDescription.java b/core/java/android/service/autofill/CustomDescription.java new file mode 100644 index 000000000000..51530d61c9f5 --- /dev/null +++ b/core/java/android/service/autofill/CustomDescription.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2017 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.autofill; + +import static android.view.autofill.Helper.sDebug; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; +import android.util.SparseArray; +import android.widget.RemoteViews; + +import com.android.internal.util.Preconditions; + +/** + * Defines a custom description for the Save UI affordance. + * + * <p>This is useful when the autofill service needs to show a detailed view of what would be saved; + * for example, when the screen contains a credit card, it could display a logo of the credit card + * bank, the last for digits of the credit card number, and its expiration number. + * + * <p>A custom description is made of 2 parts: + * <ul> + * <li>A {@link RemoteViews presentation template} containing children views. + * <li>{@link Transformation Transformations} to populate the children views. + * </ul> + * + * <p>For the credit card example mentioned above, the (simplified) template would be: + * + * <pre class="prettyprint"> + * <LinearLayout> + * <ImageView android:id="@+id/templateccLogo"/> + * <TextView android:id="@+id/templateCcNumber"/> + * <TextView android:id="@+id/templateExpDate"/> + * </LinearLayout> + * </pre> + * + * <p>Which in code translates to: + * + * <pre class="prettyprint"> + * CustomDescription.Builder buider = new Builder(new RemoteViews(pgkName, R.layout.cc_template); + * </pre> + * + * <p>Then the value of each of the 3 children would be changed at runtime based on the the value of + * the screen fields and the {@link Transformation Transformations}: + * + * <pre class="prettyprint"> + * // Image child - different logo for each bank, based on credit card prefix + * builder.addChild(R.id.templateccLogo, + * new ImageTransformation.Builder(ccNumberId) + * .addOption("^4815.*$", R.drawable.ic_credit_card_logo1) + * .addOption("^1623.*$", R.drawable.ic_credit_card_logo2) + * .addOption("^42.*$", R.drawable.ic_credit_card_logo3); + * // Masked credit card number (as .....LAST_4_DIGITS) + * builder.addChild(R.id.templateCcNumber, new CharSequenceTransformation.Builder() + * .addField(ccNumberId, "^.*(\\d\\d\\d\\d)$", "...$1") + * // Expiration date as MM / YYYY: + * builder.addChild(R.id.templateExpDate, new CharSequenceTransformation.Builder() + * .addField(ccExpMonthId, "^(\\d\\d)$", "Exp: $1") + * .addField(ccExpYearId, "^(\\d\\d)$", "/$1"); + * </pre> + * + * <p>See {@link ImageTransformation}, {@link CharSequenceTransformation} for more info about these + * transformations. + */ +// TODO(b/62534917): add integration tests +public final class CustomDescription implements Parcelable { + + private static final String TAG = "CustomDescription"; + + private final RemoteViews mPresentation; + private final SparseArray<InternalTransformation> mTransformations; + + private CustomDescription(Builder builder) { + mPresentation = builder.mPresentation; + mTransformations = builder.mTransformations; + } + + /** @hide */ + public RemoteViews getPresentation(ValueFinder finder) { + // TODO(b/62534917): need to handler errors, like not finding the ID + if (mTransformations != null) { + final int size = mTransformations.size(); + if (sDebug) Log.d(TAG, "getPresentation(): applying " + size + " transformations"); + for (int i = 0; i < size; i++) { + final int id = mTransformations.keyAt(i); + final InternalTransformation transformation = mTransformations.valueAt(i); + if (sDebug) Log.d(TAG, "#" + i + ": " + transformation); + transformation.apply(finder, mPresentation, id); + } + } + return mPresentation; + } + + /** + * Builder for {@link CustomDescription} objects. + */ + public static class Builder { + private final RemoteViews mPresentation; + + private SparseArray<InternalTransformation> mTransformations; + + /** + * Default constructor. + * + * @param parentPresentation template presentation with (optional) children views. + */ + public Builder(RemoteViews parentPresentation) { + mPresentation = parentPresentation; + } + + /** + * Adds a transformation to replace the value of a child view with the fields in the + * screen. + * + * @param id view id of the children view. + * @param transformation an implementation provided by the Android System. + * @return this builder. + * @throws IllegalArgumentException if {@code transformation} is not a class provided + * by the Android System. + */ + public Builder addChild(int id, @NonNull Transformation transformation) { + Preconditions.checkArgument((transformation instanceof InternalTransformation), + "not provided by Android System: " + transformation); + if (mTransformations == null) { + mTransformations = new SparseArray<>(); + } + mTransformations.put(id, (InternalTransformation) transformation); + return this; + } + + /** + * Creates a new {@link CustomDescription} instance. + */ + public CustomDescription build() { + return new CustomDescription(this); + } + } + + ///////////////////////////////////// + // Object "contract" methods. // + ///////////////////////////////////// + @Override + public String toString() { + if (!sDebug) return super.toString(); + + return new StringBuilder("CustomDescription: [presentation=") + .append(mPresentation) + .append(", transformations=").append(mTransformations) + .append("]").toString(); + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(mPresentation, flags); + if (mTransformations == null) { + dest.writeIntArray(null); + } else { + final int size = mTransformations.size(); + final int[] ids = new int[size]; + final InternalTransformation[] values = new InternalTransformation[size]; + for (int i = 0; i < size; i++) { + ids[i] = mTransformations.keyAt(i); + values[i] = mTransformations.valueAt(i); + } + dest.writeIntArray(ids); + dest.writeParcelableArray(values, flags); + } + } + public static final Parcelable.Creator<CustomDescription> CREATOR = + new Parcelable.Creator<CustomDescription>() { + @Override + public CustomDescription createFromParcel(Parcel parcel) { + // Always go through the builder to ensure the data ingested by + // the system obeys the contract of the builder to avoid attacks + // using specially crafted parcels. + final Builder builder = new Builder(parcel.readParcelable(null)); + final int[] ids = parcel.createIntArray(); + if (ids != null) { + final InternalTransformation[] values = + parcel.readParcelableArray(null, InternalTransformation.class); + final int size = ids.length; + for (int i = 0; i < size; i++) { + builder.addChild(ids[i], values[i]); + } + } + return builder.build(); + } + + @Override + public CustomDescription[] newArray(int size) { + return new CustomDescription[size]; + } + }; +} diff --git a/core/java/android/service/autofill/ImageTransformation.java b/core/java/android/service/autofill/ImageTransformation.java new file mode 100644 index 000000000000..9f6eedc8b3a2 --- /dev/null +++ b/core/java/android/service/autofill/ImageTransformation.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2017 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.autofill; + +import static android.view.autofill.Helper.sDebug; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.ArrayMap; +import android.util.Log; +import android.view.autofill.AutofillId; +import android.widget.ImageView; +import android.widget.RemoteViews; + +import com.android.internal.util.Preconditions; + +/** + * Replaces the content of a child {@link ImageView} of a + * {@link RemoteViews presentation template} with the first image that matches a regular expression + * (regex). + * + * <p>Typically used to display credit card logos. Example: + * + * <pre class="prettyprint"> + * new ImageTransformation.Builder(ccNumberId) + * .addOption("^4815.*$", R.drawable.ic_credit_card_logo1) + * .addOption("^1623.*$", R.drawable.ic_credit_card_logo2) + * .addOption("^42.*$", R.drawable.ic_credit_card_logo3) + * .build(); + * </pre> + * + * <p>There is no imposed limit in the number of options, but keep in mind that regexs are + * expensive to evaluate, so try to: + * <ul> + * <li>Use the minimum number of regex per image. + * <li>Add the most common images first. + * </ul> + */ +//TODO(b/62534917): add unit tests +public final class ImageTransformation extends InternalTransformation implements Parcelable { + private static final String TAG = "ImageTransformation"; + + private final AutofillId mId; + private final ArrayMap<String, Integer> mOptions; + + private ImageTransformation(Builder builder) { + mId = builder.mId; + mOptions = builder.mOptions; + } + + /** @hide */ + @Override + public void apply(@NonNull ValueFinder finder, @NonNull RemoteViews parentTemplate, + int childViewId) { + final String value = finder.findByAutofillId(mId); + if (value == null) { + Log.w(TAG, "No view for id " + mId); + return; + } + final int size = mOptions.size(); + if (sDebug) { + Log.d(TAG, size + " multiple options on id " + childViewId + " to compare against " + + value); + } + + for (int i = 0; i < size; i++) { + final String regex = mOptions.keyAt(i); + if (value.matches(regex)) { + Log.d(TAG, "Found match at " + i + ": " + regex); + parentTemplate.setImageViewResource(childViewId, mOptions.valueAt(i)); + return; + } + } + Log.w(TAG, "No match for " + value); + } + + /** + * Builder for {@link ImageTransformation} objects. + */ + public static class Builder { + private final AutofillId mId; + private ArrayMap<String, Integer> mOptions; + private boolean mDestroyed; + + /** + * Default constructor. + * + * @param id id of the screen field that will be used to evaluate whether the image should + * be used. + */ + //TODO(b/62534917): add a regex/resid so we force it to have at least one + // (and then remove the check for empty from build()) + public Builder(@NonNull AutofillId id) { + mId = Preconditions.checkNotNull(id); + } + + /** + * Adds an option to replace the child view with a different image when the regex matches. + * + * @param regex regular expression defining what should be matched to use this image. + * @param resId resource id of the image (in the autofill service's package). The + * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id. + * + * @return this build + */ + public Builder addOption(String regex, int resId) { + //TODO(b/62534917): throw exception if regex / resId are invalid + throwIfDestroyed(); + if (mOptions == null) { + mOptions = new ArrayMap<>(); + } + mOptions.put(regex, resId); + return this; + } + + /** + * Creates a new {@link ImageTransformation} instance. + * + * @throws IllegalStateException if no call to {@link #addOption(String, int)} was made. + */ + public ImageTransformation build() { + throwIfDestroyed(); + Preconditions.checkState(mOptions != null && !mOptions.isEmpty(), + "Must add at least one option"); + mDestroyed = true; + return new ImageTransformation(this); + } + + private void throwIfDestroyed() { + Preconditions.checkState(!mDestroyed, "Already called build()"); + } + } + + ///////////////////////////////////// + // Object "contract" methods. // + ///////////////////////////////////// + @Override + public String toString() { + if (!sDebug) return super.toString(); + + return "ImageTransformation: [id=" + mId + ", options=" + mOptions + "]"; + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeParcelable(mId, flags); + if (mOptions == null) { + parcel.writeStringArray(null); + return; + } + final int size = mOptions.size(); + final String[] regexs = new String[size]; + final int[] resIds = new int[size]; + for (int i = 0; i < size; i++) { + regexs[i] = mOptions.keyAt(i); + resIds[i] = mOptions.valueAt(i); + } + parcel.writeStringArray(regexs); + parcel.writeIntArray(resIds); + } + + public static final Parcelable.Creator<ImageTransformation> CREATOR = + new Parcelable.Creator<ImageTransformation>() { + @Override + public ImageTransformation createFromParcel(Parcel parcel) { + // Always go through the builder to ensure the data ingested by + // the system obeys the contract of the builder to avoid attacks + // using specially crafted parcels. + final ImageTransformation.Builder builder = + new ImageTransformation.Builder(parcel.readParcelable(null)); + final String[] regexs = parcel.createStringArray(); + if (regexs != null) { + final int[] resIds = parcel.createIntArray(); + final int size = regexs.length; + for (int i = 0; i < size; i++) { + builder.addOption(regexs[i], resIds[i]); + } + } + return builder.build(); + } + + @Override + public ImageTransformation[] newArray(int size) { + return new ImageTransformation[size]; + } + }; +} diff --git a/core/java/android/service/autofill/InternalTransformation.java b/core/java/android/service/autofill/InternalTransformation.java new file mode 100644 index 000000000000..3e51f87c7280 --- /dev/null +++ b/core/java/android/service/autofill/InternalTransformation.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2017 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.autofill; + +import android.annotation.NonNull; +import android.os.Parcelable; +import android.widget.RemoteViews; + +/** @hide */ +abstract class InternalTransformation implements Transformation, Parcelable { + + /** + * Applies this transformation to a child view of a {@link RemoteViews presentation template}. + * + * @param finder object used to find the value of a field in the screen. + * @param template the {@link RemoteViews presentation template}. + * @param childViewId resource id of the child view inside the template. + * + * @hide + */ + abstract void apply(@NonNull ValueFinder finder, @NonNull RemoteViews template, + int childViewId); +} diff --git a/core/java/android/service/autofill/InternalValidator.java b/core/java/android/service/autofill/InternalValidator.java new file mode 100644 index 000000000000..37ef96fd82ea --- /dev/null +++ b/core/java/android/service/autofill/InternalValidator.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2017 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.autofill; + +import android.annotation.NonNull; +import android.os.Parcelable; + +/** @hide */ +public abstract class InternalValidator implements Validator, Parcelable { + + /** + * Decides whether the contents of the screen are valid. + * + * @param finder object used to find the value of a field in the screen. + * @return {@code true} if the contents are valid, {@code false} otherwise. + * + * @hide + */ + public abstract boolean isValid(@NonNull ValueFinder finder); +} diff --git a/core/java/android/service/autofill/LuhnChecksumValidator.java b/core/java/android/service/autofill/LuhnChecksumValidator.java new file mode 100644 index 000000000000..713f0f9a963e --- /dev/null +++ b/core/java/android/service/autofill/LuhnChecksumValidator.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2017 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.autofill; + +import static android.view.autofill.Helper.sDebug; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.Log; +import android.view.autofill.AutofillId; + +import com.android.internal.util.Preconditions; + +/** + * Validator that returns {@code true} if the number created by concatenating all given fields + * pass a Luhn algorithm checksum. + * + * <p>See {@link SaveInfo.Builder#setValidator(Validator)} for examples. + */ +public final class LuhnChecksumValidator extends InternalValidator implements Parcelable { + private static final String TAG = "LuhnChecksumValidator"; + + private final AutofillId[] mIds; + + /** + * Default constructor. + * + * @param ids id of fields that comprises the number to be checked. + */ + public LuhnChecksumValidator(@NonNull AutofillId... ids) { + mIds = Preconditions.checkArrayElementsNotNull(ids, "ids"); + } + + /** @hide */ + @Override + public boolean isValid(@NonNull ValueFinder finder) { + if (mIds == null || mIds.length == 0) return false; + + final StringBuilder number = new StringBuilder(); + for (AutofillId id : mIds) { + final String partialNumber = finder.findByAutofillId(id); + if (partialNumber == null) { + if (sDebug) Log.d(TAG, "No partial number for id " + id); + return false; + } + number.append(partialNumber); + } + final boolean isValid = TextUtils.isDigitsOnly(number.toString()); + if (sDebug) Log.d(TAG, "Is valid: " + isValid); + // TODO(b/62534917): proper implementation - copy & paste code from: + // PaymentUtils.java + // PaymentUtilsTest.java + return isValid; + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeParcelableArray(mIds, flags); + } + + public static final Parcelable.Creator<LuhnChecksumValidator> CREATOR = + new Parcelable.Creator<LuhnChecksumValidator>() { + @Override + public LuhnChecksumValidator createFromParcel(Parcel parcel) { + return new LuhnChecksumValidator(parcel.readParcelableArray(null, AutofillId.class)); + } + + @Override + public LuhnChecksumValidator[] newArray(int size) { + return new LuhnChecksumValidator[size]; + } + }; +} diff --git a/core/java/android/service/autofill/OptionalValidators.java b/core/java/android/service/autofill/OptionalValidators.java new file mode 100644 index 000000000000..c9dd1d40e0aa --- /dev/null +++ b/core/java/android/service/autofill/OptionalValidators.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2017 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.autofill; + +import static android.view.autofill.Helper.sDebug; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.Preconditions; + +/** + * Compound validator that returns {@code true} on {@link #isValid(ValueFinder)} if any + * of its subvalidators returns {@code true} as well. + * + * <p>Used to implement an {@code OR} logical operation. + * + * @hide + */ +final class OptionalValidators extends InternalValidator { + + private final InternalValidator[] mValidators; + + OptionalValidators(@NonNull InternalValidator[] validators) { + mValidators = Preconditions.checkArrayElementsNotNull(validators, "validators"); + } + + @Override + public boolean isValid(@NonNull ValueFinder finder) { + if (mValidators == null) { + return true; + } + // TODO(b/62534917): handle errors, like not finding the ID + + for (InternalValidator validator : mValidators) { + final boolean valid = validator.isValid(finder); + if (valid) return true; + } + + return false; + } + + ///////////////////////////////////// + // Object "contract" methods. // + ///////////////////////////////////// + @Override + public String toString() { + if (!sDebug) return super.toString(); + + return new StringBuilder("OptionalValidators: [validators=").append(mValidators) + .append("]") + .toString(); + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelableArray(mValidators, flags); + } + + public static final Parcelable.Creator<OptionalValidators> CREATOR = + new Parcelable.Creator<OptionalValidators>() { + @Override + public OptionalValidators createFromParcel(Parcel parcel) { + return new OptionalValidators(parcel + .readParcelableArray(null, InternalValidator.class)); + } + + @Override + public OptionalValidators[] newArray(int size) { + return new OptionalValidators[size]; + } + }; +} diff --git a/core/java/android/service/autofill/RequiredValidators.java b/core/java/android/service/autofill/RequiredValidators.java new file mode 100644 index 000000000000..f2b7db8af7a8 --- /dev/null +++ b/core/java/android/service/autofill/RequiredValidators.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2017 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.autofill; + +import static android.view.autofill.Helper.sDebug; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.Preconditions; + +/** + * Compound validator that only returns {@code true} on {@link #isValid(ValueFinder)} if all + * of its subvalidators return {@code true} as well. + * + * <p>Used to implement an {@code AND} logical operation. + * + * @hide + */ +final class RequiredValidators extends InternalValidator { + + private final InternalValidator[] mValidators; + + RequiredValidators(@NonNull InternalValidator[] validators) { + mValidators = Preconditions.checkArrayElementsNotNull(validators, "validators"); + } + + @Override + public boolean isValid(@NonNull ValueFinder finder) { + if (mValidators == null) { + return true; + } + // TODO(b/62534917): handle errors, like not finding the ID + for (InternalValidator validator : mValidators) { + final boolean valid = validator.isValid(finder); + if (!valid) return false; + } + return true; + } + + ///////////////////////////////////// + // Object "contract" methods. // + ///////////////////////////////////// + @Override + public String toString() { + if (!sDebug) return super.toString(); + + return new StringBuilder("RequiredValidators: [validators=").append(mValidators) + .append("]") + .toString(); + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelableArray(mValidators, flags); + } + + public static final Parcelable.Creator<RequiredValidators> CREATOR = + new Parcelable.Creator<RequiredValidators>() { + @Override + public RequiredValidators createFromParcel(Parcel parcel) { + return new RequiredValidators(parcel + .readParcelableArray(null, InternalValidator.class)); + } + + @Override + public RequiredValidators[] newArray(int size) { + return new RequiredValidators[size]; + } + }; +} diff --git a/core/java/android/service/autofill/SaveInfo.java b/core/java/android/service/autofill/SaveInfo.java index 95d393b0234c..41491735df66 100644 --- a/core/java/android/service/autofill/SaveInfo.java +++ b/core/java/android/service/autofill/SaveInfo.java @@ -122,9 +122,13 @@ import java.util.Arrays; * * <p>The service can also customize some aspects of the save UI affordance: * <ul> - * <li>Add a subtitle by calling {@link Builder#setDescription(CharSequence)}. + * <li>Add a simple subtitle by calling {@link Builder#setDescription(CharSequence)}. + * <li>Add a customized subtitle by calling + * {@link Builder#setCustomDescription(CustomDescription)}. * <li>Customize the button used to reject the save request by calling * {@link Builder#setNegativeAction(int, IntentSender)}. + * <li>Decide whether the UI should be shown based on the user input validation by calling + * {@link Builder#setValidator(Validator)}. * </ul> */ public final class SaveInfo implements Parcelable { @@ -222,6 +226,8 @@ public final class SaveInfo implements Parcelable { private final AutofillId[] mOptionalIds; private final CharSequence mDescription; private final int mFlags; + private final CustomDescription mCustomDescription; + private final InternalValidator mValidator; private SaveInfo(Builder builder) { mType = builder.mType; @@ -231,6 +237,8 @@ public final class SaveInfo implements Parcelable { mOptionalIds = builder.mOptionalIds; mDescription = builder.mDescription; mFlags = builder.mFlags; + mCustomDescription = builder.mCustomDescription; + mValidator = builder.mValidator; } /** @hide */ @@ -268,6 +276,18 @@ public final class SaveInfo implements Parcelable { return mDescription; } + /** @hide */ + @Nullable + public CustomDescription getCustomDescription() { + return mCustomDescription; + } + + /** @hide */ + @Nullable + public InternalValidator getValidator() { + return mValidator; + } + /** * A builder for {@link SaveInfo} objects. */ @@ -281,12 +301,14 @@ public final class SaveInfo implements Parcelable { private CharSequence mDescription; private boolean mDestroyed; private int mFlags; + private CustomDescription mCustomDescription; + private InternalValidator mValidator; /** * Creates a new builder. * - * @param type the type of information the associated {@link FillResponse} represents, can - * be any combination of {@link SaveInfo#SAVE_DATA_TYPE_GENERIC}, + * @param type the type of information the associated {@link FillResponse} represents. It + * can be any combination of {@link SaveInfo#SAVE_DATA_TYPE_GENERIC}, * {@link SaveInfo#SAVE_DATA_TYPE_PASSWORD}, * {@link SaveInfo#SAVE_DATA_TYPE_ADDRESS}, {@link SaveInfo#SAVE_DATA_TYPE_CREDIT_CARD}, * {@link SaveInfo#SAVE_DATA_TYPE_USERNAME}, or @@ -354,21 +376,46 @@ public final class SaveInfo implements Parcelable { * * @param description a succint description. * @return This Builder. + * + * @throws IllegalStateException if this call was made after calling + * {@link #setCustomDescription(CustomDescription)}. */ public @NonNull Builder setDescription(@Nullable CharSequence description) { throwIfDestroyed(); + Preconditions.checkState(mCustomDescription == null, + "Can call setDescription() or setCustomDescription(), but not both"); mDescription = description; return this; } /** + * Sets a custom description to be shown in the UI when the user is asked to save. + * + * <p>Typically used when the service must show more info about the object being saved, + * like a credit card logo, masked number, and expiration date. + * + * @param customDescription the custom description. + * @return This Builder. + * + * @throws IllegalStateException if this call was made after calling + * {@link #setDescription(CharSequence)}. + */ + public @NonNull Builder setCustomDescription(@NonNull CustomDescription customDescription) { + throwIfDestroyed(); + Preconditions.checkState(mDescription == null, + "Can call setDescription() or setCustomDescription(), but not both"); + mCustomDescription = customDescription; + return this; + } + + /** * Sets the style and listener for the negative save action. * - * <p>This allows a fill-provider to customize the style and be + * <p>This allows an autofill service to customize the style and be * notified when the user selects the negative action in the save * UI. Note that selecting the negative action regardless of its style * and listener being customized would dismiss the save UI and if a - * custom listener intent is provided then this intent will be + * custom listener intent is provided then this intent is * started. The default style is {@link #NEGATIVE_BUTTON_STYLE_CANCEL}</p> * * @param style The action style. @@ -393,6 +440,74 @@ public final class SaveInfo implements Parcelable { } /** + * Sets an object used to validate the user input - if the input is not valid, the Save UI + * affordance is not shown. + * + * <p>Typically used to validate credit card numbers. Examples: + * + * <p>Validator for a credit number that must have exactly 16 digits: + * + * <pre class="prettyprint"> + * Validator validator = new SimpleRegexValidator(ccNumberId, "^\\d{16}$") + * </pre> + * + * <p>Validator for a credit number that must pass a Luhn checksum and either have + * 16 digits, or 15 digits starting with 108: + * + * <pre class="prettyprint"> + * import android.service.autofill.Validators; + * + * Validator validator = + * and( + * new LuhnChecksumValidator(ccNumberId), + * or( + * new SimpleRegexValidator(ccNumberId, "^\\d{16}$"), + * new SimpleRegexValidator(ccNumberId, "^108\\d{12}$") + * ) + * ); + * </pre> + * + * <p><b>NOTE: </b>the example above is just for illustrative purposes; the same validator + * could be created using a single regex for the {@code OR} part: + * + * <pre class="prettyprint"> + * Validator validator = + * and( + * new LuhnChecksumValidator(ccNumberId), + * new SimpleRegexValidator(ccNumberId, "^(\\d{16}|108\\d{12})$") + * ); + * </pre> + * + * <p>Validator for a credit number contained in just 4 fields and that must have exactly + * 4 digits on each field: + * + * <pre class="prettyprint"> + * import android.service.autofill.Validators; + * + * Validator validator = + * and( + * new SimpleRegexValidator.(ccNumberId1, "^\\d{4}$"), + * new SimpleRegexValidator.(ccNumberId2, "^\\d{4}$"), + * new SimpleRegexValidator.(ccNumberId3, "^\\d{4}$"), + * new SimpleRegexValidator.(ccNumberId4, "^\\d{4}$") + * ); + * </pre> + * + * @param validator an implementation provided by the Android System. + * @return this builder. + * + * @throws IllegalArgumentException if {@code validator} is not a class provided + * by the Android System. + */ + public @NonNull Builder setValidator(@NonNull Validator validator) { + throwIfDestroyed(); + Preconditions.checkArgument((validator instanceof InternalValidator), + "not provided by Android System: " + validator); + mValidator = (InternalValidator) validator; + return this; + } + + /** * Builds a new {@link SaveInfo} instance. */ public SaveInfo build() { @@ -406,7 +521,6 @@ public final class SaveInfo implements Parcelable { throw new IllegalStateException("Already called #build()"); } } - } ///////////////////////////////////// @@ -424,6 +538,8 @@ public final class SaveInfo implements Parcelable { .append(DebugUtils.flagsToString(SaveInfo.class, "NEGATIVE_BUTTON_STYLE_", mNegativeButtonStyle)) .append(", mFlags=").append(mFlags) + .append(", mCustomDescription=").append(mCustomDescription) + .append(", validation=").append(mValidator) .append("]").toString(); } @@ -444,6 +560,8 @@ public final class SaveInfo implements Parcelable { parcel.writeParcelable(mNegativeActionListener, flags); parcel.writeParcelableArray(mOptionalIds, flags); parcel.writeCharSequence(mDescription); + parcel.writeParcelable(mCustomDescription, flags); + parcel.writeParcelable(mValidator, flags); parcel.writeInt(mFlags); } @@ -461,6 +579,14 @@ public final class SaveInfo implements Parcelable { builder.setOptionalIds(optionalIds); } builder.setDescription(parcel.readCharSequence()); + final CustomDescription customDescripton = parcel.readParcelable(null); + if (customDescripton != null) { + builder.setCustomDescription(customDescripton); + } + final InternalValidator validator = parcel.readParcelable(null); + if (validator != null) { + builder.setValidator(validator); + } builder.setFlags(parcel.readInt()); return builder.build(); } diff --git a/core/java/android/service/autofill/SimpleRegexValidator.java b/core/java/android/service/autofill/SimpleRegexValidator.java new file mode 100644 index 000000000000..ffe0076dfb34 --- /dev/null +++ b/core/java/android/service/autofill/SimpleRegexValidator.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2017 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.autofill; + +import static android.view.autofill.Helper.sDebug; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; +import android.view.autofill.AutofillId; + +import com.android.internal.util.Preconditions; + +/** + * Defines if a field is valid based on a regular expression (regex). + * + * <p>See {@link SaveInfo.Builder#setValidator(Validator)} for examples. + */ +public final class SimpleRegexValidator extends InternalValidator implements Parcelable { + + private static final String TAG = "SimpleRegexValidator"; + + private final AutofillId mId; + private final String mRegex; + + /** + * Default constructor. + * + * @param id id of the field whose regex is applied to. + * @param regex regular expression that defines the result + * of the validator: if the regex matches the contents of + * the field identified by {@code id}, it returns {@code true}; otherwise, it + * returns {@code false}. + */ + public SimpleRegexValidator(@NonNull AutofillId id, @NonNull String regex) { + mId = Preconditions.checkNotNull(id); + //TODO(b/62534917): throw exception if regex is invalid + mRegex = Preconditions.checkNotNull(regex); + } + + /** @hide */ + @Override + public boolean isValid(@NonNull ValueFinder finder) { + final String value = finder.findByAutofillId(mId); + if (value == null) { + Log.w(TAG, "No view for id " + mId); + return false; + } + final boolean valid = value.matches(mRegex); + if (sDebug) Log.d(TAG, "isValid(): " + valid); + return valid; + } + + ///////////////////////////////////// + // Object "contract" methods. // + ///////////////////////////////////// + @Override + public String toString() { + if (!sDebug) return super.toString(); + + return "SimpleRegexValidator: [id=" + mId + ", regex=" + mRegex + "]"; + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeParcelable(mId, flags); + parcel.writeString(mRegex); + } + + public static final Parcelable.Creator<SimpleRegexValidator> CREATOR = + new Parcelable.Creator<SimpleRegexValidator>() { + @Override + public SimpleRegexValidator createFromParcel(Parcel parcel) { + return new SimpleRegexValidator(parcel.readParcelable(null), parcel.readString()); + } + + @Override + public SimpleRegexValidator[] newArray(int size) { + return new SimpleRegexValidator[size]; + } + }; +} diff --git a/core/java/android/service/autofill/Transformation.java b/core/java/android/service/autofill/Transformation.java new file mode 100644 index 000000000000..63b679d87259 --- /dev/null +++ b/core/java/android/service/autofill/Transformation.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2017 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.autofill; + +/** + * Helper class used to change a child view of a {@link RemoteViews presentation template} at + * runtime, using the values of fields contained in the screen. + * + * <p>Typically used by {@link CustomDescription} to provide a customized Save UI affordance. + */ +public interface Transformation { +} diff --git a/core/java/android/service/autofill/Validator.java b/core/java/android/service/autofill/Validator.java new file mode 100644 index 000000000000..854aa1e69db7 --- /dev/null +++ b/core/java/android/service/autofill/Validator.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2017 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.autofill; + +/** + * Helper class used to define whether the contents of a screen are valid. + * + * <p>Typically used to avoid displaying the Save UI affordance when the user input is invalid. + */ +public interface Validator { +} diff --git a/core/java/android/service/autofill/Validators.java b/core/java/android/service/autofill/Validators.java new file mode 100644 index 000000000000..51b503c21690 --- /dev/null +++ b/core/java/android/service/autofill/Validators.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2017 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.autofill; + +import android.annotation.NonNull; + +import com.android.internal.util.Preconditions; + +/** + * Factory for {@link Validator} operations. + * + * <p>See {@link SaveInfo.Builder#setValidator(Validator)} for examples. + */ +public final class Validators { + + private Validators() { + throw new UnsupportedOperationException("contains static methods only"); + } + + /** + * Creates a validator that is only valid if all {@code validators} are valid. + * + * @throws IllegalArgumentException if any element of {@code validators} is an instance of a + * class that is not provided by the Android System. + */ + @NonNull + public static Validator and(@NonNull Validator...validators) { + return new RequiredValidators(getInternalValidators(validators)); + } + + /** + * Creates a validator that is valid if any of the {@code validators} is valid. + * + * @throws IllegalArgumentException if any element of {@code validators} is an instance of a + * class that is not provided by the Android System. + */ + @NonNull + public static Validator or(@NonNull Validator...validators) { + return new OptionalValidators(getInternalValidators(validators)); + } + + private static InternalValidator[] getInternalValidators(Validator[] validators) { + Preconditions.checkArrayElementsNotNull(validators, "validators"); + + final InternalValidator[] internals = new InternalValidator[validators.length]; + + for (int i = 0; i < validators.length; i++) { + Preconditions.checkArgument((validators[i] instanceof InternalValidator), + "element " + i + " not provided by Android System: " + validators[i]); + internals[i] = (InternalValidator) validators[i]; + } + return internals; + } +} diff --git a/core/java/android/service/autofill/ValueFinder.java b/core/java/android/service/autofill/ValueFinder.java new file mode 100644 index 000000000000..d02a35890545 --- /dev/null +++ b/core/java/android/service/autofill/ValueFinder.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2017 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.autofill; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.view.autofill.AutofillId; + +/** + * Helper object used to obtain the value of a field in the screen being autofilled. + * + * @hide + */ +public interface ValueFinder { + + /** + * Gets the value of a field, or {@code null} when not found. + */ + @Nullable String findByAutofillId(@NonNull AutofillId id); +} diff --git a/core/res/res/layout/autofill_save.xml b/core/res/res/layout/autofill_save.xml index 90b74acf9459..7b8d92285c5a 100644 --- a/core/res/res/layout/autofill_save.xml +++ b/core/res/res/layout/autofill_save.xml @@ -65,6 +65,15 @@ </LinearLayout> + <!-- TODO(b/62534917) wrap content to fit exactly what was provided in the remote views ?--> + <LinearLayout + android:id="@+id/autofill_save_custom_subtitle" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:layout_marginTop="4dp" + android:visibility="gone"/> + <TextView android:id="@+id/autofill_save_subtitle" android:layout_width="fill_parent" diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 533d7de48d4b..2599b2a84d07 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2903,6 +2903,7 @@ <java-symbol type="id" name="autofill_dataset_picker"/> <java-symbol type="id" name="autofill_dataset_list"/> <java-symbol type="id" name="autofill" /> + <java-symbol type="id" name="autofill_save_custom_subtitle" /> <java-symbol type="id" name="autofill_save_title" /> <java-symbol type="id" name="autofill_save_subtitle" /> <java-symbol type="id" name="autofill_save_no" /> diff --git a/services/autofill/java/com/android/server/autofill/RemoteFillService.java b/services/autofill/java/com/android/server/autofill/RemoteFillService.java index aebe92e1687d..5e25dfa49d70 100644 --- a/services/autofill/java/com/android/server/autofill/RemoteFillService.java +++ b/services/autofill/java/com/android/server/autofill/RemoteFillService.java @@ -430,6 +430,8 @@ final class RemoteFillService implements DeathRecipient { Slog.w(LOG_TAG, getClass().getSimpleName() + " timed out"); final RemoteFillService remoteService = mWeakService.get(); if (remoteService != null) { + Slog.w(LOG_TAG, getClass().getSimpleName() + " timed out after " + + TIMEOUT_REMOTE_REQUEST_MILLIS + " ms"); fail(remoteService); } }; diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 72ad752caf19..25aa0d15b617 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -55,8 +55,10 @@ import android.service.autofill.Dataset; import android.service.autofill.FillContext; import android.service.autofill.FillRequest; import android.service.autofill.FillResponse; +import android.service.autofill.InternalValidator; import android.service.autofill.SaveInfo; import android.service.autofill.SaveRequest; +import android.service.autofill.ValueFinder; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Slog; @@ -78,6 +80,7 @@ import com.android.server.autofill.ui.AutoFillUI; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -214,7 +217,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState final int numContexts = mContexts.size(); for (int i = 0; i < numContexts; i++) { - fillContextWithAllowedValues(mContexts.get(i), flags); + fillContextWithAllowedValuesLocked(mContexts.get(i), flags); } request = new FillRequest(requestId, mContexts, mClientState, flags); @@ -227,7 +230,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState /** * Returns the ids of all entries in {@link #mViewStates} in the same order. */ - private AutofillId[] getIdsOfAllViewStates() { + private AutofillId[] getIdsOfAllViewStatesLocked() { final int numViewState = mViewStates.size(); final AutofillId[] ids = new AutofillId[numViewState]; for (int i = 0; i < numViewState; i++) { @@ -238,6 +241,32 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } /** + * Gets the value of a field, using either the {@code viewStates} or the {@code mContexts}, or + * {@code null} when not found on either of them. + */ + @Nullable + private String getValueAsString(@NonNull AutofillId id) { + AutofillValue value = null; + synchronized (mLock) { + final ViewState state = mViewStates.get(id); + if (state == null) { + if (sDebug) Slog.d(TAG, "getValue(): no view state for " + id); + return null; + } + value = state.getCurrentValue(); + if (value == null) { + if (sDebug) Slog.d(TAG, "getValue(): no current value for " + id); + value = getValueFromContexts(id); + } + } + // TODO(b/62534917): support list values, using the String provided by getAutofillOptions() + if (value != null && value.isText()) { + return value.getTextValue().toString(); + } + return null; + } + + /** * Updates values of the nodes in the context's structure so that: * - proper node is focused * - autofillValue is sent back to service when it was previously autofilled @@ -246,8 +275,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState * @param fillContext The context to be filled * @param flags The flags that started the session */ - private void fillContextWithAllowedValues(@NonNull FillContext fillContext, int flags) { - final ViewNode[] nodes = fillContext.findViewNodesByAutofillIds(getIdsOfAllViewStates()); + private void fillContextWithAllowedValuesLocked(@NonNull FillContext fillContext, int flags) { + final ViewNode[] nodes = fillContext + .findViewNodesByAutofillIds(getIdsOfAllViewStatesLocked()); final int numViewState = mViewStates.size(); for (int i = 0; i < numViewState; i++) { @@ -771,6 +801,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState boolean atLeastOneChanged = false; for (int i = 0; i < requiredIds.length; i++) { final AutofillId id = requiredIds[i]; + if (id == null) { + Slog.w(TAG, "null autofill id on " + Arrays.toString(requiredIds)); + continue; + } final ViewState viewState = mViewStates.get(id); if (viewState == null) { Slog.w(TAG, "showSaveLocked(): no ViewState for required " + id); @@ -832,11 +866,22 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } if (atLeastOneChanged) { - if (sDebug) Slog.d(TAG, "at least one field changed - showing save UI"); - mService.setSaveShown(id); - getUiForShowing().showSaveUi(mService.getServiceLabel(), saveInfo, mPackageName, - this); + if (sDebug) { + Slog.d(TAG, "at least one field changed, validate fields for save UI"); + } + final ValueFinder valueFinder = (id) -> {return getValueAsString(id);}; + final InternalValidator validator = saveInfo.getValidator(); + if (validator != null && !validator.isValid(valueFinder)) { + // TODO(b/62534917): add CTS test + Slog.i(TAG, "not showing save UI because fields failed validation"); + return true; + } + + if (sDebug) Slog.d(TAG, "Good news, everyone! All checks passed, show save UI!"); + mService.setSaveShown(id); + getUiForShowing().showSaveUi(mService.getServiceLabel(), saveInfo, + valueFinder, mPackageName, this); mIsSaving = true; return false; } @@ -897,7 +942,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState for (int contextNum = 0; contextNum < numContexts; contextNum++) { final FillContext context = mContexts.get(contextNum); - final ViewNode[] nodes = context.findViewNodesByAutofillIds(getIdsOfAllViewStates()); + final ViewNode[] nodes = + context.findViewNodesByAutofillIds(getIdsOfAllViewStatesLocked()); if (sVerbose) Slog.v(TAG, "callSaveLocked(): updating " + context); diff --git a/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java b/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java index 4f90019d32b6..8b15d506dab7 100644 --- a/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java +++ b/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java @@ -28,6 +28,7 @@ import android.os.Handler; import android.service.autofill.Dataset; import android.service.autofill.FillResponse; import android.service.autofill.SaveInfo; +import android.service.autofill.ValueFinder; import android.text.TextUtils; import android.util.Slog; import android.view.autofill.AutofillId; @@ -242,7 +243,8 @@ public final class AutoFillUI { * Shows the UI asking the user to save for autofill. */ public void showSaveUi(@NonNull CharSequence providerLabel, @NonNull SaveInfo info, - @NonNull String packageName, @NonNull AutoFillUiCallback callback) { + @NonNull ValueFinder valueFinder, @NonNull String packageName, + @NonNull AutoFillUiCallback callback) { if (sVerbose) Slog.v(TAG, "showSaveUi() for " + packageName + ": " + info); int numIds = 0; numIds += info.getRequiredIds() == null ? 0 : info.getRequiredIds().length; @@ -257,8 +259,8 @@ public final class AutoFillUI { return; } hideAllUiThread(callback); - mSaveUi = new SaveUi(mContext, providerLabel, info, - mOverlayControl, new SaveUi.OnSaveListener() { + mSaveUi = new SaveUi(mContext, providerLabel, info, valueFinder, mOverlayControl, + new SaveUi.OnSaveListener() { @Override public void onSave() { log.setType(MetricsProto.MetricsEvent.TYPE_ACTION); diff --git a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java index c9e2a928dee0..e8dc3c1aea06 100644 --- a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java +++ b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java @@ -24,13 +24,18 @@ import android.app.Dialog; import android.content.Context; import android.content.IntentSender; import android.os.Handler; +import android.service.autofill.CustomDescription; import android.service.autofill.SaveInfo; +import android.service.autofill.ValueFinder; import android.text.Html; +import android.text.method.LinkMovementMethod; import android.util.ArraySet; import android.util.Slog; import android.view.Gravity; import android.view.Window; import android.view.WindowManager; +import android.widget.LinearLayout; +import android.widget.RemoteViews; import android.widget.TextView; import android.view.LayoutInflater; import android.view.View; @@ -107,14 +112,15 @@ final class SaveUi { private boolean mDestroyed; SaveUi(@NonNull Context context, @NonNull CharSequence providerLabel, @NonNull SaveInfo info, - @NonNull OverlayControl overlayControl, @NonNull OnSaveListener listener) { + @NonNull ValueFinder valueFinder, @NonNull OverlayControl overlayControl, + @NonNull OnSaveListener listener) { mListener = new OneTimeListener(listener); mOverlayControl = overlayControl; final LayoutInflater inflater = LayoutInflater.from(context); final View view = inflater.inflate(R.layout.autofill_save, null); - final TextView titleView = (TextView) view.findViewById(R.id.autofill_save_title); + final TextView titleView = view.findViewById(R.id.autofill_save_title); final ArraySet<String> types = new ArraySet<>(3); final int type = info.getType(); @@ -135,6 +141,23 @@ final class SaveUi { types.add(context.getString(R.string.autofill_save_type_email_address)); } + final CustomDescription customDescription = info.getCustomDescription(); + + if (customDescription != null) { + // TODO(b/62534917): add CTS test + if (sDebug) Slog.d(TAG, "Using custom description"); + + final RemoteViews presentation = customDescription.getPresentation(valueFinder); + if (presentation != null) { + final View remote = presentation.apply(context, null); + final LinearLayout layout = view.findViewById(R.id.autofill_save_custom_subtitle); + layout.addView(remote); + layout.setVisibility(View.VISIBLE); + } else { + Slog.w(TAG, "could not create remote presentation for custom title"); + } + } + switch (types.size()) { case 1: mTitle = Html.fromHtml(context.getString(R.string.autofill_save_title_with_type, @@ -162,9 +185,7 @@ final class SaveUi { subTitleView.setVisibility(View.VISIBLE); } - if (sDebug) { - Slog.d(TAG, "on constructor: title=" + mTitle + ", subTitle=" + mSubTitle); - } + if (sDebug) Slog.d(TAG, "on constructor: title=" + mTitle + ", subTitle=" + mSubTitle); final TextView noButton = view.findViewById(R.id.autofill_save_no); if (info.getNegativeActionStyle() == SaveInfo.NEGATIVE_BUTTON_STYLE_REJECT) { |