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