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