Loading core/java/android/service/autofill/Dataset.java +80 −28 Original line number Diff line number Diff line Loading @@ -29,7 +29,6 @@ import android.widget.RemoteViews; import com.android.internal.util.Preconditions; import java.io.Serializable; import java.util.ArrayList; import java.util.regex.Pattern; Loading Loading @@ -99,7 +98,7 @@ public final class Dataset implements Parcelable { 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; Loading Loading @@ -132,7 +131,7 @@ public final class Dataset implements Parcelable { /** @hide */ @Nullable public Pattern getFilter(int index) { public DatasetFieldFilter getFilter(int index) { return mFieldFilters.get(index); } Loading Loading @@ -189,7 +188,7 @@ public final class Dataset implements Parcelable { 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; Loading Loading @@ -363,19 +362,21 @@ public final class Dataset implements Parcelable { * @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; } Loading @@ -398,23 +399,26 @@ public final class Dataset implements Parcelable { * @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); Loading Loading @@ -477,8 +481,8 @@ public final class Dataset implements Parcelable { 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); } Loading @@ -493,22 +497,19 @@ public final class Dataset implements Parcelable { 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)); Loading @@ -521,4 +522,55 @@ public final class Dataset implements Parcelable { 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]; } }; } } services/autofill/java/com/android/server/autofill/ui/FillUi.java +81 −33 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ import android.content.IntentSender; 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; Loading Loading @@ -58,6 +59,7 @@ import java.util.ArrayList; 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"; Loading Loading @@ -185,7 +187,7 @@ final class FillUi { 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); Loading @@ -205,21 +207,32 @@ final class FillUi { 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); Loading Loading @@ -354,7 +367,7 @@ final class FillUi { 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); Loading Loading @@ -400,13 +413,62 @@ final class FillUi { 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() + "]"; } } Loading Loading @@ -509,7 +571,7 @@ final class FillUi { 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); Loading Loading @@ -556,33 +618,14 @@ final class FillUi { 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; } Loading Loading @@ -624,6 +667,11 @@ final class FillUi { 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 { Loading Loading
core/java/android/service/autofill/Dataset.java +80 −28 Original line number Diff line number Diff line Loading @@ -29,7 +29,6 @@ import android.widget.RemoteViews; import com.android.internal.util.Preconditions; import java.io.Serializable; import java.util.ArrayList; import java.util.regex.Pattern; Loading Loading @@ -99,7 +98,7 @@ public final class Dataset implements Parcelable { 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; Loading Loading @@ -132,7 +131,7 @@ public final class Dataset implements Parcelable { /** @hide */ @Nullable public Pattern getFilter(int index) { public DatasetFieldFilter getFilter(int index) { return mFieldFilters.get(index); } Loading Loading @@ -189,7 +188,7 @@ public final class Dataset implements Parcelable { 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; Loading Loading @@ -363,19 +362,21 @@ public final class Dataset implements Parcelable { * @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; } Loading @@ -398,23 +399,26 @@ public final class Dataset implements Parcelable { * @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); Loading Loading @@ -477,8 +481,8 @@ public final class Dataset implements Parcelable { 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); } Loading @@ -493,22 +497,19 @@ public final class Dataset implements Parcelable { 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)); Loading @@ -521,4 +522,55 @@ public final class Dataset implements Parcelable { 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]; } }; } }
services/autofill/java/com/android/server/autofill/ui/FillUi.java +81 −33 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ import android.content.IntentSender; 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; Loading Loading @@ -58,6 +59,7 @@ import java.util.ArrayList; 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"; Loading Loading @@ -185,7 +187,7 @@ final class FillUi { 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); Loading @@ -205,21 +207,32 @@ final class FillUi { 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); Loading Loading @@ -354,7 +367,7 @@ final class FillUi { 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); Loading Loading @@ -400,13 +413,62 @@ final class FillUi { 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() + "]"; } } Loading Loading @@ -509,7 +571,7 @@ final class FillUi { 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); Loading Loading @@ -556,33 +618,14 @@ final class FillUi { 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; } Loading Loading @@ -624,6 +667,11 @@ final class FillUi { 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 { Loading