/* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.view.autofill; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Activity; import android.app.ActivityOptions; import android.app.Application; import android.content.ComponentName; import android.content.Intent; import android.content.IntentSender; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; import android.text.TextUtils; import android.util.Dumpable; import android.util.Log; import android.util.Slog; import android.view.KeyEvent; import android.view.View; import android.view.ViewRootImpl; import android.view.WindowManagerGlobal; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; /** * A controller to manage the autofill requests for the {@link Activity}. * * @hide */ public final class AutofillClientController implements AutofillManager.AutofillClient, Dumpable { private static final String TAG = "AutofillClientController"; private static final String LOG_TAG = "autofill_client"; public static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG); public static final String LAST_AUTOFILL_ID = "android:lastAutofillId"; public static final String AUTOFILL_RESET_NEEDED = "@android:autofillResetNeeded"; public static final String AUTO_FILL_AUTH_WHO_PREFIX = "@android:autoFillAuth:"; public static final String DUMPABLE_NAME = "AutofillManager"; /** The last autofill id that was returned from {@link #getNextAutofillId()} */ public int mLastAutofillId = View.LAST_APP_AUTOFILL_ID; @NonNull private final Activity mActivity; /** The autofill manager. Always access via {@link #getAutofillManager()}. */ @Nullable private AutofillManager mAutofillManager; /** The autofill dropdown fill ui. */ @Nullable private AutofillPopupWindow mAutofillPopupWindow; private boolean mAutoFillResetNeeded; private boolean mAutoFillIgnoreFirstResumePause; /** * AutofillClientController constructor. */ public AutofillClientController(Activity activity) { mActivity = activity; } private AutofillManager getAutofillManager() { if (mAutofillManager == null) { mAutofillManager = mActivity.getSystemService(AutofillManager.class); } return mAutofillManager; } // ------------------ Called for Activity events ------------------ /** * Called when the Activity is attached. */ public void onActivityAttached(Application application) { mActivity.setAutofillOptions(application.getAutofillOptions()); } /** * Called when the {@link Activity#onCreate(Bundle)} is called. */ public void onActivityCreated(@NonNull Bundle savedInstanceState) { mAutoFillResetNeeded = savedInstanceState.getBoolean(AUTOFILL_RESET_NEEDED, false); mLastAutofillId = savedInstanceState.getInt(LAST_AUTOFILL_ID, View.LAST_APP_AUTOFILL_ID); if (mAutoFillResetNeeded) { getAutofillManager().onCreate(savedInstanceState); } } /** * Called when the {@link Activity#onStart()} is called. */ public void onActivityStarted() { if (mAutoFillResetNeeded) { getAutofillManager().onVisibleForAutofill(); } } /** * Called when the {@link Activity#onResume()} is called. */ public void onActivityResumed() { enableAutofillCompatibilityIfNeeded(); if (mAutoFillResetNeeded) { if (!mAutoFillIgnoreFirstResumePause) { View focus = mActivity.getCurrentFocus(); if (focus != null && focus.canNotifyAutofillEnterExitEvent()) { // TODO(b/148815880): Bring up keyboard if resumed from inline authentication. // TODO: in Activity killed/recreated case, i.e. SessionLifecycleTest# // testDatasetVisibleWhileAutofilledAppIsLifecycled: the View's initial // window visibility after recreation is INVISIBLE in onResume() and next frame // ViewRootImpl.performTraversals() changes window visibility to VISIBLE. // So we cannot call View.notifyEnterOrExited() which will do nothing // when View.isVisibleToUser() is false. getAutofillManager().notifyViewEntered(focus); } } } } /** * Called when the Activity is performing resume. */ public void onActivityPerformResume(boolean followedByPause) { if (mAutoFillResetNeeded) { // When Activity is destroyed in paused state, and relaunch activity, there will be // extra onResume and onPause event, ignore the first onResume and onPause. // see ActivityThread.handleRelaunchActivity() mAutoFillIgnoreFirstResumePause = followedByPause; if (mAutoFillIgnoreFirstResumePause && DEBUG) { Slog.v(TAG, "autofill will ignore first pause when relaunching " + this); } } } /** * Called when the {@link Activity#onPause()} is called. */ public void onActivityPaused() { if (mAutoFillResetNeeded) { if (!mAutoFillIgnoreFirstResumePause) { if (DEBUG) Log.v(TAG, "autofill notifyViewExited " + this); View focus = mActivity.getCurrentFocus(); if (focus != null && focus.canNotifyAutofillEnterExitEvent()) { getAutofillManager().notifyViewExited(focus); } } else { // reset after first pause() if (DEBUG) Log.v(TAG, "autofill got first pause " + this); mAutoFillIgnoreFirstResumePause = false; } } } /** * Called when the {@link Activity#onStop()} is called. */ public void onActivityStopped(Intent intent, boolean changingConfigurations) { if (mAutoFillResetNeeded) { // If stopped without changing the configurations, the response should expire. getAutofillManager().onInvisibleForAutofill(!changingConfigurations); } else if (intent != null && intent.hasExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN) && intent.hasExtra(AutofillManager.EXTRA_RESTORE_CROSS_ACTIVITY)) { restoreAutofillSaveUi(intent); } } /** * Called when the {@link Activity#onDestroy()} is called. */ public void onActivityDestroyed() { if (mActivity.isFinishing() && mAutoFillResetNeeded) { getAutofillManager().onActivityFinishing(); } } /** * Called when the {@link Activity#onSaveInstanceState(Bundle)} is called. */ public void onSaveInstanceState(Bundle outState) { outState.putInt(LAST_AUTOFILL_ID, mLastAutofillId); if (mAutoFillResetNeeded) { outState.putBoolean(AUTOFILL_RESET_NEEDED, true); getAutofillManager().onSaveInstanceState(outState); } } /** * Called when the {@link Activity#finish()} is called. */ public void onActivityFinish(Intent intent) { // Activity was launched when user tapped a link in the Autofill Save UI - Save UI must // be restored now. if (intent != null && intent.hasExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN)) { restoreAutofillSaveUi(intent); } } /** * Called when the {@link Activity#onBackPressed()} is called. */ public void onActivityBackPressed(Intent intent) { // Activity was launched when user tapped a link in the Autofill Save UI - Save UI must // be restored now. if (intent != null && intent.hasExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN)) { restoreAutofillSaveUi(intent); } } /** * Called when the Activity is dispatching the result. */ public void onDispatchActivityResult(int requestCode, int resultCode, Intent data) { Intent resultData = (resultCode == Activity.RESULT_OK) ? data : null; getAutofillManager().onAuthenticationResult(requestCode, resultData, mActivity.getCurrentFocus()); } /** * Called when the {@link Activity#startActivity(Intent, Bundle)} is called. */ public void onStartActivity(Intent startIntent, Intent cachedIntent) { if (cachedIntent != null && cachedIntent.hasExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN) && cachedIntent.hasExtra(AutofillManager.EXTRA_RESTORE_CROSS_ACTIVITY)) { if (TextUtils.equals(mActivity.getPackageName(), startIntent.resolveActivity(mActivity.getPackageManager()).getPackageName())) { // Apply Autofill restore mechanism on the started activity by startActivity() final IBinder token = cachedIntent.getIBinderExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN); // Remove restore ability from current activity cachedIntent.removeExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN); cachedIntent.removeExtra(AutofillManager.EXTRA_RESTORE_CROSS_ACTIVITY); // Put restore token startIntent.putExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN, token); startIntent.putExtra(AutofillManager.EXTRA_RESTORE_CROSS_ACTIVITY, true); } } } /** * Restore the autofill save ui. */ public void restoreAutofillSaveUi(Intent intent) { final IBinder token = intent.getIBinderExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN); // Make only restore Autofill once intent.removeExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN); intent.removeExtra(AutofillManager.EXTRA_RESTORE_CROSS_ACTIVITY); getAutofillManager().onPendingSaveUi(AutofillManager.PENDING_UI_OPERATION_RESTORE, token); } /** * Enable autofill compatibility mode for the Activity if the compatibility mode is enabled * for the package. */ public void enableAutofillCompatibilityIfNeeded() { if (mActivity.isAutofillCompatibilityEnabled()) { final AutofillManager afm = mActivity.getSystemService(AutofillManager.class); if (afm != null) { afm.enableCompatibilityMode(); } } } @Override public String getDumpableName() { return DUMPABLE_NAME; } @Override public void dump(PrintWriter writer, String[] args) { final String prefix = ""; final AutofillManager afm = getAutofillManager(); if (afm != null) { afm.dump(prefix, writer); writer.print(prefix); writer.print("Autofill Compat Mode: "); writer.println(mActivity.isAutofillCompatibilityEnabled()); } else { writer.print(prefix); writer.println("No AutofillManager"); } } /** * Returns the next autofill ID that is unique in the activity * *
All IDs will be bigger than {@link View#LAST_APP_AUTOFILL_ID}. All IDs returned
* will be unique.
*/
public int getNextAutofillId() {
if (mLastAutofillId == Integer.MAX_VALUE - 1) {
mLastAutofillId = View.LAST_APP_AUTOFILL_ID;
}
mLastAutofillId++;
return mLastAutofillId;
}
// ------------------ AutofillClient implementation ------------------
@Override
public AutofillId autofillClientGetNextAutofillId() {
return new AutofillId(getNextAutofillId());
}
@Override
public boolean autofillClientIsCompatibilityModeEnabled() {
return mActivity.isAutofillCompatibilityEnabled();
}
@Override
public boolean autofillClientIsVisibleForAutofill() {
return mActivity.isVisibleForAutofill();
}
@Override
public ComponentName autofillClientGetComponentName() {
return mActivity.getComponentName();
}
@Override
public IBinder autofillClientGetActivityToken() {
return mActivity.getActivityToken();
}
@Override
public boolean[] autofillClientGetViewVisibility(AutofillId[] autofillIds) {
final int autofillIdCount = autofillIds.length;
final boolean[] visible = new boolean[autofillIdCount];
for (int i = 0; i < autofillIdCount; i++) {
final AutofillId autofillId = autofillIds[i];
if (autofillId == null) {
visible[i] = false;
continue;
}
final View view = autofillClientFindViewByAutofillIdTraversal(autofillId);
if (view != null) {
if (!autofillId.isVirtualInt()) {
visible[i] = view.isVisibleToUser();
} else {
visible[i] = view.isVisibleToUserForAutofill(autofillId.getVirtualChildIntId());
}
}
}
if (android.view.autofill.Helper.sVerbose) {
Log.v(TAG, "autofillClientGetViewVisibility(): " + Arrays.toString(visible));
}
return visible;
}
@Override
public View autofillClientFindViewByAccessibilityIdTraversal(int viewId, int windowId) {
final ArrayList