AutoFillUI.java revision b078212a1eb33d68f821b2aae0b18004e0c7c17e
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 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.content.Context;
24import android.content.IntentSender;
25import android.metrics.LogMaker;
26import android.os.Bundle;
27import android.os.Handler;
28import android.service.autofill.Dataset;
29import android.service.autofill.FillResponse;
30import android.service.autofill.SaveInfo;
31import android.service.autofill.ValueFinder;
32import android.text.TextUtils;
33import android.util.Slog;
34import android.view.autofill.AutofillId;
35import android.view.autofill.AutofillManager;
36import android.view.autofill.IAutofillWindowPresenter;
37import android.widget.Toast;
38
39import com.android.internal.logging.MetricsLogger;
40import com.android.internal.logging.nano.MetricsProto;
41import com.android.server.UiThread;
42
43import java.io.PrintWriter;
44
45/**
46 * Handles all autofill related UI tasks. The UI has two components:
47 * fill UI that shows a popup style window anchored at the focused
48 * input field for choosing a dataset to fill or trigger the response
49 * authentication flow; save UI that shows a toast style window for
50 * managing saving of user edits.
51 */
52public final class AutoFillUI {
53    private static final String TAG = "AutofillUI";
54
55    private final Handler mHandler = UiThread.getHandler();
56    private final @NonNull Context mContext;
57
58    private @Nullable FillUi mFillUi;
59    private @Nullable SaveUi mSaveUi;
60
61    private @Nullable AutoFillUiCallback mCallback;
62
63    private final MetricsLogger mMetricsLogger = new MetricsLogger();
64
65    private final @NonNull OverlayControl mOverlayControl;
66
67    public interface AutoFillUiCallback {
68        void authenticate(int requestId, int datasetIndex, @NonNull IntentSender intent,
69                @Nullable Bundle extras);
70        void fill(int requestId, int datasetIndex, @NonNull Dataset dataset);
71        void save();
72        void cancelSave();
73        void requestShowFillUi(AutofillId id, int width, int height,
74                IAutofillWindowPresenter presenter);
75        void requestHideFillUi(AutofillId id);
76        void startIntentSender(IntentSender intentSender);
77    }
78
79    public AutoFillUI(@NonNull Context context) {
80        mContext = context;
81        mOverlayControl = new OverlayControl(context);
82    }
83
84    public void setCallback(@NonNull AutoFillUiCallback callback) {
85        mHandler.post(() -> {
86            if (mCallback != callback) {
87                if (mCallback != null) {
88                    hideAllUiThread(mCallback);
89                }
90
91                mCallback = callback;
92            }
93        });
94    }
95
96    public void clearCallback(@NonNull AutoFillUiCallback callback) {
97        mHandler.post(() -> {
98            if (mCallback == callback) {
99                hideAllUiThread(callback);
100                mCallback = null;
101            }
102        });
103    }
104
105    /**
106     * Displays an error message to the user.
107     */
108    public void showError(int resId, @NonNull AutoFillUiCallback callback) {
109        showError(mContext.getString(resId), callback);
110    }
111
112    /**
113     * Displays an error message to the user.
114     */
115    public void showError(@Nullable CharSequence message, @NonNull AutoFillUiCallback callback) {
116        Slog.w(TAG, "showError(): " + message);
117
118        mHandler.post(() -> {
119            if (mCallback != callback) {
120                return;
121            }
122            hideAllUiThread(callback);
123            if (!TextUtils.isEmpty(message)) {
124                Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
125            }
126        });
127    }
128
129    /**
130     * Hides the fill UI.
131     */
132    public void hideFillUi(@NonNull AutoFillUiCallback callback) {
133        mHandler.post(() -> hideFillUiUiThread(callback));
134    }
135
136    /**
137     * Filters the options in the fill UI.
138     *
139     * @param filterText The filter prefix.
140     */
141    public void filterFillUi(@Nullable String filterText, @NonNull AutoFillUiCallback callback) {
142        mHandler.post(() -> {
143            if (callback != mCallback) {
144                return;
145            }
146            hideSaveUiUiThread(callback);
147            if (mFillUi != null) {
148                mFillUi.setFilterText(filterText);
149            }
150        });
151    }
152
153    /**
154     * Shows the fill UI, removing the previous fill UI if the has changed.
155     *
156     * @param focusedId the currently focused field
157     * @param response the current fill response
158     * @param filterText text of the view to be filled
159     * @param packageName package name of the activity that is filled
160     * @param callback Identifier for the caller
161     */
162    public void showFillUi(@NonNull AutofillId focusedId, @NonNull FillResponse response,
163            @Nullable String filterText, @NonNull String packageName,
164            @NonNull AutoFillUiCallback callback) {
165        if (sDebug) {
166            final int size = filterText == null ? 0 : filterText.length();
167            Slog.d(TAG, "showFillUi(): id=" + focusedId + ", filter=" + size + " chars");
168        }
169        final LogMaker log = (new LogMaker(MetricsProto.MetricsEvent.AUTOFILL_FILL_UI))
170                .setPackageName(packageName)
171                .addTaggedData(MetricsProto.MetricsEvent.FIELD_AUTOFILL_FILTERTEXT_LEN,
172                        filterText == null ? 0 : filterText.length())
173                .addTaggedData(MetricsProto.MetricsEvent.FIELD_AUTOFILL_NUM_DATASETS,
174                        response.getDatasets() == null ? 0 : response.getDatasets().size());
175
176        mHandler.post(() -> {
177            if (callback != mCallback) {
178                return;
179            }
180            hideAllUiThread(callback);
181            mFillUi = new FillUi(mContext, response, focusedId,
182                    filterText, mOverlayControl, new FillUi.Callback() {
183                @Override
184                public void onResponsePicked(FillResponse response) {
185                    log.setType(MetricsProto.MetricsEvent.TYPE_DETAIL);
186                    hideFillUiUiThread(callback);
187                    if (mCallback != null) {
188                        mCallback.authenticate(response.getRequestId(),
189                                AutofillManager.AUTHENTICATION_ID_DATASET_ID_UNDEFINED,
190                                response.getAuthentication(), response.getClientState());
191                    }
192                }
193
194                @Override
195                public void onDatasetPicked(Dataset dataset) {
196                    log.setType(MetricsProto.MetricsEvent.TYPE_ACTION);
197                    hideFillUiUiThread(callback);
198                    if (mCallback != null) {
199                        final int datasetIndex = response.getDatasets().indexOf(dataset);
200                        mCallback.fill(response.getRequestId(), datasetIndex, dataset);
201                    }
202                }
203
204                @Override
205                public void onCanceled() {
206                    log.setType(MetricsProto.MetricsEvent.TYPE_DISMISS);
207                    hideFillUiUiThread(callback);
208                }
209
210                @Override
211                public void onDestroy() {
212                    if (log.getType() == MetricsProto.MetricsEvent.TYPE_UNKNOWN) {
213                        log.setType(MetricsProto.MetricsEvent.TYPE_CLOSE);
214                    }
215                    mMetricsLogger.write(log);
216                }
217
218                @Override
219                public void requestShowFillUi(int width, int height,
220                        IAutofillWindowPresenter windowPresenter) {
221                    if (mCallback != null) {
222                        mCallback.requestShowFillUi(focusedId, width, height, windowPresenter);
223                    }
224                }
225
226                @Override
227                public void requestHideFillUi() {
228                    if (mCallback != null) {
229                        mCallback.requestHideFillUi(focusedId);
230                    }
231                }
232
233                @Override
234                public void startIntentSender(IntentSender intentSender) {
235                    if (mCallback != null) {
236                        mCallback.startIntentSender(intentSender);
237                    }
238                }
239            });
240        });
241    }
242
243    /**
244     * Shows the UI asking the user to save for autofill.
245     */
246    public void showSaveUi(@NonNull CharSequence providerLabel, @NonNull SaveInfo info,
247            @NonNull ValueFinder valueFinder, @NonNull String packageName,
248            @NonNull AutoFillUiCallback callback) {
249        if (sVerbose) Slog.v(TAG, "showSaveUi() for " + packageName + ": " + info);
250        int numIds = 0;
251        numIds += info.getRequiredIds() == null ? 0 : info.getRequiredIds().length;
252        numIds += info.getOptionalIds() == null ? 0 : info.getOptionalIds().length;
253
254        LogMaker log = (new LogMaker(MetricsProto.MetricsEvent.AUTOFILL_SAVE_UI))
255                .setPackageName(packageName).addTaggedData(
256                        MetricsProto.MetricsEvent.FIELD_AUTOFILL_NUM_IDS, numIds);
257
258        mHandler.post(() -> {
259            if (callback != mCallback) {
260                return;
261            }
262            hideAllUiThread(callback);
263            mSaveUi = new SaveUi(mContext, providerLabel, info, valueFinder, mOverlayControl,
264                    new SaveUi.OnSaveListener() {
265                @Override
266                public void onSave() {
267                    log.setType(MetricsProto.MetricsEvent.TYPE_ACTION);
268                    hideSaveUiUiThread(callback);
269                    if (mCallback != null) {
270                        mCallback.save();
271                    }
272                }
273
274                @Override
275                public void onCancel(IntentSender listener) {
276                    log.setType(MetricsProto.MetricsEvent.TYPE_DISMISS);
277                    hideSaveUiUiThread(callback);
278                    if (listener != null) {
279                        try {
280                            listener.sendIntent(mContext, 0, null, null, null);
281                        } catch (IntentSender.SendIntentException e) {
282                            Slog.e(TAG, "Error starting negative action listener: "
283                                    + listener, e);
284                        }
285                    }
286                    if (mCallback != null) {
287                        mCallback.cancelSave();
288                    }
289                }
290
291                @Override
292                public void onDestroy() {
293                    if (log.getType() == MetricsProto.MetricsEvent.TYPE_UNKNOWN) {
294                        log.setType(MetricsProto.MetricsEvent.TYPE_CLOSE);
295
296                        if (mCallback != null) {
297                            mCallback.cancelSave();
298                        }
299                    }
300                    mMetricsLogger.write(log);
301                }
302            });
303        });
304    }
305
306    /**
307     * Hides all UI affordances.
308     */
309    public void hideAll(@Nullable AutoFillUiCallback callback) {
310        mHandler.post(() -> hideAllUiThread(callback));
311    }
312
313    public void dump(PrintWriter pw) {
314        pw.println("Autofill UI");
315        final String prefix = "  ";
316        final String prefix2 = "    ";
317        if (mFillUi != null) {
318            pw.print(prefix); pw.println("showsFillUi: true");
319            mFillUi.dump(pw, prefix2);
320        } else {
321            pw.print(prefix); pw.println("showsFillUi: false");
322        }
323        if (mSaveUi != null) {
324            pw.print(prefix); pw.println("showsSaveUi: true");
325            mSaveUi.dump(pw, prefix2);
326        } else {
327            pw.print(prefix); pw.println("showsSaveUi: false");
328        }
329    }
330
331    @android.annotation.UiThread
332    private void hideFillUiUiThread(@Nullable AutoFillUiCallback callback) {
333        if (mFillUi != null && (callback == null || callback == mCallback)) {
334            mFillUi.destroy();
335            mFillUi = null;
336        }
337    }
338
339    @android.annotation.UiThread
340    private void hideSaveUiUiThread(@Nullable AutoFillUiCallback callback) {
341        if (sVerbose) {
342            Slog.v(TAG, "hideSaveUiUiThread(): mSaveUi=" + mSaveUi + ", callback=" + callback
343                    + ", mCallback=" + mCallback);
344        }
345        if (mSaveUi != null && (callback == null || callback == mCallback)) {
346            mSaveUi.destroy();
347            mSaveUi = null;
348        }
349    }
350
351    @android.annotation.UiThread
352    private void hideAllUiThread(@Nullable AutoFillUiCallback callback) {
353        hideFillUiUiThread(callback);
354        hideSaveUiUiThread(callback);
355    }
356}
357