AutoFillUI.java revision 2aedac13b71fc4f7546bc73adaef32f51b84be68
1/*
2 * Copyright (C) 2016 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 android.view.autofill.AutofillManager.AutofillCallback.EVENT_INPUT_HIDDEN;
19import static android.view.autofill.AutofillManager.AutofillCallback.EVENT_INPUT_SHOWN;
20
21import android.annotation.NonNull;
22import android.annotation.Nullable;
23import android.content.Context;
24import android.content.IntentSender;
25import android.graphics.Rect;
26import android.os.Handler;
27import android.os.IBinder;
28import android.service.autofill.Dataset;
29import android.service.autofill.FillResponse;
30import android.service.autofill.SaveInfo;
31import android.text.TextUtils;
32import android.text.format.DateUtils;
33import android.util.Slog;
34import android.view.autofill.AutofillId;
35import android.widget.Toast;
36
37import com.android.server.UiThread;
38
39import java.io.PrintWriter;
40
41/**
42 * Handles all autofill related UI tasks. The UI has two components:
43 * fill UI that shows a popup style window anchored at the focused
44 * input field for choosing a dataset to fill or trigger the response
45 * authentication flow; save UI that shows a toast style window for
46 * managing saving of user edits.
47 */
48public final class AutoFillUI {
49    private static final String TAG = "AutoFillUI";
50
51    private static final int MAX_SAVE_TIMEOUT_MS = (int) (30 * DateUtils.SECOND_IN_MILLIS);
52
53    private final Handler mHandler = UiThread.getHandler();
54    private final @NonNull Context mContext;
55
56    private @Nullable FillUi mFillUi;
57    private @Nullable SaveUi mSaveUi;
58
59    private @Nullable AutoFillUiCallback mCallback;
60    private @Nullable IBinder mWindowToken;
61
62    private int mSaveTimeoutMs = (int) (5 * DateUtils.SECOND_IN_MILLIS);
63
64    public interface AutoFillUiCallback {
65        void authenticate(@NonNull IntentSender intent);
66        void fill(@NonNull Dataset dataset);
67        void save();
68        void cancelSave();
69        void onEvent(AutofillId id, int event);
70    }
71
72    public AutoFillUI(@NonNull Context context) {
73        mContext = context;
74    }
75
76    public void setCallback(@Nullable AutoFillUiCallback callback,
77            @Nullable IBinder windowToken) {
78        mHandler.post(() -> {
79            if (mCallback != callback || mWindowToken != windowToken) {
80                hideAllUiThread();
81                mCallback = callback;
82                mWindowToken = windowToken;
83            }
84        });
85    }
86
87    /**
88     * Displays an error message to the user.
89     */
90    public void showError(@Nullable CharSequence message) {
91        mHandler.post(() -> {
92            if (!hasCallback()) {
93                return;
94            }
95            hideAllUiThread();
96            if (!TextUtils.isEmpty(message)) {
97                Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
98            }
99        });
100    }
101
102    /**
103     * Hides the fill UI.
104     */
105    public void hideFillUi(AutofillId id) {
106        mHandler.post(() -> {
107            hideFillUiUiThread();
108            if (mCallback != null) {
109                mCallback.onEvent(id, EVENT_INPUT_HIDDEN);
110            }
111        });
112    }
113
114    /**
115     * Filters the options in the fill UI.
116     *
117     * @param filterText The filter prefix.
118     */
119    public void filterFillUi(@Nullable String filterText) {
120        mHandler.post(() -> {
121            if (!hasCallback()) {
122                return;
123            }
124            hideSaveUiUiThread();
125            if (mFillUi != null) {
126                mFillUi.setFilterText(filterText);
127            }
128        });
129    }
130
131    /**
132     * Updates the position of the fill UI.
133     *
134     * @param anchoredBounds The bounds of the anchor view.
135     */
136    public void updateFillUi(@NonNull Rect anchoredBounds) {
137        mHandler.post(() -> {
138            if (!hasCallback()) {
139                return;
140            }
141            hideSaveUiUiThread();
142            if (mFillUi != null) {
143                mFillUi.update(anchoredBounds);
144            }
145        });
146    }
147
148    /**
149     * Shows the fill UI, removing the previous fill UI if the has changed.
150     *
151     * @param focusedId the currently focused field
152     * @param response the current fill response
153     * @param anchorBounds bounds of the focused view
154     * @param filterText text of the view to be filled
155     */
156    public void showFillUi(@NonNull AutofillId focusedId, @NonNull FillResponse response,
157            @NonNull Rect anchorBounds, @Nullable String filterText) {
158        mHandler.post(() -> {
159            if (!hasCallback()) {
160                return;
161            }
162            hideAllUiThread();
163            mFillUi = new FillUi(mContext, response, focusedId,
164                    mWindowToken, anchorBounds, filterText, new FillUi.Callback() {
165                @Override
166                public void onResponsePicked(FillResponse response) {
167                    hideFillUiUiThread();
168                    if (mCallback != null) {
169                        mCallback.authenticate(response.getAuthentication());
170                    }
171                }
172
173                @Override
174                public void onDatasetPicked(Dataset dataset) {
175                    hideFillUiUiThread();
176                    if (mCallback != null) {
177                        mCallback.fill(dataset);
178                    }
179                    // TODO(b/33197203): add MetricsLogger call
180                }
181
182                @Override
183                public void onCanceled() {
184                    hideFillUiUiThread();
185                    // TODO(b/33197203): add MetricsLogger call
186                }
187            });
188            mCallback.onEvent(focusedId, EVENT_INPUT_SHOWN);
189        });
190    }
191
192    /**
193     * Shows the UI asking the user to save for autofill.
194     */
195    public void showSaveUi(@NonNull CharSequence providerLabel, @NonNull SaveInfo info) {
196        mHandler.post(() -> {
197            if (!hasCallback()) {
198                return;
199            }
200            hideAllUiThread();
201            mSaveUi = new SaveUi(mContext, providerLabel, info,
202                    new SaveUi.OnSaveListener() {
203                @Override
204                public void onSave() {
205                    hideSaveUiUiThread();
206                    if (mCallback != null) {
207                        mCallback.save();
208                    }
209                    // TODO(b/33197203): add MetricsLogger call
210                }
211
212                @Override
213                public void onCancel(IntentSender listener) {
214                    // TODO(b/33197203): add MetricsLogger call
215                    hideSaveUiUiThread();
216                    if (listener != null) {
217                        try {
218                            listener.sendIntent(mContext, 0, null, null, null);
219                        } catch (IntentSender.SendIntentException e) {
220                            Slog.e(TAG, "Error starting negative action listener: "
221                                    + listener, e);
222                        }
223                    }
224                    if (mCallback != null) {
225                        mCallback.cancelSave();
226                    }
227                }
228            }, mSaveTimeoutMs);
229        });
230    }
231
232    /**
233     * Hides all UI affordances.
234     */
235    public void hideAll() {
236        mHandler.post(this::hideAllUiThread);
237    }
238
239    public void setSaveTimeout(int timeout) {
240        if (timeout > MAX_SAVE_TIMEOUT_MS) {
241            throw new IllegalArgumentException("Maximum value is " + MAX_SAVE_TIMEOUT_MS + "ms");
242        }
243        if (timeout <= 0) {
244            throw new IllegalArgumentException("Must be a positive value");
245        }
246        mSaveTimeoutMs = timeout;
247    }
248
249    public void dump(PrintWriter pw) {
250        pw.println("AufoFill UI");
251        final String prefix = "  ";
252        pw.print(prefix); pw.print("showsFillUi: "); pw.println(mFillUi != null);
253        pw.print(prefix); pw.print("showsSaveUi: "); pw.println(mSaveUi != null);
254        pw.print(prefix); pw.print("save timeout: "); pw.println(mSaveTimeoutMs);
255    }
256
257    @android.annotation.UiThread
258    private void hideFillUiUiThread() {
259        if (mFillUi != null) {
260            mFillUi.destroy();
261            mFillUi = null;
262        }
263    }
264
265    @android.annotation.UiThread
266    private void hideSaveUiUiThread() {
267        if (mSaveUi != null) {
268            mSaveUi.destroy();
269            mSaveUi = null;
270        }
271    }
272
273    @android.annotation.UiThread
274    private void hideAllUiThread() {
275        hideFillUiUiThread();
276        hideSaveUiUiThread();
277    }
278
279    private boolean hasCallback() {
280        return mCallback != null;
281    }
282}
283