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