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/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">
+ * &lt;LinearLayout&gt;
+ *   &lt;ImageView android:id="@+id/templateccLogo"/&gt;
+ *   &lt;TextView android:id="@+id/templateCcNumber"/&gt;
+ *   &lt;TextView android:id="@+id/templateExpDate"/&gt;
+ * &lt;/LinearLayout&gt;
+ * </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);
+}