Initial implementation of the new Save APIs.
Test: manual verification with sample app (CTS tests coming later)
Bug: 62534917
Change-Id: I085a9c933bb5e8316d673976e059e13abd7098e5
diff --git a/api/current.txt b/api/current.txt
index 40cb282..f673d1b 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -36907,6 +36907,30 @@
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);
@@ -36980,6 +37004,25 @@
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();
@@ -37003,10 +37046,12 @@
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 {
@@ -37017,6 +37062,24 @@
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 3231b4d..5518ca8 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -39983,6 +39983,30 @@
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);
@@ -40056,6 +40080,25 @@
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();
@@ -40079,10 +40122,12 @@
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 {
@@ -40093,6 +40138,24 @@
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 bc05da6..0230e55 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -37074,6 +37074,30 @@
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);
@@ -37147,6 +37171,25 @@
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();
@@ -37170,10 +37213,12 @@
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 {
@@ -37184,6 +37229,24 @@
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 0000000..7472aba
--- /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 0000000..51530d6
--- /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 0000000..9f6eedc
--- /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 0000000..3e51f87
--- /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 0000000..37ef96f
--- /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 0000000..713f0f9
--- /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 0000000..c9dd1d4
--- /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 0000000..f2b7db8
--- /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 95d393b..4149173 100644
--- a/core/java/android/service/autofill/SaveInfo.java
+++ b/core/java/android/service/autofill/SaveInfo.java
@@ -122,9 +122,13 @@
*
* <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 @@
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 @@
mOptionalIds = builder.mOptionalIds;
mDescription = builder.mDescription;
mFlags = builder.mFlags;
+ mCustomDescription = builder.mCustomDescription;
+ mValidator = builder.mValidator;
}
/** @hide */
@@ -268,6 +276,18 @@
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 @@
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 @@
*
* @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 @@
}
/**
+ * 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 @@
throw new IllegalStateException("Already called #build()");
}
}
-
}
/////////////////////////////////////
@@ -424,6 +538,8 @@
.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 @@
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 @@
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 0000000..ffe0076
--- /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 0000000..63b679d
--- /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 0000000..854aa1e
--- /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 0000000..51b503c
--- /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 0000000..d02a358
--- /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 90b74ac..7b8d922 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 3b98b01..d5d35c9 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2902,6 +2902,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 aebe92e..5e25dfa 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 @@
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 72ad752..25aa0d1 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.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 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 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 @@
/**
* 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 @@
}
/**
+ * 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 @@
* @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 @@
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 @@
}
}
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 @@
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 4f90019..8b15d50 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.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 @@
* 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 @@
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 c9e2a92..e8dc3c1 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.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 @@
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 @@
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 @@
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) {