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