• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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 
17 package android.view.autofill;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.app.Activity;
22 import android.app.ActivityOptions;
23 import android.app.Application;
24 import android.content.ComponentName;
25 import android.content.Intent;
26 import android.content.IntentSender;
27 import android.graphics.Rect;
28 import android.os.Bundle;
29 import android.os.IBinder;
30 import android.text.TextUtils;
31 import android.util.Dumpable;
32 import android.util.Log;
33 import android.util.Slog;
34 import android.view.KeyEvent;
35 import android.view.View;
36 import android.view.ViewRootImpl;
37 import android.view.WindowManagerGlobal;
38 
39 import java.io.PrintWriter;
40 import java.util.ArrayList;
41 import java.util.Arrays;
42 import java.util.List;
43 
44 /**
45  * A controller to manage the autofill requests for the {@link Activity}.
46  *
47  * @hide
48  */
49 public final class AutofillClientController implements AutofillManager.AutofillClient, Dumpable {
50 
51     private static final String TAG = "AutofillClientController";
52 
53     private static final String LOG_TAG = "autofill_client";
54     public static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG);
55 
56     public static final String LAST_AUTOFILL_ID = "android:lastAutofillId";
57     public static final String AUTOFILL_RESET_NEEDED = "@android:autofillResetNeeded";
58     public static final String AUTO_FILL_AUTH_WHO_PREFIX = "@android:autoFillAuth:";
59 
60     public static final String DUMPABLE_NAME = "AutofillManager";
61 
62     /** The last autofill id that was returned from {@link #getNextAutofillId()} */
63     public int mLastAutofillId = View.LAST_APP_AUTOFILL_ID;
64 
65     @NonNull
66     private final Activity mActivity;
67     /** The autofill manager. Always access via {@link #getAutofillManager()}. */
68     @Nullable
69     private AutofillManager mAutofillManager;
70     /** The autofill dropdown fill ui. */
71     @Nullable
72     private AutofillPopupWindow mAutofillPopupWindow;
73     private boolean mAutoFillResetNeeded;
74     private boolean mAutoFillIgnoreFirstResumePause;
75     private Boolean mRelayoutFix;
76 
77     /**
78      * AutofillClientController constructor.
79      */
AutofillClientController(Activity activity)80     public AutofillClientController(Activity activity) {
81         mActivity = activity;
82     }
83 
getAutofillManager()84     private AutofillManager getAutofillManager() {
85         if (mAutofillManager == null) {
86             mAutofillManager = mActivity.getSystemService(AutofillManager.class);
87         }
88         return mAutofillManager;
89     }
90 
91     /**
92      * Whether to apply relayout fixes.
93      *
94      * @hide
95      */
isRelayoutFixEnabled()96     public boolean isRelayoutFixEnabled() {
97         AutofillManager autofillManager = getAutofillManager();
98         if (autofillManager == null) {
99             if (Helper.sDebug) {
100                 Log.d(TAG, "isRelayoutFixEnabled() : getAutofillManager() == null");
101             }
102             return false;
103         }
104         if (mRelayoutFix == null) {
105             mRelayoutFix = autofillManager.isRelayoutFixEnabled();
106         }
107         return mRelayoutFix;
108     }
109 
110     // ------------------ Called for Activity events ------------------
111 
112     /**
113      * Called when the Activity is attached.
114      */
onActivityAttached(Application application)115     public void onActivityAttached(Application application) {
116         mActivity.setAutofillOptions(application.getAutofillOptions());
117     }
118 
119     /**
120      * Called when the {@link Activity#onCreate(Bundle)} is called.
121      */
onActivityCreated(@onNull Bundle savedInstanceState)122     public void onActivityCreated(@NonNull Bundle savedInstanceState) {
123         mAutoFillResetNeeded = savedInstanceState.getBoolean(AUTOFILL_RESET_NEEDED, false);
124         mLastAutofillId = savedInstanceState.getInt(LAST_AUTOFILL_ID, View.LAST_APP_AUTOFILL_ID);
125         if (mAutoFillResetNeeded) {
126             getAutofillManager().onCreate(savedInstanceState);
127         }
128     }
129 
130     /**
131      * Called when the {@link Activity#onStart()} is called.
132      */
onActivityStarted()133     public void onActivityStarted() {
134         if (mAutoFillResetNeeded) {
135             getAutofillManager().onVisibleForAutofill();
136         }
137     }
138 
139     /**
140      * Called when the {@link Activity#onResume()} is called.
141      */
onActivityResumed()142     public void onActivityResumed() {
143         if (Helper.sVerbose) {
144             Log.v(TAG, "onActivityResumed()");
145         }
146         if (isRelayoutFixEnabled()) {
147             // Do nothing here. We'll handle it in onActivityPostResumed()
148             return;
149         }
150         if (Helper.sVerbose) {
151             Log.v(TAG, "onActivityResumed(): Relayout fix not enabled");
152         }
153         forResume();
154     }
155 
156     /**
157      * Called when the {@link Activity#onPostResume()} is called.
158      */
onActivityPostResumed()159     public void onActivityPostResumed() {
160         if (Helper.sVerbose) {
161             Log.v(TAG, "onActivityPostResumed()");
162         }
163         if (!isRelayoutFixEnabled()) {
164             return;
165         }
166         if (Helper.sVerbose) {
167             Log.v(TAG, "onActivityPostResumed(): Relayout fix enabled");
168         }
169         forResume();
170     }
171 
172     /**
173      * Code to execute when an app has resumed (or is about to resume)
174      */
forResume()175     private void forResume() {
176         enableAutofillCompatibilityIfNeeded();
177         boolean relayoutFix = isRelayoutFixEnabled();
178         if (relayoutFix) {
179             if (getAutofillManager().shouldRetryFill()) {
180                 if (Helper.sVerbose) {
181                     Log.v(TAG, "forResume(): Autofill potential relayout. Retrying fill.");
182                 }
183                 getAutofillManager().attemptRefill();
184             } else {
185                 if (Helper.sVerbose) {
186                     Log.v(TAG, "forResume(): Not attempting refill.");
187                 }
188             }
189         }
190 
191         if (mAutoFillResetNeeded) {
192             if (!mAutoFillIgnoreFirstResumePause) {
193                 View focus = mActivity.getCurrentFocus();
194                 if (focus != null && focus.canNotifyAutofillEnterExitEvent()) {
195                     // TODO(b/148815880): Bring up keyboard if resumed from inline authentication.
196                     // TODO: in Activity killed/recreated case, i.e. SessionLifecycleTest#
197                     // testDatasetVisibleWhileAutofilledAppIsLifecycled: the View's initial
198                     // window visibility after recreation is INVISIBLE in onResume() and next frame
199                     // ViewRootImpl.performTraversals() changes window visibility to VISIBLE.
200                     // So we cannot call View.notifyEnterOrExited() which will do nothing
201                     // when View.isVisibleToUser() is false.
202                     if (relayoutFix && getAutofillManager().isAuthenticationPending()) {
203                         if (Helper.sVerbose) {
204                             Log.v(TAG, "forResume(): ignoring focus due to auth pending");
205                         }
206                     } else {
207                         if (Helper.sVerbose) {
208                             Log.v(TAG, "forResume(): notifyViewEntered");
209                         }
210                         getAutofillManager().notifyViewEntered(focus);
211                     }
212                 }
213             }
214         }
215     }
216 
217     /**
218      * Called when the Activity is performing resume.
219      */
onActivityPerformResume(boolean followedByPause)220     public void onActivityPerformResume(boolean followedByPause) {
221         if (mAutoFillResetNeeded) {
222             // When Activity is destroyed in paused state, and relaunch activity, there will be
223             // extra onResume and onPause event,  ignore the first onResume and onPause.
224             // see ActivityThread.handleRelaunchActivity()
225             mAutoFillIgnoreFirstResumePause = followedByPause;
226             if (mAutoFillIgnoreFirstResumePause && DEBUG) {
227                 Slog.v(TAG, "autofill will ignore first pause when relaunching " + this);
228             }
229         }
230     }
231 
232     /**
233      * Called when the {@link Activity#onPause()} is called.
234      */
onActivityPaused()235     public void onActivityPaused() {
236         if (mAutoFillResetNeeded) {
237             if (!mAutoFillIgnoreFirstResumePause) {
238                 if (DEBUG) Log.v(TAG, "autofill notifyViewExited " + this);
239                 View focus = mActivity.getCurrentFocus();
240                 if (focus != null && focus.canNotifyAutofillEnterExitEvent()) {
241                     getAutofillManager().notifyViewExited(focus);
242                 }
243             } else {
244                 // reset after first pause()
245                 if (DEBUG) Log.v(TAG, "autofill got first pause " + this);
246                 mAutoFillIgnoreFirstResumePause = false;
247             }
248         }
249     }
250 
251     /**
252      * Called when the {@link Activity#onStop()} is called.
253      */
onActivityStopped(Intent intent, boolean changingConfigurations)254     public void onActivityStopped(Intent intent, boolean changingConfigurations) {
255         if (mAutoFillResetNeeded) {
256             // If stopped without changing the configurations, the response should expire.
257             getAutofillManager().onInvisibleForAutofill(!changingConfigurations);
258         } else if (intent != null
259                 && intent.hasExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN)
260                 && intent.hasExtra(AutofillManager.EXTRA_RESTORE_CROSS_ACTIVITY)) {
261             restoreAutofillSaveUi(intent);
262         }
263     }
264 
265     /**
266      * Called when the {@link Activity#onDestroy()} is called.
267      */
onActivityDestroyed()268     public void onActivityDestroyed() {
269         if (mActivity.isFinishing() && mAutoFillResetNeeded) {
270             getAutofillManager().onActivityFinishing();
271         }
272     }
273 
274     /**
275      * Called when the {@link Activity#onSaveInstanceState(Bundle)} is called.
276      */
onSaveInstanceState(Bundle outState)277     public void onSaveInstanceState(Bundle outState) {
278         outState.putInt(LAST_AUTOFILL_ID, mLastAutofillId);
279         if (mAutoFillResetNeeded) {
280             outState.putBoolean(AUTOFILL_RESET_NEEDED, true);
281             getAutofillManager().onSaveInstanceState(outState);
282         }
283     }
284 
285     /**
286      * Called when the {@link Activity#finish()} is called.
287      */
onActivityFinish(Intent intent)288     public void onActivityFinish(Intent intent) {
289         // Activity was launched when user tapped a link in the Autofill Save UI - Save UI must
290         // be restored now.
291         if (intent != null && intent.hasExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN)) {
292             restoreAutofillSaveUi(intent);
293         }
294     }
295 
296     /**
297      * Called when the {@link Activity#onBackPressed()} is called.
298      */
onActivityBackPressed(Intent intent)299     public void onActivityBackPressed(Intent intent) {
300         // Activity was launched when user tapped a link in the Autofill Save UI - Save UI must
301         // be restored now.
302         if (intent != null && intent.hasExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN)) {
303             restoreAutofillSaveUi(intent);
304         }
305     }
306 
307     /**
308      * Called when the Activity is dispatching the result.
309      */
onDispatchActivityResult(int requestCode, int resultCode, Intent data)310     public void onDispatchActivityResult(int requestCode, int resultCode, Intent data) {
311         Intent resultData = (resultCode == Activity.RESULT_OK) ? data : null;
312         getAutofillManager().onAuthenticationResult(requestCode, resultData,
313                 mActivity.getCurrentFocus());
314     }
315 
316     /**
317      * Called when the {@link Activity#startActivity(Intent, Bundle)} is called.
318      */
onStartActivity(Intent startIntent, Intent cachedIntent)319     public void onStartActivity(Intent startIntent, Intent cachedIntent) {
320         if (cachedIntent != null
321                 && cachedIntent.hasExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN)
322                 && cachedIntent.hasExtra(AutofillManager.EXTRA_RESTORE_CROSS_ACTIVITY)) {
323             if (TextUtils.equals(mActivity.getPackageName(),
324                     startIntent.resolveActivity(mActivity.getPackageManager()).getPackageName())) {
325                 // Apply Autofill restore mechanism on the started activity by startActivity()
326                 final IBinder token =
327                         cachedIntent.getIBinderExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN);
328                 // Remove restore ability from current activity
329                 cachedIntent.removeExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN);
330                 cachedIntent.removeExtra(AutofillManager.EXTRA_RESTORE_CROSS_ACTIVITY);
331                 // Put restore token
332                 startIntent.putExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN, token);
333                 startIntent.putExtra(AutofillManager.EXTRA_RESTORE_CROSS_ACTIVITY, true);
334             }
335         }
336     }
337 
338     /**
339      * Restore the autofill save ui.
340      */
restoreAutofillSaveUi(Intent intent)341     public void restoreAutofillSaveUi(Intent intent) {
342         final IBinder token =
343                 intent.getIBinderExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN);
344         // Make only restore Autofill once
345         intent.removeExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN);
346         intent.removeExtra(AutofillManager.EXTRA_RESTORE_CROSS_ACTIVITY);
347         getAutofillManager().onPendingSaveUi(AutofillManager.PENDING_UI_OPERATION_RESTORE,
348                 token);
349     }
350 
351     /**
352      * Enable autofill compatibility mode for the Activity if the compatibility mode is enabled
353      * for the package.
354      */
enableAutofillCompatibilityIfNeeded()355     public void enableAutofillCompatibilityIfNeeded() {
356         if (mActivity.isAutofillCompatibilityEnabled()) {
357             final AutofillManager afm = mActivity.getSystemService(AutofillManager.class);
358             if (afm != null) {
359                 afm.enableCompatibilityMode();
360             }
361         }
362     }
363 
364     @Override
getDumpableName()365     public String getDumpableName() {
366         return DUMPABLE_NAME;
367     }
368 
369     @Override
dump(PrintWriter writer, String[] args)370     public void dump(PrintWriter writer, String[] args) {
371         final String prefix = "";
372         final AutofillManager afm = getAutofillManager();
373         if (afm != null) {
374             afm.dump(prefix, writer);
375             writer.print(prefix); writer.print("Autofill Compat Mode: ");
376             writer.println(mActivity.isAutofillCompatibilityEnabled());
377         } else {
378             writer.print(prefix); writer.println("No AutofillManager");
379         }
380     }
381 
382     /**
383      * Returns the next autofill ID that is unique in the activity
384      *
385      * <p>All IDs will be bigger than {@link View#LAST_APP_AUTOFILL_ID}. All IDs returned
386      * will be unique.
387      */
getNextAutofillId()388     public int getNextAutofillId() {
389         if (mLastAutofillId == Integer.MAX_VALUE - 1) {
390             mLastAutofillId = View.LAST_APP_AUTOFILL_ID;
391         }
392 
393         mLastAutofillId++;
394 
395         return mLastAutofillId;
396     }
397 
398     // ------------------ AutofillClient implementation ------------------
399 
400     @Override
autofillClientGetNextAutofillId()401     public AutofillId autofillClientGetNextAutofillId() {
402         return new AutofillId(getNextAutofillId());
403     }
404 
405     @Override
autofillClientIsCompatibilityModeEnabled()406     public boolean autofillClientIsCompatibilityModeEnabled() {
407         return mActivity.isAutofillCompatibilityEnabled();
408     }
409 
410     @Override
autofillClientIsVisibleForAutofill()411     public boolean autofillClientIsVisibleForAutofill() {
412         return mActivity.isVisibleForAutofill();
413     }
414 
415     @Override
autofillClientGetComponentName()416     public ComponentName autofillClientGetComponentName() {
417         return mActivity.getComponentName();
418     }
419 
420     @Override
autofillClientGetActivityToken()421     public IBinder autofillClientGetActivityToken() {
422         return mActivity.getActivityToken();
423     }
424 
425     @Override
autofillClientGetViewVisibility(AutofillId[] autofillIds)426     public boolean[] autofillClientGetViewVisibility(AutofillId[] autofillIds) {
427         final int autofillIdCount = autofillIds.length;
428         final boolean[] visible = new boolean[autofillIdCount];
429         for (int i = 0; i < autofillIdCount; i++) {
430             final AutofillId autofillId = autofillIds[i];
431             if (autofillId == null) {
432                 visible[i] = false;
433                 continue;
434             }
435             final View view = autofillClientFindViewByAutofillIdTraversal(autofillId);
436             if (view != null) {
437                 if (!autofillId.isVirtualInt()) {
438                     visible[i] = view.isVisibleToUser();
439                 } else {
440                     visible[i] = view.isVisibleToUserForAutofill(autofillId.getVirtualChildIntId());
441                 }
442             }
443         }
444         if (android.view.autofill.Helper.sVerbose) {
445             Log.v(TAG, "autofillClientGetViewVisibility(): " + Arrays.toString(visible));
446         }
447         return visible;
448     }
449 
450     @Override
autofillClientFindViewByAccessibilityIdTraversal(int viewId, int windowId)451     public View autofillClientFindViewByAccessibilityIdTraversal(int viewId, int windowId) {
452         final ArrayList<ViewRootImpl> roots = WindowManagerGlobal.getInstance()
453                 .getRootViews(mActivity.getActivityToken());
454         for (int rootNum = 0; rootNum < roots.size(); rootNum++) {
455             final View rootView = roots.get(rootNum).getView();
456             if (rootView != null && rootView.getAccessibilityWindowId() == windowId) {
457                 final View view = rootView.findViewByAccessibilityIdTraversal(viewId);
458                 if (view != null) {
459                     return view;
460                 }
461             }
462         }
463         return null;
464     }
465 
466     @Override
autofillClientFindViewByAutofillIdTraversal(AutofillId autofillId)467     public View autofillClientFindViewByAutofillIdTraversal(AutofillId autofillId) {
468         if (autofillId == null) return null;
469         final ArrayList<ViewRootImpl> roots =
470                 WindowManagerGlobal.getInstance().getRootViews(mActivity.getActivityToken());
471         for (int rootNum = 0; rootNum < roots.size(); rootNum++) {
472             final View rootView = roots.get(rootNum).getView();
473 
474             if (rootView != null) {
475                 final View view = rootView.findViewByAutofillIdTraversal(autofillId.getViewId());
476                 if (view != null) {
477                     return view;
478                 }
479             }
480         }
481         return null;
482     }
483 
484     @Override
autofillClientFindViewsByAutofillIdTraversal(AutofillId[] autofillIds)485     public View[] autofillClientFindViewsByAutofillIdTraversal(AutofillId[] autofillIds) {
486         final View[] views = new View[autofillIds.length];
487         final ArrayList<ViewRootImpl> roots =
488                 WindowManagerGlobal.getInstance().getRootViews(mActivity.getActivityToken());
489 
490         for (int rootNum = 0; rootNum < roots.size(); rootNum++) {
491             final View rootView = roots.get(rootNum).getView();
492 
493             if (rootView != null) {
494                 final int viewCount = autofillIds.length;
495                 for (int viewNum = 0; viewNum < viewCount; viewNum++) {
496                     if (autofillIds[viewNum] != null && views[viewNum] == null) {
497                         views[viewNum] = rootView.findViewByAutofillIdTraversal(
498                                 autofillIds[viewNum].getViewId());
499                     }
500                 }
501             }
502         }
503         return views;
504     }
505 
506     @Override
autofillClientFindAutofillableViewsByTraversal()507     public List<View> autofillClientFindAutofillableViewsByTraversal() {
508         final ArrayList<View> views = new ArrayList<>();
509         final ArrayList<ViewRootImpl> roots =
510                 WindowManagerGlobal.getInstance().getRootViews(mActivity.getActivityToken());
511 
512         for (int rootNum = 0; rootNum < roots.size(); rootNum++) {
513             final View rootView = roots.get(rootNum).getView();
514 
515             if (rootView != null) {
516                 rootView.findAutofillableViewsByTraversal(views);
517             }
518         }
519         return views;
520     }
521 
522     @Override
autofillClientIsFillUiShowing()523     public boolean autofillClientIsFillUiShowing() {
524         return mAutofillPopupWindow != null && mAutofillPopupWindow.isShowing();
525     }
526 
527     @Override
autofillClientRequestHideFillUi()528     public boolean autofillClientRequestHideFillUi() {
529         if (mAutofillPopupWindow == null) {
530             return false;
531         }
532         mAutofillPopupWindow.dismiss();
533         mAutofillPopupWindow = null;
534         return true;
535     }
536 
537     @Override
autofillClientRequestShowFillUi(@onNull View anchor, int width, int height, @Nullable Rect anchorBounds, IAutofillWindowPresenter presenter)538     public boolean autofillClientRequestShowFillUi(@NonNull View anchor, int width,
539             int height, @Nullable Rect anchorBounds, IAutofillWindowPresenter presenter) {
540         final boolean wasShowing;
541 
542         if (mAutofillPopupWindow == null) {
543             wasShowing = false;
544             mAutofillPopupWindow = new AutofillPopupWindow(presenter);
545         } else {
546             wasShowing = mAutofillPopupWindow.isShowing();
547         }
548         mAutofillPopupWindow.update(anchor, 0, 0, width, height, anchorBounds);
549 
550         return !wasShowing && mAutofillPopupWindow.isShowing();
551     }
552 
553     @Override
autofillClientDispatchUnhandledKey(View anchor, KeyEvent keyEvent)554     public void autofillClientDispatchUnhandledKey(View anchor, KeyEvent keyEvent) {
555         ViewRootImpl rootImpl = anchor.getViewRootImpl();
556         if (rootImpl != null) {
557             // don't care if anchorView is current focus, for example a custom view may only receive
558             // touchEvent, not focusable but can still trigger autofill window. The Key handling
559             // might be inside parent of the custom view.
560             rootImpl.dispatchKeyFromAutofill(keyEvent);
561         }
562     }
563 
564     @Override
isDisablingEnterExitEventForAutofill()565     public boolean isDisablingEnterExitEventForAutofill() {
566         return mAutoFillIgnoreFirstResumePause || !mActivity.isResumed();
567     }
568 
569     @Override
autofillClientResetableStateAvailable()570     public void autofillClientResetableStateAvailable() {
571         mAutoFillResetNeeded = true;
572     }
573 
574     @Override
autofillClientRunOnUiThread(Runnable action)575     public void autofillClientRunOnUiThread(Runnable action) {
576         mActivity.runOnUiThread(action);
577     }
578 
579     @Override
autofillClientAuthenticate(int authenticationId, IntentSender intent, Intent fillInIntent, boolean authenticateInline)580     public void autofillClientAuthenticate(int authenticationId, IntentSender intent,
581             Intent fillInIntent, boolean authenticateInline) {
582         try {
583             ActivityOptions activityOptions = ActivityOptions.makeBasic()
584                     .setPendingIntentBackgroundActivityStartMode(
585                         ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
586             mActivity.startIntentSenderForResult(intent, AUTO_FILL_AUTH_WHO_PREFIX,
587                     authenticationId, fillInIntent, 0, 0, activityOptions.toBundle());
588         } catch (IntentSender.SendIntentException e) {
589             Log.e(TAG, "authenticate() failed for intent:" + intent, e);
590         }
591     }
592 
593     @Override
isActivityResumed()594     public boolean isActivityResumed() {
595         return mActivity.isResumed();
596     }
597 }
598