/* * Copyright (C) 2017 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 static android.service.autofill.FillRequest.FLAG_IME_SHOWING; import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST; import static android.service.autofill.FillRequest.FLAG_PASSWORD_INPUT_TYPE; import static android.service.autofill.FillRequest.FLAG_PCC_DETECTION; import static android.service.autofill.FillRequest.FLAG_RESET_FILL_DIALOG_STATE; import static android.service.autofill.FillRequest.FLAG_SCREEN_HAS_CREDMAN_FIELD; import static android.service.autofill.FillRequest.FLAG_SUPPORTS_FILL_DIALOG; import static android.service.autofill.FillRequest.FLAG_VIEW_NOT_FOCUSED; import static android.service.autofill.FillRequest.FLAG_VIEW_REQUESTS_CREDMAN_SERVICE; import static android.view.ContentInfo.SOURCE_AUTOFILL; import static android.view.autofill.Helper.sDebug; import static android.view.autofill.Helper.sVerbose; import static android.view.autofill.Helper.toList; import android.accessibilityservice.AccessibilityServiceInfo; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresFeature; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TestApi; import android.app.ActivityOptions; import android.app.assist.AssistStructure.ViewNode; import android.app.assist.AssistStructure.ViewNodeBuilder; import android.app.assist.AssistStructure.ViewNodeParcelable; import android.content.AutofillOptions; import android.content.ClipData; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentSender; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.credentials.GetCredentialException; import android.credentials.GetCredentialResponse; import android.graphics.Rect; import android.metrics.LogMaker; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Parcelable; import android.os.RemoteException; import android.os.SystemClock; import android.service.autofill.AutofillService; import android.service.autofill.FillEventHistory; import android.service.autofill.Flags; import android.service.autofill.UserData; import android.service.credentials.CredentialProviderService; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.DebugUtils; import android.util.Log; import android.util.Slog; import android.util.SparseArray; import android.view.Choreographer; import android.view.ContentInfo; import android.view.KeyEvent; import android.view.View; import android.view.ViewRootImpl; import android.view.WindowInsets; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeProvider; import android.view.accessibility.AccessibilityWindowInfo; import android.view.inputmethod.InputMethodManager; import android.widget.CheckBox; import android.widget.DatePicker; import android.widget.EditText; import android.widget.RadioGroup; import android.widget.Spinner; import android.widget.TextView; import android.widget.TimePicker; import com.android.internal.annotations.GuardedBy; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.os.IResultReceiver; import com.android.internal.util.ArrayUtils; import com.android.internal.util.SyncResultReceiver; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.io.PrintWriter; import java.io.Serializable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import sun.misc.Cleaner; //TODO: use java.lang.ref.Cleaner once Android supports Java 9 /** *

The {@link AutofillManager} class provides ways for apps and custom views to * integrate with the Autofill Framework lifecycle. * *

To learn about using Autofill in your app, read * the Autofill Framework guides. * *

Autofill lifecycle

* *

The autofill lifecycle starts with the creation of an autofill context associated with an * activity context. The autofill context is created when one of the following methods is called for * the first time in an activity context, and the current user has an enabled autofill service: * *

* *

Typically, the context is automatically created when the first view of the activity is * focused because {@code View.onFocusChanged()} indirectly calls * {@link #notifyViewEntered(View)}. App developers can call {@link #requestAutofill(View)} to * explicitly create it (for example, a custom view developer could offer a contextual menu action * in a text-field view to let users manually request autofill). * *

After the context is created, the Android System creates a {@link android.view.ViewStructure} * that represents the view hierarchy by calling * {@link View#dispatchProvideAutofillStructure(android.view.ViewStructure, int)} in the root views * of all application windows. By default, {@code dispatchProvideAutofillStructure()} results in * subsequent calls to {@link View#onProvideAutofillStructure(android.view.ViewStructure, int)} and * {@link View#onProvideAutofillVirtualStructure(android.view.ViewStructure, int)} for each view in * the hierarchy. * *

The resulting {@link android.view.ViewStructure} is then passed to the autofill service, which * parses it looking for views that can be autofilled. If the service finds such views, it returns * a data structure to the Android System containing the following optional info: * *

* *

When the service returns datasets, the Android System displays an autofill dataset picker * UI associated with the view, when the view is focused on and is part of a dataset. * The application can be notified when the UI is shown by registering an * {@link AutofillCallback} through {@link #registerCallback(AutofillCallback)}. When the user * selects a dataset from the UI, all views present in the dataset are autofilled, through * calls to {@link View#autofill(AutofillValue)} or {@link View#autofill(SparseArray)}. * *

When the service returns ids of savable views, the Android System keeps track of changes * made to these views, so they can be used to determine if the autofill save UI is shown later. * *

The context is then finished when one of the following occurs: * *

* *

Finally, after the autofill context is commited (i.e., not cancelled), the Android System * shows an autofill save UI if the value of savable views have changed. If the user selects the * option to Save, the current value of the views is then sent to the autofill service. * *

Additional notes

* *

It is safe to call AutofillManager methods from any thread. */ @SystemService(Context.AUTOFILL_MANAGER_SERVICE) @RequiresFeature(PackageManager.FEATURE_AUTOFILL) public final class AutofillManager { private static final String TAG = "AutofillManager"; /** * Intent extra: The assist structure which captures the filled screen. * *

* Type: {@link android.app.assist.AssistStructure} */ public static final String EXTRA_ASSIST_STRUCTURE = "android.view.autofill.extra.ASSIST_STRUCTURE"; /** * Intent extra: The result of an authentication operation. It is * either a fully populated {@link android.service.autofill.FillResponse} * or a fully populated {@link android.service.autofill.Dataset} if * a response or a dataset is being authenticated respectively. * *

* Type: {@link android.service.autofill.FillResponse} or a * {@link android.service.autofill.Dataset} */ public static final String EXTRA_AUTHENTICATION_RESULT = "android.view.autofill.extra.AUTHENTICATION_RESULT"; /** * Intent extra: The optional boolean extra field provided by the * {@link android.service.autofill.AutofillService} accompanying the {@link * android.service.autofill.Dataset} result of an authentication operation. * *

Before {@link android.os.Build.VERSION_CODES#R}, if the authentication result is a * {@link android.service.autofill.Dataset}, it'll be used to autofill the fields, and also * replace the existing dataset in the cached {@link android.service.autofill.FillResponse}. * That means if the user clears the field values, the autofill suggestion will show up again * with the new authenticated Dataset. * *

In {@link android.os.Build.VERSION_CODES#R}, we added an exception to this behavior * that if the Dataset being authenticated is a pinned dataset (see * {@link android.service.autofill.InlinePresentation#isPinned()}), the old Dataset will not be * replaced. * *

In {@link android.os.Build.VERSION_CODES#S}, we added this boolean extra field to * allow the {@link android.service.autofill.AutofillService} to explicitly specify whether * the returned authenticated Dataset is ephemeral. An ephemeral Dataset will be used to * autofill once and then thrown away. Therefore, when the boolean extra is set to true, the * returned Dataset will not replace the old dataset from the existing * {@link android.service.autofill.FillResponse}. When it's set to false, it will. When it's not * set, the old dataset will be replaced, unless it is a pinned inline suggestion, which is * consistent with the behavior in {@link android.os.Build.VERSION_CODES#R}. */ public static final String EXTRA_AUTHENTICATION_RESULT_EPHEMERAL_DATASET = "android.view.autofill.extra.AUTHENTICATION_RESULT_EPHEMERAL_DATASET"; /** * Intent extra: The optional extras provided by the * {@link android.service.autofill.AutofillService}. * *

For example, when the service responds to a {@link * android.service.autofill.FillCallback#onSuccess(android.service.autofill.FillResponse)} with * a {@code FillResponse} that requires authentication, the Intent that launches the * service authentication will contain the Bundle set by * {@link android.service.autofill.FillResponse.Builder#setClientState(Bundle)} on this extra. * *

On Android {@link android.os.Build.VERSION_CODES#P} and higher, the autofill service * can also add this bundle to the {@link Intent} set as the * {@link android.app.Activity#setResult(int, Intent) result} for an authentication request, * so the bundle can be recovered later on * {@link android.service.autofill.SaveRequest#getClientState()}. * *

* Type: {@link android.os.Bundle} */ public static final String EXTRA_CLIENT_STATE = "android.view.autofill.extra.CLIENT_STATE"; /** * @hide */ public static final String EXTRA_AUTH_STATE = "android.view.autofill.extra.AUTH_STATE"; /** * Intent extra: the {@link android.view.inputmethod.InlineSuggestionsRequest} in the * autofill request. * *

This is filled in the authentication intent so the * {@link android.service.autofill.AutofillService} can use it to create the inline * suggestion {@link android.service.autofill.Dataset} in the response, if the original autofill * request contains the {@link android.view.inputmethod.InlineSuggestionsRequest}. */ public static final String EXTRA_INLINE_SUGGESTIONS_REQUEST = "android.view.autofill.extra.INLINE_SUGGESTIONS_REQUEST"; /** @hide */ public static final String EXTRA_RESTORE_SESSION_TOKEN = "android.view.autofill.extra.RESTORE_SESSION_TOKEN"; /** @hide */ public static final String EXTRA_RESTORE_CROSS_ACTIVITY = "android.view.autofill.extra.RESTORE_CROSS_ACTIVITY"; /** * Internal extra used to pass a binder to the {@link IAugmentedAutofillManagerClient}. * * @hide */ public static final String EXTRA_AUGMENTED_AUTOFILL_CLIENT = "android.view.autofill.extra.AUGMENTED_AUTOFILL_CLIENT"; /** * Internal extra used to pass the fill request id in client state of * {@link ConvertCredentialResponse} * * @hide */ public static final String EXTRA_AUTOFILL_REQUEST_ID = "android.view.autofill.extra.AUTOFILL_REQUEST_ID"; /** * Autofill Hint to indicate that it can match any field. * * @hide */ @TestApi public static final String ANY_HINT = "any"; private static final String SESSION_ID_TAG = "android:sessionId"; private static final String STATE_TAG = "android:state"; private static final String LAST_AUTOFILLED_DATA_TAG = "android:lastAutoFilledData"; /** @hide */ public static final int ACTION_START_SESSION = 1; /** @hide */ public static final int ACTION_VIEW_ENTERED = 2; /** @hide */ public static final int ACTION_VIEW_EXITED = 3; /** @hide */ public static final int ACTION_VALUE_CHANGED = 4; /** @hide */ public static final int ACTION_RESPONSE_EXPIRED = 5; /** @hide */ public static final int NO_LOGGING = 0; /** @hide */ public static final int FLAG_ADD_CLIENT_ENABLED = 0x1; /** @hide */ public static final int FLAG_ADD_CLIENT_DEBUG = 0x2; /** @hide */ public static final int FLAG_ADD_CLIENT_VERBOSE = 0x4; /** @hide */ public static final int FLAG_ADD_CLIENT_ENABLED_FOR_AUGMENTED_AUTOFILL_ONLY = 0x8; // NOTE: flag below is used by the session start receiver only, hence it can have values above /** @hide */ public static final int RECEIVER_FLAG_SESSION_FOR_AUGMENTED_AUTOFILL_ONLY = 0x1; /** @hide */ public static final int DEFAULT_LOGGING_LEVEL = Build.IS_DEBUGGABLE ? AutofillManager.FLAG_ADD_CLIENT_DEBUG : AutofillManager.NO_LOGGING; /** @hide */ public static final int DEFAULT_MAX_PARTITIONS_SIZE = 10; /** Which bits in an authentication id are used for the dataset id */ private static final int AUTHENTICATION_ID_DATASET_ID_MASK = 0xFFFF; /** How many bits in an authentication id are used for the dataset id */ private static final int AUTHENTICATION_ID_DATASET_ID_SHIFT = 16; /** @hide The index for an undefined data set */ public static final int AUTHENTICATION_ID_DATASET_ID_UNDEFINED = 0xFFFF; /** * Used on {@link #onPendingSaveUi(int, IBinder)} to cancel the pending UI. * * @hide */ public static final int PENDING_UI_OPERATION_CANCEL = 1; /** * Used on {@link #onPendingSaveUi(int, IBinder)} to restore the pending UI. * * @hide */ public static final int PENDING_UI_OPERATION_RESTORE = 2; /** * Initial state of the autofill context, set when there is no session (i.e., when * {@link #mSessionId} is {@link #NO_SESSION}). * *

In this state, app callbacks (such as {@link #notifyViewEntered(View)}) are notified to * the server. * * @hide */ public static final int STATE_UNKNOWN = 0; /** * State where the autofill context hasn't been {@link #commit() finished} nor * {@link #cancel() canceled} yet. * * @hide */ public static final int STATE_ACTIVE = 1; /** * State where the autofill context was finished by the server because the autofill * service could not autofill the activity. * *

In this state, most apps callback (such as {@link #notifyViewEntered(View)}) are ignored, * exception {@link #requestAutofill(View)} (and {@link #requestAutofill(View, int, Rect)}). * * @hide */ public static final int STATE_FINISHED = 2; /** * State where the autofill context has been {@link #commit() finished} but the server still has * a session because the Save UI hasn't been dismissed yet. * * @hide */ public static final int STATE_SHOWING_SAVE_UI = 3; /** * State where the autofill is disabled because the service cannot autofill the activity at all. * *

In this state, every call is ignored, even {@link #requestAutofill(View)} * (and {@link #requestAutofill(View, int, Rect)}). * * @hide */ public static final int STATE_DISABLED_BY_SERVICE = 4; /** * Same as {@link #STATE_UNKNOWN}, but used on * {@link AutofillManagerClient#setSessionFinished(int, List)} when the session was finished * because the URL bar changed on client mode * * @hide */ public static final int STATE_UNKNOWN_COMPAT_MODE = 5; /** * Same as {@link #STATE_UNKNOWN}, but used on * {@link AutofillManagerClient#setSessionFinished(int, List)} when the session was finished * because the service failed to fullfil a request. * * @hide */ public static final int STATE_UNKNOWN_FAILED = 6; /** * Same as {@link #STATE_ACTIVE}, but when pending authentication after * {@link AutofillManagerClient#authenticate(int, int, IntentSender, Intent, boolean)} * * @hide */ public static final int STATE_PENDING_AUTHENTICATION = 7; /** * Timeout in ms for calls to the field classification service. * @hide */ public static final int FC_SERVICE_TIMEOUT = 5000; /** * Timeout for calls to system_server. */ private static final int SYNC_CALLS_TIMEOUT_MS = 5000; /** * @hide */ @TestApi public static final int MAX_TEMP_AUGMENTED_SERVICE_DURATION_MS = 1_000 * 60 * 2; // 2 minutes /** * Disables Augmented Autofill. * * @hide */ @TestApi public static final int FLAG_SMART_SUGGESTION_OFF = 0x0; /** * Displays the Augment Autofill window using the same mechanism (such as a popup-window * attached to the focused view) as the standard autofill. * * @hide */ @TestApi public static final int FLAG_SMART_SUGGESTION_SYSTEM = 0x1; /** @hide */ @IntDef(flag = false, value = { FLAG_SMART_SUGGESTION_OFF, FLAG_SMART_SUGGESTION_SYSTEM }) @Retention(RetentionPolicy.SOURCE) public @interface SmartSuggestionMode {} /** @hide */ public static final int RESULT_OK = 0; /** @hide */ public static final int RESULT_CODE_NOT_SERVICE = -1; /** * Reasons to commit the Autofill context. * *

If adding a new reason, modify * {@link com.android.server.autofill.PresentationStatsEventLogger#getNoPresentationEventReason(int)} * as well.

* * @hide */ @IntDef(prefix = { "COMMIT_REASON_" }, value = { COMMIT_REASON_UNKNOWN, COMMIT_REASON_ACTIVITY_FINISHED, COMMIT_REASON_VIEW_COMMITTED, COMMIT_REASON_VIEW_CLICKED, COMMIT_REASON_VIEW_CHANGED, COMMIT_REASON_SESSION_DESTROYED }) @Retention(RetentionPolicy.SOURCE) public @interface AutofillCommitReason {} /** * Autofill context was committed because of an unknown reason. * * @hide */ public static final int COMMIT_REASON_UNKNOWN = 0; /** * Autofill context was committed because activity finished. * * @hide */ public static final int COMMIT_REASON_ACTIVITY_FINISHED = 1; /** * Autofill context was committed because {@link #commit()} was called. * * @hide */ public static final int COMMIT_REASON_VIEW_COMMITTED = 2; /** * Autofill context was committed because view was clicked. * * @hide */ public static final int COMMIT_REASON_VIEW_CLICKED = 3; /** * Autofill context was committed because of view changed. * * @hide */ public static final int COMMIT_REASON_VIEW_CHANGED = 4; /** * Autofill context was committed because of the session was destroyed. * * @hide */ public static final int COMMIT_REASON_SESSION_DESTROYED = 5; /** * Makes an authentication id from a request id and a dataset id. * * @param requestId The request id. * @param datasetId The dataset id. * @return The authentication id. * @hide */ public static int makeAuthenticationId(int requestId, int datasetId) { return (requestId << AUTHENTICATION_ID_DATASET_ID_SHIFT) | (datasetId & AUTHENTICATION_ID_DATASET_ID_MASK); } /** * Gets the request id from an authentication id. * * @param authRequestId The authentication id. * @return The request id. * @hide */ public static int getRequestIdFromAuthenticationId(int authRequestId) { return (authRequestId >> AUTHENTICATION_ID_DATASET_ID_SHIFT); } /** * Gets the dataset id from an authentication id. * * @param authRequestId The authentication id. * @return The dataset id. * @hide */ public static int getDatasetIdFromAuthenticationId(int authRequestId) { return (authRequestId & AUTHENTICATION_ID_DATASET_ID_MASK); } private final MetricsLogger mMetricsLogger = new MetricsLogger(); /** * There is currently no session running. * {@hide} */ public static final int NO_SESSION = Integer.MAX_VALUE; /** @hide **/ public static final String PINNED_DATASET_ID = "PINNED_DATASET_ID"; private final IAutoFillManager mService; private final Object mLock = new Object(); @GuardedBy("mLock") private IAutoFillManagerClient mServiceClient; @GuardedBy("mLock") private Cleaner mServiceClientCleaner; @GuardedBy("mLock") private IAugmentedAutofillManagerClient mAugmentedAutofillServiceClient; @GuardedBy("mLock") private AutofillCallback mCallback; private final Context mContext; @GuardedBy("mLock") private int mSessionId = NO_SESSION; @GuardedBy("mLock") private int mState = STATE_UNKNOWN; @GuardedBy("mLock") private boolean mEnabled; /** If a view changes to this mapping the autofill operation was successful */ @GuardedBy("mLock") @Nullable private ParcelableMap mLastAutofilledData; /** If view tracking is enabled, contains the tracking state */ @GuardedBy("mLock") @Nullable private TrackedViews mTrackedViews; /** Views that are only tracked because they are fillable and could be anchoring the UI. */ @GuardedBy("mLock") @Nullable private ArraySet mFillableIds; /** id of last requested autofill ui */ @Nullable private AutofillId mIdShownFillUi; /** * Views that were already "entered" - if they're entered again when the session is not active, * they're ignored * */ @GuardedBy("mLock") @Nullable private ArraySet mEnteredIds; /** * Views that were otherwised not important for autofill but triggered a session because the * context is allowlisted for augmented autofill. */ @GuardedBy("mLock") @Nullable private Set mEnteredForAugmentedAutofillIds; /** If set, session is commited when the field is clicked. */ @GuardedBy("mLock") @Nullable private AutofillId mSaveTriggerId; /** set to true when onInvisibleForAutofill is called, used by onAuthenticationResult */ @GuardedBy("mLock") private boolean mOnInvisibleCalled; /** If set, session is commited when the activity is finished; otherwise session is canceled. */ @GuardedBy("mLock") private boolean mSaveOnFinish; /** If compatibility mode is enabled - this is a bridge to interact with a11y */ @GuardedBy("mLock") private CompatibilityBridge mCompatibilityBridge; @Nullable private final AutofillOptions mOptions; /** When set, session is only used for augmented autofill requests. */ @GuardedBy("mLock") private boolean mForAugmentedAutofillOnly; /** * When set, standard autofill is disabled, but sessions can still be created for augmented * autofill only. */ @GuardedBy("mLock") private boolean mEnabledForAugmentedAutofillOnly; private boolean mScreenHasCredmanField; /** * Indicates whether there is already a field to do a fill request after * the activity started. * * Autofill will automatically trigger a fill request after activity * start if there is any field is autofillable. But if there is a field that * triggered autofill, it is unnecessary to trigger again through * AutofillManager#notifyViewEnteredForFillDialog. */ private AtomicBoolean mIsFillRequested; @Nullable private List mFillDialogTriggerIds; private final boolean mIsFillDialogEnabled; private final boolean mIsFillAndSaveDialogDisabledForCredentialManager; // Indicate whether trigger fill request on unimportant views is enabled private boolean mIsTriggerFillRequestOnUnimportantViewEnabled = false; // Indicate whether to apply heuristic check on important views before trigger fill request private boolean mIsTriggerFillRequestOnFilteredImportantViewsEnabled; // Indicate whether to enable autofill for all view types private boolean mShouldEnableAutofillOnAllViewTypes; // A set containing all non-autofillable ime actions passed by flag private Set mNonAutofillableImeActionIdSet = new ArraySet<>(); // If a package is fully denied, then all views that marked as not // important for autofill will not trigger fill request private boolean mIsPackageFullyDeniedForAutofill = false; // If a package is partially denied, autofill manager will check whether // current activity is in deny set to decide whether to trigger fill request private boolean mIsPackagePartiallyDeniedForAutofill = false; // A deny set read from device config private Set mDeniedActivitySet = new ArraySet<>(); // If a package is fully allowed, all views in package will skip the heuristic check private boolean mIsPackageFullyAllowedForAutofill = false; // If a package is partially denied, autofill manager will check whether // current activity is in allowed activity set. If it's allowed activity, then autofill manager // will skip the heuristic check private boolean mIsPackagePartiallyAllowedForAutofill = false; // An allowed activity set read from device config private Set mAllowedActivitySet = new ArraySet<>(); // Whether to enable multi-line check when checking whether view is autofillable private boolean mShouldEnableMultilineFilter; // Indicate whether should include all view with autofill type not none in assist structure private boolean mShouldIncludeAllViewsWithAutofillTypeNotNoneInAssistStructure; // Indicate whether should include all view in assist structure private boolean mShouldIncludeAllChildrenViewInAssistStructure; // Indicate whether WebView should always be included in the assist structure private boolean mShouldAlwaysIncludeWebviewInAssistStructure; // Indicate whether invisibles views should be included in the assist structure private boolean mShouldIncludeInvisibleViewInAssistStructure; // Controls logic around apps changing some properties of their views when activity loses // focus due to autofill showing biometric activity, password manager, or password breach check. private boolean mRelayoutFix; // Indicates whether the credman integration is enabled. private final boolean mIsCredmanIntegrationEnabled; // Indicates whether called the showAutofillDialog() method. private boolean mShowAutofillDialogCalled = false; // Cached autofill feature flag private boolean mShouldIgnoreCredentialViews = false; private final String[] mFillDialogEnabledHints; // Tracked all views that have appeared, including views that there are no // dataset in responses. Used to avoid request pre-fill request again and again. private final ArraySet mAllTrackedViews = new ArraySet<>(); /** @hide */ public interface AutofillClient { /** * Asks the client to start an authentication flow. * * @param authenticationId A unique id of the authentication operation. * @param intent The authentication intent. * @param fillInIntent The authentication fill-in intent. */ void autofillClientAuthenticate(int authenticationId, IntentSender intent, Intent fillInIntent, boolean authenticateInline); /** * Tells the client this manager has state to be reset. */ void autofillClientResetableStateAvailable(); /** * Request showing the autofill UI. * * @param anchor The real view the UI needs to anchor to. * @param width The width of the fill UI content. * @param height The height of the fill UI content. * @param virtualBounds The bounds of the virtual decendant of the anchor. * @param presenter The presenter that controls the fill UI window. * @return Whether the UI was shown. */ boolean autofillClientRequestShowFillUi(@NonNull View anchor, int width, int height, @Nullable Rect virtualBounds, IAutofillWindowPresenter presenter); /** * Dispatch unhandled keyevent from Autofill window * @param anchor The real view the UI needs to anchor to. * @param keyEvent Unhandled KeyEvent from autofill window. */ void autofillClientDispatchUnhandledKey(@NonNull View anchor, @NonNull KeyEvent keyEvent); /** * Request hiding the autofill UI. * * @return Whether the UI was hidden. */ boolean autofillClientRequestHideFillUi(); /** * Gets whether the fill UI is currenlty being shown. * * @return Whether the fill UI is currently being shown */ boolean autofillClientIsFillUiShowing(); /** * Checks if views are currently attached and visible. * * @return And array with {@code true} iff the view is attached or visible */ @NonNull boolean[] autofillClientGetViewVisibility(@NonNull AutofillId[] autofillIds); /** * Checks is the client is currently visible as understood by autofill. * * @return {@code true} if the client is currently visible */ boolean autofillClientIsVisibleForAutofill(); /** * Client might disable enter/exit event e.g. when activity is paused. */ boolean isDisablingEnterExitEventForAutofill(); /** * Finds views by traversing the hierarchies of the client. * * @param autofillIds The autofill ids of the views to find * * @return And array containing the views (empty if no views found). */ @NonNull View[] autofillClientFindViewsByAutofillIdTraversal( @NonNull AutofillId[] autofillIds); /** * Finds a view by traversing the hierarchies of the client. * * @param autofillId The autofill id of the views to find * * @return The view, or {@code null} if not found */ @Nullable View autofillClientFindViewByAutofillIdTraversal(@NonNull AutofillId autofillId); /** * Finds a view by a11y id in a given client window. * * @param viewId The accessibility id of the views to find * @param windowId The accessibility window id where to search * * @return The view, or {@code null} if not found */ @Nullable View autofillClientFindViewByAccessibilityIdTraversal(int viewId, int windowId); /** * Runs the specified action on the UI thread. */ void autofillClientRunOnUiThread(Runnable action); /** * Gets the complete component name of this client. */ ComponentName autofillClientGetComponentName(); /** * Gets the activity token */ @Nullable IBinder autofillClientGetActivityToken(); /** * @return Whether compatibility mode is enabled. */ boolean autofillClientIsCompatibilityModeEnabled(); /** * Gets the next unique autofill ID. * *

Typically used to manage views whose content is recycled - see * {@link View#setAutofillId(AutofillId)} for more info. * * @return An ID that is unique in the activity. */ @Nullable AutofillId autofillClientGetNextAutofillId(); } /** * @hide */ public AutofillManager(Context context, IAutoFillManager service) { mContext = Objects.requireNonNull(context, "context cannot be null"); mService = service; mOptions = context.getAutofillOptions(); mIsFillRequested = new AtomicBoolean(false); mIsFillDialogEnabled = AutofillFeatureFlags.isFillDialogEnabled(); mFillDialogEnabledHints = AutofillFeatureFlags.getFillDialogEnabledHints(); mIsFillAndSaveDialogDisabledForCredentialManager = AutofillFeatureFlags.isFillAndSaveDialogDisabledForCredentialManager(); if (sDebug) { Log.d(TAG, "Fill dialog is enabled:" + mIsFillDialogEnabled + ", hints=" + Arrays.toString(mFillDialogEnabledHints)); } if (mOptions != null) { sDebug = (mOptions.loggingLevel & FLAG_ADD_CLIENT_DEBUG) != 0; sVerbose = (mOptions.loggingLevel & FLAG_ADD_CLIENT_VERBOSE) != 0; } mIsTriggerFillRequestOnUnimportantViewEnabled = AutofillFeatureFlags.isTriggerFillRequestOnUnimportantViewEnabled(); mIsTriggerFillRequestOnFilteredImportantViewsEnabled = AutofillFeatureFlags.isTriggerFillRequestOnFilteredImportantViewsEnabled(); mShouldEnableAutofillOnAllViewTypes = AutofillFeatureFlags.shouldEnableAutofillOnAllViewTypes(); mNonAutofillableImeActionIdSet = AutofillFeatureFlags.getNonAutofillableImeActionIdSetFromFlag(); mShouldEnableMultilineFilter = AutofillFeatureFlags.shouldEnableMultilineFilter(); final String denyListString = AutofillFeatureFlags.getDenylistStringFromFlag(); final String allowlistString = AutofillFeatureFlags.getAllowlistStringFromFlag(); final String packageName = mContext.getPackageName(); mIsPackageFullyDeniedForAutofill = isPackageFullyAllowedOrDeniedForAutofill(denyListString, packageName); mIsPackageFullyAllowedForAutofill = isPackageFullyAllowedOrDeniedForAutofill(allowlistString, packageName); if (!mIsPackageFullyDeniedForAutofill) { mIsPackagePartiallyDeniedForAutofill = isPackagePartiallyDeniedOrAllowedForAutofill(denyListString, packageName); } if (!mIsPackageFullyAllowedForAutofill) { mIsPackagePartiallyAllowedForAutofill = isPackagePartiallyDeniedOrAllowedForAutofill(allowlistString, packageName); } if (mIsPackagePartiallyDeniedForAutofill) { mDeniedActivitySet = getDeniedOrAllowedActivitySetFromString( denyListString, packageName); } if (mIsPackagePartiallyAllowedForAutofill) { mAllowedActivitySet = getDeniedOrAllowedActivitySetFromString( allowlistString, packageName); } mShouldIncludeAllViewsWithAutofillTypeNotNoneInAssistStructure = AutofillFeatureFlags.shouldIncludeAllViewsAutofillTypeNotNoneInAssistStructrue(); mShouldIncludeAllChildrenViewInAssistStructure = AutofillFeatureFlags.shouldIncludeAllChildrenViewInAssistStructure(); mShouldAlwaysIncludeWebviewInAssistStructure = AutofillFeatureFlags.shouldAlwaysIncludeWebviewInAssistStructure(); mShouldIncludeInvisibleViewInAssistStructure = AutofillFeatureFlags.shouldIncludeInvisibleViewInAssistStructure(); mRelayoutFix = AutofillFeatureFlags.shouldIgnoreRelayoutWhenAuthPending(); mIsCredmanIntegrationEnabled = Flags.autofillCredmanIntegration(); } /** * Whether to apply heuristic check on important views before triggering fill request * * @hide */ public boolean isTriggerFillRequestOnFilteredImportantViewsEnabled() { return mIsTriggerFillRequestOnFilteredImportantViewsEnabled; } /** * Whether to trigger fill request on not important views that passes heuristic check * * @hide */ public boolean isTriggerFillRequestOnUnimportantViewEnabled() { return mIsTriggerFillRequestOnUnimportantViewEnabled; } /** * Whether view passes the imeAction check * */ private boolean isPassingImeActionCheck(EditText editText) { final int actionId = editText.getImeOptions(); if (mNonAutofillableImeActionIdSet.contains(String.valueOf(actionId))) { Log.d(TAG, "view not autofillable - not passing ime action check"); return false; } return true; } /** * Checks whether the view passed in is not multiline text * * @param editText the view that passed to this check * @return true if the view input is not multiline, false otherwise */ private boolean isPassingMultilineCheck(EditText editText) { // check if min line is set to be greater than 1 if (editText.getMinLines() > 1) { Log.d(TAG, "view not autofillable - has multiline input type"); return false; } return true; } private boolean isPackageFullyAllowedOrDeniedForAutofill( @NonNull String listString, @NonNull String packageName) { // If "PackageName:;" is in the string, then it the package is fully denied or allowed for // autofill, depending on which string is passed to this function return listString.indexOf(packageName + ":;") != -1; } private boolean isPackagePartiallyDeniedOrAllowedForAutofill( @NonNull String listString, @NonNull String packageName) { // If "PackageName:" is in string when "PackageName:;" is not, then it means there are // specific activities to be allowed or denied. So the package is partially allowed or // denied for autofill. return listString.indexOf(packageName + ":") != -1; } /** * @hide */ public boolean shouldIncludeAllChildrenViewsWithAutofillTypeNotNoneInAssistStructure() { return mShouldIncludeAllViewsWithAutofillTypeNotNoneInAssistStructure; } /** * @hide */ public boolean shouldIncludeAllChildrenViewInAssistStructure() { return mShouldIncludeAllChildrenViewInAssistStructure; } /** * @hide */ public boolean shouldAlwaysIncludeWebviewInAssistStructure() { return mShouldAlwaysIncludeWebviewInAssistStructure; } /** * @hide */ public boolean shouldIncludeInvisibleViewInAssistStructure() { return mShouldIncludeInvisibleViewInAssistStructure; } /** * Get the denied or allowed activitiy names under specified package from the list string and * set it in fields accordingly * * For example, if the package name is Package1, and the string is * "Package1:Activity1,Activity2;", then the extracted activity set would be * {Activity1, Activity2} * * @param listString Denylist that is got from device config. For example, * "Package1:Activity1,Activity2;Package2:;" * @param packageName Specify which package to extract.For example, "Package1" * * @return the extracted activity set, For example, {Activity1, Activity2} */ private Set getDeniedOrAllowedActivitySetFromString( @NonNull String listString, @NonNull String packageName) { // 1. Get the index of where the Package name starts final int packageInStringIndex = listString.indexOf(packageName + ":"); // 2. Get the ";" index after this index of package final int firstNextSemicolonIndex = listString.indexOf(";", packageInStringIndex); // 3. Get the activity names substring between the indexes final int activityStringStartIndex = packageInStringIndex + packageName.length() + 1; if (activityStringStartIndex >= firstNextSemicolonIndex) { Log.e(TAG, "Failed to get denied activity names from list because it's wrongly " + "formatted"); return new ArraySet<>(); } final String activitySubstring = listString.substring(activityStringStartIndex, firstNextSemicolonIndex); // 4. Split the activity name substring final String[] activityStringArray = activitySubstring.split(","); // 5. return the extracted activities in a set return new ArraySet<>(Arrays.asList(activityStringArray)); } /** * Check whether autofill is denied for current activity or package. If current activity or * package is denied, then the view won't trigger fill request. * * @hide */ public boolean isActivityDeniedForAutofill() { if (mIsPackageFullyDeniedForAutofill) { return true; } if (mIsPackagePartiallyDeniedForAutofill) { final AutofillClient client = getClient(); if (client == null) { return false; } final ComponentName clientActivity = client.autofillClientGetComponentName(); if (mDeniedActivitySet.contains(clientActivity.flattenToShortString())) { return true; } } return false; } /** * Check whether current activity is allowlisted for autofill. * * If it is, the view in current activity will bypass heuristic check when checking whether it's * autofillable * * @hide */ public boolean isActivityAllowedForAutofill() { if (mIsPackageFullyAllowedForAutofill) { return true; } if (mIsPackagePartiallyAllowedForAutofill) { final AutofillClient client = getClient(); if (client == null) { return false; } final ComponentName clientActivity = client.autofillClientGetComponentName(); if (mAllowedActivitySet.contains(clientActivity.flattenToShortString())) { return true; } } return false; } /** * Check heuristics and other rules to determine if view is autofillable * * Note: this function should be only called only when autofill for all apps is turned on. The * calling method needs to check the corresponding flag to make sure that before calling into * this function. * * @hide */ public boolean isAutofillable(View view) { // Duplicate the autofill type check here because ViewGroup will call this function to // decide whether to include view in assist structure. // Also keep the autofill type check inside View#IsAutofillable() to serve as an early out // or if other functions need to call it. if (view.getAutofillType() == View.AUTOFILL_TYPE_NONE) return false; // denylist only applies to not important views if (!view.isImportantForAutofill() && isActivityDeniedForAutofill()) { return false; } if (isActivityAllowedForAutofill()) { return true; } if (view instanceof EditText) { if (mShouldEnableMultilineFilter && !isPassingMultilineCheck((EditText) view)) { return false; } return isPassingImeActionCheck((EditText) view); } // Skip view type check if view is important for autofill or // shouldEnableAutofillOnAllViewTypes flag is turned on if (view.isImportantForAutofill() || mShouldEnableAutofillOnAllViewTypes) { return true; } if (view instanceof CheckBox || view instanceof Spinner || view instanceof DatePicker || view instanceof TimePicker || view instanceof RadioGroup) { return true; } Log.d(TAG, "view is not autofillable - not important and filtered by view type check"); return false; } /** * @hide */ public void enableCompatibilityMode() { synchronized (mLock) { // The accessibility manager is a singleton so we may need to plug // different bridge based on which activity is currently focused // in the current process. Since compat would be rarely used, just // create and register a new instance every time. if (sDebug) { Slog.d(TAG, "creating CompatibilityBridge for " + mContext); } mCompatibilityBridge = new CompatibilityBridge(); } } /** * Restore state after activity lifecycle * * @param savedInstanceState The state to be restored * * {@hide} */ public void onCreate(Bundle savedInstanceState) { if (!hasAutofillFeature()) { return; } synchronized (mLock) { mLastAutofilledData = savedInstanceState.getParcelable(LAST_AUTOFILLED_DATA_TAG, android.view.autofill.ParcelableMap.class); if (isActiveLocked()) { Log.w(TAG, "New session was started before onCreate()"); return; } mSessionId = savedInstanceState.getInt(SESSION_ID_TAG, NO_SESSION); mState = savedInstanceState.getInt(STATE_TAG, STATE_UNKNOWN); if (mSessionId != NO_SESSION) { final boolean clientAdded = tryAddServiceClientIfNeededLocked(); final AutofillClient client = getClient(); if (client != null) { final SyncResultReceiver receiver = new SyncResultReceiver( SYNC_CALLS_TIMEOUT_MS); try { boolean sessionWasRestored = false; if (clientAdded) { mService.restoreSession(mSessionId, client.autofillClientGetActivityToken(), mServiceClient.asBinder(), receiver); sessionWasRestored = receiver.getIntResult() == 1; } else { Log.w(TAG, "No service client for session " + mSessionId); } if (!sessionWasRestored) { Log.w(TAG, "Session " + mSessionId + " could not be restored"); mSessionId = NO_SESSION; mState = STATE_UNKNOWN; } else { if (sDebug) { Log.d(TAG, "session " + mSessionId + " was restored"); } client.autofillClientResetableStateAvailable(); } } catch (RemoteException e) { Log.e(TAG, "Could not figure out if there was an autofill session", e); } catch (SyncResultReceiver.TimeoutException e) { Log.e(TAG, "Fail to get session restore status: " + e); } } } } } /** * Called once the client becomes visible. * * @see AutofillClient#autofillClientIsVisibleForAutofill() * * {@hide} */ public void onVisibleForAutofill() { // This gets called when the client just got visible at which point the visibility // of the tracked views may not have been computed (due to a pending layout, etc). // While generally we have no way to know when the UI has settled. We will evaluate // the tracked views state at the end of next frame to guarantee that everything // that may need to be laid out is laid out. Choreographer.getInstance().postCallback(Choreographer.CALLBACK_COMMIT, () -> { synchronized (mLock) { if (mEnabled && isActiveLocked() && mTrackedViews != null) { mTrackedViews.onVisibleForAutofillChangedLocked(); } } }, null); } /** * Called once the client becomes invisible. * * @see AutofillClient#autofillClientIsVisibleForAutofill() * * @param isExpiredResponse The response has expired or not * * {@hide} */ public void onInvisibleForAutofill(boolean isExpiredResponse) { synchronized (mLock) { mOnInvisibleCalled = true; if (isExpiredResponse) { // Notify service the response has expired. updateSessionLocked(/* id= */ null, /* bounds= */ null, /* value= */ null, ACTION_RESPONSE_EXPIRED, /* flags= */ 0); } } } /** * Save state before activity lifecycle * * @param outState Place to store the state * * {@hide} */ public void onSaveInstanceState(Bundle outState) { if (!hasAutofillFeature()) { return; } synchronized (mLock) { if (mSessionId != NO_SESSION) { outState.putInt(SESSION_ID_TAG, mSessionId); } if (mState != STATE_UNKNOWN) { outState.putInt(STATE_TAG, mState); } if (mLastAutofilledData != null) { outState.putParcelable(LAST_AUTOFILLED_DATA_TAG, mLastAutofilledData); } } } /** * @hide */ @GuardedBy("mLock") public boolean isCompatibilityModeEnabledLocked() { return mCompatibilityBridge != null; } /** * Checks whether autofill is enabled for the current user. * *

Typically used to determine whether the option to explicitly request autofill should * be offered - see {@link #requestAutofill(View)}. * * @return whether autofill is enabled for the current user. */ public boolean isEnabled() { if (!hasAutofillFeature()) { return false; } synchronized (mLock) { if (isDisabledByServiceLocked()) { return false; } final boolean clientAdded = tryAddServiceClientIfNeededLocked(); return clientAdded ? mEnabled : false; } } /** * Should always be called from {@link AutofillService#getFillEventHistory()}. * * @hide */ @Nullable public FillEventHistory getFillEventHistory() { try { final SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS); mService.getFillEventHistory(receiver); return receiver.getParcelableResult(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } catch (SyncResultReceiver.TimeoutException e) { Log.e(TAG, "Fail to get fill event history: " + e); return null; } } /** * Explicitly requests a new autofill context. * *

Normally, the autofill context is automatically started if necessary when * {@link #notifyViewEntered(View)} is called, but this method should be used in the * cases where it must be explicitly started. For example, when the view offers an AUTOFILL * option on its contextual overflow menu, and the user selects it. * * @param view view requesting the new autofill context. */ public void requestAutofill(@NonNull View view) { int flags = FLAG_MANUAL_REQUEST; if (!view.isFocused()) { flags |= FLAG_VIEW_NOT_FOCUSED; } notifyViewEntered(view, flags); } /** * Explicitly cancels the current session and requests a new autofill context. * *

Normally, the autofill context is automatically started if necessary when * {@link #notifyViewEntered(View)} is called, but this method should be used in * cases where it must be explicitly started or restarted. Currently, this method should only * be called by * {@link android.service.autofill.augmented.AugmentedAutofillService#requestAutofill( * ComponentName, AutofillId)} to cancel the current session and trigger the autofill flow in * a new session, giving the autofill service or the augmented autofill service a chance to * send updated suggestions. * * @param view view requesting the new autofill context. */ void requestAutofillFromNewSession(@NonNull View view) { cancel(); notifyViewEntered(view); } /** * Explicitly requests a new autofill context for virtual views. * *

Normally, the autofill context is automatically started if necessary when * {@link #notifyViewEntered(View, int, Rect)} is called, but this method should be used in the * cases where it must be explicitly started. For example, when the virtual view offers an * AUTOFILL option on its contextual overflow menu, and the user selects it. * *

The virtual view boundaries must be absolute screen coordinates. For example, if the * parent view uses {@code bounds} to draw the virtual view inside its Canvas, * the absolute bounds could be calculated by: * *

     *   int offset[] = new int[2];
     *   getLocationOnScreen(offset);
     *   Rect absBounds = new Rect(bounds.left + offset[0],
     *       bounds.top + offset[1],
     *       bounds.right + offset[0], bounds.bottom + offset[1]);
     * 
* * @param view the virtual view parent. * @param virtualId id identifying the virtual child inside the parent view. * @param absBounds absolute boundaries of the virtual view in the screen. */ public void requestAutofill(@NonNull View view, int virtualId, @NonNull Rect absBounds) { int flags = FLAG_MANUAL_REQUEST; if (!view.isFocused()) { flags |= FLAG_VIEW_NOT_FOCUSED; } notifyViewEntered(view, virtualId, absBounds, flags); } /** * Called when a {@link View} that supports autofill is entered. * * @param view {@link View} that was entered. */ public void notifyViewEntered(@NonNull View view) { notifyViewEntered(view, 0); } /** * Called when the virtual views are ready to the user for autofill. * * This method is used to notify autofill system the views are ready to the user. And then * Autofill can do initialization if needed before the user starts to input. For example, do * a pre-fill request for the * fill dialog. * * @param view the host view that holds a virtual view hierarchy. * @param infos extra information for the virtual views. The key is virtual id which represents * the virtual view in the host view. * * @throws IllegalArgumentException if the {@code infos} was empty */ public void notifyVirtualViewsReady( @NonNull View view, @NonNull SparseArray infos) { Objects.requireNonNull(infos); if (infos.size() == 0) { throw new IllegalArgumentException("No VirtualViewInfo found"); } boolean isCredmanRequested = false; if (shouldSuppressDialogsForCredman(view) && mIsFillAndSaveDialogDisabledForCredentialManager) { mScreenHasCredmanField = true; if (isCredmanRequested(view)) { if (sDebug) { Log.d(TAG, "Prefetching fill response for credMan: " + view.getAutofillId().toString()); } isCredmanRequested = true; } else { if (sDebug) { Log.d(TAG, "Ignoring Fill Dialog request since important for credMan:" + view.getAutofillId().toString()); } return; } } for (int i = 0; i < infos.size(); i++) { final VirtualViewFillInfo info = infos.valueAt(i); final int virtualId = infos.keyAt(i); notifyViewReadyInner(getAutofillId(view, virtualId), (info == null) ? null : info.getAutofillHints(), isCredmanRequested); } } /** * The {@link AutofillFeatureFlags#DEVICE_CONFIG_AUTOFILL_DIALOG_ENABLED} is {@code true} or * the view have the allowed autofill hints, performs a fill request to know there is any field * supported fill dialog. * * @hide */ public void notifyViewEnteredForFillDialog(View v) { boolean isCredmanRequested = false; if (shouldSuppressDialogsForCredman(v) && mIsFillAndSaveDialogDisabledForCredentialManager) { mScreenHasCredmanField = true; if (isCredmanRequested(v)) { if (sDebug) { Log.d(TAG, "Prefetching fill response for credMan: " + v.getAutofillId().toString()); } isCredmanRequested = true; } else { if (sDebug) { Log.d(TAG, "Ignoring Fill Dialog request since important for credMan:" + v.getAutofillId().toString()); } return; } } notifyViewReadyInner(v.getAutofillId(), v.getAutofillHints(), isCredmanRequested); } private void notifyViewReadyInner(AutofillId id, @Nullable String[] autofillHints, boolean isCredmanRequested) { if (sDebug) { Log.d(TAG, "notifyViewReadyInner:" + id); } if (!hasAutofillFeature()) { return; } synchronized (mLock) { if (mAllTrackedViews.contains(id)) { // The id is tracked and will not trigger pre-fill request again. return; } // Add the id as tracked to avoid triggering fill request again and again. mAllTrackedViews.add(id); if (mTrackedViews != null) { // To support the fill dialog can show for the autofillable Views in // different pages but in the same Activity. We need to reset the // mIsFillRequested flag to allow asking for a new FillRequest when // user switches to other page mTrackedViews.checkViewState(id); } } // Skip if the fill request has been performed for a view. if (mIsFillRequested.get()) { return; } // Start session with PCC flag to get assist structure and send field classification request // to PCC classification service. if (AutofillFeatureFlags.isAutofillPccClassificationEnabled()) { synchronized (mLock) { // If session has already been created, that'd mean we already have issued the // detection request previously. It is possible in cases like autofocus that this // method isn't invoked, so the server should still handle such cases where fill // request comes in but PCC Detection hasn't been triggered. There is no benefit to // trigger PCC Detection separately in those cases. if (!isActiveLocked()) { final boolean clientAdded = tryAddServiceClientIfNeededLocked(isCredmanRequested); if (clientAdded) { startSessionLocked(/* id= */ AutofillId.NO_AUTOFILL_ID, /* bounds= */ null, /* value= */ null, /* flags= */ FLAG_PCC_DETECTION); } else { if (sVerbose) { Log.v(TAG, "not starting session: no service client"); } } } } } // Check if framework should send pre-fill request for fill dialog boolean shouldSendPreFillRequestForFillDialog = false; if (mIsFillDialogEnabled) { shouldSendPreFillRequestForFillDialog = true; } else if (autofillHints != null) { // check if supported autofill hint is present for (String autofillHint : autofillHints) { for (String filldialogEnabledHint : mFillDialogEnabledHints) { if (filldialogEnabledHint.equalsIgnoreCase(autofillHint)) { shouldSendPreFillRequestForFillDialog = true; break; } } if (shouldSendPreFillRequestForFillDialog) break; } } if (shouldSendPreFillRequestForFillDialog) { if (sDebug) { Log.d(TAG, "Triggering pre-emptive request for fill dialog."); } int flags = FLAG_SUPPORTS_FILL_DIALOG; flags |= FLAG_VIEW_NOT_FOCUSED; if (isCredmanRequested) { if (sDebug) { Log.d(TAG, "Pre fill request is triggered for credMan"); } flags |= FLAG_VIEW_REQUESTS_CREDMAN_SERVICE; } synchronized (mLock) { // To match the id of the IME served view, used AutofillId.NO_AUTOFILL_ID on prefill // request, because IME will reset the id of IME served view to 0 when activity // start and does not focus on any view. If the id of the prefill request does // not match the IME served view's, Autofill will be blocking to wait inline // request from the IME. notifyViewEnteredLocked(/* view= */ null, AutofillId.NO_AUTOFILL_ID, /* bounds= */ null, /* value= */ null, flags); } } return; } private boolean hasFillDialogUiFeature() { return mIsFillDialogEnabled || !ArrayUtils.isEmpty(mFillDialogEnabledHints); } private int getImeStateFlag(View v) { if (v == null) return 0; final WindowInsets rootWindowInsets = v.getRootWindowInsets(); if (rootWindowInsets != null && rootWindowInsets.isVisible(WindowInsets.Type.ime())) { return FLAG_IME_SHOWING; } return 0; } @GuardedBy("mLock") private boolean shouldIgnoreViewEnteredLocked(@NonNull AutofillId id, int flags) { if (isDisabledByServiceLocked()) { if (sVerbose) { Log.v(TAG, "ignoring notifyViewEntered(flags=" + flags + ", view=" + id + ") on state " + getStateAsStringLocked() + " because disabled by svc"); } return true; } if (isFinishedLocked()) { // Session already finished: ignore if automatic request and view already entered if ((flags & FLAG_MANUAL_REQUEST) == 0 && mEnteredIds != null && mEnteredIds.contains(id)) { if (sVerbose) { Log.v(TAG, "ignoring notifyViewEntered(flags=" + flags + ", view=" + id + ") on state " + getStateAsStringLocked() + " because view was already entered: " + mEnteredIds); } return true; } } return false; } private boolean isClientVisibleForAutofillLocked() { final AutofillClient client = getClient(); return client != null && client.autofillClientIsVisibleForAutofill(); } private boolean isClientDisablingEnterExitEvent() { final AutofillClient client = getClient(); return client != null && client.isDisablingEnterExitEventForAutofill(); } private void notifyViewEntered(@NonNull View view, int flags) { if (!hasAutofillFeature()) { return; } AutofillCallback callback; synchronized (mLock) { callback = notifyViewEnteredLocked( view, view.getAutofillId(), /* bounds= */ null, view.getAutofillValue(), flags); } if (callback != null) { mCallback.onAutofillEvent(view, AutofillCallback.EVENT_INPUT_UNAVAILABLE); } } /** * Called when a {@link View} that supports autofill is exited. * * @param view {@link View} that was exited. */ public void notifyViewExited(@NonNull View view) { if (!hasAutofillFeature()) { return; } synchronized (mLock) { notifyViewExitedLocked(view); } } @GuardedBy("mLock") void notifyViewExitedLocked(@NonNull View view) { final boolean clientAdded = tryAddServiceClientIfNeededLocked(); if (clientAdded && (mEnabled || mEnabledForAugmentedAutofillOnly) && isActiveLocked()) { // dont notify exited when Activity is already in background if (!isClientDisablingEnterExitEvent()) { final AutofillId id = view.getAutofillId(); // Update focus on existing session. updateSessionLocked(id, null, null, ACTION_VIEW_EXITED, 0); } } } /** * Called when a {@link View view's} visibility changed. * * @param view {@link View} that was exited. * @param isVisible visible if the view is visible in the view hierarchy. */ public void notifyViewVisibilityChanged(@NonNull View view, boolean isVisible) { notifyViewVisibilityChangedInternal(view, 0, isVisible, false); } /** * Called when a virtual view's visibility changed. * * @param view {@link View} that was exited. * @param virtualId id identifying the virtual child inside the parent view. * @param isVisible visible if the view is visible in the view hierarchy. */ public void notifyViewVisibilityChanged(@NonNull View view, int virtualId, boolean isVisible) { notifyViewVisibilityChangedInternal(view, virtualId, isVisible, true); } /** * Called when a view/virtual view's visibility changed. * * @param view {@link View} that was exited. * @param virtualId id identifying the virtual child inside the parent view. * @param isVisible visible if the view is visible in the view hierarchy. * @param virtual Whether the view is virtual. */ private void notifyViewVisibilityChangedInternal(@NonNull View view, int virtualId, boolean isVisible, boolean virtual) { synchronized (mLock) { if (mForAugmentedAutofillOnly) { if (sVerbose) { Log.v(TAG, "notifyViewVisibilityChanged(): ignoring on augmented only mode"); } return; } if (mRelayoutFix && mState == STATE_PENDING_AUTHENTICATION) { if (sVerbose) { Log.v(TAG, "notifyViewVisibilityChanged(): ignoring in auth pending mode"); } return; } if (mEnabled && isActiveLocked()) { final AutofillId id = virtual ? getAutofillId(view, virtualId) : view.getAutofillId(); if (sVerbose) Log.v(TAG, "visibility changed for " + id + ": " + isVisible); if (!isVisible && mFillableIds != null) { if (mFillableIds.contains(id)) { if (sDebug) Log.d(TAG, "Hidding UI when view " + id + " became invisible"); requestHideFillUi(id, view); } } if (mTrackedViews != null) { mTrackedViews.notifyViewVisibilityChangedLocked(id, isVisible); } else if (sVerbose) { Log.v(TAG, "Ignoring visibility change on " + id + ": no tracked views"); } } else if (!virtual && isVisible) { startAutofillIfNeededLocked(view); } } } /** * Called when a virtual view that supports autofill is entered. * *

The virtual view boundaries must be absolute screen coordinates. For example, if the * parent, non-virtual view uses {@code bounds} to draw the virtual view inside its Canvas, * the absolute bounds could be calculated by: * *

     *   int offset[] = new int[2];
     *   getLocationOnScreen(offset);
     *   Rect absBounds = new Rect(bounds.left + offset[0],
     *       bounds.top + offset[1],
     *       bounds.right + offset[0], bounds.bottom + offset[1]);
     * 
* * @param view the virtual view parent. * @param virtualId id identifying the virtual child inside the parent view. * @param absBounds absolute boundaries of the virtual view in the screen. */ public void notifyViewEntered(@NonNull View view, int virtualId, @NonNull Rect absBounds) { notifyViewEntered(view, virtualId, absBounds, 0); } private void notifyViewEntered(View view, int virtualId, Rect bounds, int flags) { if (!hasAutofillFeature()) { return; } AutofillCallback callback; synchronized (mLock) { callback = notifyViewEnteredLocked( view, getAutofillId(view, virtualId), bounds, /* value= */ null, flags); } if (callback != null) { callback.onAutofillEvent(view, virtualId, AutofillCallback.EVENT_INPUT_UNAVAILABLE); } } /** Returns AutofillCallback if need fire EVENT_INPUT_UNAVAILABLE */ @GuardedBy("mLock") private AutofillCallback notifyViewEnteredLocked(@Nullable View view, AutofillId id, Rect bounds, AutofillValue value, int flags) { if (shouldIgnoreViewEnteredLocked(id, flags)) return null; boolean credmanRequested = isCredmanRequested(view); final boolean clientAdded = tryAddServiceClientIfNeededLocked(credmanRequested); if (!clientAdded) { if (sVerbose) Log.v(TAG, "ignoring notifyViewEntered(" + id + "): no service client"); return null; } if (!mEnabled && !mEnabledForAugmentedAutofillOnly) { if (sVerbose) { Log.v(TAG, "ignoring notifyViewEntered(" + id + "): disabled"); } return mCallback; } if (mIsCredmanIntegrationEnabled && isCredmanRequested(view)) { flags |= FLAG_VIEW_REQUESTS_CREDMAN_SERVICE; } mIsFillRequested.set(true); // don't notify entered when Activity is already in background if (!isClientDisablingEnterExitEvent()) { if (view instanceof TextView && ((TextView) view).isAnyPasswordInputType()) { flags |= FLAG_PASSWORD_INPUT_TYPE; } // Update session when screen has credman field if (AutofillFeatureFlags.isFillAndSaveDialogDisabledForCredentialManager() && mScreenHasCredmanField) { flags |= FLAG_SCREEN_HAS_CREDMAN_FIELD; if (sVerbose) { Log.v(TAG, "updating session with flag screen has credman view"); } } flags |= getImeStateFlag(view); if (!isActiveLocked()) { // Starts new session. startSessionLocked(id, bounds, value, flags); } else { // Update focus on existing session. if (mForAugmentedAutofillOnly && (flags & FLAG_MANUAL_REQUEST) != 0) { if (sDebug) { Log.d(TAG, "notifyViewEntered(" + id + "): resetting " + "mForAugmentedAutofillOnly on manual request"); } mForAugmentedAutofillOnly = false; } if ((flags & FLAG_SUPPORTS_FILL_DIALOG) != 0) { flags |= FLAG_RESET_FILL_DIALOG_STATE; } updateSessionLocked(id, bounds, value, ACTION_VIEW_ENTERED, flags); } addEnteredIdLocked(id); } return null; } @GuardedBy("mLock") private void addEnteredIdLocked(@NonNull AutofillId id) { if (mEnteredIds == null) { mEnteredIds = new ArraySet<>(1); } id.resetSessionId(); mEnteredIds.add(id); } /** * Called when a virtual view that supports autofill is exited. * * @param view the virtual view parent. * @param virtualId id identifying the virtual child inside the parent view. */ public void notifyViewExited(@NonNull View view, int virtualId) { if (sVerbose) Log.v(TAG, "notifyViewExited(" + view.getAutofillId() + ", " + virtualId); if (!hasAutofillFeature()) { return; } synchronized (mLock) { notifyViewExitedLocked(view, virtualId); } } @GuardedBy("mLock") private void notifyViewExitedLocked(@NonNull View view, int virtualId) { final boolean clientAdded = tryAddServiceClientIfNeededLocked(); if (clientAdded && (mEnabled || mEnabledForAugmentedAutofillOnly) && isActiveLocked()) { // don't notify exited when Activity is already in background if (!isClientDisablingEnterExitEvent()) { final AutofillId id = getAutofillId(view, virtualId); // Update focus on existing session. updateSessionLocked(id, null, null, ACTION_VIEW_EXITED, 0); } } } /** * Called to indicate the value of an autofillable {@link View} changed. * * @param view view whose value changed. */ public void notifyValueChanged(View view) { if (!hasAutofillFeature()) { return; } AutofillId id = null; boolean valueWasRead = false; AutofillValue value = null; synchronized (mLock) { // If the session is gone some fields might still be highlighted, hence we have to // remove the isAutofilled property even if no sessions are active. if (mLastAutofilledData == null) { view.setAutofilled(false, false); } else { id = view.getAutofillId(); if (mLastAutofilledData.containsKey(id)) { value = view.getAutofillValue(); valueWasRead = true; final boolean hideHighlight = mLastAutofilledData.keySet().size() == 1; if (Objects.equals(mLastAutofilledData.get(id), value)) { view.setAutofilled(true, hideHighlight); try { mService.setViewAutofilled(mSessionId, id, mContext.getUserId()); } catch (RemoteException e) { // The failure could be a consequence of something going wrong on the // server side. Do nothing here since it's just logging, but it's // possible follow-up actions may fail. } } else { view.setAutofilled(false, false); mLastAutofilledData.remove(id); } } else { view.setAutofilled(false, false); } } if (!mEnabled || !isActiveLocked()) { if (!startAutofillIfNeededLocked(view)) { if (sVerbose) { Log.v(TAG, "notifyValueChanged(" + view.getAutofillId() + "): ignoring on state " + getStateAsStringLocked()); } } return; } if (id == null) { id = view.getAutofillId(); } if (!valueWasRead) { value = view.getAutofillValue(); } updateSessionLocked(id, null, value, ACTION_VALUE_CHANGED, getImeStateFlag(view)); } } /** * Called to indicate the value of an autofillable virtual view has changed. * * @param view the virtual view parent. * @param virtualId id identifying the virtual child inside the parent view. * @param value new value of the child. */ public void notifyValueChanged(View view, int virtualId, AutofillValue value) { if (!hasAutofillFeature()) { return; } synchronized (mLock) { if (!mEnabled || !isActiveLocked()) { if (sVerbose) { Log.v(TAG, "notifyValueChanged(" + view.getAutofillId() + ":" + virtualId + "): ignoring on state " + getStateAsStringLocked()); } return; } final AutofillId id = getAutofillId(view, virtualId); updateSessionLocked(id, null, value, ACTION_VALUE_CHANGED, getImeStateFlag(view)); } } /** * Called to indicate a {@link View} is clicked. * * @param view view that has been clicked. */ public void notifyViewClicked(@NonNull View view) { notifyViewClicked(view.getAutofillId()); } /** * Called to indicate a virtual view has been clicked. * * @param view the virtual view parent. * @param virtualId id identifying the virtual child inside the parent view. */ public void notifyViewClicked(@NonNull View view, int virtualId) { notifyViewClicked(getAutofillId(view, virtualId)); } private void notifyViewClicked(AutofillId id) { if (!hasAutofillFeature()) { return; } if (sVerbose) Log.v(TAG, "notifyViewClicked(): id=" + id + ", trigger=" + mSaveTriggerId); synchronized (mLock) { if (!mEnabled || !isActiveLocked()) { return; } if (mSaveTriggerId != null && mSaveTriggerId.equals(id)) { if (sDebug) Log.d(TAG, "triggering commit by click of " + id); commitLocked(/* commitReason= */ COMMIT_REASON_VIEW_CLICKED); mMetricsLogger.write(newLog(MetricsEvent.AUTOFILL_SAVE_EXPLICITLY_TRIGGERED)); } } } /** * Called by {@link android.app.Activity} to commit or cancel the session on finish. * * @hide */ public void onActivityFinishing() { if (!hasAutofillFeature()) { return; } synchronized (mLock) { if (mSaveOnFinish) { if (sDebug) Log.d(TAG, "onActivityFinishing(): calling commitLocked()"); commitLocked(/* commitReason= */ COMMIT_REASON_ACTIVITY_FINISHED); } else { if (sDebug) Log.d(TAG, "onActivityFinishing(): calling cancelLocked()"); cancelLocked(); } } } /** * Called to indicate the current autofill context should be commited. * *

This method is typically called by {@link View Views} that manage virtual views; for * example, when the view is rendering an {@code HTML} page with a form and virtual views * that represent the HTML elements, it should call this method after the form is submitted and * another page is rendered. * *

Note: This method does not need to be called on regular application lifecycle * methods such as {@link android.app.Activity#finish()}. */ public void commit() { if (!hasAutofillFeature()) { return; } if (sVerbose) Log.v(TAG, "commit() called by app"); synchronized (mLock) { commitLocked(/* commitReason= */ COMMIT_REASON_VIEW_COMMITTED); } } @GuardedBy("mLock") private void commitLocked(@AutofillCommitReason int commitReason) { if (!mEnabled && !isActiveLocked()) { return; } finishSessionLocked(/* commitReason= */ commitReason); } /** * Called to indicate the current autofill context should be cancelled. * *

This method is typically called by {@link View Views} that manage virtual views; for * example, when the view is rendering an {@code HTML} page with a form and virtual views * that represent the HTML elements, it should call this method if the user does not post the * form but moves to another form in this page. * *

Note: This method does not need to be called on regular application lifecycle * methods such as {@link android.app.Activity#finish()}. */ public void cancel() { if (sVerbose) Log.v(TAG, "cancel() called by app or augmented autofill service"); if (!hasAutofillFeature()) { return; } synchronized (mLock) { cancelLocked(); } } @GuardedBy("mLock") private void cancelLocked() { if (!mEnabled && !isActiveLocked()) { return; } cancelSessionLocked(); } /** @hide */ public void disableOwnedAutofillServices() { disableAutofillServices(); } /** * If the app calling this API has enabled autofill services they * will be disabled. */ public void disableAutofillServices() { if (!hasAutofillFeature()) { return; } try { mService.disableOwnedAutofillServices(mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Returns {@code true} if the calling application provides a {@link AutofillService} that is * enabled for the current user, or {@code false} otherwise. */ public boolean hasEnabledAutofillServices() { if (mService == null) return false; final SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS); try { mService.isServiceEnabled(mContext.getUserId(), mContext.getPackageName(), receiver); return receiver.getIntResult() == 1; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } catch (SyncResultReceiver.TimeoutException e) { throw new RuntimeException("Fail to get enabled autofill services status. " + e); } } /** * Returns the component name of the {@link AutofillService} that is enabled for the current * user. */ @Nullable public ComponentName getAutofillServiceComponentName() { if (mService == null) return null; final SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS); try { mService.getAutofillServiceComponentName(receiver); return receiver.getParcelableResult(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } catch (SyncResultReceiver.TimeoutException e) { throw new RuntimeException("Fail to get autofill services component name. " + e); } } /** * Gets the id of the {@link UserData} used for * field classification. * *

This method is useful when the service must check the status of the {@link UserData} in * the device without fetching the whole object. * *

Note: This method should only be called by an app providing an autofill service, * and it's ignored if the caller currently doesn't have an enabled autofill service for * the user. * * @return id of the {@link UserData} previously set by {@link #setUserData(UserData)} * or {@code null} if it was reset or if the caller currently does not have an enabled autofill * service for the user. */ @Nullable public String getUserDataId() { try { final SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS); mService.getUserDataId(receiver); return receiver.getStringResult(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } catch (SyncResultReceiver.TimeoutException e) { throw new RuntimeException("Fail to get user data id for field classification. " + e); } } /** * Gets the user data used for * field classification. * *

Note: This method should only be called by an app providing an autofill service, * and it's ignored if the caller currently doesn't have an enabled autofill service for * the user. * * @return value previously set by {@link #setUserData(UserData)} or {@code null} if it was * reset or if the caller currently does not have an enabled autofill service for the user. */ @Nullable public UserData getUserData() { try { final SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS); mService.getUserData(receiver); return receiver.getParcelableResult(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } catch (SyncResultReceiver.TimeoutException e) { throw new RuntimeException("Fail to get user data for field classification. " + e); } } /** * Sets the {@link UserData} used for * field classification * *

Note: This method should only be called by an app providing an autofill service, * and it's ignored if the caller currently doesn't have an enabled autofill service for * the user. */ public void setUserData(@Nullable UserData userData) { try { mService.setUserData(userData); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Checks if field classification is * enabled. * *

As field classification is an expensive operation, it could be disabled, either * temporarily (for example, because the service exceeded a rate-limit threshold) or * permanently (for example, because the device is a low-level device). * *

Note: This method should only be called by an app providing an autofill service, * and it's ignored if the caller currently doesn't have an enabled autofill service for * the user. */ public boolean isFieldClassificationEnabled() { final SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS); try { mService.isFieldClassificationEnabled(receiver); return receiver.getIntResult() == 1; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } catch (SyncResultReceiver.TimeoutException e) { throw new RuntimeException("Fail to get field classification enabled status. " + e); } } /** * Gets the name of the default algorithm used for * field classification. * *

The default algorithm is used when the algorithm on {@link UserData} is invalid or not * set. * *

Note: This method should only be called by an app providing an autofill service, * and it's ignored if the caller currently doesn't have an enabled autofill service for * the user. */ @Nullable public String getDefaultFieldClassificationAlgorithm() { final SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS); try { mService.getDefaultFieldClassificationAlgorithm(receiver); return receiver.getStringResult(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } catch (SyncResultReceiver.TimeoutException e) { throw new RuntimeException("Fail to get default field classification algorithm. " + e); } } /** * Gets the name of all algorithms currently available for * field classification. * *

Note: This method should only be called by an app providing an autofill service, * and it returns an empty list if the caller currently doesn't have an enabled autofill service * for the user. */ @NonNull public List getAvailableFieldClassificationAlgorithms() { final SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS); try { mService.getAvailableFieldClassificationAlgorithms(receiver); final String[] algorithms = receiver.getStringArrayResult(); return algorithms != null ? Arrays.asList(algorithms) : Collections.emptyList(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } catch (SyncResultReceiver.TimeoutException e) { throw new RuntimeException("Fail to get available field classification algorithms. " + e); } } /** * Returns {@code true} if autofill is supported by the current device and * is supported for this user. * *

Autofill is typically supported, but it could be unsupported in cases like: *

    *
  1. Low-end devices. *
  2. Device policy rules that forbid its usage. *
*/ public boolean isAutofillSupported() { if (mService == null) return false; final SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS); try { mService.isServiceSupported(mContext.getUserId(), receiver); return receiver.getIntResult() == 1; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } catch (SyncResultReceiver.TimeoutException e) { throw new RuntimeException("Fail to get autofill supported status. " + e); } } // Note: don't need to use locked suffix because mContext is final. private AutofillClient getClient() { final AutofillClient client = mContext.getAutofillClient(); if (client == null && sVerbose) { Log.v(TAG, "No AutofillClient for " + mContext.getPackageName() + " on context " + mContext); } return client; } /** * Check if autofill ui is showing, must be called on UI thread. * @hide */ public boolean isAutofillUiShowing() { final AutofillClient client = mContext.getAutofillClient(); return client != null && client.autofillClientIsFillUiShowing(); } /** @hide */ public boolean shouldIgnoreCredentialViews() { return mShouldIgnoreCredentialViews; } /** @hide */ public void onAuthenticationResult(int authenticationId, Intent data, View focusView) { if (!hasAutofillFeature()) { return; } // TODO: the result code is being ignored, so this method is not reliably // handling the cases where it's not RESULT_OK: it works fine if the service does not // set the EXTRA_AUTHENTICATION_RESULT extra, but it could cause weird results if the // service set the extra and returned RESULT_CANCELED... if (sDebug) { Log.d(TAG, "onAuthenticationResult(): id= " + authenticationId + ", data=" + data); } synchronized (mLock) { if (!isActiveLocked()) { Log.w(TAG, "onAuthenticationResult(): sessionId=" + mSessionId + " not active"); return; } mState = STATE_ACTIVE; // If authenticate activity closes itself during onCreate(), there is no onStop/onStart // of app activity. We enforce enter event to re-show fill ui in such case. // CTS example: // LoginActivityTest#testDatasetAuthTwoFieldsUserCancelsFirstAttempt // LoginActivityTest#testFillResponseAuthBothFieldsUserCancelsFirstAttempt if (!mOnInvisibleCalled && focusView != null && focusView.canNotifyAutofillEnterExitEvent()) { notifyViewExitedLocked(focusView); notifyViewEnteredLocked(focusView, focusView.getAutofillId(), /* bounds= */ null, focusView.getAutofillValue(), /* flags= */ 0); } if (data == null) { // data is set to null when result is not RESULT_OK Log.i(TAG, "onAuthenticationResult(): empty intent"); return; } final Parcelable result; if (data.getParcelableExtra(EXTRA_AUTHENTICATION_RESULT) != null) { result = data.getParcelableExtra(EXTRA_AUTHENTICATION_RESULT); } else if (data.getParcelableExtra( CredentialProviderService.EXTRA_GET_CREDENTIAL_RESPONSE) != null && Flags.autofillCredmanIntegration()) { result = data.getParcelableExtra( CredentialProviderService.EXTRA_GET_CREDENTIAL_RESPONSE); } else { result = null; } final Bundle responseData = new Bundle(); responseData.putParcelable(EXTRA_AUTHENTICATION_RESULT, result); Serializable exception = data.getSerializableExtra( CredentialProviderService.EXTRA_GET_CREDENTIAL_EXCEPTION, GetCredentialException.class); if (exception != null && Flags.autofillCredmanIntegration()) { responseData.putSerializable( CredentialProviderService.EXTRA_GET_CREDENTIAL_EXCEPTION, exception); } final Bundle newClientState = data.getBundleExtra(EXTRA_CLIENT_STATE); if (newClientState != null) { responseData.putBundle(EXTRA_CLIENT_STATE, newClientState); } if (data.hasExtra(EXTRA_AUTHENTICATION_RESULT_EPHEMERAL_DATASET)) { responseData.putBoolean(EXTRA_AUTHENTICATION_RESULT_EPHEMERAL_DATASET, data.getBooleanExtra(EXTRA_AUTHENTICATION_RESULT_EPHEMERAL_DATASET, false)); } try { mService.setAuthenticationResult(responseData, mSessionId, authenticationId, mContext.getUserId()); } catch (RemoteException e) { Log.e(TAG, "Error delivering authentication result", e); } } } /** * Gets the next unique autofill ID for the activity context. * *

Typically used to manage views whose content is recycled - see * {@link View#setAutofillId(AutofillId)} for more info. * * @return An ID that is unique in the activity, or {@code null} if autofill is not supported in * the {@link Context} associated with this {@link AutofillManager}. */ @Nullable public AutofillId getNextAutofillId() { final AutofillClient client = getClient(); if (client == null) return null; final AutofillId id = client.autofillClientGetNextAutofillId(); if (id == null && sDebug) { Log.d(TAG, "getNextAutofillId(): client " + client + " returned null"); } return id; } private static AutofillId getAutofillId(View parent, int virtualId) { return new AutofillId(parent.getAutofillViewId(), virtualId); } @GuardedBy("mLock") private void startSessionLocked(@NonNull AutofillId id, @NonNull Rect bounds, @NonNull AutofillValue value, int flags) { if (mEnteredForAugmentedAutofillIds != null && mEnteredForAugmentedAutofillIds.contains(id) || mEnabledForAugmentedAutofillOnly) { if (sVerbose) Log.v(TAG, "Starting session for augmented autofill on " + id); flags |= FLAG_ADD_CLIENT_ENABLED_FOR_AUGMENTED_AUTOFILL_ONLY; } if (sVerbose) { Log.v(TAG, "startSessionLocked(): id=" + id + ", bounds=" + bounds + ", value=" + value + ", flags=" + flags + ", state=" + getStateAsStringLocked() + ", compatMode=" + isCompatibilityModeEnabledLocked() + ", augmentedOnly=" + mForAugmentedAutofillOnly + ", enabledAugmentedOnly=" + mEnabledForAugmentedAutofillOnly + ", enteredIds=" + mEnteredIds); } // We need to reset the augmented-only state when a manual request is made, as it's possible // that the service returned null for the first request and now the user is manually // requesting autofill to trigger a custom UI provided by the service. if (mForAugmentedAutofillOnly && !mEnabledForAugmentedAutofillOnly && (flags & FLAG_MANUAL_REQUEST) != 0) { if (sVerbose) { Log.v(TAG, "resetting mForAugmentedAutofillOnly on manual autofill request"); } mForAugmentedAutofillOnly = false; } if (mState != STATE_UNKNOWN && !isFinishedLocked() && (flags & FLAG_MANUAL_REQUEST) == 0) { if (sVerbose) { Log.v(TAG, "not automatically starting session for " + id + " on state " + getStateAsStringLocked() + " and flags " + flags); } return; } try { final AutofillClient client = getClient(); if (client == null) return; // NOTE: getClient() already logged it.. final SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS); final ComponentName clientActivity = client.autofillClientGetComponentName(); if (!mEnabledForAugmentedAutofillOnly && mOptions != null && mOptions.isAutofillDisabledLocked(clientActivity)) { if (mOptions.isAugmentedAutofillEnabled(mContext)) { if (sDebug) { Log.d(TAG, "startSession(" + clientActivity + "): disabled by service but " + "allowlisted for augmented autofill"); flags |= FLAG_ADD_CLIENT_ENABLED_FOR_AUGMENTED_AUTOFILL_ONLY; } } else { if (sDebug) { Log.d(TAG, "startSession(" + clientActivity + "): ignored because " + "disabled by service and not allowlisted for augmented autofill"); } setSessionFinished(AutofillManager.STATE_DISABLED_BY_SERVICE, null); client.autofillClientResetableStateAvailable(); return; } } mService.startSession(client.autofillClientGetActivityToken(), mServiceClient.asBinder(), id, bounds, value, mContext.getUserId(), mCallback != null, flags, clientActivity, isCompatibilityModeEnabledLocked(), receiver); mSessionId = receiver.getIntResult(); if (mSessionId != NO_SESSION) { mState = STATE_ACTIVE; } final int extraFlags = receiver.getOptionalExtraIntResult(0); if ((extraFlags & RECEIVER_FLAG_SESSION_FOR_AUGMENTED_AUTOFILL_ONLY) != 0) { if (sDebug) Log.d(TAG, "startSession(" + clientActivity + "): for augmented only"); mForAugmentedAutofillOnly = true; } client.autofillClientResetableStateAvailable(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } catch (SyncResultReceiver.TimeoutException e) { // no-op, just log the error message. Log.w(TAG, "Exception getting result from SyncResultReceiver: " + e); } } @GuardedBy("mLock") private void finishSessionLocked(@AutofillCommitReason int commitReason) { if (sVerbose) Log.v(TAG, "finishSessionLocked(): " + getStateAsStringLocked()); if (!isActiveLocked()) return; try { mService.finishSession(mSessionId, mContext.getUserId(), commitReason); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } resetSessionLocked(/* resetEnteredIds= */ true); } @GuardedBy("mLock") private void cancelSessionLocked() { if (sVerbose) Log.v(TAG, "cancelSessionLocked(): " + getStateAsStringLocked()); if (!isActiveLocked()) return; try { mService.cancelSession(mSessionId, mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } resetSessionLocked(/* resetEnteredIds= */ true); } @GuardedBy("mLock") private void resetSessionLocked(boolean resetEnteredIds) { mSessionId = NO_SESSION; mState = STATE_UNKNOWN; mTrackedViews = null; mFillableIds = null; mSaveTriggerId = null; mIdShownFillUi = null; mIsFillRequested.set(false); mShowAutofillDialogCalled = false; mFillDialogTriggerIds = null; mScreenHasCredmanField = false; mAllTrackedViews.clear(); if (resetEnteredIds) { mEnteredIds = null; } } @GuardedBy("mLock") private void updateSessionLocked(AutofillId id, Rect bounds, AutofillValue value, int action, int flags) { if (sVerbose) { Log.v(TAG, "updateSessionLocked(): id=" + id + ", bounds=" + bounds + ", value=" + value + ", action=" + action + ", flags=" + flags); } try { mService.updateSession(mSessionId, id, bounds, value, action, flags, mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Tries to add AutofillManagerClient to service if it does not been added. Returns {@code true} * if the AutofillManagerClient is added successfully or is already added. Otherwise, * returns {@code false}. */ @GuardedBy("mLock") private boolean tryAddServiceClientIfNeededLocked() { return tryAddServiceClientIfNeededLocked(/*credmanRequested=*/ false); } @GuardedBy("mLock") private boolean tryAddServiceClientIfNeededLocked(boolean credmanRequested) { final AutofillClient client = getClient(); if (client == null) { return false; } if (mService == null) { Log.w(TAG, "Autofill service is null!"); return false; } if (mServiceClient == null) { mServiceClient = new AutofillManagerClient(this); try { final int userId = mContext.getUserId(); final SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS); mService.addClient(mServiceClient, client.autofillClientGetComponentName(), userId, receiver, credmanRequested); int flags = 0; try { flags = receiver.getIntResult(); } catch (SyncResultReceiver.TimeoutException e) { Log.w(TAG, "Failed to initialize autofill: " + e); // Reset the states initialized above. mService.removeClient(mServiceClient, userId); mServiceClient = null; return false; } mEnabled = (flags & FLAG_ADD_CLIENT_ENABLED) != 0; sDebug = (flags & FLAG_ADD_CLIENT_DEBUG) != 0; sVerbose = (flags & FLAG_ADD_CLIENT_VERBOSE) != 0; mEnabledForAugmentedAutofillOnly = (flags & FLAG_ADD_CLIENT_ENABLED_FOR_AUGMENTED_AUTOFILL_ONLY) != 0; if (sVerbose) { Log.v(TAG, "receiver results: flags=" + flags + " enabled=" + mEnabled + ", enabledForAugmentedOnly: " + mEnabledForAugmentedAutofillOnly); } final IAutoFillManager service = mService; final IAutoFillManagerClient serviceClient = mServiceClient; mServiceClientCleaner = Cleaner.create(this, () -> { // TODO(b/123100811): call service to also remove reference to // mAugmentedAutofillServiceClient try { service.removeClient(serviceClient, userId); } catch (RemoteException e) { } }); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } return true; } @GuardedBy("mLock") private boolean startAutofillIfNeededLocked(View view) { if (mState == STATE_UNKNOWN && mSessionId == NO_SESSION && view instanceof EditText && !TextUtils.isEmpty(((EditText) view).getText()) && !view.isFocused() && view.isImportantForAutofill() && view.isLaidOut() && view.isVisibleToUser()) { final boolean clientAdded = tryAddServiceClientIfNeededLocked(); if (sVerbose) { Log.v(TAG, "startAutofillIfNeededLocked(): enabled=" + mEnabled + " mServiceClient=" + mServiceClient); } if (clientAdded && mEnabled && !isClientDisablingEnterExitEvent()) { final AutofillId id = view.getAutofillId(); final AutofillValue value = view.getAutofillValue(); // Starts new session. startSessionLocked(id, /* bounds= */ null, /* value= */ null, /* flags= */ 0); // Updates value. updateSessionLocked(id, /* bounds= */ null, value, ACTION_VALUE_CHANGED, /* flags= */ 0); addEnteredIdLocked(id); return true; } } return false; } /** * Registers a {@link AutofillCallback} to receive autofill events. * * @param callback callback to receive events. */ public void registerCallback(@Nullable AutofillCallback callback) { if (!hasAutofillFeature()) { return; } synchronized (mLock) { if (callback == null) return; final boolean hadCallback = mCallback != null; mCallback = callback; if (!hadCallback) { try { mService.setHasCallback(mSessionId, mContext.getUserId(), true); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } } } /** * Unregisters a {@link AutofillCallback} to receive autofill events. * * @param callback callback to stop receiving events. */ public void unregisterCallback(@Nullable AutofillCallback callback) { if (!hasAutofillFeature()) { return; } synchronized (mLock) { if (callback == null || mCallback == null || callback != mCallback) return; mCallback = null; try { mService.setHasCallback(mSessionId, mContext.getUserId(), false); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } } /** * Explicitly limits augmented autofill to the given packages and activities. * *

To reset the allowlist, call it passing {@code null} to both arguments. * *

Useful when the service wants to restrict augmented autofill to a category of apps, like * apps that uses addresses. For example, if the service wants to support augmented autofill on * all activities of app {@code AddressApp1} and just activities {@code act1} and {@code act2} * of {@code AddressApp2}, it would call: * {@code setAugmentedAutofillWhitelist(Arrays.asList("AddressApp1"), * Arrays.asList(new ComponentName("AddressApp2", "act1"), * new ComponentName("AddressApp2", "act2")));} * *

Note: This method should only be called by the app providing the augmented autofill * service, and it's ignored if the caller isn't it. * * @hide */ @SystemApi public void setAugmentedAutofillWhitelist(@Nullable Set packages, @Nullable Set activities) { if (!hasAutofillFeature()) { return; } final SyncResultReceiver resultReceiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS); int resultCode; try { mService.setAugmentedAutofillWhitelist(toList(packages), toList(activities), resultReceiver); resultCode = resultReceiver.getIntResult(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } catch (SyncResultReceiver.TimeoutException e) { Log.e(TAG, "Fail to get the result of set AugmentedAutofill whitelist. " + e); return; } switch (resultCode) { case RESULT_OK: return; case RESULT_CODE_NOT_SERVICE: throw new SecurityException("caller is not user's Augmented Autofill Service"); default: Log.wtf(TAG, "setAugmentedAutofillWhitelist(): received invalid result: " + resultCode); } } /** * Notifies that a non-autofillable view was entered because the activity is allowlisted for * augmented autofill. * *

This method is necessary to set the right flag on start, so the server-side session * doesn't trigger the standard autofill workflow, but the augmented's instead. * * @hide */ public void notifyViewEnteredForAugmentedAutofill(@NonNull View view) { final AutofillId id = view.getAutofillId(); synchronized (mLock) { if (mEnteredForAugmentedAutofillIds == null) { mEnteredForAugmentedAutofillIds = new ArraySet<>(1); } mEnteredForAugmentedAutofillIds.add(id); } } private void requestShowFillUi(int sessionId, AutofillId id, int width, int height, Rect anchorBounds, IAutofillWindowPresenter presenter) { final View anchor = findView(id); if (anchor == null) { return; } AutofillCallback callback = null; synchronized (mLock) { if (mSessionId == sessionId) { AutofillClient client = getClient(); if (client != null) { if (client.autofillClientRequestShowFillUi(anchor, width, height, anchorBounds, presenter)) { callback = mCallback; mIdShownFillUi = id; } } } } if (callback != null) { if (id.isVirtualInt()) { callback.onAutofillEvent(anchor, id.getVirtualChildIntId(), AutofillCallback.EVENT_INPUT_SHOWN); } else { callback.onAutofillEvent(anchor, AutofillCallback.EVENT_INPUT_SHOWN); } } } private void authenticate(int sessionId, int authenticationId, IntentSender intent, Intent fillInIntent, boolean authenticateInline) { synchronized (mLock) { if (sessionId == mSessionId) { if (mRelayoutFix) { mState = STATE_PENDING_AUTHENTICATION; } final AutofillClient client = getClient(); if (client != null) { // clear mOnInvisibleCalled and we will see if receive onInvisibleForAutofill() // before onAuthenticationResult() mOnInvisibleCalled = false; client.autofillClientAuthenticate(authenticationId, intent, fillInIntent, authenticateInline); } } } } private void dispatchUnhandledKey(int sessionId, AutofillId id, KeyEvent keyEvent) { final View anchor = findView(id); if (anchor == null) { return; } synchronized (mLock) { if (mSessionId == sessionId) { AutofillClient client = getClient(); if (client != null) { client.autofillClientDispatchUnhandledKey(anchor, keyEvent); } } } } /** @hide */ public static final int SET_STATE_FLAG_ENABLED = 0x01; /** @hide */ public static final int SET_STATE_FLAG_RESET_SESSION = 0x02; /** @hide */ public static final int SET_STATE_FLAG_RESET_CLIENT = 0x04; /** @hide */ public static final int SET_STATE_FLAG_DEBUG = 0x08; /** @hide */ public static final int SET_STATE_FLAG_VERBOSE = 0x10; /** @hide */ public static final int SET_STATE_FLAG_FOR_AUTOFILL_ONLY = 0x20; private void setState(int flags) { if (sVerbose) { Log.v(TAG, "setState(" + flags + ": " + DebugUtils.flagsToString(AutofillManager.class, "SET_STATE_FLAG_", flags) + ")"); } synchronized (mLock) { if ((flags & SET_STATE_FLAG_FOR_AUTOFILL_ONLY) != 0) { mForAugmentedAutofillOnly = true; // NOTE: returning right away as this is the only flag set, at least currently... return; } mEnabled = (flags & SET_STATE_FLAG_ENABLED) != 0; if (!mEnabled || (flags & SET_STATE_FLAG_RESET_SESSION) != 0) { // Reset the session state resetSessionLocked(/* resetEnteredIds= */ true); } if ((flags & SET_STATE_FLAG_RESET_CLIENT) != 0) { // Reset connection to system mServiceClient = null; mAugmentedAutofillServiceClient = null; if (mServiceClientCleaner != null) { mServiceClientCleaner.clean(); mServiceClientCleaner = null; } notifyReenableAutofill(); } } sDebug = (flags & SET_STATE_FLAG_DEBUG) != 0; sVerbose = (flags & SET_STATE_FLAG_VERBOSE) != 0; } /** * Sets a view as autofilled if the current value is the {code targetValue}. * * @param view The view that is to be autofilled * @param targetValue The value we want to fill into view */ private void setAutofilledIfValuesIs(@NonNull View view, @Nullable AutofillValue targetValue, boolean hideHighlight) { AutofillValue currentValue = view.getAutofillValue(); if (Objects.equals(currentValue, targetValue)) { synchronized (mLock) { if (mLastAutofilledData == null) { mLastAutofilledData = new ParcelableMap(1); } mLastAutofilledData.put(view.getAutofillId(), targetValue); } view.setAutofilled(true, hideHighlight); try { mService.setViewAutofilled(mSessionId, view.getAutofillId(), mContext.getUserId()); } catch (RemoteException e) { // The failure could be a consequence of something going wrong on the server side. // Do nothing here since it's just logging, but it's possible follow-up actions may // fail. } } } private void onGetCredentialException(int sessionId, AutofillId id, String errorType, String errorMsg) { synchronized (mLock) { if (sessionId != mSessionId) { Log.w(TAG, "onGetCredentialException afm sessionIds don't match"); return; } final AutofillClient client = getClient(); if (client == null) { Log.w(TAG, "onGetCredentialException afm client id null"); return; } ArrayList failedIds = new ArrayList<>(); final View[] views = client.autofillClientFindViewsByAutofillIdTraversal( Helper.toArray(new ArrayList<>(Collections.singleton(id)))); if (views == null || views.length == 0) { Log.w(TAG, "onGetCredentialException afm client view not found"); return; } final View view = views[0]; if (view == null) { Log.i(TAG, "onGetCredentialException View is null"); // Most likely view has been removed after the initial request was sent to the // the service; this is fine, but we need to update the view status in the // server side so it can be triggered again. Log.d(TAG, "onGetCredentialException(): no View with id " + id); failedIds.add(id); } if (id.isVirtualInt()) { Log.i(TAG, "onGetCredentialException afm client id is virtual"); // TODO(b/326314286): Handle virtual views } else { Log.i(TAG, "onGetCredentialException afm client id is NOT virtual"); view.onGetCredentialException(errorType, errorMsg); } handleFailedIdsLocked(failedIds); } } private void onGetCredentialResponse(int sessionId, AutofillId id, GetCredentialResponse response) { synchronized (mLock) { if (sessionId != mSessionId) { Log.w(TAG, "onGetCredentialResponse afm sessionIds don't match"); return; } final AutofillClient client = getClient(); if (client == null) { Log.w(TAG, "onGetCredentialResponse afm client id null"); return; } ArrayList failedIds = new ArrayList<>(); final View[] views = client.autofillClientFindViewsByAutofillIdTraversal( Helper.toArray(new ArrayList<>(Collections.singleton(id)))); if (views == null || views.length == 0) { Log.w(TAG, "onGetCredentialResponse afm client view not found"); return; } final View view = views[0]; if (view == null) { Log.i(TAG, "onGetCredentialResponse View is null"); // Most likely view has been removed after the initial request was sent to the // the service; this is fine, but we need to update the view status in the // server side so it can be triggered again. Log.d(TAG, "onGetCredentialResponse(): no View with id " + id); failedIds.add(id); } if (id.isVirtualInt()) { Log.i(TAG, "onGetCredentialResponse afm client id is virtual"); // TODO(b/326314286): Handle virtual views } else { Log.i(TAG, "onGetCredentialResponse afm client id is NOT virtual"); view.onGetCredentialResponse(response); } handleFailedIdsLocked(failedIds); } } @GuardedBy("mLock") private void handleFailedIdsLocked(@NonNull ArrayList failedIds) { if (!failedIds.isEmpty() && sVerbose) { Log.v(TAG, "autofill(): total failed views: " + failedIds); } try { mService.setAutofillFailure(mSessionId, failedIds, mContext.getUserId()); } catch (RemoteException e) { // In theory, we could ignore this error since it's not a big deal, but // in reality, we rather crash the app anyways, as the failure could be // a consequence of something going wrong on the server side... throw e.rethrowFromSystemServer(); } } private void autofill(int sessionId, List ids, List values, boolean hideHighlight) { synchronized (mLock) { if (sessionId != mSessionId) { return; } final AutofillClient client = getClient(); if (client == null) { return; } final int itemCount = ids.size(); int numApplied = 0; ArrayMap> virtualValues = null; final View[] views = client.autofillClientFindViewsByAutofillIdTraversal( Helper.toArray(ids)); ArrayList failedIds = new ArrayList<>(); for (int i = 0; i < itemCount; i++) { final AutofillId id = ids.get(i); final AutofillValue value = values.get(i); final View view = views[i]; if (view == null) { // Most likely view has been removed after the initial request was sent to the // the service; this is fine, but we need to update the view status in the // server side so it can be triggered again. Log.d(TAG, "autofill(): no View with id " + id); failedIds.add(id); continue; } if (id.isVirtualInt()) { if (virtualValues == null) { // Most likely there will be just one view with virtual children. virtualValues = new ArrayMap<>(1); } SparseArray valuesByParent = virtualValues.get(view); if (valuesByParent == null) { // We don't know the size yet, but usually it will be just a few fields... valuesByParent = new SparseArray<>(5); virtualValues.put(view, valuesByParent); } valuesByParent.put(id.getVirtualChildIntId(), value); } else { // Mark the view as to be autofilled with 'value' if (mLastAutofilledData == null) { mLastAutofilledData = new ParcelableMap(itemCount - i); } mLastAutofilledData.put(id, value); view.autofill(value); // Set as autofilled if the values match now, e.g. when the value was updated // synchronously. // If autofill happens async, the view is set to autofilled in // notifyValueChanged. setAutofilledIfValuesIs(view, value, hideHighlight); numApplied++; } } handleFailedIdsLocked(failedIds); if (virtualValues != null) { for (int i = 0; i < virtualValues.size(); i++) { final View parent = virtualValues.keyAt(i); final SparseArray childrenValues = virtualValues.valueAt(i); parent.autofill(childrenValues); numApplied += childrenValues.size(); // TODO: we should provide a callback so the parent can call failures; something // like notifyAutofillFailed(View view, int[] childrenIds); } } mMetricsLogger.write(newLog(MetricsEvent.AUTOFILL_DATASET_APPLIED) .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_VALUES, itemCount) .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_VIEWS_FILLED, numApplied)); } } private void autofillContent(int sessionId, AutofillId id, ClipData clip) { synchronized (mLock) { if (sessionId != mSessionId) { return; } final AutofillClient client = getClient(); if (client == null) { return; } final View view = client.autofillClientFindViewByAutofillIdTraversal(id); if (view == null) { // Most likely view has been removed after the initial request was sent to the // the service; this is fine, but we need to update the view status in the // server side so it can be triggered again. Log.d(TAG, "autofillContent(): no view with id " + id); reportAutofillContentFailure(id); return; } ContentInfo payload = new ContentInfo.Builder(clip, SOURCE_AUTOFILL).build(); ContentInfo result = view.performReceiveContent(payload); if (result != null) { Log.w(TAG, "autofillContent(): receiver could not insert content: id=" + id + ", view=" + view + ", clip=" + clip); reportAutofillContentFailure(id); return; } mMetricsLogger.write(newLog(MetricsEvent.AUTOFILL_DATASET_APPLIED) .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_VALUES, 1) .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_VIEWS_FILLED, 1)); } } private void reportAutofillContentFailure(AutofillId id) { try { mService.setAutofillFailure(mSessionId, Collections.singletonList(id), mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } private LogMaker newLog(int category) { final LogMaker log = new LogMaker(category) .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SESSION_ID, mSessionId); if (isCompatibilityModeEnabledLocked()) { log.addTaggedData(MetricsEvent.FIELD_AUTOFILL_COMPAT_MODE, 1); } final AutofillClient client = getClient(); if (client == null) { // Client should never be null here, but it doesn't hurt to check... log.setPackageName(mContext.getPackageName()); } else { // Remove activity name from logging final ComponentName sanitizedComponentName = new ComponentName(client.autofillClientGetComponentName().getPackageName(), ""); log.setComponentName(sanitizedComponentName); } return log; } /** * Set the tracked views. * * @param trackedIds The views to be tracked. * @param saveOnAllViewsInvisible Finish the session once all tracked views are invisible. * @param saveOnFinish Finish the session once the activity is finished. * @param fillableIds Views that might anchor FillUI. * @param saveTriggerId View that when clicked triggers commit(). */ private void setTrackedViews(int sessionId, @Nullable AutofillId[] trackedIds, boolean saveOnAllViewsInvisible, boolean saveOnFinish, @Nullable AutofillId[] fillableIds, @Nullable AutofillId saveTriggerId) { if (saveTriggerId != null) { saveTriggerId.resetSessionId(); } synchronized (mLock) { if (sVerbose) { Log.v(TAG, "setTrackedViews(): sessionId=" + sessionId + ", trackedIds=" + Arrays.toString(trackedIds) + ", saveOnAllViewsInvisible=" + saveOnAllViewsInvisible + ", saveOnFinish=" + saveOnFinish + ", fillableIds=" + Arrays.toString(fillableIds) + ", saveTrigerId=" + saveTriggerId + ", mFillableIds=" + mFillableIds + ", mEnabled=" + mEnabled + ", mSessionId=" + mSessionId); } if (mEnabled && mSessionId == sessionId) { mSaveOnFinish = saveOnFinish; if (fillableIds != null) { if (mFillableIds == null) { mFillableIds = new ArraySet<>(fillableIds.length); } for (AutofillId id : fillableIds) { if (id != null) { id.resetSessionId(); mFillableIds.add(id); } } } if (mSaveTriggerId != null && !mSaveTriggerId.equals(saveTriggerId)) { // Turn off trigger on previous view id. setNotifyOnClickLocked(mSaveTriggerId, false); } if (saveTriggerId != null && !saveTriggerId.equals(mSaveTriggerId)) { // Turn on trigger on new view id. mSaveTriggerId = saveTriggerId; setNotifyOnClickLocked(mSaveTriggerId, true); } if (!saveOnAllViewsInvisible) { trackedIds = null; } final ArraySet allFillableIds = new ArraySet<>(); if (mFillableIds != null) { allFillableIds.addAll(mFillableIds); } if (trackedIds != null) { for (AutofillId id : trackedIds) { if (id != null) { id.resetSessionId(); allFillableIds.add(id); } } } if (!allFillableIds.isEmpty()) { mTrackedViews = new TrackedViews(trackedIds, Helper.toArray(allFillableIds)); } else { mTrackedViews = null; } } } } private void setNotifyOnClickLocked(@NonNull AutofillId id, boolean notify) { final View view = findView(id); if (view == null) { Log.w(TAG, "setNotifyOnClick(): invalid id: " + id); return; } view.setNotifyAutofillManagerOnClick(notify); } private void setSaveUiState(int sessionId, boolean shown) { if (sDebug) Log.d(TAG, "setSaveUiState(" + sessionId + "): " + shown); synchronized (mLock) { if (mSessionId != NO_SESSION) { // Race condition: app triggered a new session after the previous session was // finished but before server called setSaveUiState() - need to cancel the new // session to avoid further inconsistent behavior. Log.w(TAG, "setSaveUiState(" + sessionId + ", " + shown + ") called on existing session " + mSessionId + "; cancelling it"); cancelSessionLocked(); } if (shown) { mSessionId = sessionId; mState = STATE_SHOWING_SAVE_UI; } else { mSessionId = NO_SESSION; mState = STATE_UNKNOWN; } } } /** * Marks the state of the session as finished. * * @param newState {@link #STATE_FINISHED} (because the autofill service returned a {@code null} * FillResponse), {@link #STATE_UNKNOWN} (because the session was removed), * {@link #STATE_UNKNOWN_COMPAT_MODE} (beucase the session was finished when the URL bar * changed on compat mode), {@link #STATE_UNKNOWN_FAILED} (because the session was finished * when the service failed to fullfil the request, or {@link #STATE_DISABLED_BY_SERVICE} * (because the autofill service disabled further autofill requests for the activity). * @param autofillableIds list of ids that could trigger autofill, use to not handle a new * session when they're entered. */ private void setSessionFinished(int newState, @Nullable List autofillableIds) { if (autofillableIds != null) { for (int i = 0; i < autofillableIds.size(); i++) { autofillableIds.get(i).resetSessionId(); } } synchronized (mLock) { if (sVerbose) { Log.v(TAG, "setSessionFinished(): from " + getStateAsStringLocked() + " to " + getStateAsString(newState) + "; autofillableIds=" + autofillableIds); } if (autofillableIds != null) { mEnteredIds = new ArraySet<>(autofillableIds); } if (newState == STATE_UNKNOWN_COMPAT_MODE || newState == STATE_UNKNOWN_FAILED) { resetSessionLocked(/* resetEnteredIds= */ true); mState = STATE_UNKNOWN; } else { resetSessionLocked(/* resetEnteredIds= */ false); mState = newState; } } } /** * Gets a {@link AugmentedAutofillManagerClient} for this {@link AutofillManagerClient}. * *

These are 2 distinct objects because we need to restrict what the Augmented Autofill * service can do (which is defined by {@code IAugmentedAutofillManagerClient.aidl}). */ private void getAugmentedAutofillClient(@NonNull IResultReceiver result) { synchronized (mLock) { if (mAugmentedAutofillServiceClient == null) { mAugmentedAutofillServiceClient = new AugmentedAutofillManagerClient(this); } final Bundle resultData = new Bundle(); resultData.putBinder(EXTRA_AUGMENTED_AUTOFILL_CLIENT, mAugmentedAutofillServiceClient.asBinder()); try { result.send(0, resultData); } catch (RemoteException e) { Log.w(TAG, "Could not send AugmentedAutofillClient back: " + e); } } } private void requestShowSoftInput(@NonNull AutofillId id) { if (sVerbose) Log.v(TAG, "requestShowSoftInput(" + id + ")"); final AutofillClient client = getClient(); if (client == null) { return; } final View view = client.autofillClientFindViewByAutofillIdTraversal(id); if (view == null) { if (sVerbose) Log.v(TAG, "View is not found"); return; } final Handler handler = view.getHandler(); if (handler == null) { if (sVerbose) Log.v(TAG, "Ignoring requestShowSoftInput due to no handler in view"); return; } if (handler.getLooper() != Looper.myLooper()) { // The view is running on a different thread than our own, so we need to reschedule // our work for over there. if (sVerbose) Log.v(TAG, "Scheduling showSoftInput() on the view UI thread"); handler.post(() -> requestShowSoftInputInViewThread(view)); } else { requestShowSoftInputInViewThread(view); } } // This method must be called from within the View thread. private static void requestShowSoftInputInViewThread(@NonNull View view) { if (!view.isFocused()) { Log.w(TAG, "Ignoring requestShowSoftInput() due to non-focused view"); return; } final InputMethodManager inputMethodManager = view.getContext().getSystemService( InputMethodManager.class); boolean ret = inputMethodManager.showSoftInput(view, /*flags=*/ 0); if (sVerbose) Log.v(TAG, " InputMethodManager.showSoftInput returns " + ret); } /** @hide */ public void requestHideFillUi() { requestHideFillUi(mIdShownFillUi, true); } private void requestHideFillUi(AutofillId id, boolean force) { final View anchor = id == null ? null : findView(id); if (sVerbose) Log.v(TAG, "requestHideFillUi(" + id + "): anchor = " + anchor); if (anchor == null) { if (force) { // When user taps outside autofill window, force to close fill ui even id does // not match. AutofillClient client = getClient(); if (client != null) { client.autofillClientRequestHideFillUi(); } } return; } requestHideFillUi(id, anchor); } private void requestHideFillUi(AutofillId id, View anchor) { AutofillCallback callback = null; synchronized (mLock) { // We do not check the session id for two reasons: // 1. If local and remote session id are off sync the UI would be stuck shown // 2. There is a race between the user state being destroyed due the fill // service being uninstalled and the UI being dismissed. AutofillClient client = getClient(); if (client != null) { if (client.autofillClientRequestHideFillUi()) { mIdShownFillUi = null; callback = mCallback; } } } if (callback != null) { if (id.isVirtualInt()) { callback.onAutofillEvent(anchor, id.getVirtualChildIntId(), AutofillCallback.EVENT_INPUT_HIDDEN); } else { callback.onAutofillEvent(anchor, AutofillCallback.EVENT_INPUT_HIDDEN); } } } private void notifyDisableAutofill(long disableDuration, ComponentName componentName) { synchronized (mLock) { if (mOptions == null) { return; } long expiration = SystemClock.elapsedRealtime() + disableDuration; // Protect it against overflow if (expiration < 0) { expiration = Long.MAX_VALUE; } if (componentName != null) { if (mOptions.disabledActivities == null) { mOptions.disabledActivities = new ArrayMap<>(); } mOptions.disabledActivities.put(componentName.flattenToString(), expiration); } else { mOptions.appDisabledExpiration = expiration; } } } void notifyReenableAutofill() { synchronized (mLock) { if (mOptions == null) { return; } mOptions.appDisabledExpiration = 0; mOptions.disabledActivities = null; } } private void notifyNoFillUi(int sessionId, AutofillId id, int sessionFinishedState) { if (sVerbose) { Log.v(TAG, "notifyNoFillUi(): sessionFinishedState=" + sessionFinishedState); } final View anchor = findView(id); if (anchor == null) { return; } notifyCallback(sessionId, id, AutofillCallback.EVENT_INPUT_UNAVAILABLE); if (sessionFinishedState != STATE_UNKNOWN) { // Callback call was "hijacked" to also update the session state. setSessionFinished(sessionFinishedState, /* autofillableIds= */ null); } } private void notifyCallback( int sessionId, AutofillId id, @AutofillCallback.AutofillEventType int event) { if (sVerbose) { Log.v(TAG, "notifyCallback(): sessionId=" + sessionId + ", autofillId=" + id + ", event=" + event); } final View anchor = findView(id); if (anchor == null) { return; } AutofillCallback callback = null; synchronized (mLock) { if (mSessionId == sessionId && getClient() != null) { callback = mCallback; } } if (callback != null) { if (id.isVirtualInt()) { callback.onAutofillEvent( anchor, id.getVirtualChildIntId(), event); } else { callback.onAutofillEvent(anchor, event); } } } private boolean shouldSuppressDialogsForCredman(View view) { if (view == null) { return false; } // isCredential field indicates that the developer might be calling Credman, and we should // suppress autofill dialogs. But it is not a good enough indicator that there is a valid // credman option. if (view.isCredential()) { return true; } return containsAutofillHintPrefix(view, View.AUTOFILL_HINT_CREDENTIAL_MANAGER); } private boolean isCredmanRequested(View view) { if (view == null) { return false; } if (view.getViewCredentialHandler() != null) { return true; } String[] hints = view.getAutofillHints(); if (hints == null) { return false; } // if hint starts with 'credential=', then we assume that there is a valid // credential option set by the client. return containsAutofillHintPrefix(view, View.AUTOFILL_HINT_CREDENTIAL_MANAGER + "="); } private boolean containsAutofillHintPrefix(View view, String prefix) { String[] hints = view.getAutofillHints(); if (hints == null) { return false; } for (String hint : hints) { if (hint != null && hint.startsWith(prefix)) { return true; } } return false; } /** * Find a single view by its id. * * @param autofillId The autofill id of the view * * @return The view or {@code null} if view was not found */ private View findView(@NonNull AutofillId autofillId) { final AutofillClient client = getClient(); if (client != null) { return client.autofillClientFindViewByAutofillIdTraversal(autofillId); } return null; } /** @hide */ public boolean hasAutofillFeature() { return mService != null; } /** @hide */ public void onPendingSaveUi(int operation, IBinder token) { if (sVerbose) Log.v(TAG, "onPendingSaveUi(" + operation + "): " + token); synchronized (mLock) { try { mService.onPendingSaveUi(operation, token); } catch (RemoteException e) { Log.e(TAG, "Error in onPendingSaveUi: ", e); } } } /** @hide */ public void dump(String outerPrefix, PrintWriter pw) { synchronized (mLock) { pw.print(outerPrefix); pw.println("AutofillManager:"); final String pfx = outerPrefix + " "; pw.print(pfx); pw.print("sessionId: "); pw.println(mSessionId); pw.print(pfx); pw.print("state: "); pw.println(getStateAsStringLocked()); pw.print(pfx); pw.print("context: "); pw.println(mContext); pw.print(pfx); pw.print("service client: "); pw.println(mServiceClient); final AutofillClient client = getClient(); if (client != null) { pw.print(pfx); pw.print("client: "); pw.print(client); pw.print(" ("); pw.print(client.autofillClientGetActivityToken()); pw.println(')'); } pw.print(pfx); pw.print("enabled: "); pw.println(mEnabled); pw.print(pfx); pw.print("enabledAugmentedOnly: "); pw.println(mForAugmentedAutofillOnly); pw.print(pfx); pw.print("hasService: "); pw.println(mService != null); pw.print(pfx); pw.print("hasCallback: "); pw.println(mCallback != null); pw.print(pfx); pw.print("onInvisibleCalled "); pw.println(mOnInvisibleCalled); pw.print(pfx); pw.print("last autofilled data: "); pw.println(mLastAutofilledData); pw.print(pfx); pw.print("id of last fill UI shown: "); pw.println(mIdShownFillUi); pw.print(pfx); pw.print("tracked views: "); if (mTrackedViews == null) { pw.println("null"); } else { final String pfx2 = pfx + " "; pw.println(); pw.print(pfx2); pw.print("visible:"); pw.println(mTrackedViews.mVisibleTrackedIds); pw.print(pfx2); pw.print("invisible:"); pw.println(mTrackedViews.mInvisibleTrackedIds); } pw.print(pfx); pw.print("fillable ids: "); pw.println(mFillableIds); pw.print(pfx); pw.print("entered ids: "); pw.println(mEnteredIds); if (mEnteredForAugmentedAutofillIds != null) { pw.print(pfx); pw.print("entered ids for augmented autofill: "); pw.println(mEnteredForAugmentedAutofillIds); } if (mForAugmentedAutofillOnly) { pw.print(pfx); pw.println("For Augmented Autofill Only"); } pw.print(pfx); pw.print("save trigger id: "); pw.println(mSaveTriggerId); pw.print(pfx); pw.print("save on finish(): "); pw.println(mSaveOnFinish); if (mOptions != null) { pw.print(pfx); pw.print("options: "); mOptions.dumpShort(pw); pw.println(); } pw.print(pfx); pw.print("fill dialog enabled: "); pw.println(mIsFillDialogEnabled); pw.print(pfx); pw.print("fill dialog enabled hints: "); pw.println(Arrays.toString(mFillDialogEnabledHints)); pw.print(pfx); pw.print("compat mode enabled: "); if (mCompatibilityBridge != null) { final String pfx2 = pfx + " "; pw.println("true"); pw.print(pfx2); pw.print("windowId: "); pw.println(mCompatibilityBridge.mFocusedWindowId); pw.print(pfx2); pw.print("nodeId: "); pw.println(mCompatibilityBridge.mFocusedNodeId); pw.print(pfx2); pw.print("virtualId: "); pw.println(AccessibilityNodeInfo .getVirtualDescendantId(mCompatibilityBridge.mFocusedNodeId)); pw.print(pfx2); pw.print("focusedBounds: "); pw.println(mCompatibilityBridge.mFocusedBounds); } else { pw.println("false"); } pw.print(pfx); pw.print("debug: "); pw.print(sDebug); pw.print(" verbose: "); pw.println(sVerbose); } } @GuardedBy("mLock") private String getStateAsStringLocked() { return getStateAsString(mState); } @NonNull private static String getStateAsString(int state) { switch (state) { case STATE_UNKNOWN: return "UNKNOWN"; case STATE_ACTIVE: return "ACTIVE"; case STATE_PENDING_AUTHENTICATION: return "PENDING_AUTHENTICATION"; case STATE_FINISHED: return "FINISHED"; case STATE_SHOWING_SAVE_UI: return "SHOWING_SAVE_UI"; case STATE_DISABLED_BY_SERVICE: return "DISABLED_BY_SERVICE"; case STATE_UNKNOWN_COMPAT_MODE: return "UNKNOWN_COMPAT_MODE"; case STATE_UNKNOWN_FAILED: return "UNKNOWN_FAILED"; default: return "INVALID:" + state; } } /** @hide */ public static String getSmartSuggestionModeToString(@SmartSuggestionMode int flags) { switch (flags) { case FLAG_SMART_SUGGESTION_OFF: return "OFF"; case FLAG_SMART_SUGGESTION_SYSTEM: return "SYSTEM"; default: return "INVALID:" + flags; } } @GuardedBy("mLock") private boolean isActiveLocked() { return mState == STATE_ACTIVE || isPendingAuthenticationLocked(); } @GuardedBy("mLock") private boolean isPendingAuthenticationLocked() { return mRelayoutFix && mState == STATE_PENDING_AUTHENTICATION; } @GuardedBy("mLock") private boolean isDisabledByServiceLocked() { return mState == STATE_DISABLED_BY_SERVICE; } @GuardedBy("mLock") private boolean isFinishedLocked() { return mState == STATE_FINISHED; } private void post(Runnable runnable) { final AutofillClient client = getClient(); if (client == null) { if (sVerbose) Log.v(TAG, "ignoring post() because client is null"); return; } client.autofillClientRunOnUiThread(runnable); } private void setFillDialogTriggerIds(@Nullable List ids) { mFillDialogTriggerIds = ids; } /** * If autofill suggestions for a * * dialog-style UI are available for {@code view}, shows a dialog allowing the user to * select a suggestion and returns {@code true}. *

* The dialog may not be shown if the autofill service does not support it, if the autofill * request has not returned a response yet, if the dialog was shown previously, or if the * input method is already shown. *

* It is recommended apps to call this method the first time a user focuses on * an autofill-able form, and to avoid showing the input method if the dialog is shown. If * this method returns {@code false}, you should then instead show the input method (assuming * that is how the view normally handles the focus event). If the user re-focuses on the view, * you should not call this method again so as to not disrupt usage of the input method. * * @param view the view for which to show autofill suggestions. This is typically a view * receiving a focus event. The autofill suggestions shown will include content for * related views as well. * @return {@code true} if the autofill dialog is being shown */ // TODO(b/210926084): Consider whether to include the one-time show logic within this method. public boolean showAutofillDialog(@NonNull View view) { Objects.requireNonNull(view); if (shouldShowAutofillDialog(view, view.getAutofillId())) { mShowAutofillDialogCalled = true; final WeakReference wrView = new WeakReference<>(view); // The id matches a trigger id, this will trigger the fill dialog. post(() -> { final View v = wrView.get(); if (v != null) { notifyViewEntered(v); } }); return true; } return false; } /** * If autofill suggestions for a * * dialog-style UI are available for virtual {@code view}, shows a dialog allowing the user * to select a suggestion and returns {@code true}. *

* The dialog may not be shown if the autofill service does not support it, if the autofill * request has not returned a response yet, if the dialog was shown previously, or if the * input method is already shown. *

* It is recommended apps to call this method the first time a user focuses on * an autofill-able form, and to avoid showing the input method if the dialog is shown. If * this method returns {@code false}, you should then instead show the input method (assuming * that is how the view normally handles the focus event). If the user re-focuses on the view, * you should not call this method again so as to not disrupt usage of the input method. * * @param view the view hosting the virtual view hierarchy which is used to show autofill * suggestions. * @param virtualId id identifying the virtual view inside the host view. * @return {@code true} if the autofill dialog is being shown */ public boolean showAutofillDialog(@NonNull View view, int virtualId) { Objects.requireNonNull(view); if (shouldShowAutofillDialog(view, getAutofillId(view, virtualId))) { mShowAutofillDialogCalled = true; final WeakReference wrView = new WeakReference<>(view); // The id matches a trigger id, this will trigger the fill dialog. post(() -> { final View v = wrView.get(); if (v != null) { notifyViewEntered(v, virtualId, /* bounds= */ null, /* flags= */ 0); } }); return true; } return false; } private boolean shouldShowAutofillDialog(View view, AutofillId id) { if (!hasFillDialogUiFeature() || mShowAutofillDialogCalled || mFillDialogTriggerIds == null || mScreenHasCredmanField) { return false; } if (getImeStateFlag(view) == FLAG_IME_SHOWING) { // IME is showing return false; } final int size = mFillDialogTriggerIds.size(); for (int i = 0; i < size; i++) { AutofillId fillId = mFillDialogTriggerIds.get(i); if (fillId.equalsIgnoreSession(id)) { return true; } } return false; } /** * Implementation of the accessibility based compatibility. */ private final class CompatibilityBridge implements AccessibilityManager.AccessibilityPolicy { @GuardedBy("mLock") private final Rect mFocusedBounds = new Rect(); @GuardedBy("mLock") private final Rect mTempBounds = new Rect(); @GuardedBy("mLock") private int mFocusedWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID; @GuardedBy("mLock") private long mFocusedNodeId = AccessibilityNodeInfo.UNDEFINED_NODE_ID; // Need to report a fake service in case a11y clients check the service list @NonNull @GuardedBy("mLock") AccessibilityServiceInfo mCompatServiceInfo; CompatibilityBridge() { final AccessibilityManager am = AccessibilityManager.getInstance(mContext); am.setAccessibilityPolicy(this); } private AccessibilityServiceInfo getCompatServiceInfo() { synchronized (mLock) { if (mCompatServiceInfo != null) { return mCompatServiceInfo; } final Intent intent = new Intent(); intent.setComponent(new ComponentName("android", "com.android.server.autofill.AutofillCompatAccessibilityService")); final ResolveInfo resolveInfo = mContext.getPackageManager().resolveService( intent, PackageManager.MATCH_SYSTEM_ONLY | PackageManager.GET_META_DATA); try { mCompatServiceInfo = new AccessibilityServiceInfo(resolveInfo, mContext); } catch (XmlPullParserException | IOException e) { Log.e(TAG, "Cannot find compat autofill service:" + intent); throw new IllegalStateException("Cannot find compat autofill service"); } return mCompatServiceInfo; } } @Override public boolean isEnabled(boolean accessibilityEnabled) { return true; } @Override public int getRelevantEventTypes(int relevantEventTypes) { return relevantEventTypes | AccessibilityEvent.TYPE_VIEW_FOCUSED | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED | AccessibilityEvent.TYPE_VIEW_CLICKED | AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED; } @Override public List getInstalledAccessibilityServiceList( List installedServices) { if (installedServices == null) { installedServices = new ArrayList<>(); } installedServices.add(getCompatServiceInfo()); return installedServices; } @Override public List getEnabledAccessibilityServiceList( int feedbackTypeFlags, List enabledService) { if (enabledService == null) { enabledService = new ArrayList<>(); } enabledService.add(getCompatServiceInfo()); return enabledService; } @Override public AccessibilityEvent onAccessibilityEvent(AccessibilityEvent event, boolean accessibilityEnabled, int relevantEventTypes) { final int type = event.getEventType(); if (sVerbose) { // NOTE: this is waaay spammy, but that's life. Log.v(TAG, "onAccessibilityEvent(" + AccessibilityEvent.eventTypeToString(type) + "): virtualId=" + AccessibilityNodeInfo.getVirtualDescendantId(event.getSourceNodeId()) + ", client=" + getClient()); } switch (type) { case AccessibilityEvent.TYPE_VIEW_FOCUSED: { synchronized (mLock) { if (mFocusedWindowId == event.getWindowId() && mFocusedNodeId == event.getSourceNodeId()) { return event; } if (mFocusedWindowId != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID && mFocusedNodeId != AccessibilityNodeInfo.UNDEFINED_NODE_ID) { notifyViewExited(mFocusedWindowId, mFocusedNodeId); mFocusedWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID; mFocusedNodeId = AccessibilityNodeInfo.UNDEFINED_NODE_ID; mFocusedBounds.set(0, 0, 0, 0); } final int windowId = event.getWindowId(); final long nodeId = event.getSourceNodeId(); if (notifyViewEntered(windowId, nodeId, mFocusedBounds)) { mFocusedWindowId = windowId; mFocusedNodeId = nodeId; } } } break; case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED: { synchronized (mLock) { if (mFocusedWindowId == event.getWindowId() && mFocusedNodeId == event.getSourceNodeId()) { notifyValueChanged(event.getWindowId(), event.getSourceNodeId()); } } } break; case AccessibilityEvent.TYPE_VIEW_CLICKED: { synchronized (mLock) { notifyViewClicked(event.getWindowId(), event.getSourceNodeId()); } } break; case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: { final AutofillClient client = getClient(); if (client != null) { synchronized (mLock) { if (client.autofillClientIsFillUiShowing()) { notifyViewEntered(mFocusedWindowId, mFocusedNodeId, mFocusedBounds); } updateTrackedViewsLocked(); } } } break; } return accessibilityEnabled ? event : null; } private boolean notifyViewEntered(int windowId, long nodeId, Rect focusedBounds) { final int virtualId = AccessibilityNodeInfo.getVirtualDescendantId(nodeId); if (!isVirtualNode(virtualId)) { return false; } final View view = findViewByAccessibilityId(windowId, nodeId); if (view == null) { return false; } final AccessibilityNodeInfo node = findVirtualNodeByAccessibilityId(view, virtualId); if (node == null) { return false; } if (!node.isEditable()) { return false; } final Rect newBounds = mTempBounds; node.getBoundsInScreen(newBounds); if (newBounds.equals(focusedBounds)) { return false; } focusedBounds.set(newBounds); AutofillManager.this.notifyViewEntered(view, virtualId, newBounds); return true; } private void notifyViewExited(int windowId, long nodeId) { final int virtualId = AccessibilityNodeInfo.getVirtualDescendantId(nodeId); if (!isVirtualNode(virtualId)) { return; } final View view = findViewByAccessibilityId(windowId, nodeId); if (view == null) { return; } AutofillManager.this.notifyViewExited(view, virtualId); } private void notifyValueChanged(int windowId, long nodeId) { final int virtualId = AccessibilityNodeInfo.getVirtualDescendantId(nodeId); if (!isVirtualNode(virtualId)) { return; } final View view = findViewByAccessibilityId(windowId, nodeId); if (view == null) { return; } final AccessibilityNodeInfo node = findVirtualNodeByAccessibilityId(view, virtualId); if (node == null) { return; } AutofillManager.this.notifyValueChanged(view, virtualId, AutofillValue.forText(node.getText())); } private void notifyViewClicked(int windowId, long nodeId) { final int virtualId = AccessibilityNodeInfo.getVirtualDescendantId(nodeId); if (!isVirtualNode(virtualId)) { return; } final View view = findViewByAccessibilityId(windowId, nodeId); if (view == null) { return; } final AccessibilityNodeInfo node = findVirtualNodeByAccessibilityId(view, virtualId); if (node == null) { return; } AutofillManager.this.notifyViewClicked(view, virtualId); } @GuardedBy("mLock") private void updateTrackedViewsLocked() { if (mTrackedViews != null) { mTrackedViews.onVisibleForAutofillChangedLocked(); } } private View findViewByAccessibilityId(int windowId, long nodeId) { final AutofillClient client = getClient(); if (client == null) { return null; } final int viewId = AccessibilityNodeInfo.getAccessibilityViewId(nodeId); return client.autofillClientFindViewByAccessibilityIdTraversal(viewId, windowId); } private AccessibilityNodeInfo findVirtualNodeByAccessibilityId(View view, int virtualId) { final AccessibilityNodeProvider provider = view.getAccessibilityNodeProvider(); if (provider == null) { return null; } return provider.createAccessibilityNodeInfo(virtualId); } private boolean isVirtualNode(int nodeId) { return nodeId != AccessibilityNodeProvider.HOST_VIEW_ID && nodeId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID; } } /** * View tracking information. Once all tracked views become invisible the session is finished. */ private class TrackedViews { /** Visible tracked views */ @NonNull private final ArraySet mVisibleTrackedIds; /** Invisible tracked views */ @NonNull private final ArraySet mInvisibleTrackedIds; /** Visible tracked views for fill dialog */ @NonNull private final ArraySet mVisibleDialogTrackedIds; /** Invisible tracked views for fill dialog */ @NonNull private final ArraySet mInvisibleDialogTrackedIds; boolean mHasNewTrackedView; boolean mIsTrackedSaveView; /** * Check if set is null or value is in set. * * @param set The set or null (== empty set) * @param value The value that might be in the set * * @return {@code true} iff set is not empty and value is in set */ // TODO: move to Helper as static method private boolean isInSet(@Nullable ArraySet set, T value) { return set != null && set.contains(value); } /** * Add a value to a set. If set is null, create a new set. * * @param set The set or null (== empty set) * @param valueToAdd The value to add * * @return The set including the new value. If set was {@code null}, a set containing only * the new value. */ // TODO: move to Helper as static method @NonNull private ArraySet addToSet(@Nullable ArraySet set, T valueToAdd) { if (set == null) { set = new ArraySet<>(1); } set.add(valueToAdd); return set; } /** * Remove a value from a set. * * @param set The set or null (== empty set) * @param valueToRemove The value to remove * * @return The set without the removed value. {@code null} if set was null, or is empty * after removal. */ // TODO: move to Helper as static method @Nullable private ArraySet removeFromSet(@Nullable ArraySet set, T valueToRemove) { if (set == null) { return null; } set.remove(valueToRemove); if (set.isEmpty()) { return null; } return set; } /** * Set the tracked views. * * @param trackedIds The views to be tracked */ TrackedViews(@Nullable AutofillId[] trackedIds, @Nullable AutofillId[] allTrackedIds) { mVisibleTrackedIds = new ArraySet<>(); mInvisibleTrackedIds = new ArraySet<>(); if (!ArrayUtils.isEmpty(trackedIds)) { mIsTrackedSaveView = true; initialTrackedViews(trackedIds, mVisibleTrackedIds, mInvisibleTrackedIds); } mVisibleDialogTrackedIds = new ArraySet<>(); mInvisibleDialogTrackedIds = new ArraySet<>(); if (!ArrayUtils.isEmpty(allTrackedIds)) { initialTrackedViews(allTrackedIds, mVisibleDialogTrackedIds, mInvisibleDialogTrackedIds); mAllTrackedViews.addAll(Arrays.asList(allTrackedIds)); } if (sVerbose) { Log.v(TAG, "TrackedViews(trackedIds=" + Arrays.toString(trackedIds) + "): " + " mVisibleTrackedIds=" + mVisibleTrackedIds + " mInvisibleTrackedIds=" + mInvisibleTrackedIds + " allTrackedIds=" + Arrays.toString(allTrackedIds) + " mVisibleDialogTrackedIds=" + mVisibleDialogTrackedIds + " mInvisibleDialogTrackedIds=" + mInvisibleDialogTrackedIds); } if (mIsTrackedSaveView && mVisibleTrackedIds.isEmpty()) { finishSessionLocked(/* commitReason= */ COMMIT_REASON_VIEW_CHANGED); } } private void initialTrackedViews(AutofillId[] trackedIds, @NonNull ArraySet visibleSet, @NonNull ArraySet invisibleSet) { final boolean[] isVisible; final AutofillClient client = getClient(); if (ArrayUtils.isEmpty(trackedIds) || client == null) { return; } if (client.autofillClientIsVisibleForAutofill()) { if (sVerbose) Log.v(TAG, "client is visible, check tracked ids"); isVisible = client.autofillClientGetViewVisibility(trackedIds); } else { // All false isVisible = new boolean[trackedIds.length]; } final int numIds = trackedIds.length; for (int i = 0; i < numIds; i++) { final AutofillId id = trackedIds[i]; id.resetSessionId(); if (isVisible[i]) { addToSet(visibleSet, id); } else { addToSet(invisibleSet, id); } } } /** * Called when a {@link View view's} visibility changes. * * @param id the id of the view/virtual view whose visibility changed. * @param isVisible visible if the view is visible in the view hierarchy. */ @GuardedBy("mLock") void notifyViewVisibilityChangedLocked(@NonNull AutofillId id, boolean isVisible) { if (sDebug) { Log.d(TAG, "notifyViewVisibilityChangedLocked(): id=" + id + " isVisible=" + isVisible); } if (isClientVisibleForAutofillLocked()) { if (isVisible) { if (isInSet(mInvisibleTrackedIds, id)) { removeFromSet(mInvisibleTrackedIds, id); addToSet(mVisibleTrackedIds, id); } if (isInSet(mInvisibleDialogTrackedIds, id)) { removeFromSet(mInvisibleDialogTrackedIds, id); addToSet(mVisibleDialogTrackedIds, id); } } else { if (isInSet(mVisibleTrackedIds, id)) { removeFromSet(mVisibleTrackedIds, id); addToSet(mInvisibleTrackedIds, id); } if (isInSet(mVisibleDialogTrackedIds, id)) { removeFromSet(mVisibleDialogTrackedIds, id); addToSet(mInvisibleDialogTrackedIds, id); } } } if (mIsTrackedSaveView && mVisibleTrackedIds.isEmpty()) { if (sVerbose) { Log.v(TAG, "No more visible ids. Invisible = " + mInvisibleTrackedIds); } finishSessionLocked(/* commitReason= */ COMMIT_REASON_VIEW_CHANGED); } if (mVisibleDialogTrackedIds.isEmpty()) { if (sVerbose) { Log.v(TAG, "No more visible ids. Invisible = " + mInvisibleDialogTrackedIds); } processNoVisibleTrackedAllViews(); } } /** * Called once the client becomes visible. * * @see AutofillClient#autofillClientIsVisibleForAutofill() */ @GuardedBy("mLock") void onVisibleForAutofillChangedLocked() { // The visibility of the views might have changed while the client was not be visible, // hence update the visibility state for all views. AutofillClient client = getClient(); if (client != null) { if (sVerbose) { Log.v(TAG, "onVisibleForAutofillChangedLocked(): inv= " + mInvisibleTrackedIds + " vis=" + mVisibleTrackedIds); } onVisibleForAutofillChangedInternalLocked(mVisibleTrackedIds, mInvisibleTrackedIds); onVisibleForAutofillChangedInternalLocked( mVisibleDialogTrackedIds, mInvisibleDialogTrackedIds); } if (mIsTrackedSaveView && mVisibleTrackedIds.isEmpty()) { if (sVerbose) { Log.v(TAG, "onVisibleForAutofillChangedLocked(): no more visible ids"); } finishSessionLocked(/* commitReason= */ COMMIT_REASON_VIEW_CHANGED); } if (mVisibleDialogTrackedIds.isEmpty()) { if (sVerbose) { Log.v(TAG, "onVisibleForAutofillChangedLocked(): no more visible ids"); } processNoVisibleTrackedAllViews(); } } void onVisibleForAutofillChangedInternalLocked(@NonNull ArraySet visibleSet, @NonNull ArraySet invisibleSet) { // The visibility of the views might have changed while the client was not be visible, // hence update the visibility state for all views. if (sVerbose) { Log.v(TAG, "onVisibleForAutofillChangedLocked(): inv= " + invisibleSet + " vis=" + visibleSet); } ArraySet allTrackedIds = new ArraySet<>(); allTrackedIds.addAll(visibleSet); allTrackedIds.addAll(invisibleSet); if (!allTrackedIds.isEmpty()) { visibleSet.clear(); invisibleSet.clear(); initialTrackedViews(Helper.toArray(allTrackedIds), visibleSet, invisibleSet); } } private void processNoVisibleTrackedAllViews() { mShowAutofillDialogCalled = false; } void checkViewState(AutofillId id) { if (mHasNewTrackedView) { return; } // First one new tracks view mIsFillRequested.set(false); mHasNewTrackedView = true; } } /** * Callback for autofill related events. * *

Typically used for applications that display their own "auto-complete" views, so they can * enable / disable such views when the autofill UI is shown / hidden. */ public abstract static class AutofillCallback { /** @hide */ @IntDef(prefix = { "EVENT_INPUT_" }, value = { EVENT_INPUT_SHOWN, EVENT_INPUT_HIDDEN, EVENT_INPUT_UNAVAILABLE }) @Retention(RetentionPolicy.SOURCE) public @interface AutofillEventType {} /** * The autofill input UI associated with the view was shown. * *

If the view provides its own auto-complete UI and its currently shown, it * should be hidden upon receiving this event. */ public static final int EVENT_INPUT_SHOWN = 1; /** * The autofill input UI associated with the view was hidden. * *

If the view provides its own auto-complete UI that was hidden upon a * {@link #EVENT_INPUT_SHOWN} event, it could be shown again now. */ public static final int EVENT_INPUT_HIDDEN = 2; /** * The autofill input UI associated with the view isn't shown because * autofill is not available. * *

If the view provides its own auto-complete UI but was not displaying it * to avoid flickering, it could shown it upon receiving this event. */ public static final int EVENT_INPUT_UNAVAILABLE = 3; /** * Called after a change in the autofill state associated with a view. * * @param view view associated with the change. * * @param event currently either {@link #EVENT_INPUT_SHOWN} or {@link #EVENT_INPUT_HIDDEN}. */ public void onAutofillEvent(@NonNull View view, @AutofillEventType int event) { } /** * Called after a change in the autofill state associated with a virtual view. * * @param view parent view associated with the change. * @param virtualId id identifying the virtual child inside the parent view. * * @param event currently either {@link #EVENT_INPUT_SHOWN} or {@link #EVENT_INPUT_HIDDEN}. */ public void onAutofillEvent(@NonNull View view, int virtualId, @AutofillEventType int event) { } } private static final class AutofillManagerClient extends IAutoFillManagerClient.Stub { private final WeakReference mAfm; private AutofillManagerClient(AutofillManager autofillManager) { mAfm = new WeakReference<>(autofillManager); } @Override public void setState(int flags) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.setState(flags)); } } @Override public void autofill(int sessionId, List ids, List values, boolean hideHighlight) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.autofill(sessionId, ids, values, hideHighlight)); } } @Override public void onGetCredentialResponse(int sessionId, AutofillId id, GetCredentialResponse response) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.onGetCredentialResponse(sessionId, id, response)); } } @Override public void onGetCredentialException(int sessionId, AutofillId id, String errorType, String errorMsg) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.onGetCredentialException(sessionId, id, errorType, errorMsg)); } } @Override public void autofillContent(int sessionId, AutofillId id, ClipData content) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.autofillContent(sessionId, id, content)); } } @Override public void authenticate(int sessionId, int authenticationId, IntentSender intent, Intent fillInIntent, boolean authenticateInline) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.authenticate(sessionId, authenticationId, intent, fillInIntent, authenticateInline)); } } @Override public void requestShowFillUi(int sessionId, AutofillId id, int width, int height, Rect anchorBounds, IAutofillWindowPresenter presenter) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.requestShowFillUi(sessionId, id, width, height, anchorBounds, presenter)); } } @Override public void requestHideFillUi(int sessionId, AutofillId id) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.requestHideFillUi(id, false)); } } @Override public void requestHideFillUiWhenDestroyed(int sessionId, AutofillId id) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.requestHideFillUi(id, true)); } } @Override public void notifyNoFillUi(int sessionId, AutofillId id, int sessionFinishedState) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.notifyNoFillUi(sessionId, id, sessionFinishedState)); } } @Override public void notifyFillUiShown(int sessionId, AutofillId id) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post( () -> afm.notifyCallback( sessionId, id, AutofillCallback.EVENT_INPUT_SHOWN)); } } @Override public void notifyFillUiHidden(int sessionId, AutofillId id) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post( () -> afm.notifyCallback( sessionId, id, AutofillCallback.EVENT_INPUT_HIDDEN)); } } @Override public void notifyDisableAutofill(long disableDuration, ComponentName componentName) throws RemoteException { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.notifyDisableAutofill(disableDuration, componentName)); } } @Override public void dispatchUnhandledKey(int sessionId, AutofillId id, KeyEvent fullScreen) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.dispatchUnhandledKey(sessionId, id, fullScreen)); } } @Override public void startIntentSender(IntentSender intentSender, Intent intent) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> { try { Bundle options = ActivityOptions.makeBasic() .setPendingIntentBackgroundActivityStartMode( ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) .toBundle(); afm.mContext.startIntentSender(intentSender, intent, 0, 0, 0, options); } catch (IntentSender.SendIntentException e) { Log.e(TAG, "startIntentSender() failed for intent:" + intentSender, e); } }); } } @Override public void setTrackedViews(int sessionId, AutofillId[] ids, boolean saveOnAllViewsInvisible, boolean saveOnFinish, AutofillId[] fillableIds, AutofillId saveTriggerId) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.setTrackedViews(sessionId, ids, saveOnAllViewsInvisible, saveOnFinish, fillableIds, saveTriggerId)); } } @Override public void setSaveUiState(int sessionId, boolean shown) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.setSaveUiState(sessionId, shown)); } } @Override public void setSessionFinished(int newState, List autofillableIds) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.setSessionFinished(newState, autofillableIds)); } } @Override public void getAugmentedAutofillClient(IResultReceiver result) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.getAugmentedAutofillClient(result)); } } @Override public void requestShowSoftInput(@NonNull AutofillId id) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.requestShowSoftInput(id)); } } @Override public void notifyFillDialogTriggerIds(List ids) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.setFillDialogTriggerIds(ids)); } } } private static final class AugmentedAutofillManagerClient extends IAugmentedAutofillManagerClient.Stub { private final WeakReference mAfm; private AugmentedAutofillManagerClient(AutofillManager autofillManager) { mAfm = new WeakReference<>(autofillManager); } @Nullable @Override public ViewNodeParcelable getViewNodeParcelable(@NonNull AutofillId id) { final AutofillManager afm = mAfm.get(); if (afm == null) return null; final View view = getView(afm, id); if (view == null) { Log.w(TAG, "getViewNodeParcelable(" + id + "): could not find view"); return null; } final ViewRootImpl root = view.getViewRootImpl(); if (root != null && (root.getWindowFlags() & WindowManager.LayoutParams.FLAG_SECURE) == 0) { ViewNodeBuilder viewStructure = new ViewNodeBuilder(); viewStructure.setAutofillId(view.getAutofillId()); view.onProvideAutofillStructure(viewStructure, /* flags= */ 0); // TODO(b/141703532): We don't call View#onProvideAutofillVirtualStructure for // efficiency reason. But this also means we will return null for virtual views // for now. We will add a new API to fetch the view node info of the virtual // child view. ViewNode viewNode = viewStructure.getViewNode(); if (viewNode != null && id.equals(viewNode.getAutofillId())) { return new ViewNodeParcelable(viewNode); } } return null; } @Override public Rect getViewCoordinates(@NonNull AutofillId id) { final AutofillManager afm = mAfm.get(); if (afm == null) return null; final View view = getView(afm, id); if (view == null) { return null; } final Rect windowVisibleDisplayFrame = new Rect(); view.getWindowVisibleDisplayFrame(windowVisibleDisplayFrame); final int[] location = new int[2]; view.getLocationOnScreen(location); final Rect rect = new Rect(location[0], location[1] - windowVisibleDisplayFrame.top, location[0] + view.getWidth(), location[1] - windowVisibleDisplayFrame.top + view.getHeight()); if (sVerbose) { Log.v(TAG, "Coordinates for " + id + ": " + rect); } return rect; } @Override public void autofill(int sessionId, List ids, List values, boolean hideHighlight) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.autofill(sessionId, ids, values, hideHighlight)); } } @Override public void requestShowFillUi(int sessionId, AutofillId id, int width, int height, Rect anchorBounds, IAutofillWindowPresenter presenter) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.requestShowFillUi(sessionId, id, width, height, anchorBounds, presenter)); } } @Override public void requestHideFillUi(int sessionId, AutofillId id) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.requestHideFillUi(id, false)); } } @Override public boolean requestAutofill(int sessionId, AutofillId id) { final AutofillManager afm = mAfm.get(); if (afm == null || afm.mSessionId != sessionId) { if (sDebug) { Slog.d(TAG, "Autofill not available or sessionId doesn't match"); } return false; } final View view = getView(afm, id); if (view == null || !view.isFocused()) { if (sDebug) { Slog.d(TAG, "View not available or is not on focus"); } return false; } if (sVerbose) { Log.v(TAG, "requestAutofill() by AugmentedAutofillService."); } afm.post(() -> afm.requestAutofillFromNewSession(view)); return true; } @Nullable private View getView(@NonNull AutofillManager afm, @NonNull AutofillId id) { final AutofillClient client = afm.getClient(); if (client == null) { Log.w(TAG, "getView(" + id + "): no autofill client"); return null; } View view = client.autofillClientFindViewByAutofillIdTraversal(id); if (view == null) { Log.w(TAG, "getView(" + id + "): could not find view"); } return view; } } }