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  */
16 package com.android.server.autofill.ui;
17 
18 import static com.android.server.autofill.Helper.sDebug;
19 import static com.android.server.autofill.Helper.sVerbose;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.content.ComponentName;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.IntentSender;
27 import android.graphics.drawable.Drawable;
28 import android.metrics.LogMaker;
29 import android.os.Bundle;
30 import android.os.Handler;
31 import android.os.IBinder;
32 import android.os.RemoteException;
33 import android.service.autofill.Dataset;
34 import android.service.autofill.FillResponse;
35 import android.service.autofill.SaveInfo;
36 import android.service.autofill.ValueFinder;
37 import android.text.TextUtils;
38 import android.util.Slog;
39 import android.view.KeyEvent;
40 import android.view.autofill.AutofillId;
41 import android.view.autofill.AutofillManager;
42 import android.view.autofill.IAutofillWindowPresenter;
43 import android.widget.Toast;
44 
45 import com.android.internal.logging.MetricsLogger;
46 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
47 import com.android.server.LocalServices;
48 import com.android.server.UiModeManagerInternal;
49 import com.android.server.UiThread;
50 import com.android.server.autofill.Helper;
51 
52 import java.io.PrintWriter;
53 
54 /**
55  * Handles all autofill related UI tasks. The UI has two components:
56  * fill UI that shows a popup style window anchored at the focused
57  * input field for choosing a dataset to fill or trigger the response
58  * authentication flow; save UI that shows a toast style window for
59  * managing saving of user edits.
60  */
61 public final class AutoFillUI {
62     private static final String TAG = "AutofillUI";
63 
64     private final Handler mHandler = UiThread.getHandler();
65     private final @NonNull Context mContext;
66 
67     private @Nullable FillUi mFillUi;
68     private @Nullable SaveUi mSaveUi;
69 
70     private @Nullable AutoFillUiCallback mCallback;
71 
72     private final MetricsLogger mMetricsLogger = new MetricsLogger();
73 
74     private final @NonNull OverlayControl mOverlayControl;
75     private final @NonNull UiModeManagerInternal mUiModeMgr;
76 
77     private @Nullable Runnable mCreateFillUiRunnable;
78     private @Nullable AutoFillUiCallback mSaveUiCallback;
79 
80     public interface AutoFillUiCallback {
authenticate(int requestId, int datasetIndex, @NonNull IntentSender intent, @Nullable Bundle extras, boolean authenticateInline)81         void authenticate(int requestId, int datasetIndex, @NonNull IntentSender intent,
82                 @Nullable Bundle extras, boolean authenticateInline);
fill(int requestId, int datasetIndex, @NonNull Dataset dataset)83         void fill(int requestId, int datasetIndex, @NonNull Dataset dataset);
save()84         void save();
cancelSave()85         void cancelSave();
requestShowFillUi(AutofillId id, int width, int height, IAutofillWindowPresenter presenter)86         void requestShowFillUi(AutofillId id, int width, int height,
87                 IAutofillWindowPresenter presenter);
requestHideFillUi(AutofillId id)88         void requestHideFillUi(AutofillId id);
startIntentSenderAndFinishSession(IntentSender intentSender)89         void startIntentSenderAndFinishSession(IntentSender intentSender);
startIntentSender(IntentSender intentSender, Intent intent)90         void startIntentSender(IntentSender intentSender, Intent intent);
dispatchUnhandledKey(AutofillId id, KeyEvent keyEvent)91         void dispatchUnhandledKey(AutofillId id, KeyEvent keyEvent);
cancelSession()92         void cancelSession();
93     }
94 
AutoFillUI(@onNull Context context)95     public AutoFillUI(@NonNull Context context) {
96         mContext = context;
97         mOverlayControl = new OverlayControl(context);
98         mUiModeMgr = LocalServices.getService(UiModeManagerInternal.class);
99     }
100 
setCallback(@onNull AutoFillUiCallback callback)101     public void setCallback(@NonNull AutoFillUiCallback callback) {
102         mHandler.post(() -> {
103             if (mCallback != callback) {
104                 if (mCallback != null) {
105                     if (isSaveUiShowing()) {
106                         // keeps showing the save UI
107                         hideFillUiUiThread(callback, true);
108                     } else {
109                         hideAllUiThread(mCallback);
110                     }
111                 }
112                 mCallback = callback;
113             }
114         });
115     }
116 
clearCallback(@onNull AutoFillUiCallback callback)117     public void clearCallback(@NonNull AutoFillUiCallback callback) {
118         mHandler.post(() -> {
119             if (mCallback == callback) {
120                 hideAllUiThread(callback);
121                 mCallback = null;
122             }
123         });
124     }
125 
126     /**
127      * Displays an error message to the user.
128      */
showError(int resId, @NonNull AutoFillUiCallback callback)129     public void showError(int resId, @NonNull AutoFillUiCallback callback) {
130         showError(mContext.getString(resId), callback);
131     }
132 
133     /**
134      * Displays an error message to the user.
135      */
showError(@ullable CharSequence message, @NonNull AutoFillUiCallback callback)136     public void showError(@Nullable CharSequence message, @NonNull AutoFillUiCallback callback) {
137         Slog.w(TAG, "showError(): " + message);
138 
139         mHandler.post(() -> {
140             if (mCallback != callback) {
141                 return;
142             }
143             hideAllUiThread(callback);
144             if (!TextUtils.isEmpty(message)) {
145                 Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
146             }
147         });
148     }
149 
150     /**
151      * Hides the fill UI.
152      */
hideFillUi(@onNull AutoFillUiCallback callback)153     public void hideFillUi(@NonNull AutoFillUiCallback callback) {
154         mHandler.post(() -> hideFillUiUiThread(callback, true));
155     }
156 
157     /**
158      * Filters the options in the fill UI.
159      *
160      * @param filterText The filter prefix.
161      */
filterFillUi(@ullable String filterText, @NonNull AutoFillUiCallback callback)162     public void filterFillUi(@Nullable String filterText, @NonNull AutoFillUiCallback callback) {
163         mHandler.post(() -> {
164             if (callback != mCallback) {
165                 return;
166             }
167             if (mFillUi != null) {
168                 mFillUi.setFilterText(filterText);
169             }
170         });
171     }
172 
173     /**
174      * Shows the fill UI, removing the previous fill UI if the has changed.
175      *
176      * @param focusedId the currently focused field
177      * @param response the current fill response
178      * @param filterText text of the view to be filled
179      * @param servicePackageName package name of the autofill service filling the activity
180      * @param componentName component name of the activity that is filled
181      * @param serviceLabel label of autofill service
182      * @param serviceIcon icon of autofill service
183      * @param callback identifier for the caller
184      * @param sessionId id of the autofill session
185      * @param compatMode whether the app is being autofilled in compatibility mode.
186      */
showFillUi(@onNull AutofillId focusedId, @NonNull FillResponse response, @Nullable String filterText, @Nullable String servicePackageName, @NonNull ComponentName componentName, @NonNull CharSequence serviceLabel, @NonNull Drawable serviceIcon, @NonNull AutoFillUiCallback callback, int sessionId, boolean compatMode)187     public void showFillUi(@NonNull AutofillId focusedId, @NonNull FillResponse response,
188             @Nullable String filterText, @Nullable String servicePackageName,
189             @NonNull ComponentName componentName, @NonNull CharSequence serviceLabel,
190             @NonNull Drawable serviceIcon, @NonNull AutoFillUiCallback callback, int sessionId,
191             boolean compatMode) {
192         if (sDebug) {
193             final int size = filterText == null ? 0 : filterText.length();
194             Slog.d(TAG, "showFillUi(): id=" + focusedId + ", filter=" + size + " chars");
195         }
196         final LogMaker log = Helper
197                 .newLogMaker(MetricsEvent.AUTOFILL_FILL_UI, componentName, servicePackageName,
198                         sessionId, compatMode)
199                 .addTaggedData(MetricsEvent.FIELD_AUTOFILL_FILTERTEXT_LEN,
200                         filterText == null ? 0 : filterText.length())
201                 .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_DATASETS,
202                         response.getDatasets() == null ? 0 : response.getDatasets().size());
203 
204         final Runnable createFillUiRunnable = () -> {
205             if (callback != mCallback) {
206                 return;
207             }
208             hideAllUiThread(callback);
209             mFillUi = new FillUi(mContext, response, focusedId,
210                     filterText, mOverlayControl, serviceLabel, serviceIcon,
211                     mUiModeMgr.isNightMode(),
212                     new FillUi.Callback() {
213                 @Override
214                 public void onResponsePicked(FillResponse response) {
215                     log.setType(MetricsEvent.TYPE_DETAIL);
216                     hideFillUiUiThread(callback, true);
217                     if (mCallback != null) {
218                         mCallback.authenticate(response.getRequestId(),
219                                 AutofillManager.AUTHENTICATION_ID_DATASET_ID_UNDEFINED,
220                                 response.getAuthentication(), response.getClientState(),
221                                 /* authenticateInline= */ false);
222                     }
223                 }
224 
225                 @Override
226                 public void onDatasetPicked(Dataset dataset) {
227                     log.setType(MetricsEvent.TYPE_ACTION);
228                     hideFillUiUiThread(callback, true);
229                     if (mCallback != null) {
230                         final int datasetIndex = response.getDatasets().indexOf(dataset);
231                         mCallback.fill(response.getRequestId(), datasetIndex, dataset);
232                     }
233                 }
234 
235                 @Override
236                 public void onCanceled() {
237                     log.setType(MetricsEvent.TYPE_DISMISS);
238                     hideFillUiUiThread(callback, true);
239                 }
240 
241                 @Override
242                 public void onDestroy() {
243                     if (log.getType() == MetricsEvent.TYPE_UNKNOWN) {
244                         log.setType(MetricsEvent.TYPE_CLOSE);
245                     }
246                     mMetricsLogger.write(log);
247                 }
248 
249                 @Override
250                 public void requestShowFillUi(int width, int height,
251                         IAutofillWindowPresenter windowPresenter) {
252                     if (mCallback != null) {
253                         mCallback.requestShowFillUi(focusedId, width, height, windowPresenter);
254                     }
255                 }
256 
257                 @Override
258                 public void requestHideFillUi() {
259                     if (mCallback != null) {
260                         mCallback.requestHideFillUi(focusedId);
261                     }
262                 }
263 
264                 @Override
265                 public void startIntentSender(IntentSender intentSender) {
266                     if (mCallback != null) {
267                         mCallback.startIntentSenderAndFinishSession(intentSender);
268                     }
269                 }
270 
271                 @Override
272                 public void dispatchUnhandledKey(KeyEvent keyEvent) {
273                     if (mCallback != null) {
274                         mCallback.dispatchUnhandledKey(focusedId, keyEvent);
275                     }
276                 }
277 
278                 @Override
279                 public void cancelSession() {
280                     if (mCallback != null) {
281                         mCallback.cancelSession();
282                     }
283                 }
284             });
285         };
286 
287         if (isSaveUiShowing()) {
288             // postpone creating the fill UI for showing the save UI
289             if (sDebug) Slog.d(TAG, "postpone fill UI request..");
290             mCreateFillUiRunnable = createFillUiRunnable;
291         } else {
292             mHandler.post(createFillUiRunnable);
293         }
294     }
295 
296     /**
297      * Shows the UI asking the user to save for autofill.
298      */
showSaveUi(@onNull CharSequence serviceLabel, @NonNull Drawable serviceIcon, @Nullable String servicePackageName, @NonNull SaveInfo info, @NonNull ValueFinder valueFinder, @NonNull ComponentName componentName, @NonNull AutoFillUiCallback callback, @NonNull PendingUi pendingSaveUi, boolean isUpdate, boolean compatMode)299     public void showSaveUi(@NonNull CharSequence serviceLabel, @NonNull Drawable serviceIcon,
300             @Nullable String servicePackageName, @NonNull SaveInfo info,
301             @NonNull ValueFinder valueFinder, @NonNull ComponentName componentName,
302             @NonNull AutoFillUiCallback callback, @NonNull PendingUi pendingSaveUi,
303             boolean isUpdate, boolean compatMode) {
304         if (sVerbose) {
305             Slog.v(TAG, "showSaveUi(update=" + isUpdate + ") for " + componentName.toShortString()
306                     + ": " + info);
307         }
308         int numIds = 0;
309         numIds += info.getRequiredIds() == null ? 0 : info.getRequiredIds().length;
310         numIds += info.getOptionalIds() == null ? 0 : info.getOptionalIds().length;
311 
312         final LogMaker log = Helper
313                 .newLogMaker(MetricsEvent.AUTOFILL_SAVE_UI, componentName, servicePackageName,
314                         pendingSaveUi.sessionId, compatMode)
315                 .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_IDS, numIds);
316         if (isUpdate) {
317             log.addTaggedData(MetricsEvent.FIELD_AUTOFILL_UPDATE, 1);
318         }
319 
320         mHandler.post(() -> {
321             if (callback != mCallback) {
322                 return;
323             }
324             hideAllUiThread(callback);
325             mSaveUiCallback = callback;
326             mSaveUi = new SaveUi(mContext, pendingSaveUi, serviceLabel, serviceIcon,
327                     servicePackageName, componentName, info, valueFinder, mOverlayControl,
328                     new SaveUi.OnSaveListener() {
329                 @Override
330                 public void onSave() {
331                     log.setType(MetricsEvent.TYPE_ACTION);
332                     hideSaveUiUiThread(callback);
333                     callback.save();
334                     destroySaveUiUiThread(pendingSaveUi, true);
335                 }
336 
337                 @Override
338                 public void onCancel(IntentSender listener) {
339                     log.setType(MetricsEvent.TYPE_DISMISS);
340                     hideSaveUiUiThread(callback);
341                     if (listener != null) {
342                         try {
343                             listener.sendIntent(mContext, 0, null, null, null);
344                         } catch (IntentSender.SendIntentException e) {
345                             Slog.e(TAG, "Error starting negative action listener: "
346                                     + listener, e);
347                         }
348                     }
349                     callback.cancelSave();
350                     destroySaveUiUiThread(pendingSaveUi, true);
351                 }
352 
353                 @Override
354                 public void onDestroy() {
355                     if (log.getType() == MetricsEvent.TYPE_UNKNOWN) {
356                         log.setType(MetricsEvent.TYPE_CLOSE);
357 
358                         callback.cancelSave();
359                     }
360                     mMetricsLogger.write(log);
361                 }
362 
363                 @Override
364                 public void startIntentSender(IntentSender intentSender, Intent intent) {
365                     callback.startIntentSender(intentSender, intent);
366                 }
367             }, mUiModeMgr.isNightMode(), isUpdate, compatMode);
368         });
369     }
370 
371     /**
372      * Executes an operation in the pending save UI, if any.
373      */
onPendingSaveUi(int operation, @NonNull IBinder token)374     public void onPendingSaveUi(int operation, @NonNull IBinder token) {
375         mHandler.post(() -> {
376             if (mSaveUi != null) {
377                 mSaveUi.onPendingUi(operation, token);
378             } else {
379                 Slog.w(TAG, "onPendingSaveUi(" + operation + "): no save ui");
380             }
381         });
382     }
383 
384     /**
385      * Hides all autofill UIs.
386      */
hideAll(@ullable AutoFillUiCallback callback)387     public void hideAll(@Nullable AutoFillUiCallback callback) {
388         mHandler.post(() -> hideAllUiThread(callback));
389     }
390 
391     /**
392      * Destroy all autofill UIs.
393      */
destroyAll(@ullable PendingUi pendingSaveUi, @Nullable AutoFillUiCallback callback, boolean notifyClient)394     public void destroyAll(@Nullable PendingUi pendingSaveUi,
395             @Nullable AutoFillUiCallback callback, boolean notifyClient) {
396         mHandler.post(() -> destroyAllUiThread(pendingSaveUi, callback, notifyClient));
397     }
398 
isSaveUiShowing()399     public boolean isSaveUiShowing() {
400         return mSaveUi == null ? false : mSaveUi.isShowing();
401     }
402 
dump(PrintWriter pw)403     public void dump(PrintWriter pw) {
404         pw.println("Autofill UI");
405         final String prefix = "  ";
406         final String prefix2 = "    ";
407         pw.print(prefix); pw.print("Night mode: "); pw.println(mUiModeMgr.isNightMode());
408         if (mFillUi != null) {
409             pw.print(prefix); pw.println("showsFillUi: true");
410             mFillUi.dump(pw, prefix2);
411         } else {
412             pw.print(prefix); pw.println("showsFillUi: false");
413         }
414         if (mSaveUi != null) {
415             pw.print(prefix); pw.println("showsSaveUi: true");
416             mSaveUi.dump(pw, prefix2);
417         } else {
418             pw.print(prefix); pw.println("showsSaveUi: false");
419         }
420     }
421 
422     @android.annotation.UiThread
hideFillUiUiThread(@ullable AutoFillUiCallback callback, boolean notifyClient)423     private void hideFillUiUiThread(@Nullable AutoFillUiCallback callback, boolean notifyClient) {
424         if (mFillUi != null && (callback == null || callback == mCallback)) {
425             mFillUi.destroy(notifyClient);
426             mFillUi = null;
427         }
428     }
429 
430     @android.annotation.UiThread
431     @Nullable
hideSaveUiUiThread(@ullable AutoFillUiCallback callback)432     private PendingUi hideSaveUiUiThread(@Nullable AutoFillUiCallback callback) {
433         if (sVerbose) {
434             Slog.v(TAG, "hideSaveUiUiThread(): mSaveUi=" + mSaveUi + ", callback=" + callback
435                     + ", mCallback=" + mCallback);
436         }
437 
438         if (mSaveUi != null && mSaveUiCallback == callback) {
439             return mSaveUi.hide();
440         }
441         return null;
442     }
443 
444     @android.annotation.UiThread
destroySaveUiUiThread(@ullable PendingUi pendingSaveUi, boolean notifyClient)445     private void destroySaveUiUiThread(@Nullable PendingUi pendingSaveUi, boolean notifyClient) {
446         if (mSaveUi == null) {
447             // Calling destroySaveUiUiThread() twice is normal - it usually happens when the
448             // first call is made after the SaveUI is hidden and the second when the session is
449             // finished.
450             if (sDebug) Slog.d(TAG, "destroySaveUiUiThread(): already destroyed");
451             return;
452         }
453 
454         if (sDebug) Slog.d(TAG, "destroySaveUiUiThread(): " + pendingSaveUi);
455         mSaveUi.destroy();
456         mSaveUi = null;
457         mSaveUiCallback = null;
458         if (pendingSaveUi != null && notifyClient) {
459             try {
460                 if (sDebug) Slog.d(TAG, "destroySaveUiUiThread(): notifying client");
461                 pendingSaveUi.client.setSaveUiState(pendingSaveUi.sessionId, false);
462             } catch (RemoteException e) {
463                 Slog.e(TAG, "Error notifying client to set save UI state to hidden: " + e);
464             }
465         }
466 
467         if (mCreateFillUiRunnable != null) {
468             if (sDebug) Slog.d(TAG, "start the pending fill UI request..");
469             mHandler.post(mCreateFillUiRunnable);
470             mCreateFillUiRunnable = null;
471         }
472     }
473 
474     @android.annotation.UiThread
destroyAllUiThread(@ullable PendingUi pendingSaveUi, @Nullable AutoFillUiCallback callback, boolean notifyClient)475     private void destroyAllUiThread(@Nullable PendingUi pendingSaveUi,
476             @Nullable AutoFillUiCallback callback, boolean notifyClient) {
477         hideFillUiUiThread(callback, notifyClient);
478         destroySaveUiUiThread(pendingSaveUi, notifyClient);
479     }
480 
481     @android.annotation.UiThread
hideAllUiThread(@ullable AutoFillUiCallback callback)482     private void hideAllUiThread(@Nullable AutoFillUiCallback callback) {
483         hideFillUiUiThread(callback, true);
484         final PendingUi pendingSaveUi = hideSaveUiUiThread(callback);
485         if (pendingSaveUi != null && pendingSaveUi.getState() == PendingUi.STATE_FINISHED) {
486             if (sDebug) {
487                 Slog.d(TAG, "hideAllUiThread(): "
488                         + "destroying Save UI because pending restoration is finished");
489             }
490             destroySaveUiUiThread(pendingSaveUi, true);
491         }
492     }
493 }
494