Changed dataset methods that take a Pattern filter to accept null.

In this case, null means the field should not be filterable, which is useful
in the cases where it represents a non-authenticated value like a password.

Test: atest CtsAutoFillServiceTestCases:LoginActivityTest#filterTextDisabledUsingNullRegex
Test: atest CtsAutoFillServiceTestCases:LoginActivityTest

Bug: 71359055
Fixes: 72711778

Change-Id: Idc8248f6c95fdb50b934840689616e42ddd6378c
diff --git a/core/java/android/service/autofill/Dataset.java b/core/java/android/service/autofill/Dataset.java
index 266bcda..f32dee1 100644
--- a/core/java/android/service/autofill/Dataset.java
+++ b/core/java/android/service/autofill/Dataset.java
@@ -29,7 +29,6 @@
 
 import com.android.internal.util.Preconditions;
 
-import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.regex.Pattern;
 
@@ -99,7 +98,7 @@
     private final ArrayList<AutofillId> mFieldIds;
     private final ArrayList<AutofillValue> mFieldValues;
     private final ArrayList<RemoteViews> mFieldPresentations;
-    private final ArrayList<Pattern> mFieldFilters;
+    private final ArrayList<DatasetFieldFilter> mFieldFilters;
     private final RemoteViews mPresentation;
     private final IntentSender mAuthentication;
     @Nullable String mId;
@@ -132,7 +131,7 @@
 
     /** @hide */
     @Nullable
-    public Pattern getFilter(int index) {
+    public DatasetFieldFilter getFilter(int index) {
         return mFieldFilters.get(index);
     }
 
@@ -189,7 +188,7 @@
         private ArrayList<AutofillId> mFieldIds;
         private ArrayList<AutofillValue> mFieldValues;
         private ArrayList<RemoteViews> mFieldPresentations;
-        private ArrayList<Pattern> mFieldFilters;
+        private ArrayList<DatasetFieldFilter> mFieldFilters;
         private RemoteViews mPresentation;
         private IntentSender mAuthentication;
         private boolean mDestroyed;
@@ -363,19 +362,21 @@
          * @param value the value to be autofilled. Pass {@code null} if you do not have the value
          *        but the target view is a logical part of the dataset. For example, if
          *        the dataset needs authentication and you have no access to the value.
-         * @param filter regex used to determine if the dataset should be shown in the autofill UI.
+         * @param filter regex used to determine if the dataset should be shown in the autofill UI;
+         *        when {@code null}, it disables filtering on that dataset (this is the recommended
+         *        approach when {@code value} is not {@code null} and field contains sensitive data
+         *        such as passwords).
          *
          * @return this builder.
          * @throws IllegalStateException if the builder was constructed without a
          *         {@link RemoteViews presentation}.
          */
         public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value,
-                @NonNull Pattern filter) {
+                @Nullable Pattern filter) {
             throwIfDestroyed();
-            Preconditions.checkNotNull(filter, "filter cannot be null");
             Preconditions.checkState(mPresentation != null,
                     "Dataset presentation not set on constructor");
-            setLifeTheUniverseAndEverything(id, value, null, filter);
+            setLifeTheUniverseAndEverything(id, value, null, new DatasetFieldFilter(filter));
             return this;
         }
 
@@ -398,23 +399,26 @@
          * @param value the value to be autofilled. Pass {@code null} if you do not have the value
          *        but the target view is a logical part of the dataset. For example, if
          *        the dataset needs authentication and you have no access to the value.
+         * @param filter regex used to determine if the dataset should be shown in the autofill UI;
+         *        when {@code null}, it disables filtering on that dataset (this is the recommended
+         *        approach when {@code value} is not {@code null} and field contains sensitive data
+         *        such as passwords).
          * @param presentation the presentation used to visualize this field.
-         * @param filter regex used to determine if the dataset should be shown in the autofill UI.
          *
          * @return this builder.
          */
         public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value,
