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