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