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