FillUi.java revision 2ee821b6757ffc07b474c254b50e3d45e774172f
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) Slog.d(TAG, "No dataset matches filter: " + mFilterText); 208 mCallback.requestHideFillUi(); 209 } else { 210 if (updateContentSize()) { 211 mCallback.requestShowFillUi(mContentWidth, mContentHeight, mWindowPresenter); 212 } 213 if (mAdapter.getCount() > VISIBLE_OPTIONS_MAX_COUNT) { 214 mListView.setVerticalScrollBarEnabled(true); 215 mListView.onVisibilityAggregated(true); 216 } else { 217 mListView.setVerticalScrollBarEnabled(false); 218 } 219 if (mAdapter.getCount() != oldCount) { 220 mListView.requestLayout(); 221 } 222 } 223 }); 224 } 225 226 public void setFilterText(@Nullable String filterText) { 227 throwIfDestroyed(); 228 if (mAdapter == null) { 229 return; 230 } 231 232 if (filterText == null) { 233 filterText = null; 234 } else { 235 filterText = filterText.toLowerCase(); 236 } 237 238 if (Objects.equal(mFilterText, filterText)) { 239 return; 240 } 241 mFilterText = filterText; 242 243 applyNewFilterText(); 244 } 245 246 public void destroy() { 247 throwIfDestroyed(); 248 mCallback.onDestroy(); 249 mCallback.requestHideFillUi(); 250 mDestroyed = true; 251 } 252 253 private boolean updateContentSize() { 254 if (mAdapter == null) { 255 return false; 256 } 257 boolean changed = false; 258 if (mAdapter.getCount() <= 0) { 259 if (mContentWidth != 0) { 260 mContentWidth = 0; 261 changed = true; 262 } 263 if (mContentHeight != 0) { 264 mContentHeight = 0; 265 changed = true; 266 } 267 return changed; 268 } 269 270 Point maxSize = mTempPoint; 271 resolveMaxWindowSize(mContext, maxSize); 272 273 mContentWidth = 0; 274 mContentHeight = 0; 275 276 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(maxSize.x, 277 MeasureSpec.AT_MOST); 278 final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxSize.y, 279 MeasureSpec.AT_MOST); 280 final int itemCount = mAdapter.getCount(); 281 for (int i = 0; i < itemCount; i++) { 282 View view = mAdapter.getItem(i).getView(); 283 view.measure(widthMeasureSpec, heightMeasureSpec); 284 final int clampedMeasuredWidth = Math.min(view.getMeasuredWidth(), maxSize.x); 285 final int newContentWidth = Math.max(mContentWidth, clampedMeasuredWidth); 286 if (newContentWidth != mContentWidth) { 287 mContentWidth = newContentWidth; 288 changed = true; 289 } 290 // Update the width to fit only the first items up to max count 291 if (i < VISIBLE_OPTIONS_MAX_COUNT) { 292 final int clampedMeasuredHeight = Math.min(view.getMeasuredHeight(), maxSize.y); 293 final int newContentHeight = mContentHeight + clampedMeasuredHeight; 294 if (newContentHeight != mContentHeight) { 295 mContentHeight = newContentHeight; 296 changed = true; 297 } 298 } 299 } 300 return changed; 301 } 302 303 private void throwIfDestroyed() { 304 if (mDestroyed) { 305 throw new IllegalStateException("cannot interact with a destroyed instance"); 306 } 307 } 308 309 private static void resolveMaxWindowSize(Context context, Point outPoint) { 310 context.getDisplay().getSize(outPoint); 311 TypedValue typedValue = sTempTypedValue; 312 context.getTheme().resolveAttribute(R.attr.autofillDatasetPickerMaxWidth, 313 typedValue, true); 314 outPoint.x = (int) typedValue.getFraction(outPoint.x, outPoint.x); 315 context.getTheme().resolveAttribute(R.attr.autofillDatasetPickerMaxHeight, 316 typedValue, true); 317 outPoint.y = (int) typedValue.getFraction(outPoint.y, outPoint.y); 318 } 319 320 private static class ViewItem { 321 private final String mValue; 322 private final Dataset mDataset; 323 private final View mView; 324 325 ViewItem(Dataset dataset, String value, View view) { 326 mDataset = dataset; 327 mValue = value; 328 mView = view; 329 } 330 331 public View getView() { 332 return mView; 333 } 334 335 public Dataset getDataset() { 336 return mDataset; 337 } 338 339 public String getValue() { 340 return mValue; 341 } 342 343 @Override 344 public String toString() { 345 // Used for filtering in the adapter 346 return mValue; 347 } 348 } 349 350 private final class AutofillWindowPresenter extends IAutofillWindowPresenter.Stub { 351 @Override 352 public void show(WindowManager.LayoutParams p, Rect transitionEpicenter, 353 boolean fitsSystemWindows, int layoutDirection) { 354 if (sVerbose) { 355 Slog.v(TAG, "AutofillWindowPresenter.show(): fit=" + fitsSystemWindows 356 + ", epicenter="+ transitionEpicenter + ", dir=" + layoutDirection 357 + ", params=" + p); 358 } 359 UiThread.getHandler().post(() -> mWindow.show(p)); 360 } 361 362 @Override 363 public void hide(Rect transitionEpicenter) { 364 UiThread.getHandler().post(mWindow::hide); 365 } 366 } 367 368 final class AnchoredWindow implements View.OnTouchListener { 369 private final @NonNull OverlayControl mOverlayControl; 370 private final WindowManager mWm; 371 private final View mContentView; 372 private boolean mShowing; 373 374 /** 375 * Constructor. 376 * 377 * @param contentView content of the window 378 */ 379 AnchoredWindow(View contentView, @NonNull OverlayControl overlayControl) { 380 mWm = contentView.getContext().getSystemService(WindowManager.class); 381 mContentView = contentView; 382 mOverlayControl = overlayControl; 383 } 384 385 /** 386 * Shows the window. 387 */ 388 public void show(WindowManager.LayoutParams params) { 389 if (sVerbose) Slog.v(TAG, "show(): showing=" + mShowing + ", params="+ params); 390 try { 391 if (!mShowing) { 392 params.accessibilityTitle = mContentView.getContext() 393 .getString(R.string.autofill_picker_accessibility_title); 394 mWm.addView(mContentView, params); 395 mContentView.setOnTouchListener(this); 396 mOverlayControl.hideOverlays(); 397 mShowing = true; 398 } else { 399 mWm.updateViewLayout(mContentView, params); 400 } 401 } catch (WindowManager.BadTokenException e) { 402 if (sDebug) Slog.d(TAG, "Filed with with token " + params.token + " gone."); 403 mCallback.onDestroy(); 404 } catch (IllegalStateException e) { 405 // WM throws an ISE if mContentView was added twice; this should never happen - 406 // since show() and hide() are always called in the UIThread - but when it does, 407 // it should not crash the system. 408 Slog.e(TAG, "Exception showing window " + params, e); 409 mCallback.onDestroy(); 410 } 411 } 412 413 /** 414 * Hides the window. 415 */ 416 void hide() { 417 try { 418 if (mShowing) { 419 mContentView.setOnTouchListener(null); 420 mWm.removeView(mContentView); 421 mShowing = false; 422 } 423 } catch (IllegalStateException e) { 424 // WM might thrown an ISE when removing the mContentView; this should never 425 // happen - since show() and hide() are always called in the UIThread - but if it 426 // does, it should not crash the system. 427 Slog.e(TAG, "Exception hiding window ", e); 428 mCallback.onDestroy(); 429 } finally { 430 mOverlayControl.showOverlays(); 431 } 432 } 433 434 @Override 435 public boolean onTouch(View view, MotionEvent event) { 436 // When the window is touched outside, hide the window. 437 if (view == mContentView && event.getAction() == MotionEvent.ACTION_OUTSIDE) { 438 mCallback.onCanceled(); 439 return true; 440 } 441 return false; 442 } 443 444 } 445 446 public void dump(PrintWriter pw, String prefix) { 447 pw.print(prefix); pw.print("mCallback: "); pw.println(mCallback != null); 448 pw.print(prefix); pw.print("mListView: "); pw.println(mListView); 449 pw.print(prefix); pw.print("mAdapter: "); pw.println(mAdapter != null); 450 pw.print(prefix); pw.print("mFilterText: "); pw.println(mFilterText); 451 pw.print(prefix); pw.print("mContentWidth: "); pw.println(mContentWidth); 452 pw.print(prefix); pw.print("mContentHeight: "); pw.println(mContentHeight); 453 pw.print(prefix); pw.print("mDestroyed: "); pw.println(mDestroyed); 454 pw.print(prefix); pw.print("mWindow: "); 455 if (mWindow == null) { 456 pw.println("N/A"); 457 } else { 458 final String prefix2 = prefix + " "; 459 pw.println(); 460 pw.print(prefix2); pw.print("showing: "); pw.println(mWindow.mShowing); 461 pw.print(prefix2); pw.print("view: "); pw.println(mWindow.mContentView); 462 pw.print(prefix2); pw.print("screen coordinates: "); 463 if (mWindow.mContentView == null) { 464 pw.println("N/A"); 465 } else { 466 final int[] coordinates = mWindow.mContentView.getLocationOnScreen(); 467 pw.print(coordinates[0]); pw.print("x"); pw.println(coordinates[1]); 468 } 469 } 470 } 471 472 private void announceSearchResultIfNeeded() { 473 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 474 if (mAnnounceFilterResult == null) { 475 mAnnounceFilterResult = new AnnounceFilterResult(); 476 } 477 mAnnounceFilterResult.post(); 478 } 479 } 480 481 private final class ItemsAdapter extends BaseAdapter implements Filterable { 482 private @NonNull final List<ViewItem> mAllItems; 483 484 private @NonNull final List<ViewItem> mFilteredItems = new ArrayList<>(); 485 486 ItemsAdapter(@NonNull List<ViewItem> items) { 487 mAllItems = Collections.unmodifiableList(new ArrayList<>(items)); 488 mFilteredItems.addAll(items); 489 } 490 491 @Override 492 public Filter getFilter() { 493 return new Filter() { 494 @Override 495 protected FilterResults performFiltering(CharSequence constraint) { 496 // No locking needed as mAllItems is final an immutable 497 final FilterResults results = new FilterResults(); 498 if (TextUtils.isEmpty(constraint)) { 499 results.values = mAllItems; 500 results.count = mAllItems.size(); 501 return results; 502 } 503 final List<ViewItem> filteredItems = new ArrayList<>(); 504 final String constraintLowerCase = constraint.toString().toLowerCase(); 505 final int itemCount = mAllItems.size(); 506 for (int i = 0; i < itemCount; i++) { 507 final ViewItem item = mAllItems.get(i); 508 final String value = item.getValue(); 509 // No value, i.e. null, matches any filter 510 if (value == null 511 || value.toLowerCase().startsWith(constraintLowerCase)) { 512 filteredItems.add(item); 513 } 514 } 515 results.values = filteredItems; 516 results.count = filteredItems.size(); 517 return results; 518 } 519 520 @Override 521 protected void publishResults(CharSequence constraint, FilterResults results) { 522 final boolean resultCountChanged; 523 final int oldItemCount = mFilteredItems.size(); 524 mFilteredItems.clear(); 525 @SuppressWarnings("unchecked") 526 final List<ViewItem> items = (List<ViewItem>) results.values; 527 mFilteredItems.addAll(items); 528 resultCountChanged = (oldItemCount != mFilteredItems.size()); 529 if (resultCountChanged) { 530 announceSearchResultIfNeeded(); 531 } 532 notifyDataSetChanged(); 533 } 534 }; 535 } 536 537 @Override 538 public int getCount() { 539 return mFilteredItems.size(); 540 } 541 542 @Override 543 public ViewItem getItem(int position) { 544 return mFilteredItems.get(position); 545 } 546 547 @Override 548 public long getItemId(int position) { 549 return position; 550 } 551 552 @Override 553 public View getView(int position, View convertView, ViewGroup parent) { 554 return getItem(position).getView(); 555 } 556 } 557 558 private final class AnnounceFilterResult implements Runnable { 559 private static final int SEARCH_RESULT_ANNOUNCEMENT_DELAY = 1000; // 1 sec 560 561 public void post() { 562 remove(); 563 mListView.postDelayed(this, SEARCH_RESULT_ANNOUNCEMENT_DELAY); 564 } 565 566 public void remove() { 567 mListView.removeCallbacks(this); 568 } 569 570 @Override 571 public void run() { 572 final int count = mListView.getAdapter().getCount(); 573 final String text; 574 if (count <= 0) { 575 text = mContext.getString(R.string.autofill_picker_no_suggestions); 576 } else { 577 text = mContext.getResources().getQuantityString( 578 R.plurals.autofill_picker_some_suggestions, count, count); 579 } 580 mListView.announceForAccessibility(text); 581 } 582 } 583} 584