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