/* * 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. * *
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. * *
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 ArraySetTypically 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 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:
*
* 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:
*
* 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 Autofill is typically supported, but it could be unsupported in cases like:
* 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 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 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
* 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
* 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 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
* 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
* 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.
*
*
*
*/
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.
*
*