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