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