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‐ 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 {