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