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