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