/* * 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_MANUAL_REQUEST; import static android.view.autofill.Helper.sDebug; import static android.view.autofill.Helper.sVerbose; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemService; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentSender; import android.graphics.Rect; import android.metrics.LogMaker; import android.os.Bundle; import android.os.IBinder; import android.os.Parcelable; import android.os.RemoteException; import android.service.autofill.AutofillService; import android.service.autofill.FillEventHistory; import android.service.autofill.UserData; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; import android.util.SparseArray; import android.view.View; import com.android.internal.annotations.GuardedBy; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.Preconditions; import java.io.PrintWriter; 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; // TODO: use java.lang.ref.Cleaner once Android supports Java 9 import sun.misc.Cleaner; /** * The {@link AutofillManager} provides ways for apps and custom views to integrate with the * Autofill Framework 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: * *
Tipically, 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 into its methods from any thread. */ @SystemService(Context.AUTOFILL_MANAGER_SERVICE) 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 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_RESTORE_SESSION_TOKEN = "android.view.autofill.extra.RESTORE_SESSION_TOKEN"; 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 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; /** 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;
/**
* Timeout in ms for calls to the field classification service.
* @hide
*/
public static final int FC_SERVICE_TIMEOUT = 5000;
/**
* 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.MIN_VALUE;
private final IAutoFillManager mService;
private final Object mLock = new Object();
@GuardedBy("mLock")
private IAutoFillManagerClient mServiceClient;
@GuardedBy("mLock")
private Cleaner mServiceClientCleaner;
@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 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;
}
ensureServiceClientAddedIfNeededLocked();
return mEnabled;
}
}
/**
* Should always be called from {@link AutofillService#getFillEventHistory()}.
*
* @hide
*/
@Nullable public FillEventHistory getFillEventHistory() {
try {
return mService.getFillEventHistory();
} catch (RemoteException e) {
e.rethrowFromSystemServer();
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) {
notifyViewEntered(view, FLAG_MANUAL_REQUEST);
}
/**
* 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;
}
synchronized (mLock) {
commitLocked();
}
}
private void commitLocked() {
if (!mEnabled && !isActiveLocked()) {
return;
}
finishSessionLocked();
}
/**
* 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");
if (!hasAutofillFeature()) {
return;
}
synchronized (mLock) {
cancelLocked();
}
}
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;
try {
return mService.isServiceEnabled(mContext.getUserId(), mContext.getPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Returns the component name of the {@link AutofillService} that is enabled for the current
* user.
*/
@Nullable
public ComponentName getAutofillServiceComponentName() {
if (mService == null) return null;
try {
return mService.getAutofillServiceComponentName();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* 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 {
return mService.getUserData();
} catch (RemoteException e) {
e.rethrowFromSystemServer();
return null;
}
}
/**
* Sets 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.
*/
public void setUserData(@Nullable UserData userData) {
try {
mService.setUserData(userData);
} catch (RemoteException e) {
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() {
try {
return mService.isFieldClassificationEnabled();
} catch (RemoteException e) {
e.rethrowFromSystemServer();
return false;
}
}
/**
* 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() {
try {
return mService.getDefaultFieldClassificationAlgorithm();
} catch (RemoteException e) {
e.rethrowFromSystemServer();
return null;
}
}
/**
* 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 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) {
notifyViewEntered(view, virtualId, absBounds, FLAG_MANUAL_REQUEST);
}
/**
* 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);
}
private boolean shouldIgnoreViewEnteredLocked(@NonNull View view, int flags) {
if (isDisabledByServiceLocked()) {
if (sVerbose) {
Log.v(TAG, "ignoring notifyViewEntered(flags=" + flags + ", view=" + view
+ ") on state " + getStateAsStringLocked());
}
return true;
}
if (sVerbose && isFinishedLocked()) {
Log.v(TAG, "not ignoring notifyViewEntered(flags=" + flags + ", view=" + view
+ ") on state " + getStateAsStringLocked());
}
return false;
}
private boolean isClientVisibleForAutofillLocked() {
final AutofillClient client = getClient();
return client != null && client.isVisibleForAutofill();
}
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, flags);
}
if (callback != null) {
mCallback.onAutofillEvent(view, AutofillCallback.EVENT_INPUT_UNAVAILABLE);
}
}
/** Returns AutofillCallback if need fire EVENT_INPUT_UNAVAILABLE */
private AutofillCallback notifyViewEnteredLocked(@NonNull View view, int flags) {
if (shouldIgnoreViewEnteredLocked(view, flags)) return null;
AutofillCallback callback = null;
ensureServiceClientAddedIfNeededLocked();
if (!mEnabled) {
if (mCallback != null) {
callback = mCallback;
}
} else {
// don't notify entered when Activity is already in background
if (!isClientDisablingEnterExitEvent()) {
final AutofillId id = getAutofillId(view);
final AutofillValue value = view.getAutofillValue();
if (!isActiveLocked()) {
// Starts new session.
startSessionLocked(id, null, value, flags);
} else {
// Update focus on existing session.
updateSessionLocked(id, null, value, ACTION_VIEW_ENTERED, flags);
}
}
}
return callback;
}
/**
* 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);
}
}
void notifyViewExitedLocked(@NonNull View view) {
ensureServiceClientAddedIfNeededLocked();
if (mEnabled && isActiveLocked()) {
// dont notify exited when Activity is already in background
if (!isClientDisablingEnterExitEvent()) {
final AutofillId id = getAutofillId(view);
// 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 (mEnabled && isActiveLocked()) {
final AutofillId id = virtual ? getAutofillId(view, virtualId)
: view.getAutofillId();
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);
}
}
}
}
/**
* Called when a virtual view that supports autofill is entered.
*
*
* 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, virtualId, bounds, flags);
}
if (callback != null) {
callback.onAutofillEvent(view, virtualId,
AutofillCallback.EVENT_INPUT_UNAVAILABLE);
}
}
/** Returns AutofillCallback if need fire EVENT_INPUT_UNAVAILABLE */
private AutofillCallback notifyViewEnteredLocked(View view, int virtualId, Rect bounds,
int flags) {
AutofillCallback callback = null;
if (shouldIgnoreViewEnteredLocked(view, flags)) return callback;
ensureServiceClientAddedIfNeededLocked();
if (!mEnabled) {
if (mCallback != null) {
callback = mCallback;
}
} else {
// don't notify entered when Activity is already in background
if (!isClientDisablingEnterExitEvent()) {
final AutofillId id = getAutofillId(view, virtualId);
if (!isActiveLocked()) {
// Starts new session.
startSessionLocked(id, bounds, null, flags);
} else {
// Update focus on existing session.
updateSessionLocked(id, bounds, null, ACTION_VIEW_ENTERED, flags);
}
}
}
return callback;
}
/**
* 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 (!hasAutofillFeature()) {
return;
}
synchronized (mLock) {
notifyViewExitedLocked(view, virtualId);
}
}
private void notifyViewExitedLocked(@NonNull View view, int virtualId) {
ensureServiceClientAddedIfNeededLocked();
if (mEnabled && 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);
} else {
id = getAutofillId(view);
if (mLastAutofilledData.containsKey(id)) {
value = view.getAutofillValue();
valueWasRead = true;
if (Objects.equals(mLastAutofilledData.get(id), value)) {
view.setAutofilled(true);
} else {
view.setAutofilled(false);
mLastAutofilledData.remove(id);
}
} else {
view.setAutofilled(false);
}
}
if (!mEnabled || !isActiveLocked()) {
if (sVerbose && mEnabled) {
Log.v(TAG, "notifyValueChanged(" + view + "): ignoring on state "
+ getStateAsStringLocked());
}
return;
}
if (id == null) {
id = getAutofillId(view);
}
if (!valueWasRead) {
value = view.getAutofillValue();
}
updateSessionLocked(id, null, value, ACTION_VALUE_CHANGED, 0);
}
}
/**
* 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()) {
return;
}
final AutofillId id = getAutofillId(view, virtualId);
updateSessionLocked(id, null, value, ACTION_VALUE_CHANGED, 0);
}
}
/**
* Called when a {@link View} is clicked. Currently only used by views that should trigger save.
*
* @hide
*/
public void notifyViewClicked(View view) {
final AutofillId id = view.getAutofillId();
if (sVerbose) Log.v(TAG, "notifyViewClicked(): id=" + id + ", trigger=" + mSaveTriggerId);
synchronized (mLock) {
if (mSaveTriggerId != null && mSaveTriggerId.equals(id)) {
if (sDebug) Log.d(TAG, "triggering commit by click of " + id);
commitLocked();
mMetricsLogger.action(MetricsEvent.AUTOFILL_SAVE_EXPLICITLY_TRIGGERED,
mContext.getPackageName());
}
}
}
/**
* Called by {@link android.app.Activity} to commit or cancel the session on finish.
*
* @hide
*/
public void onActivityFinished() {
if (!hasAutofillFeature()) {
return;
}
synchronized (mLock) {
if (mSaveOnFinish) {
if (sDebug) Log.d(TAG, "Committing session on finish() as requested by service");
commitLocked();
} else {
if (sDebug) Log.d(TAG, "Cancelling session on finish() as requested by service");
cancelLocked();
}
}
}
/**
* Called to indicate the current autofill context should be commited.
*
*
*
*/
public boolean isAutofillSupported() {
if (mService == null) return false;
try {
return mService.isServiceSupported(mContext.getUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
// Note: don't need to use locked suffix because mContext is final.
private AutofillClient getClient() {
final AutofillClient client = mContext.getAutofillClient();
if (client == null && sDebug) {
Log.d(TAG, "No AutofillClient for " + mContext.getPackageName() + " on context "
+ mContext);
}
return client;
}
/** @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(): d=" + data);
synchronized (mLock) {
if (!isActiveLocked()) {
return;
}
// 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, 0);
}
if (data == null) {
// data is set to null when result is not RESULT_OK
return;
}
final Parcelable result = data.getParcelableExtra(EXTRA_AUTHENTICATION_RESULT);
final Bundle responseData = new Bundle();
responseData.putParcelable(EXTRA_AUTHENTICATION_RESULT, result);
final Bundle newClientState = data.getBundleExtra(EXTRA_CLIENT_STATE);
if (newClientState != null) {
responseData.putBundle(EXTRA_CLIENT_STATE, newClientState);
}
try {
mService.setAuthenticationResult(responseData, mSessionId, authenticationId,
mContext.getUserId());
} catch (RemoteException e) {
Log.e(TAG, "Error delivering authentication result", e);
}
}
}
private static AutofillId getAutofillId(View view) {
return new AutofillId(view.getAutofillViewId());
}
private static AutofillId getAutofillId(View parent, int virtualId) {
return new AutofillId(parent.getAutofillViewId(), virtualId);
}
private void startSessionLocked(@NonNull AutofillId id, @NonNull Rect bounds,
@NonNull AutofillValue value, int flags) {
if (sVerbose) {
Log.v(TAG, "startSessionLocked(): id=" + id + ", bounds=" + bounds + ", value=" + value
+ ", flags=" + flags + ", state=" + getStateAsStringLocked());
}
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 logd it..
mSessionId = mService.startSession(mContext.getActivityToken(),
mServiceClient.asBinder(), id, bounds, value, mContext.getUserId(),
mCallback != null, flags, client.getComponentName());
if (mSessionId != NO_SESSION) {
mState = STATE_ACTIVE;
}
client.autofillCallbackResetableStateAvailable();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
private void finishSessionLocked() {
if (sVerbose) Log.v(TAG, "finishSessionLocked(): " + getStateAsStringLocked());
if (!isActiveLocked()) return;
try {
mService.finishSession(mSessionId, mContext.getUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
resetSessionLocked();
}
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();
}
private void resetSessionLocked() {
mSessionId = NO_SESSION;
mState = STATE_UNKNOWN;
mTrackedViews = null;
mFillableIds = null;
mSaveTriggerId = null;
}
private void updateSessionLocked(AutofillId id, Rect bounds, AutofillValue value, int action,
int flags) {
if (sVerbose && action != ACTION_VIEW_EXITED) {
Log.v(TAG, "updateSessionLocked(): id=" + id + ", bounds=" + bounds
+ ", value=" + value + ", action=" + action + ", flags=" + flags);
}
boolean restartIfNecessary = (flags & FLAG_MANUAL_REQUEST) != 0;
try {
if (restartIfNecessary) {
final AutofillClient client = getClient();
if (client == null) return; // NOTE: getClient() already logd it..
final int newId = mService.updateOrRestartSession(mContext.getActivityToken(),
mServiceClient.asBinder(), id, bounds, value, mContext.getUserId(),
mCallback != null, flags, client.getComponentName(), mSessionId, action);
if (newId != mSessionId) {
if (sDebug) Log.d(TAG, "Session restarted: " + mSessionId + "=>" + newId);
mSessionId = newId;
mState = (mSessionId == NO_SESSION) ? STATE_UNKNOWN : STATE_ACTIVE;
client.autofillCallbackResetableStateAvailable();
}
} else {
mService.updateSession(mSessionId, id, bounds, value, action, flags,
mContext.getUserId());
}
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
private void ensureServiceClientAddedIfNeededLocked() {
if (getClient() == null) {
return;
}
if (mServiceClient == null) {
mServiceClient = new AutofillManagerClient(this);
try {
final int userId = mContext.getUserId();
final int flags = mService.addClient(mServiceClient, userId);
mEnabled = (flags & FLAG_ADD_CLIENT_ENABLED) != 0;
sDebug = (flags & FLAG_ADD_CLIENT_DEBUG) != 0;
sVerbose = (flags & FLAG_ADD_CLIENT_VERBOSE) != 0;
final IAutoFillManager service = mService;
final IAutoFillManagerClient serviceClient = mServiceClient;
mServiceClientCleaner = Cleaner.create(this, () -> {
try {
service.removeClient(serviceClient, userId);
} catch (RemoteException e) {
}
});
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
}
/**
* 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();
}
}
}
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.autofillCallbackRequestShowFillUi(anchor, width, height,
anchorBounds, presenter) && mCallback != null) {
callback = mCallback;
}
}
}
}
if (callback != null) {
if (id.isVirtual()) {
callback.onAutofillEvent(anchor, id.getVirtualChildId(),
AutofillCallback.EVENT_INPUT_SHOWN);
} else {
callback.onAutofillEvent(anchor, AutofillCallback.EVENT_INPUT_SHOWN);
}
}
}
private void authenticate(int sessionId, int authenticationId, IntentSender intent,
Intent fillInIntent) {
synchronized (mLock) {
if (sessionId == mSessionId) {
final AutofillClient client = getClient();
if (client != null) {
// clear mOnInvisibleCalled and we will see if receive onInvisibleForAutofill()
// before onAuthenticationResult()
mOnInvisibleCalled = false;
client.autofillCallbackAuthenticate(authenticationId, intent, fillInIntent);
}
}
}
}
/** @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;
private void setState(int flags) {
if (sVerbose) Log.v(TAG, "setState(" + flags + ")");
synchronized (mLock) {
mEnabled = (flags & SET_STATE_FLAG_ENABLED) != 0;
if (!mEnabled || (flags & SET_STATE_FLAG_RESET_SESSION) != 0) {
// Reset the session state
resetSessionLocked();
}
if ((flags & SET_STATE_FLAG_RESET_CLIENT) != 0) {
// Reset connection to system
mServiceClient = null;
if (mServiceClientCleaner != null) {
mServiceClientCleaner.clean();
mServiceClientCleaner = null;
}
}
}
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) {
AutofillValue currentValue = view.getAutofillValue();
if (Objects.equals(currentValue, targetValue)) {
synchronized (mLock) {
if (mLastAutofilledData == null) {
mLastAutofilledData = new ParcelableMap(1);
}
mLastAutofilledData.put(getAutofillId(view), targetValue);
}
view.setAutofilled(true);
}
}
private void autofill(int sessionId, List