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