-                @NonNull Pattern filter, @NonNull RemoteViews presentation) {
+                @Nullable Pattern filter, @NonNull RemoteViews presentation) {
             throwIfDestroyed();
-            Preconditions.checkNotNull(filter, "filter cannot be null");
             Preconditions.checkNotNull(presentation, "presentation cannot be null");
-            setLifeTheUniverseAndEverything(id, value, presentation, filter);
+            setLifeTheUniverseAndEverything(id, value, presentation,
+                    new DatasetFieldFilter(filter));
             return this;
         }
 
         private void setLifeTheUniverseAndEverything(@NonNull AutofillId id,
                 @Nullable AutofillValue value, @Nullable RemoteViews presentation,
-                @Nullable Pattern filter) {
+                @Nullable DatasetFieldFilter filter) {
             Preconditions.checkNotNull(id, "id cannot be null");
             if (mFieldIds != null) {
                 final int existingIdx = mFieldIds.indexOf(id);
@@ -477,8 +481,8 @@
         parcel.writeParcelable(mPresentation, flags);
         parcel.writeTypedList(mFieldIds, flags);
         parcel.writeTypedList(mFieldValues, flags);
-        parcel.writeParcelableList(mFieldPresentations, flags);
-        parcel.writeSerializable(mFieldFilters);
+        parcel.writeTypedList(mFieldPresentations, flags);
+        parcel.writeTypedList(mFieldFilters, flags);
         parcel.writeParcelable(mAuthentication, flags);
         parcel.writeString(mId);
     }
@@ -493,22 +497,19 @@
             final Builder builder = (presentation == null)
                     ? new Builder()
                     : new Builder(presentation);
-            final ArrayList<AutofillId> ids = parcel.createTypedArrayList(AutofillId.CREATOR);
+            final ArrayList<AutofillId> ids =
+                    parcel.createTypedArrayList(AutofillId.CREATOR);
             final ArrayList<AutofillValue> values =
                     parcel.createTypedArrayList(AutofillValue.CREATOR);
-            final ArrayList<RemoteViews> presentations = new ArrayList<>();
-            parcel.readParcelableList(presentations, null);
-            @SuppressWarnings("unchecked")
-            final ArrayList<Serializable> filters =
-                    (ArrayList<Serializable>) parcel.readSerializable();
-            final int idCount = (ids != null) ? ids.size() : 0;
-            final int valueCount = (values != null) ? values.size() : 0;
-            for (int i = 0; i < idCount; i++) {
+            final ArrayList<RemoteViews> presentations =
+                    parcel.createTypedArrayList(RemoteViews.CREATOR);
+            final ArrayList<DatasetFieldFilter> filters =
+                    parcel.createTypedArrayList(DatasetFieldFilter.CREATOR);
+            for (int i = 0; i < ids.size(); i++) {
                 final AutofillId id = ids.get(i);
-                final AutofillValue value = (valueCount > i) ? values.get(i) : null;
-                final RemoteViews fieldPresentation = presentations.isEmpty() ? null
-                        : presentations.get(i);
-                final Pattern filter = (Pattern) filters.get(i);
+                final AutofillValue value = values.get(i);
+                final RemoteViews fieldPresentation = presentations.get(i);
+                final DatasetFieldFilter filter = filters.get(i);
                 builder.setLifeTheUniverseAndEverything(id, value, fieldPresentation, filter);
             }
             builder.setAuthentication(parcel.readParcelable(null));
@@ -521,4 +522,55 @@
             return new Dataset[size];
         }
     };
+
+    /**
+     * Helper class used to indicate when the service explicitly set a {@link Pattern} filter for a
+     * dataset field&dash; we cannot use a {@link Pattern} directly because then we wouldn't be
+     * able to differentiate whether the service explicitly passed a {@code null} filter to disable
+     * filter, or when it called the methods that does not take a filter {@link Pattern}.
+     *
+     * @hide
+     */
+    public static final class DatasetFieldFilter implements Parcelable {
+
+        @Nullable
+        public final Pattern pattern;
+
+        private DatasetFieldFilter(@Nullable Pattern pattern) {
+            this.pattern = pattern;
+        }
+
+        @Override
+        public String toString() {
+            if (!sDebug) return super.toString();
+
+            // Cannot log pattern because it could contain PII
+            return pattern == null ? "null" : pattern.pattern().length() + "_chars";
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel parcel, int flags) {
+            parcel.writeSerializable(pattern);
+        }
+
+        @SuppressWarnings("hiding")
+        public static final Creator<DatasetFieldFilter> CREATOR =
+                new Creator<DatasetFieldFilter>() {
+
+            @Override
+            public DatasetFieldFilter createFromParcel(Parcel parcel) {
+                return new DatasetFieldFilter((Pattern) parcel.readSerializable());
+            }
+
+            @Override
+            public DatasetFieldFilter[] newArray(int size) {
+                return new DatasetFieldFilter[size];
+            }
+        };
+    }
 }
