FillUi.java revision 2e3b0c11c3290ff0a31d562a4c192fe3536c2cd8
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; 19 20import android.annotation.NonNull; 21import android.annotation.Nullable; 22import android.app.PendingIntent; 23import android.content.Context; 24import android.content.Intent; 25import android.content.IntentSender; 26import android.graphics.Point; 27import android.graphics.Rect; 28import android.service.autofill.Dataset; 29import android.service.autofill.FillResponse; 30import android.util.Slog; 31import android.util.TypedValue; 32import android.view.LayoutInflater; 33import android.view.MotionEvent; 34import android.view.View; 35import android.view.View.MeasureSpec; 36import android.view.ViewGroup; 37import android.view.WindowManager; 38import android.view.autofill.AutofillId; 39import android.view.autofill.AutofillValue; 40import android.view.autofill.IAutofillWindowPresenter; 41import android.widget.ArrayAdapter; 42import android.widget.ListView; 43import android.widget.RemoteViews; 44 45import com.android.internal.R; 46import com.android.server.UiThread; 47import libcore.util.Objects; 48 49import java.io.PrintWriter; 50import java.util.ArrayList; 51 52final class FillUi { 53 private static final String TAG = "FillUi"; 54 55 private static final int VISIBLE_OPTIONS_MAX_COUNT = 3; 56 57 private static final TypedValue sTempTypedValue = new TypedValue(); 58 59 interface Callback { 60 void onResponsePicked(@NonNull FillResponse response); 61 void onDatasetPicked(@NonNull Dataset dataset); 62 void onCanceled(); 63 void onDestroy(); 64 void requestShowFillUi(int width, int height, 65 IAutofillWindowPresenter windowPresenter); 66 void requestHideFillUi(); 67 void startIntentSender(IntentSender intentSender); 68 } 69 70 private final @NonNull Point mTempPoint = new Point(); 71 72 private final @NonNull AutofillWindowPresenter mWindowPresenter = 73 new AutofillWindowPresenter(); 74 75 private final @NonNull Context mContext; 76 77 private final @NonNull AnchoredWindow mWindow; 78 79 private final @NonNull Callback mCallback; 80 81 private final @NonNull ListView mListView; 82 83 private final @Nullable ArrayAdapter<ViewItem> mAdapter; 84 85 private @Nullable String mFilterText; 86 87 private int mContentWidth; 88 private int mContentHeight; 89 90 private boolean mDestroyed; 91 92 FillUi(@NonNull Context context, @NonNull FillResponse response, 93 @NonNull AutofillId focusedViewId, @NonNull @Nullable String filterText, 94 @NonNull Callback callback) { 95 mContext = context; 96 mCallback = callback; 97 98 final LayoutInflater inflater = LayoutInflater.from(context); 99 final ViewGroup decor = (ViewGroup) inflater.inflate( 100 R.layout.autofill_dataset_picker, null); 101 102 final RemoteViews.OnClickHandler interceptionHandler = new RemoteViews.OnClickHandler() { 103 @Override 104 public boolean onClickHandler(View view, PendingIntent pendingIntent, 105 Intent fillInIntent) { 106 if (pendingIntent != null) { 107 mCallback.startIntentSender(pendingIntent.getIntentSender()); 108 } 109 return true; 110 } 111 }; 112 113 if (response.getAuthentication() != null) { 114 mListView = null; 115 mAdapter = null; 116 117 final View content; 118 try { 119 content = response.getPresentation().apply(context, decor, interceptionHandler); 120 decor.addView(content); 121 } catch (RuntimeException e) { 122 callback.onCanceled(); 123 Slog.e(TAG, "Error inflating remote views", e); 124 mWindow = null; 125 return; 126 } 127 128 Point maxSize = mTempPoint; 129 resolveMaxWindowSize(context, maxSize); 130 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(maxSize.x, 131 MeasureSpec.AT_MOST); 132 final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxSize.y, 133 MeasureSpec.AT_MOST); 134 135 decor.measure(widthMeasureSpec, heightMeasureSpec); 136 decor.setOnClickListener(v -> mCallback.onResponsePicked(response)); 137 mContentWidth = content.getMeasuredWidth(); 138 mContentHeight = content.getMeasuredHeight(); 139 140 mWindow = new AnchoredWindow(decor); 141 mCallback.requestShowFillUi(mContentWidth, mContentHeight, mWindowPresenter); 142 } else { 143 final int datasetCount = response.getDatasets().size(); 144 final ArrayList<ViewItem> items = new ArrayList<>(datasetCount); 145 for (int i = 0; i < datasetCount; i++) { 146 final Dataset dataset = response.getDatasets().get(i); 147 final int index = dataset.getFieldIds().indexOf(focusedViewId); 148 if (index >= 0) { 149 final RemoteViews presentation = dataset.getFieldPresentation(index); 150 final View view; 151 try { 152 view = presentation.apply(context, null, interceptionHandler); 153 } catch (RuntimeException e) { 154 Slog.e(TAG, "Error inflating remote views", e); 155 continue; 156 } 157 final AutofillValue value = dataset.getFieldValues().get(index); 158 String valueText = null; 159 if (value.isText()) { 160 valueText = value.getTextValue().toString().toLowerCase(); 161 } 162 163 items.add(new ViewItem(dataset, valueText, view)); 164 } 165 } 166 167 mAdapter = new ArrayAdapter<ViewItem>(context, 0, items) { 168 @Override 169 public View getView(int position, View convertView, ViewGroup parent) { 170 return getItem(position).getView(); 171 } 172 }; 173 174 mListView = decor.findViewById(R.id.autofill_dataset_list); 175 mListView.setAdapter(mAdapter); 176 mListView.setVisibility(View.VISIBLE); 177 mListView.setOnItemClickListener((adapter, view, position, id) -> { 178 final ViewItem vi = mAdapter.getItem(position); 179 mCallback.onDatasetPicked(vi.getDataset()); 180 }); 181 182 if (filterText == null) { 183 mFilterText = null; 184 } else { 185 mFilterText = filterText.toLowerCase(); 186 } 187 188 applyNewFilterText(); 189 mWindow = new AnchoredWindow(decor); 190 } 191 } 192 193 private void applyNewFilterText() { 194 final int oldCount = mAdapter.getCount(); 195 mAdapter.getFilter().filter(mFilterText, (count) -> { 196 if (mDestroyed) { 197 return; 198 } 199 if (count <= 0) { 200 mCallback.requestHideFillUi(); 201 } else { 202 if (updateContentSize()) { 203 mCallback.requestShowFillUi(mContentWidth, mContentHeight, mWindowPresenter); 204 } 205 if (mAdapter.getCount() > VISIBLE_OPTIONS_MAX_COUNT) { 206 mListView.setVerticalScrollBarEnabled(true); 207 mListView.onVisibilityAggregated(true); 208 } else { 209 mListView.setVerticalScrollBarEnabled(false); 210 } 211 if (mAdapter.getCount() != oldCount) { 212 mListView.requestLayout(); 213 } 214 } 215 }); 216 } 217 218 public void setFilterText(@Nullable String filterText) { 219 throwIfDestroyed(); 220 if (mAdapter == null) { 221 return; 222 } 223 224 if (filterText == null) { 225 filterText = null; 226 } else { 227 filterText = filterText.toLowerCase(); 228 } 229 230 if (Objects.equal(mFilterText, filterText)) { 231 return; 232 } 233 mFilterText = filterText; 234 235 applyNewFilterText(); 236 } 237 238 public void destroy() { 239 throwIfDestroyed(); 240 mCallback.onDestroy(); 241 mCallback.requestHideFillUi(); 242 mDestroyed = true; 243 } 244 245 private boolean updateContentSize() { 246 if (mAdapter == null) { 247 return false; 248 } 249 boolean changed = false; 250 if (mAdapter.getCount() <= 0) { 251 if (mContentWidth != 0) { 252 mContentWidth = 0; 253 changed = true; 254 } 255 if (mContentHeight != 0) { 256 mContentHeight = 0; 257 changed = true; 258 } 259 return changed; 260 } 261 262 Point maxSize = mTempPoint; 263 resolveMaxWindowSize(mContext, maxSize); 264 265 mContentWidth = 0; 266 mContentHeight = 0; 267 268 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(maxSize.x, 269 MeasureSpec.AT_MOST); 270 final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxSize.y, 271 MeasureSpec.AT_MOST); 272 273 final int itemCount = Math.min(mAdapter.getCount(), VISIBLE_OPTIONS_MAX_COUNT); 274 for (int i = 0; i < itemCount; i++) { 275 View view = mAdapter.getItem(i).getView(); 276 view.measure(widthMeasureSpec, heightMeasureSpec); 277 final int clampedMeasuredWidth = Math.min(view.getMeasuredWidth(), maxSize.x); 278 final int newContentWidth = Math.max(mContentWidth, clampedMeasuredWidth); 279 if (newContentWidth != mContentWidth) { 280 mContentWidth = newContentWidth; 281 changed = true; 282 } 283 final int clampedMeasuredHeight = Math.min(view.getMeasuredHeight(), maxSize.y); 284 final int newContentHeight = mContentHeight + clampedMeasuredHeight; 285 if (newContentHeight != mContentHeight) { 286 mContentHeight = newContentHeight; 287 changed = true; 288 } 289 } 290 return changed; 291 } 292 293 private void throwIfDestroyed() { 294 if (mDestroyed) { 295 throw new IllegalStateException("cannot interact with a destroyed instance"); 296 } 297 } 298 299 private static void resolveMaxWindowSize(Context context, Point outPoint) { 300 context.getDisplay().getSize(outPoint); 301 TypedValue typedValue = sTempTypedValue; 302 context.getTheme().resolveAttribute(R.attr.autofillDatasetPickerMaxWidth, 303 typedValue, true); 304 outPoint.x = (int) typedValue.getFraction(outPoint.x, outPoint.x); 305 context.getTheme().resolveAttribute(R.attr.autofillDatasetPickerMaxHeight, 306 typedValue, true); 307 outPoint.y = (int) typedValue.getFraction(outPoint.y, outPoint.y); 308 } 309 310 private static class ViewItem { 311 private final String mValue; 312 private final Dataset mDataset; 313 private final View mView; 314 315 ViewItem(Dataset dataset, String value, View view) { 316 mDataset = dataset; 317 mValue = value; 318 mView = view; 319 } 320 321 public View getView() { 322 return mView; 323 } 324 325 public Dataset getDataset() { 326 return mDataset; 327 } 328 329 @Override 330 public String toString() { 331 // Used for filtering in the adapter 332 return mValue; 333 } 334 } 335 336 private final class AutofillWindowPresenter extends IAutofillWindowPresenter.Stub { 337 @Override 338 public void show(WindowManager.LayoutParams p, Rect transitionEpicenter, 339 boolean fitsSystemWindows, int layoutDirection) { 340 UiThread.getHandler().post(() -> mWindow.show(p)); 341 } 342 343 @Override 344 public void hide(Rect transitionEpicenter) { 345 UiThread.getHandler().post(mWindow::hide); 346 } 347 } 348 349 final class AnchoredWindow implements View.OnTouchListener { 350 private final WindowManager mWm; 351 private final View mContentView; 352 private boolean mShowing; 353 354 /** 355 * Constructor. 356 * 357 * @param contentView content of the window 358 */ 359 AnchoredWindow(View contentView) { 360 mWm = contentView.getContext().getSystemService(WindowManager.class); 361 mContentView = contentView; 362 } 363 364 /** 365 * Shows the window. 366 */ 367 public void show(WindowManager.LayoutParams params) { 368 try { 369 if (!mShowing) { 370 params.accessibilityTitle = mContentView.getContext() 371 .getString(R.string.autofill_picker_accessibility_title); 372 mWm.addView(mContentView, params); 373 mContentView.setOnTouchListener(this); 374 mShowing = true; 375 } else { 376 mWm.updateViewLayout(mContentView, params); 377 } 378 } catch (WindowManager.BadTokenException e) { 379 if (sDebug) Slog.d(TAG, "Filed with with token " + params.token + " gone."); 380 mCallback.onDestroy(); 381 } 382 } 383 384 /** 385 * Hides the window. 386 */ 387 void hide() { 388 if (mShowing) { 389 mContentView.setOnTouchListener(null); 390 mWm.removeView(mContentView); 391 mShowing = false; 392 } 393 } 394 395 @Override 396 public boolean onTouch(View view, MotionEvent event) { 397 // When the window is touched outside, hide the window. 398 if (view == mContentView && event.getAction() == MotionEvent.ACTION_OUTSIDE) { 399 mCallback.onCanceled(); 400 return true; 401 } 402 return false; 403 } 404 } 405 406 public void dump(PrintWriter pw, String prefix) { 407 pw.print(prefix); pw.print("mCallback: "); pw.println(mCallback != null); 408 pw.print(prefix); pw.print("mListView: "); pw.println(mListView); 409 pw.print(prefix); pw.print("mAdapter: "); pw.println(mAdapter != null); 410 pw.print(prefix); pw.print("mFilterText: "); pw.println(mFilterText); 411 pw.print(prefix); pw.print("mContentWidth: "); pw.println(mContentWidth); 412 pw.print(prefix); pw.print("mContentHeight: "); pw.println(mContentHeight); 413 pw.print(prefix); pw.print("mDestroyed: "); pw.println(mDestroyed); 414 } 415} 416