| /* |
| * 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 |
| * |
| * <p>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<ViewRootImpl> roots = WindowManagerGlobal.getInstance() |
| .getRootViews(mActivity.getActivityToken()); |
| for (int rootNum = 0; rootNum < roots.size(); rootNum++) { |
| final View rootView = roots.get(rootNum).getView(); |
| if (rootView != null && rootView.getAccessibilityWindowId() == windowId) { |
| final View view = rootView.findViewByAccessibilityIdTraversal(viewId); |
| if (view != null) { |
| return view; |
| } |
| } |
| } |
| return null; |
| } |
| |
| @Override |
| public View autofillClientFindViewByAutofillIdTraversal(AutofillId autofillId) { |
| if (autofillId == null) return null; |
| final ArrayList<ViewRootImpl> roots = |
| WindowManagerGlobal.getInstance().getRootViews(mActivity.getActivityToken()); |
| for (int rootNum = 0; rootNum < roots.size(); rootNum++) { |
| final View rootView = roots.get(rootNum).getView(); |
| |
| if (rootView != null) { |
| final View view = rootView.findViewByAutofillIdTraversal(autofillId.getViewId()); |
| if (view != null) { |
| return view; |
| } |
| } |
| } |
| return null; |
| } |
| |
| @Override |
| public View[] autofillClientFindViewsByAutofillIdTraversal(AutofillId[] autofillIds) { |
| final View[] views = new View[autofillIds.length]; |
| final ArrayList<ViewRootImpl> roots = |
| WindowManagerGlobal.getInstance().getRootViews(mActivity.getActivityToken()); |
| |
| for (int rootNum = 0; rootNum < roots.size(); rootNum++) { |
| final View rootView = roots.get(rootNum).getView(); |
| |
| if (rootView != null) { |
| final int viewCount = autofillIds.length; |
| for (int viewNum = 0; viewNum < viewCount; viewNum++) { |
| if (autofillIds[viewNum] != null && views[viewNum] == null) { |
| views[viewNum] = rootView.findViewByAutofillIdTraversal( |
| autofillIds[viewNum].getViewId()); |
| } |
| } |
| } |
| } |
| return views; |
| } |
| |
| @Override |
| public boolean autofillClientIsFillUiShowing() { |
| return mAutofillPopupWindow != null && mAutofillPopupWindow.isShowing(); |
| } |
| |
| @Override |
| public boolean autofillClientRequestHideFillUi() { |
| if (mAutofillPopupWindow == null) { |
| return false; |
| } |
| mAutofillPopupWindow.dismiss(); |
| mAutofillPopupWindow = null; |
| return true; |
| } |
| |
| @Override |
| public boolean autofillClientRequestShowFillUi(@NonNull View anchor, int width, |
| int height, @Nullable Rect anchorBounds, IAutofillWindowPresenter presenter) { |
| final boolean wasShowing; |
| |
| if (mAutofillPopupWindow == null) { |
| wasShowing = false; |
| mAutofillPopupWindow = new AutofillPopupWindow(presenter); |
| } else { |
| wasShowing = mAutofillPopupWindow.isShowing(); |
| } |
| mAutofillPopupWindow.update(anchor, 0, 0, width, height, anchorBounds); |
| |
| return !wasShowing && mAutofillPopupWindow.isShowing(); |
| } |
| |
| @Override |
| public void autofillClientDispatchUnhandledKey(View anchor, KeyEvent keyEvent) { |
| ViewRootImpl rootImpl = anchor.getViewRootImpl(); |
| if (rootImpl != null) { |
| // don't care if anchorView is current focus, for example a custom view may only receive |
| // touchEvent, not focusable but can still trigger autofill window. The Key handling |
| // might be inside parent of the custom view. |
| rootImpl.dispatchKeyFromAutofill(keyEvent); |
| } |
| } |
| |
| @Override |
| public boolean isDisablingEnterExitEventForAutofill() { |
| return mAutoFillIgnoreFirstResumePause || !mActivity.isResumed(); |
| } |
| |
| @Override |
| public void autofillClientResetableStateAvailable() { |
| mAutoFillResetNeeded = true; |
| } |
| |
| @Override |
| public void autofillClientRunOnUiThread(Runnable action) { |
| mActivity.runOnUiThread(action); |
| } |
| |
| @Override |
| public void autofillClientAuthenticate(int authenticationId, IntentSender intent, |
| Intent fillInIntent, boolean authenticateInline) { |
| try { |
| ActivityOptions activityOptions = ActivityOptions.makeBasic() |
| .setPendingIntentBackgroundActivityStartMode( |
| ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); |
| mActivity.startIntentSenderForResult(intent, AUTO_FILL_AUTH_WHO_PREFIX, |
| authenticationId, fillInIntent, 0, 0, activityOptions.toBundle()); |
| } catch (IntentSender.SendIntentException e) { |
| Log.e(TAG, "authenticate() failed for intent:" + intent, e); |
| } |
| } |
| } |