diff --git a/services/autofill/java/com/android/server/autofill/ui/FillUi.java b/services/autofill/java/com/android/server/autofill/ui/FillUi.java
index 5ee3cbf..5f112c7 100644
--- a/services/autofill/java/com/android/server/autofill/ui/FillUi.java
+++ b/services/autofill/java/com/android/server/autofill/ui/FillUi.java
@@ -27,6 +27,7 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.service.autofill.Dataset;
+import android.service.autofill.Dataset.DatasetFieldFilter;
 import android.service.autofill.FillResponse;
 import android.text.TextUtils;
 import android.util.Slog;
@@ -58,6 +59,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
 final class FillUi {
     private static final String TAG = "FillUi";
@@ -185,7 +187,7 @@
             final ArrayList<ViewItem> items = new ArrayList<>(totalItems);
             if (header != null) {
                 if (sVerbose) Slog.v(TAG, "adding header");
-                items.add(new ViewItem(null, null, null, header));
+                items.add(new ViewItem(null, null, false, null, header));
             }
             for (int i = 0; i < datasetCount; i++) {
                 final Dataset dataset = response.getDatasets().get(i);
@@ -205,21 +207,32 @@
                         Slog.e(TAG, "Error inflating remote views", e);
                         continue;
                     }
-                    final Pattern filter = dataset.getFilter(index);
+                    final DatasetFieldFilter filter = dataset.getFilter(index);
+                    Pattern filterPattern = null;
                     String valueText = null;
+                    boolean filterable = true;
                     if (filter == null) {
                         final AutofillValue value = dataset.getFieldValues().get(index);
                         if (value != null && value.isText()) {
                             valueText = value.getTextValue().toString().toLowerCase();
                         }
+                    } else {
+                        filterPattern = filter.pattern;
+                        if (filterPattern == null) {
+                            if (sVerbose) {
+                                Slog.v(TAG, "Explicitly disabling filter at id " + focusedViewId
+                                        + " for dataset #" + index);
+                            }
+                            filterable = false;
+                        }
                     }
 
-                    items.add(new ViewItem(dataset, filter, valueText, view));
+                    items.add(new ViewItem(dataset, filterPattern, filterable, valueText, view));
                 }
             }
             if (footer != null) {
                 if (sVerbose) Slog.v(TAG, "adding footer");
-                items.add(new ViewItem(null, null, null, footer));
+                items.add(new ViewItem(null, null, false, null, footer));
             }
 
             mAdapter = new ItemsAdapter(items);
