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