FillUi.java revision b078212a1eb33d68f821b2aae0b18004e0c7c17e
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 */ 16package com.android.server.autofill.ui; 17 18import static com.android.server.autofill.Helper.sDebug; 19import static com.android.server.autofill.Helper.sVerbose; 20 21import android.annotation.NonNull; 22import android.annotation.Nullable; 23import android.app.PendingIntent; 24import android.content.Context; 25import android.content.Intent; 26import android.content.IntentSender; 27import android.graphics.Point; 28import android.graphics.Rect; 29import android.service.autofill.Dataset; 30import android.service.autofill.FillResponse; 31import android.text.TextUtils; 32import android.util.Slog; 33import android.util.TypedValue; 34import android.view.LayoutInflater; 35import android.view.MotionEvent; 36import android.view.View; 37import android.view.View.MeasureSpec; 38import android.view.ViewGroup; 39import android.view.WindowManager; 40import android.view.accessibility.AccessibilityManager; 41import android.view.autofill.AutofillId; 42import android.view.autofill.AutofillValue; 43import android.view.autofill.IAutofillWindowPresenter; 44import android.widget.BaseAdapter; 45import android.widget.Filter; 46import android.widget.Filterable; 47import android.widget.ListView; 48import android.widget.RemoteViews; 49 50import com.android.internal.R; 51import com.android.server.UiThread; 52import libcore.util.Objects; 53 54import java.io.PrintWriter; 55import java.util.ArrayList; 56import java.util.Collections; 57import java.util.List; 58 59final class FillUi { 60 private static final String TAG = "FillUi"; 61 62 private static final int VISIBLE_OPTIONS_MAX_COUNT = 3; 63 64 private static final TypedValue sTempTypedValue = new TypedValue(); 65 66 interface Callback { 67 void onResponsePicked(@NonNull FillResponse response); 68 void onDatasetPicked(@NonNull Dataset dataset); 69 void onCanceled(); 70 void onDestroy(); 71 void requestShowFillUi(int width, int height, 72 IAutofillWindowPresenter windowPresenter); 73 void requestHideFillUi(); 74 void startIntentSender(IntentSender intentSender); 75 } 76 77 private final @NonNull Point mTempPoint = new Point(); 78 79 private final @NonNull AutofillWindowPresenter mWindowPresenter = 80 new AutofillWindowPresenter(); 81 82 private final @NonNull Context mContext; 83 84 private final @NonNull AnchoredWindow mWindow; 85 86 private final @NonNull Callback mCallback; 87 88 private final @NonNull ListView mListView; 89 90 private final @Nullable ItemsAdapter mAdapter; 91 92 private @Nullable String mFilterText; 93 94 private @Nullable AnnounceFilterResult mAnnounceFilterResult; 95 96 private int mContentWidth; 97 private int mContentHeight; 98 99 private boolean mDestroyed; 100 101 FillUi(@NonNull Context context, @NonNull FillResponse response, 102 @NonNull AutofillId focusedViewId, @NonNull @Nullable String filterText, 103 @NonNull OverlayControl overlayControl, @NonNull Callback callback) { 104 mContext = context; 105 mCallback = callback; 106 107 final LayoutInflater inflater = LayoutInflater.from(context); 108 final ViewGroup decor = (ViewGroup) inflater.inflate( 109 R.layout.autofill_dataset_picker, null); 110 111 final RemoteViews.OnClickHandler interceptionHandler = new RemoteViews.OnClickHandler() { 112 @Override 113 public boolean onClickHandler(View view, PendingIntent pendingIntent, 114 Intent fillInIntent) { 115 if (pendingIntent != null) { 116 mCallback.startIntentSender(pendingIntent.getIntentSender()); 117 } 118 return true; 119 } 120 }; 121 122 if (response.getAuthentication() != null) { 123 mListView = null; 124 mAdapter = null; 125 126 final View content; 127 try { 128 content = response.getPresentation().apply(context, decor, interceptionHandler); 129 decor.addView(content); 130 } catch (RuntimeException e) { 131 callback.onCanceled(); 132 Slog.e(TAG, "Error inflating remote views", e); 133 mWindow = null; 134 return; 135 } 136 137 Point maxSize = mTempPoint; 138 resolveMaxWindowSize(context, maxSize); 139 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(maxSize.x, 140 MeasureSpec.AT_MOST); 141 final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxSize.y, 142 MeasureSpec.AT_MOST); 143 144 decor.measure(widthMeasureSpec, heightMeasureSpec); 145 decor.setOnClickListener(v -> mCallback.onResponsePicked(response)); 146 mContentWidth = content.getMeasuredWidth(); 147 mContentHeight = content.getMeasuredHeight(); 148 149 mWindow = new AnchoredWindow(decor, overlayControl); 150 mCallback.requestShowFillUi(mContentWidth, mContentHeight, mWindowPresenter); 151 } else { 152 final int datasetCount = response.getDatasets().size(); 153 final ArrayList<ViewItem> items = new ArrayList<>(datasetCount); 154 for (int i = 0; i < datasetCount; i++) { 155 final Dataset dataset = response.getDatasets().get(i); 156 final int index = dataset.getFieldIds().indexOf(focusedViewId); 157 if (index >= 0) { 158 final RemoteViews presentation = dataset.getFieldPresentation(index); 159 final View view; 160 try { 161 if (sVerbose) Slog.v(TAG, "setting remote view for " + focusedViewId); 162 view = presentation.apply(context, null, interceptionHandler); 163 } catch (RuntimeException e) { 164 Slog.e(TAG, "Error inflating remote views", e); 165 continue; 166 } 167 final AutofillValue value = dataset.getFieldValues().get(index); 168 String valueText = null; 169 // If the dataset needs auth - don't add its text to allow guessing 170 // its content based on how filtering behaves. 171 if (value != null && value.isText() && dataset.getAuthentication() == null) { 172 valueText = value.getTextValue().toString().toLowerCase(); 173 } 174 175 items.add(new ViewItem(dataset, valueText, view)); 176 } 177 } 178 179 mAdapter = new ItemsAdapter(items); 180 181 mListView = decor.findViewById(R.id.autofill_dataset_list); 182 mListView.setAdapter(mAdapter); 183 mListView.setVisibility(View.VISIBLE); 184 mListView.setOnItemClickListener((adapter, view, position, id) -> { 185 final ViewItem vi = mAdapter.getItem(position); 186 mCallback.onDatasetPicked(vi.getDataset()); 187 }); 188 189 if (filterText == null) { 190 mFilterText = null; 191 } else { 192 mFilterText = filterText.toLowerCase(); 193 } 194 195 applyNewFilterText(); 196 mWindow = new AnchoredWindow(decor, overlayControl); 197 } 198 } 199 200 private void applyNewFilterText() { 201 final int oldCount = mAdapter.getCount(); 202 mAdapter.getFilter().filter(mFilterText, (count) -> { 203 if (mDestroyed) { 204 return; 205 } 206 if (count <= 0) { 207 if (sDebug) { 208 final int size = mFilterText == null ? 0 : mFilterText.length(); 209 Slog.d(TAG, "No dataset matches filter with " + size + " chars"); 210 } 211 mCallback.requestHideFillUi(); 212 } else { 213 if (updateContentSize()) { 214 mCallback.requestShowFillUi(mContentWidth, mContentHeight, mWindowPresenter); 215 } 216 if (mAdapter.getCount() > VISIBLE_OPTIONS_MAX_COUNT) { 217 mListView.setVerticalScrollBarEnabled(true); 218 mListView.onVisibilityAggregated(true); 219 } else { 220 mListView.setVerticalScrollBarEnabled(false); 221 } 222 if (mAdapter.getCount() != oldCount) { 223 mListView.requestLayout(); 224 } 225 } 226 }); 227 } 228 229 public void setFilterText(@Nullable String filterText) { 230 throwIfDestroyed(); 231 if (mAdapter == null) { 232 return; 233 } 234 235 if (filterText == null) { 236 filterText = null; 237 } else { 238 filterText = filterText.toLowerCase(); 239 } 240 241 if (Objects.equal(mFilterText, filterText)) { 242 return; 243 } 244 mFilterText = filterText; 245 246 applyNewFilterText(); 247 } 248 249 public void destroy() { 250 throwIfDestroyed(); 251 mCallback.onDestroy(); 252 mCallback.requestHideFillUi(); 253 mDestroyed = true; 254 } 255 256 private boolean updateContentSize() { 257 if (mAdapter == null) { 258 return false; 259 } 260 boolean changed = false; 261 if (mAdapter.getCount() <= 0) { 262 if (mContentWidth != 0) { 263 mContentWidth = 0; 264 changed = true; 265 } 266 if (mContentHeight != 0) { 267 mContentHeight = 0; 268 changed = true; 269 } 270 return changed; 271 } 272 273 Point maxSize = mTempPoint; 274 resolveMaxWindowSize(mContext, maxSize); 275 276 mContentWidth = 0; 277 mContentHeight = 0; 278 279 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(maxSize.x, 280 MeasureSpec.AT_MOST); 281 final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxSize.y, 282 MeasureSpec.AT_MOST); 283 final int itemCount = mAdapter.getCount(); 284 for (int i = 0; i < itemCount; i++) { 285 View view = mAdapter.getItem(i).getView(); 286 view.measure(widthMeasureSpec, heightMeasureSpec); 287 final int clampedMeasuredWidth = Math.min(view.getMeasuredWidth(), maxSize.x); 288 final int newContentWidth = Math.max(mContentWidth, clampedMeasuredWidth); 289 if (newContentWidth != mContentWidth) { 290 mContentWidth = newContentWidth; 291 changed = true; 292 } 293 // Update the width to fit only the first items up to max count 294 if (i < VISIBLE_OPTIONS_MAX_COUNT) { 295 final int clampedMeasuredHeight = Math.min(view.getMeasuredHeight(), maxSize.y); 296 final int newContentHeight = mContentHeight + clampedMeasuredHeight; 297 if (newContentHeight != mContentHeight) { 298 mContentHeight = newContentHeight; 299 changed = true; 300 } 301 } 302 } 303 return changed; 304 } 305 306 private void throwIfDestroyed() { 307 if (mDestroyed) { 308 throw new IllegalStateException("cannot interact with a destroyed instance"); 309 } 310 } 311 312 private static void resolveMaxWindowSize(Context context, Point outPoint) { 313 context.getDisplay().getSize(outPoint); 314 TypedValue typedValue = sTempTypedValue; 315 context.getTheme().resolveAttribute(R.attr.autofillDatasetPickerMaxWidth, 316 typedValue, true); 317 outPoint.x = (int) typedValue.getFraction(outPoint.x, outPoint.x); 318 context.getTheme().resolveAttribute(R.attr.autofillDatasetPickerMaxHeight, 319 typedValue, true); 320 outPoint.y = (int) typedValue.getFraction(outPoint.y, outPoint.y); 321 } 322 323 private static class ViewItem { 324 private final String mValue; 325 private final Dataset mDataset; 326 private final View mView; 327 328 ViewItem(Dataset dataset, String value, View view) { 329 mDataset = dataset; 330 mValue = value; 331 mView = view; 332 } 333 334 public View getView() { 335 return mView; 336 } 337 338 public Dataset getDataset() { 339 return mDataset; 340 } 341 342 public String getValue() { 343 return mValue; 344 } 345 346 @Override 347 public String toString() { 348 // Used for filtering in the adapter 349 return mValue; 350 } 351 } 352 353 private final class AutofillWindowPresenter extends IAutofillWindowPresenter.Stub { 354 @Override 355 public void show(WindowManager.LayoutParams p, Rect transitionEpicenter, 356 boolean fitsSystemWindows, int layoutDirection) { 357 if (sVerbose) { 358 Slog.v(TAG, "AutofillWindowPresenter.show(): fit=" + fitsSystemWindows 359 + ", epicenter="+ transitionEpicenter + ", dir=" + layoutDirection 360 + ", params=" + p); 361 } 362 UiThread.getHandler().post(() -> mWindow.show(p)); 363 } 364 365 @Override 366 public void hide(Rect transitionEpicenter) { 367 UiThread.getHandler().post(mWindow::hide); 368 } 369 } 370 371 final class AnchoredWindow implements View.OnTouchListener { 372 private final @NonNull OverlayControl mOverlayControl; 373 private final WindowManager mWm; 374 private final View mContentView; 375 private boolean mShowing; 376 377 /** 378 * Constructor. 379 * 380 * @param contentView content of the window 381 */ 382 AnchoredWindow(View contentView, @NonNull OverlayControl overlayControl) { 383 mWm = contentView.getContext().getSystemService(WindowManager.class); 384 mContentView = contentView; 385 mOverlayControl = overlayControl; 386 } 387 388 /** 389 * Shows the window. 390 */ 391 public void show(WindowManager.LayoutParams params) { 392 if (sVerbose) Slog.v(TAG, "show(): showing=" + mShowing + ", params="+ params); 393 try { 394 if (!mShowing) { 395 params.accessibilityTitle = mContentView.getContext() 396 .getString(R.string.autofill_picker_accessibility_title); 397 mWm.addView(mContentView, params); 398 mContentView.setOnTouchListener(this); 399 mOverlayControl.hideOverlays(); 400 mShowing = true; 401 } else { 402 mWm.updateViewLayout(mContentView, params); 403 } 404 } catch (WindowManager.BadTokenException e) { 405 if (sDebug) Slog.d(TAG, "Filed with with token " + params.token + " gone."); 406 mCallback.onDestroy(); 407 } catch (IllegalStateException e) { 408 // WM throws an ISE if mContentView was added twice; this should never happen - 409 // since show() and hide() are always called in the UIThread - but when it does, 410 // it should not crash the system. 411 Slog.e(TAG, "Exception showing window " + params, e); 412 mCallback.onDestroy(); 413 } 414 } 415 416 /** 417 * Hides the window. 418 */ 419 void hide() { 420 try { 421 if (mShowing) { 422 mContentView.setOnTouchListener(null); 423 mWm.removeView(mContentView); 424 mShowing = false; 425 } 426 } catch (IllegalStateException e) { 427 // WM might thrown an ISE when removing the mContentView; this should never 428 // happen - since show() and hide() are always called in the UIThread - but if it 429 // does, it should not crash the system. 430 Slog.e(TAG, "Exception hiding window ", e); 431 mCallback.onDestroy(); 432 } finally { 433 mOverlayControl.showOverlays(); 434 } 435 } 436 437 @Override 438 public boolean onTouch(View view, MotionEvent event) { 439 // When the window is touched outside, hide the window. 440 if (view == mContentView && event.getAction() == MotionEvent.ACTION_OUTSIDE) { 441 mCallback.onCanceled(); 442 return true; 443 } 444 return false; 445 } 446 447 } 448 449 public void dump(PrintWriter pw, String prefix) { 450 pw.print(prefix); pw.print("mCallback: "); pw.println(mCallback != null); 451 pw.print(prefix); pw.print("mListView: "); pw.println(mListView); 452 pw.print(prefix); pw.print("mAdapter: "); pw.println(mAdapter != null); 453 pw.print(prefix); pw.print("mFilterText: "); pw.println(mFilterText); 454 pw.print(prefix); pw.print("mContentWidth: "); pw.println(mContentWidth); 455 pw.print(prefix); pw.print("mContentHeight: "); pw.println(mContentHeight); 456 pw.print(prefix); pw.print("mDestroyed: "); pw.println(mDestroyed); 457 pw.print(prefix); pw.print("mWindow: "); 458 if (mWindow == null) { 459 pw.println("N/A"); 460 } else { 461 final String prefix2 = prefix + " "; 462 pw.println(); 463 pw.print(prefix2); pw.print("showing: "); pw.println(mWindow.mShowing); 464 pw.print(prefix2); pw.print("view: "); pw.println(mWindow.mContentView); 465 pw.print(prefix2); pw.print("screen coordinates: "); 466 if (mWindow.mContentView == null) { 467 pw.println("N/A"); 468 } else { 469 final int[] coordinates = mWindow.mContentView.getLocationOnScreen(); 470 pw.print(coordinates[0]); pw.print("x"); pw.println(coordinates[1]); 471 } 472 } 473 } 474 475 private void announceSearchResultIfNeeded() { 476 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 477 if (mAnnounceFilterResult == null) { 478 mAnnounceFilterResult = new AnnounceFilterResult(); 479 } 480 mAnnounceFilterResult.post(); 481 } 482 } 483 484 private final class ItemsAdapter extends BaseAdapter implements Filterable { 485 private @NonNull final List<ViewItem> mAllItems; 486 487 private @NonNull final List<ViewItem> mFilteredItems = new ArrayList<>(); 488 489 ItemsAdapter(@NonNull List<ViewItem> items) { 490 mAllItems = Collections.unmodifiableList(new ArrayList<>(items)); 491 mFilteredItems.addAll(items); 492 } 493 494 @Override 495 public Filter getFilter() { 496 return new Filter() { 497 @Override 498 protected FilterResults performFiltering(CharSequence constraint) { 499 // No locking needed as mAllItems is final an immutable 500 final FilterResults results = new FilterResults(); 501 if (TextUtils.isEmpty(constraint)) { 502 results.values = mAllItems; 503 results.count = mAllItems.size(); 504 return results; 505 } 506 final List<ViewItem> filteredItems = new ArrayList<>(); 507 final String constraintLowerCase = constraint.toString().toLowerCase(); 508 final int itemCount = mAllItems.size(); 509 for (int i = 0; i < itemCount; i++) { 510 final ViewItem item = mAllItems.get(i); 511 final String value = item.getValue(); 512 // No value, i.e. null, matches any filter 513 if (value == null 514 || value.toLowerCase().startsWith(constraintLowerCase)) { 515 filteredItems.add(item); 516 } 517 } 518 results.values = filteredItems; 519 results.count = filteredItems.size(); 520 return results; 521 } 522 523 @Override 524 protected void publishResults(CharSequence constraint, FilterResults results) { 525 final boolean resultCountChanged; 526 final int oldItemCount = mFilteredItems.size(); 527 mFilteredItems.clear(); 528 @SuppressWarnings("unchecked") 529 final List<ViewItem> items = (List<ViewItem>) results.values; 530 mFilteredItems.addAll(items); 531 resultCountChanged = (oldItemCount != mFilteredItems.size()); 532 if (resultCountChanged) { 533 announceSearchResultIfNeeded(); 534 } 535 notifyDataSetChanged(); 536 } 537 }; 538 } 539 540 @Override 541 public int getCount() { 542 return mFilteredItems.size(); 543 } 544 545 @Override 546 public ViewItem getItem(int position) { 547 return mFilteredItems.get(position); 548 } 549 550 @Override 551 public long getItemId(int position) { 552 return position; 553 } 554 555 @Override 556 public View getView(int position, View convertView, ViewGroup parent) { 557 return getItem(position).getView(); 558 } 559 } 560 561 private final class AnnounceFilterResult implements Runnable { 562 private static final int SEARCH_RESULT_ANNOUNCEMENT_DELAY = 1000; // 1 sec 563 564 public void post() { 565 remove(); 566 mListView.postDelayed(this, SEARCH_RESULT_ANNOUNCEMENT_DELAY); 567 } 568 569 public void remove() { 570 mListView.removeCallbacks(this); 571 } 572 573 @Override 574 public void run() { 575 final int count = mListView.getAdapter().getCount(); 576 final String text; 577 if (count <= 0) { 578 text = mContext.getString(R.string.autofill_picker_no_suggestions); 579 } else { 580 text = mContext.getResources().getQuantityString( 581 R.plurals.autofill_picker_some_suggestions, count, count); 582 } 583 mListView.announceForAccessibility(text); 584 } 585 } 586} 587