• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.server.autofill.ui;
17 
18 import static com.android.server.autofill.Helper.paramsToString;
19 import static com.android.server.autofill.Helper.sDebug;
20 import static com.android.server.autofill.Helper.sFullScreenMode;
21 import static com.android.server.autofill.Helper.sVerbose;
22 
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.content.Context;
26 import android.content.IntentSender;
27 import android.content.pm.PackageManager;
28 import android.graphics.Point;
29 import android.graphics.Rect;
30 import android.graphics.drawable.Drawable;
31 import android.service.autofill.Dataset;
32 import android.service.autofill.Dataset.DatasetFieldFilter;
33 import android.service.autofill.FillResponse;
34 import android.text.TextUtils;
35 import android.util.PluralsMessageFormatter;
36 import android.util.Slog;
37 import android.util.TypedValue;
38 import android.view.ContextThemeWrapper;
39 import android.view.KeyEvent;
40 import android.view.LayoutInflater;
41 import android.view.View;
42 import android.view.View.MeasureSpec;
43 import android.view.ViewGroup;
44 import android.view.ViewGroup.LayoutParams;
45 import android.view.WindowManager;
46 import android.view.accessibility.AccessibilityManager;
47 import android.view.autofill.AutofillId;
48 import android.view.autofill.AutofillValue;
49 import android.view.autofill.IAutofillWindowPresenter;
50 import android.widget.BaseAdapter;
51 import android.widget.Filter;
52 import android.widget.Filterable;
53 import android.widget.ImageView;
54 import android.widget.LinearLayout;
55 import android.widget.ListView;
56 import android.widget.RemoteViews;
57 import android.widget.TextView;
58 
59 import com.android.internal.R;
60 import com.android.server.UiThread;
61 import com.android.server.autofill.AutofillManagerService;
62 import com.android.server.autofill.Helper;
63 
64 import java.io.PrintWriter;
65 import java.util.ArrayList;
66 import java.util.Collections;
67 import java.util.HashMap;
68 import java.util.List;
69 import java.util.Map;
70 import java.util.Objects;
71 import java.util.regex.Pattern;
72 import java.util.stream.Collectors;
73 
74 final class FillUi {
75     private static final String TAG = "FillUi";
76 
77     private static final int THEME_ID_LIGHT =
78             com.android.internal.R.style.Theme_DeviceDefault_Light_Autofill;
79     private static final int THEME_ID_DARK =
80             com.android.internal.R.style.Theme_DeviceDefault_Autofill;
81 
82     private static final TypedValue sTempTypedValue = new TypedValue();
83 
84     interface Callback {
onResponsePicked(@onNull FillResponse response)85         void onResponsePicked(@NonNull FillResponse response);
onDatasetPicked(@onNull Dataset dataset)86         void onDatasetPicked(@NonNull Dataset dataset);
onCanceled()87         void onCanceled();
onDestroy()88         void onDestroy();
requestShowFillUi(int width, int height, IAutofillWindowPresenter windowPresenter)89         void requestShowFillUi(int width, int height,
90                 IAutofillWindowPresenter windowPresenter);
requestHideFillUi()91         void requestHideFillUi();
startIntentSender(IntentSender intentSender)92         void startIntentSender(IntentSender intentSender);
dispatchUnhandledKey(KeyEvent keyEvent)93         void dispatchUnhandledKey(KeyEvent keyEvent);
cancelSession()94         void cancelSession();
95     }
96 
97     private final @NonNull Point mTempPoint = new Point();
98 
99     private final @NonNull AutofillWindowPresenter mWindowPresenter =
100             new AutofillWindowPresenter();
101 
102     private final @NonNull Context mContext;
103 
104     private final @NonNull AnchoredWindow mWindow;
105 
106     private final @NonNull Callback mCallback;
107 
108     private final @Nullable View mHeader;
109     private final @NonNull ListView mListView;
110     private final @Nullable View mFooter;
111 
112     private final @Nullable ItemsAdapter mAdapter;
113 
114     private @Nullable String mFilterText;
115 
116     private @Nullable AnnounceFilterResult mAnnounceFilterResult;
117 
118     private final boolean mFullScreen;
119     private final int mVisibleDatasetsMaxCount;
120     private int mContentWidth;
121     private int mContentHeight;
122 
123     private boolean mDestroyed;
124 
125     private final int mThemeId;
126 
isFullScreen(Context context)127     public static boolean isFullScreen(Context context) {
128         if (sFullScreenMode != null) {
129             if (sVerbose) Slog.v(TAG, "forcing full-screen mode to " + sFullScreenMode);
130             return sFullScreenMode;
131         }
132         return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
133     }
134 
FillUi(@onNull Context context, @NonNull FillResponse response, @NonNull AutofillId focusedViewId, @Nullable String filterText, @NonNull OverlayControl overlayControl, @NonNull CharSequence serviceLabel, @NonNull Drawable serviceIcon, boolean nightMode, @NonNull Callback callback)135     FillUi(@NonNull Context context, @NonNull FillResponse response,
136             @NonNull AutofillId focusedViewId, @Nullable String filterText,
137             @NonNull OverlayControl overlayControl, @NonNull CharSequence serviceLabel,
138             @NonNull Drawable serviceIcon, boolean nightMode, @NonNull Callback callback) {
139         if (sVerbose) Slog.v(TAG, "nightMode: " + nightMode);
140         mThemeId = nightMode ? THEME_ID_DARK : THEME_ID_LIGHT;
141         mCallback = callback;
142         mFullScreen = isFullScreen(context);
143         mContext = new ContextThemeWrapper(context, mThemeId);
144 
145         final LayoutInflater inflater = LayoutInflater.from(mContext);
146 
147         final RemoteViews headerPresentation = response.getHeader();
148         final RemoteViews footerPresentation = response.getFooter();
149         final ViewGroup decor;
150         if (mFullScreen) {
151             decor = (ViewGroup) inflater.inflate(R.layout.autofill_dataset_picker_fullscreen, null);
152         } else if (headerPresentation != null || footerPresentation != null) {
153             decor = (ViewGroup) inflater.inflate(R.layout.autofill_dataset_picker_header_footer,
154                     null);
155         } else {
156             decor = (ViewGroup) inflater.inflate(R.layout.autofill_dataset_picker, null);
157         }
158         decor.setClipToOutline(true);
159         final TextView titleView = decor.findViewById(R.id.autofill_dataset_title);
160         if (titleView != null) {
161             titleView.setText(mContext.getString(R.string.autofill_window_title, serviceLabel));
162         }
163         final ImageView iconView = decor.findViewById(R.id.autofill_dataset_icon);
164         if (iconView != null) {
165             iconView.setImageDrawable(serviceIcon);
166         }
167 
168         // In full screen we only initialize size once assuming screen size never changes
169         if (mFullScreen) {
170             final Point outPoint = mTempPoint;
171             mContext.getDisplayNoVerify().getSize(outPoint);
172             // full with of screen and half height of screen
173             mContentWidth = LayoutParams.MATCH_PARENT;
174             mContentHeight = outPoint.y / 2;
175             if (sVerbose) {
176                 Slog.v(TAG, "initialized fillscreen LayoutParams "
177                         + mContentWidth + "," + mContentHeight);
178             }
179         }
180 
181         // Send unhandled keyevent to app window.
182         decor.addOnUnhandledKeyEventListener((View view, KeyEvent event) -> {
183             switch (event.getKeyCode() ) {
184                 case KeyEvent.KEYCODE_BACK:
185                 case KeyEvent.KEYCODE_ESCAPE:
186                 case KeyEvent.KEYCODE_ENTER:
187                 case KeyEvent.KEYCODE_DPAD_CENTER:
188                 case KeyEvent.KEYCODE_DPAD_LEFT:
189                 case KeyEvent.KEYCODE_DPAD_UP:
190                 case KeyEvent.KEYCODE_DPAD_RIGHT:
191                 case KeyEvent.KEYCODE_DPAD_DOWN:
192                     return false;
193                 default:
194                     mCallback.dispatchUnhandledKey(event);
195                     return true;
196             }
197         });
198 
199         if (AutofillManagerService.getVisibleDatasetsMaxCount() > 0) {
200             mVisibleDatasetsMaxCount = AutofillManagerService.getVisibleDatasetsMaxCount();
201             if (sVerbose) {
202                 Slog.v(TAG, "overriding maximum visible datasets to " + mVisibleDatasetsMaxCount);
203             }
204         } else {
205             mVisibleDatasetsMaxCount = mContext.getResources()
206                     .getInteger(com.android.internal.R.integer.autofill_max_visible_datasets);
207         }
208 
209         final RemoteViews.InteractionHandler interceptionHandler = (view, pendingIntent, r) -> {
210             if (pendingIntent != null) {
211                 mCallback.startIntentSender(pendingIntent.getIntentSender());
212             }
213             return true;
214         };
215 
216         if (response.getAuthentication() != null) {
217             mHeader = null;
218             mListView = null;
219             mFooter = null;
220             mAdapter = null;
221 
222             // insert authentication item under autofill_dataset_picker
223             ViewGroup container = decor.findViewById(R.id.autofill_dataset_picker);
224             final View content;
225             try {
226                 content = response.getPresentation().applyWithTheme(
227                         mContext, decor, interceptionHandler, mThemeId);
228                 container.addView(content);
229             } catch (RuntimeException e) {
230                 callback.onCanceled();
231                 Slog.e(TAG, "Error inflating remote views", e);
232                 mWindow = null;
233                 return;
234             }
235             container.setFocusable(true);
236             container.setOnClickListener(v -> mCallback.onResponsePicked(response));
237 
238             if (!mFullScreen) {
239                 final Point maxSize = mTempPoint;
240                 resolveMaxWindowSize(mContext, maxSize);
241                 // fullScreen mode occupy the full width defined by autofill_dataset_picker_max_width
242                 content.getLayoutParams().width = mFullScreen ? maxSize.x
243                         : ViewGroup.LayoutParams.WRAP_CONTENT;
244                 content.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
245                 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(maxSize.x,
246                         MeasureSpec.AT_MOST);
247                 final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxSize.y,
248                         MeasureSpec.AT_MOST);
249 
250                 decor.measure(widthMeasureSpec, heightMeasureSpec);
251                 mContentWidth = content.getMeasuredWidth();
252                 mContentHeight = content.getMeasuredHeight();
253             }
254 
255             mWindow = new AnchoredWindow(decor, overlayControl);
256             requestShowFillUi();
257         } else {
258             final int datasetCount = response.getDatasets().size();
259             if (sVerbose) {
260                 Slog.v(TAG, "Number datasets: " + datasetCount + " max visible: "
261                         + mVisibleDatasetsMaxCount);
262             }
263 
264             RemoteViews.InteractionHandler interactionBlocker = null;
265             if (headerPresentation != null) {
266                 interactionBlocker = newInteractionBlocker();
267                 mHeader = headerPresentation.applyWithTheme(
268                         mContext, null, interactionBlocker, mThemeId);
269                 final LinearLayout headerContainer =
270                         decor.findViewById(R.id.autofill_dataset_header);
271                 applyCancelAction(mHeader, response.getCancelIds());
272                 if (sVerbose) Slog.v(TAG, "adding header");
273                 headerContainer.addView(mHeader);
274                 headerContainer.setVisibility(View.VISIBLE);
275             } else {
276                 mHeader = null;
277             }
278 
279             if (footerPresentation != null) {
280                 final LinearLayout footerContainer =
281                         decor.findViewById(R.id.autofill_dataset_footer);
282                 if (footerContainer != null) {
283                     if (interactionBlocker == null) { // already set for header
284                         interactionBlocker = newInteractionBlocker();
285                     }
286                     mFooter = footerPresentation.applyWithTheme(
287                             mContext, null, interactionBlocker, mThemeId);
288                     applyCancelAction(mFooter, response.getCancelIds());
289                     // Footer not supported on some platform e.g. TV
290                     if (sVerbose) Slog.v(TAG, "adding footer");
291                     footerContainer.addView(mFooter);
292                     footerContainer.setVisibility(View.VISIBLE);
293                 } else {
294                     mFooter = null;
295                 }
296             } else {
297                 mFooter = null;
298             }
299 
300             final ArrayList<ViewItem> items = new ArrayList<>(datasetCount);
301             for (int i = 0; i < datasetCount; i++) {
302                 final Dataset dataset = response.getDatasets().get(i);
303                 final int index = dataset.getFieldIds().indexOf(focusedViewId);
304                 if (index >= 0) {
305                     final RemoteViews presentation = dataset.getFieldPresentation(index);
306                     if (presentation == null) {
307                         Slog.w(TAG, "not displaying UI on field " + focusedViewId + " because "
308                                 + "service didn't provide a presentation for it on " + dataset);
309                         continue;
310                     }
311                     final View view;
312                     try {
313                         if (sVerbose) Slog.v(TAG, "setting remote view for " + focusedViewId);
314                         view = presentation.applyWithTheme(
315                                 mContext, null, interceptionHandler, mThemeId);
316                     } catch (RuntimeException e) {
317                         Slog.e(TAG, "Error inflating remote views", e);
318                         continue;
319                     }
320                     // TODO: Extract the shared filtering logic here and in FillUi to a common
321                     //  method.
322                     final DatasetFieldFilter filter = dataset.getFilter(index);
323                     Pattern filterPattern = null;
324                     String valueText = null;
325                     boolean filterable = true;
326                     if (filter == null) {
327                         final AutofillValue value = dataset.getFieldValues().get(index);
328                         if (value != null && value.isText()) {
329                             valueText = value.getTextValue().toString().toLowerCase();
330                         }
331                     } else {
332                         filterPattern = filter.pattern;
333                         if (filterPattern == null) {
334                             if (sVerbose) {
335                                 Slog.v(TAG, "Explicitly disabling filter at id " + focusedViewId
336                                         + " for dataset #" + index);
337                             }
338                             filterable = false;
339                         }
340                     }
341 
342                     applyCancelAction(view, response.getCancelIds());
343                     items.add(new ViewItem(dataset, filterPattern, filterable, valueText, view));
344                 }
345             }
346 
347             mAdapter = new ItemsAdapter(items);
348 
349             mListView = decor.findViewById(R.id.autofill_dataset_list);
350             mListView.setAdapter(mAdapter);
351             mListView.setVisibility(View.VISIBLE);
352             mListView.setOnItemClickListener((adapter, view, position, id) -> {
353                 final ViewItem vi = mAdapter.getItem(position);
354                 mCallback.onDatasetPicked(vi.dataset);
355             });
356 
357             if (filterText == null) {
358                 mFilterText = null;
359             } else {
360                 mFilterText = filterText.toLowerCase();
361             }
362 
363             applyNewFilterText();
364             mWindow = new AnchoredWindow(decor, overlayControl);
365         }
366     }
367 
applyCancelAction(View rootView, int[] ids)368     private void applyCancelAction(View rootView, int[] ids) {
369         if (ids == null) {
370             return;
371         }
372 
373         if (sDebug) Slog.d(TAG, "fill UI has " + ids.length + " actions");
374         if (!(rootView instanceof ViewGroup)) {
375             Slog.w(TAG, "cannot apply actions because fill UI root is not a "
376                     + "ViewGroup: " + rootView);
377             return;
378         }
379 
380         // Apply click actions.
381         final ViewGroup root = (ViewGroup) rootView;
382         for (int i = 0; i < ids.length; i++) {
383             final int id = ids[i];
384             final View child = root.findViewById(id);
385             if (child == null) {
386                 Slog.w(TAG, "Ignoring cancel action for view " + id
387                         + " because it's not on " + root);
388                 continue;
389             }
390             child.setOnClickListener((v) -> {
391                 if (sVerbose) {
392                     Slog.v(TAG, " Cancelling session after " + v + " clicked");
393                 }
394                 mCallback.cancelSession();
395             });
396         }
397     }
398 
requestShowFillUi()399     void requestShowFillUi() {
400         mCallback.requestShowFillUi(mContentWidth, mContentHeight, mWindowPresenter);
401     }
402 
403     /**
404      * Creates a remoteview interceptor used to block clicks or other interactions.
405      */
newInteractionBlocker()406     private RemoteViews.InteractionHandler newInteractionBlocker() {
407         return (view, pendingIntent, response) -> {
408             if (sVerbose) Slog.v(TAG, "Ignoring click on " + view);
409             return true;
410         };
411     }
412 
applyNewFilterText()413     private void applyNewFilterText() {
414         final int oldCount = mAdapter.getCount();
415         mAdapter.getFilter().filter(mFilterText, (count) -> {
416             if (mDestroyed) {
417                 return;
418             }
419             if (count <= 0) {
420                 if (sDebug) {
421                     final int size = mFilterText == null ? 0 : mFilterText.length();
422                     Slog.d(TAG, "No dataset matches filter with " + size + " chars");
423                 }
424                 mCallback.requestHideFillUi();
425             } else {
426                 if (updateContentSize()) {
427                     requestShowFillUi();
428                 }
429                 if (mAdapter.getCount() > mVisibleDatasetsMaxCount) {
430                     mListView.setVerticalScrollBarEnabled(true);
431                     mListView.onVisibilityAggregated(true);
432                 } else {
433                     mListView.setVerticalScrollBarEnabled(false);
434                 }
435                 if (mAdapter.getCount() != oldCount) {
436                     mListView.requestLayout();
437                 }
438             }
439         });
440     }
441 
setFilterText(@ullable String filterText)442     public void setFilterText(@Nullable String filterText) {
443         throwIfDestroyed();
444         if (mAdapter == null) {
445             // ViewState doesn't not support filtering - typically when it's for an authenticated
446             // FillResponse.
447             if (TextUtils.isEmpty(filterText)) {
448                 requestShowFillUi();
449             } else {
450                 mCallback.requestHideFillUi();
451             }
452             return;
453         }
454 
455         if (filterText == null) {
456             filterText = null;
457         } else {
458             filterText = filterText.toLowerCase();
459         }
460 
461         if (Objects.equals(mFilterText, filterText)) {
462             return;
463         }
464         mFilterText = filterText;
465 
466         applyNewFilterText();
467     }
468 
destroy(boolean notifyClient)469     public void destroy(boolean notifyClient) {
470         throwIfDestroyed();
471         if (mWindow != null) {
472             mWindow.hide(false);
473         }
474         mCallback.onDestroy();
475         if (notifyClient) {
476             mCallback.requestHideFillUi();
477         }
478         mDestroyed = true;
479     }
480 
updateContentSize()481     private boolean updateContentSize() {
482         if (mAdapter == null) {
483             return false;
484         }
485         if (mFullScreen) {
486             // always request show fill window with fixed size for fullscreen
487             return true;
488         }
489         boolean changed = false;
490         if (mAdapter.getCount() <= 0) {
491             if (mContentWidth != 0) {
492                 mContentWidth = 0;
493                 changed = true;
494             }
495             if (mContentHeight != 0) {
496                 mContentHeight = 0;
497                 changed = true;
498             }
499             return changed;
500         }
501 
502         Point maxSize = mTempPoint;
503         resolveMaxWindowSize(mContext, maxSize);
504 
505         mContentWidth = 0;
506         mContentHeight = 0;
507 
508         final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(maxSize.x,
509                 MeasureSpec.AT_MOST);
510         final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxSize.y,
511                 MeasureSpec.AT_MOST);
512         final int itemCount = mAdapter.getCount();
513 
514         if (mHeader != null) {
515             mHeader.measure(widthMeasureSpec, heightMeasureSpec);
516             changed |= updateWidth(mHeader, maxSize);
517             changed |= updateHeight(mHeader, maxSize);
518         }
519 
520         for (int i = 0; i < itemCount; i++) {
521             final View view = mAdapter.getItem(i).view;
522             view.measure(widthMeasureSpec, heightMeasureSpec);
523             changed |= updateWidth(view, maxSize);
524             if (i < mVisibleDatasetsMaxCount) {
525                 changed |= updateHeight(view, maxSize);
526             }
527         }
528 
529         if (mFooter != null) {
530             mFooter.measure(widthMeasureSpec, heightMeasureSpec);
531             changed |= updateWidth(mFooter, maxSize);
532             changed |= updateHeight(mFooter, maxSize);
533         }
534         return changed;
535     }
536 
updateWidth(View view, Point maxSize)537     private boolean updateWidth(View view, Point maxSize) {
538         boolean changed = false;
539         final int clampedMeasuredWidth = Math.min(view.getMeasuredWidth(), maxSize.x);
540         final int newContentWidth = Math.max(mContentWidth, clampedMeasuredWidth);
541         if (newContentWidth != mContentWidth) {
542             mContentWidth = newContentWidth;
543             changed = true;
544         }
545         return changed;
546     }
547 
updateHeight(View view, Point maxSize)548     private boolean updateHeight(View view, Point maxSize) {
549         boolean changed = false;
550         final int clampedMeasuredHeight = Math.min(view.getMeasuredHeight(), maxSize.y);
551         final int newContentHeight = mContentHeight + clampedMeasuredHeight;
552         if (newContentHeight != mContentHeight) {
553             mContentHeight = newContentHeight;
554             changed = true;
555         }
556         return changed;
557     }
558 
throwIfDestroyed()559     private void throwIfDestroyed() {
560         if (mDestroyed) {
561             throw new IllegalStateException("cannot interact with a destroyed instance");
562         }
563     }
564 
resolveMaxWindowSize(Context context, Point outPoint)565     private static void resolveMaxWindowSize(Context context, Point outPoint) {
566         context.getDisplayNoVerify().getSize(outPoint);
567         final TypedValue typedValue = sTempTypedValue;
568         context.getTheme().resolveAttribute(R.attr.autofillDatasetPickerMaxWidth,
569                 typedValue, true);
570         outPoint.x = (int) typedValue.getFraction(outPoint.x, outPoint.x);
571         context.getTheme().resolveAttribute(R.attr.autofillDatasetPickerMaxHeight,
572                 typedValue, true);
573         outPoint.y = (int) typedValue.getFraction(outPoint.y, outPoint.y);
574     }
575 
576     /**
577      * An item for the list view - either a (clickable) dataset or a (read-only) header / footer.
578      */
579     private static class ViewItem {
580         public final @Nullable String value;
581         public final @Nullable Dataset dataset;
582         public final @NonNull View view;
583         public final @Nullable Pattern filter;
584         public final boolean filterable;
585 
586         /**
587          * Default constructor.
588          *
589          * @param dataset dataset associated with the item or {@code null} if it's a header or
590          * footer (TODO(b/69796626): make @NonNull if header/footer is refactored out of the list)
591          * @param filter optional filter set by the service to determine how the item should be
592          * filtered
593          * @param filterable optional flag set by the service to indicate this item should not be
594          * filtered (typically used when the dataset has value but it's sensitive, like a password)
595          * @param value dataset value
596          * @param view dataset presentation.
597          */
ViewItem(@ullable Dataset dataset, @Nullable Pattern filter, boolean filterable, @Nullable String value, @NonNull View view)598         ViewItem(@Nullable Dataset dataset, @Nullable Pattern filter, boolean filterable,
599                 @Nullable String value, @NonNull View view) {
600             this.dataset = dataset;
601             this.value = value;
602             this.view = view;
603             this.filter = filter;
604             this.filterable = filterable;
605         }
606 
607         /**
608          * Returns whether this item matches the value input by the user so it can be included
609          * in the filtered datasets.
610          */
611         // TODO: Extract the shared filtering logic here and in FillUi to a common method.
matches(CharSequence filterText)612         public boolean matches(CharSequence filterText) {
613             if (TextUtils.isEmpty(filterText)) {
614                 // Always show item when the user input is empty
615                 return true;
616             }
617             if (!filterable) {
618                 // Service explicitly disabled filtering using a null Pattern.
619                 return false;
620             }
621             final String constraintLowerCase = filterText.toString().toLowerCase();
622             if (filter != null) {
623                 // Uses pattern provided by service
624                 return filter.matcher(constraintLowerCase).matches();
625             } else {
626                 // Compares it with dataset value with dataset
627                 return (value == null)
628                         ? (dataset.getAuthentication() == null)
629                         : value.toLowerCase().startsWith(constraintLowerCase);
630             }
631         }
632 
633         @Override
toString()634         public String toString() {
635             final StringBuilder builder = new StringBuilder("ViewItem:[view=")
636                     .append(view.getAutofillId());
637             final String datasetId = dataset == null ? null : dataset.getId();
638             if (datasetId != null) {
639                 builder.append(", dataset=").append(datasetId);
640             }
641             if (value != null) {
642                 // Cannot print value because it could contain PII
643                 builder.append(", value=").append(value.length()).append("_chars");
644             }
645             if (filterable) {
646                 builder.append(", filterable");
647             }
648             if (filter != null) {
649                 // Filter should not have PII, but it could be a huge regexp
650                 builder.append(", filter=").append(filter.pattern().length()).append("_chars");
651             }
652             return builder.append(']').toString();
653         }
654     }
655 
656     private final class AutofillWindowPresenter extends IAutofillWindowPresenter.Stub {
657         @Override
show(WindowManager.LayoutParams p, Rect transitionEpicenter, boolean fitsSystemWindows, int layoutDirection)658         public void show(WindowManager.LayoutParams p, Rect transitionEpicenter,
659                 boolean fitsSystemWindows, int layoutDirection) {
660             if (sVerbose) {
661                 Slog.v(TAG, "AutofillWindowPresenter.show(): fit=" + fitsSystemWindows
662                         + ", params=" + paramsToString(p));
663             }
664             UiThread.getHandler().post(() -> mWindow.show(p));
665         }
666 
667         @Override
hide(Rect transitionEpicenter)668         public void hide(Rect transitionEpicenter) {
669             UiThread.getHandler().post(mWindow::hide);
670         }
671     }
672 
673     final class AnchoredWindow {
674         private final @NonNull OverlayControl mOverlayControl;
675         private final WindowManager mWm;
676         private final View mContentView;
677         private boolean mShowing;
678         // Used on dump only
679         private WindowManager.LayoutParams mShowParams;
680 
681         /**
682          * Constructor.
683          *
684          * @param contentView content of the window
685          */
AnchoredWindow(View contentView, @NonNull OverlayControl overlayControl)686         AnchoredWindow(View contentView, @NonNull OverlayControl overlayControl) {
687             mWm = contentView.getContext().getSystemService(WindowManager.class);
688             mContentView = contentView;
689             mOverlayControl = overlayControl;
690         }
691 
692         /**
693          * Shows the window.
694          */
show(WindowManager.LayoutParams params)695         public void show(WindowManager.LayoutParams params) {
696             mShowParams = params;
697             if (sVerbose) {
698                 Slog.v(TAG, "show(): showing=" + mShowing + ", params=" + paramsToString(params));
699             }
700             try {
701                 params.packageName = "android";
702                 params.setTitle("Autofill UI"); // Title is set for debugging purposes
703                 if (!mShowing) {
704                     params.accessibilityTitle = mContentView.getContext()
705                             .getString(R.string.autofill_picker_accessibility_title);
706                     mWm.addView(mContentView, params);
707                     mOverlayControl.hideOverlays();
708                     mShowing = true;
709                 } else {
710                     mWm.updateViewLayout(mContentView, params);
711                 }
712             } catch (WindowManager.BadTokenException e) {
713                 if (sDebug) Slog.d(TAG, "Filed with with token " + params.token + " gone.");
714                 mCallback.onDestroy();
715             } catch (IllegalStateException e) {
716                 // WM throws an ISE if mContentView was added twice; this should never happen -
717                 // since show() and hide() are always called in the UIThread - but when it does,
718                 // it should not crash the system.
719                 Slog.wtf(TAG, "Exception showing window " + params, e);
720                 mCallback.onDestroy();
721             }
722         }
723 
724         /**
725          * Hides the window.
726          */
hide()727         void hide() {
728             hide(true);
729         }
730 
hide(boolean destroyCallbackOnError)731         void hide(boolean destroyCallbackOnError) {
732             try {
733                 if (mShowing) {
734                     mWm.removeView(mContentView);
735                     mShowing = false;
736                 }
737             } catch (IllegalStateException e) {
738                 // WM might thrown an ISE when removing the mContentView; this should never
739                 // happen - since show() and hide() are always called in the UIThread - but if it
740                 // does, it should not crash the system.
741                 Slog.e(TAG, "Exception hiding window ", e);
742                 if (destroyCallbackOnError) {
743                     mCallback.onDestroy();
744                 }
745             } finally {
746                 mOverlayControl.showOverlays();
747             }
748         }
749     }
750 
dump(PrintWriter pw, String prefix)751     public void dump(PrintWriter pw, String prefix) {
752         pw.print(prefix); pw.print("mCallback: "); pw.println(mCallback != null);
753         pw.print(prefix); pw.print("mFullScreen: "); pw.println(mFullScreen);
754         pw.print(prefix); pw.print("mVisibleDatasetsMaxCount: "); pw.println(
755                 mVisibleDatasetsMaxCount);
756         if (mHeader != null) {
757             pw.print(prefix); pw.print("mHeader: "); pw.println(mHeader);
758         }
759         if (mListView != null) {
760             pw.print(prefix); pw.print("mListView: "); pw.println(mListView);
761         }
762         if (mFooter != null) {
763             pw.print(prefix); pw.print("mFooter: "); pw.println(mFooter);
764         }
765         if (mAdapter != null) {
766             pw.print(prefix); pw.print("mAdapter: "); pw.println(mAdapter);
767         }
768         if (mFilterText != null) {
769             pw.print(prefix); pw.print("mFilterText: ");
770             Helper.printlnRedactedText(pw, mFilterText);
771         }
772         pw.print(prefix); pw.print("mContentWidth: "); pw.println(mContentWidth);
773         pw.print(prefix); pw.print("mContentHeight: "); pw.println(mContentHeight);
774         pw.print(prefix); pw.print("mDestroyed: "); pw.println(mDestroyed);
775         pw.print(prefix); pw.print("theme id: "); pw.print(mThemeId);
776         switch (mThemeId) {
777             case THEME_ID_DARK:
778                 pw.println(" (dark)");
779                 break;
780             case THEME_ID_LIGHT:
781                 pw.println(" (light)");
782                 break;
783             default:
784                 pw.println("(UNKNOWN_MODE)");
785                 break;
786         }
787         if (mWindow != null) {
788             pw.print(prefix); pw.print("mWindow: ");
789             final String prefix2 = prefix + "  ";
790             pw.println();
791             pw.print(prefix2); pw.print("showing: "); pw.println(mWindow.mShowing);
792             pw.print(prefix2); pw.print("view: "); pw.println(mWindow.mContentView);
793             if (mWindow.mShowParams != null) {
794                 pw.print(prefix2); pw.print("params: "); pw.println(mWindow.mShowParams);
795             }
796             pw.print(prefix2); pw.print("screen coordinates: ");
797             if (mWindow.mContentView == null) {
798                 pw.println("N/A");
799             } else {
800                 final int[] coordinates = mWindow.mContentView.getLocationOnScreen();
801                 pw.print(coordinates[0]); pw.print("x"); pw.println(coordinates[1]);
802             }
803         }
804     }
805 
announceSearchResultIfNeeded()806     private void announceSearchResultIfNeeded() {
807         if (AccessibilityManager.getInstance(mContext).isEnabled()) {
808             if (mAnnounceFilterResult == null) {
809                 mAnnounceFilterResult = new AnnounceFilterResult();
810             }
811             mAnnounceFilterResult.post();
812         }
813     }
814 
815     private final class ItemsAdapter extends BaseAdapter implements Filterable {
816         private @NonNull final List<ViewItem> mAllItems;
817 
818         private @NonNull final List<ViewItem> mFilteredItems = new ArrayList<>();
819 
ItemsAdapter(@onNull List<ViewItem> items)820         ItemsAdapter(@NonNull List<ViewItem> items) {
821             mAllItems = Collections.unmodifiableList(new ArrayList<>(items));
822             mFilteredItems.addAll(items);
823         }
824 
825         @Override
getFilter()826         public Filter getFilter() {
827             return new Filter() {
828                 @Override
829                 protected FilterResults performFiltering(CharSequence filterText) {
830                     // No locking needed as mAllItems is final an immutable
831                     final List<ViewItem> filtered = mAllItems.stream()
832                             .filter((item) -> item.matches(filterText))
833                             .collect(Collectors.toList());
834                     final FilterResults results = new FilterResults();
835                     results.values = filtered;
836                     results.count = filtered.size();
837                     return results;
838                 }
839 
840                 @Override
841                 protected void publishResults(CharSequence constraint, FilterResults results) {
842                     final boolean resultCountChanged;
843                     final int oldItemCount = mFilteredItems.size();
844                     mFilteredItems.clear();
845                     if (results.count > 0) {
846                         @SuppressWarnings("unchecked")
847                         final List<ViewItem> items = (List<ViewItem>) results.values;
848                         mFilteredItems.addAll(items);
849                     }
850                     resultCountChanged = (oldItemCount != mFilteredItems.size());
851                     if (resultCountChanged) {
852                         announceSearchResultIfNeeded();
853                     }
854                     notifyDataSetChanged();
855                 }
856             };
857         }
858 
859         @Override
860         public int getCount() {
861             return mFilteredItems.size();
862         }
863 
864         @Override
865         public ViewItem getItem(int position) {
866             return mFilteredItems.get(position);
867         }
868 
869         @Override
870         public long getItemId(int position) {
871             return position;
872         }
873 
874         @Override
875         public View getView(int position, View convertView, ViewGroup parent) {
876             return getItem(position).view;
877         }
878 
879         @Override
880         public String toString() {
881             return "ItemsAdapter: [all=" + mAllItems + ", filtered=" + mFilteredItems + "]";
882         }
883     }
884 
885     private final class AnnounceFilterResult implements Runnable {
886         private static final int SEARCH_RESULT_ANNOUNCEMENT_DELAY = 1000; // 1 sec
887 
888         public void post() {
889             remove();
890             mListView.postDelayed(this, SEARCH_RESULT_ANNOUNCEMENT_DELAY);
891         }
892 
893         public void remove() {
894             mListView.removeCallbacks(this);
895         }
896 
897         @Override
898         public void run() {
899             final int count = mListView.getAdapter().getCount();
900             final String text;
901             if (count <= 0) {
902                 text = mContext.getString(R.string.autofill_picker_no_suggestions);
903             } else {
904                 Map<String, Object> arguments = new HashMap<>();
905                 arguments.put("count", count);
906                 text = PluralsMessageFormatter.format(mContext.getResources(),
907                         arguments,
908                         R.string.autofill_picker_some_suggestions);
909             }
910             mListView.announceForAccessibility(text);
911         }
912     }
913 }
914