Home | History | Annotate | Download | only in ui
      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 import static com.android.server.autofill.Helper.sVisibleDatasetsMaxCount;
     23 
     24 import android.annotation.AttrRes;
     25 import android.annotation.NonNull;
     26 import android.annotation.Nullable;
     27 import android.app.PendingIntent;
     28 import android.content.Context;
     29 import android.graphics.drawable.Drawable;
     30 import android.view.ContextThemeWrapper;
     31 import android.content.Intent;
     32 import android.content.IntentSender;
     33 import android.content.pm.PackageManager;
     34 import android.graphics.Point;
     35 import android.graphics.Rect;
     36 import android.service.autofill.Dataset;
     37 import android.service.autofill.Dataset.DatasetFieldFilter;
     38 import android.service.autofill.FillResponse;
     39 import android.text.TextUtils;
     40 import android.util.AttributeSet;
     41 import android.util.Slog;
     42 import android.util.TypedValue;
     43 import android.view.KeyEvent;
     44 import android.view.LayoutInflater;
     45 import android.view.View;
     46 import android.view.View.MeasureSpec;
     47 import android.view.ViewGroup;
     48 import android.view.ViewGroup.LayoutParams;
     49 import android.view.WindowManager;
     50 import android.view.accessibility.AccessibilityManager;
     51 import android.view.autofill.AutofillId;
     52 import android.view.autofill.AutofillValue;
     53 import android.view.autofill.IAutofillWindowPresenter;
     54 import android.widget.BaseAdapter;
     55 import android.widget.Filter;
     56 import android.widget.Filterable;
     57 import android.widget.FrameLayout;
     58 import android.widget.ImageView;
     59 import android.widget.LinearLayout;
     60 import android.widget.ListView;
     61 import android.widget.RemoteViews;
     62 import android.widget.TextView;
     63 
     64 import com.android.internal.R;
     65 import com.android.server.UiThread;
     66 import com.android.server.autofill.Helper;
     67 
     68 import java.io.PrintWriter;
     69 import java.util.ArrayList;
     70 import java.util.Collections;
     71 import java.util.List;
     72 import java.util.Objects;
     73 import java.util.regex.Pattern;
     74 import java.util.stream.Collectors;
     75 
     76 final 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