@@ -354,7 +367,7 @@
                 MeasureSpec.AT_MOST);
         final int itemCount = mAdapter.getCount();
         for (int i = 0; i < itemCount; i++) {
-            View view = mAdapter.getItem(i).view;
+            final View view = mAdapter.getItem(i).view;
             view.measure(widthMeasureSpec, heightMeasureSpec);
             final int clampedMeasuredWidth = Math.min(view.getMeasuredWidth(), maxSize.x);
             final int newContentWidth = Math.max(mContentWidth, clampedMeasuredWidth);
@@ -400,13 +413,62 @@
         public final @Nullable Dataset dataset;
         public final @NonNull View view;
         public final @Nullable Pattern filter;
+        public final boolean filterable;
 
-        ViewItem(@Nullable Dataset dataset, @Nullable Pattern filter, @Nullable String value,
-                @NonNull View view) {
+        /**
+         * Default constructor.
+         *
+         * @param dataset dataset associated with the item or {@code null} if it's a header or
+         * footer (TODO(b/69796626): make @NonNull if header/footer is refactored out of the list)
+         * @param filter optional filter set by the service to determine how the item should be
+         * filtered
+         * @param filterable optional flag set by the service to indicate this item should not be
+         * filtered (typically used when the dataset has value but it's sensitive, like a password)
+         * @param value dataset value
+         * @param view dataset presentation.
+         */
+        ViewItem(@Nullable Dataset dataset, @Nullable Pattern filter, boolean filterable,
+                @Nullable String value, @NonNull View view) {
             this.dataset = dataset;
             this.value = value;
             this.view = view;
             this.filter = filter;
+            this.filterable = filterable;
+        }
+
+        /**
+         * Returns whether this item matches the value input by the user so it can be included
+         * in the filtered datasets.
+         */
+        public boolean matches(CharSequence filterText) {
+            if (TextUtils.isEmpty(filterText)) {
+                // Always show item when the user input is empty
+                return true;
+            }
+            if (!filterable) {
+                // Service explicitly disabled filtering using a null Pattern.
+                return false;
+            }
+            final String constraintLowerCase = filterText.toString().toLowerCase();
+            if (filter != null) {
+                // Uses pattern provided by service
+                return filter.matcher(constraintLowerCase).matches();
+            } else {
+                // Compares it with dataset value with dataset
+                return (value == null)
+                        ? (dataset.getAuthentication() == null)
+                        : value.toLowerCase().startsWith(constraintLowerCase);
+            }
+        }
+
+        @Override
+        public String toString() {
+            return "ViewItem: [dataset=" + (dataset == null ? "null" : dataset.getId())
+                    + ", value=" + (value == null ? "null" : value.length() + "_chars")
+                    + ", filterable=" + filterable
+                    + ", filter=" + (filter == null ? "null" : filter.pattern().length() + "_chars")
+                    + ", view=" + view.getAutofillId()
+                    + "]";
         }
     }
 
@@ -509,7 +571,7 @@
     public void dump(PrintWriter pw, String prefix) {
         pw.print(prefix); pw.print("mCallback: "); pw.println(mCallback != null);
         pw.print(prefix); pw.print("mListView: "); pw.println(mListView);
-        pw.print(prefix); pw.print("mAdapter: "); pw.println(mAdapter != null);
+        pw.print(prefix); pw.print("mAdapter: "); pw.println(mAdapter);
         pw.print(prefix); pw.print("mFilterText: ");
         Helper.printlnRedactedText(pw, mFilterText);
         pw.print(prefix); pw.print("mContentWidth: "); pw.println(mContentWidth);
@@ -556,33 +618,14 @@
         public Filter getFilter() {
             return new Filter() {
                 @Override
-                protected FilterResults performFiltering(CharSequence constraint) {
+                protected FilterResults performFiltering(CharSequence filterText) {
                     // No locking needed as mAllItems is final an immutable
+                    final List<ViewItem> filtered = mAllItems.stream()
+                            .filter((item) -> item.matches(filterText))
+                            .collect(Collectors.toList());
                     final FilterResults results = new FilterResults();
-                    if (TextUtils.isEmpty(constraint)) {
-                        results.values = mAllItems;
-                        results.count = mAllItems.size();
-                        return results;
-                    }
-                    final List<ViewItem> filteredItems = new ArrayList<>();
-                    final String constraintLowerCase = constraint.toString().toLowerCase();
-                    final int itemCount = mAllItems.size();
-                    for (int i = 0; i < itemCount; i++) {
-                        final ViewItem item = mAllItems.get(i);
-                        final boolean matches;
-                        if (item.filter != null) {
-                            matches = item.filter.matcher(constraintLowerCase).matches();
-                        } else {
-                            matches = (item.value == null)
-                                    ? (item.dataset.getAuthentication() == null)
-                                    : item.value.toLowerCase().startsWith(constraintLowerCase);
-                        }
-                        if (matches) {
-                            filteredItems.add(item);
-                        }
-                    }
-                    results.values = filteredItems;
-                    results.count = filteredItems.size();
+                    results.values = filtered;
+                    results.count = filtered.size();
                     return results;
                 }
 
@@ -624,6 +667,11 @@
         public View getView(int position, View convertView, ViewGroup parent) {
             return getItem(position).view;
         }
+
+        @Override
+        public String toString() {
+            return "ItemsAdapter: [all=" + mAllItems + ", filtered=" + mFilteredItems + "]";
+        }
     }
 
     private final class AnnounceFilterResult implements Runnable {