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