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