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