/* * 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 mFillableIds; /** If set, session is commited when the field is clicked. */ @GuardedBy("mLock") @Nullable private AutofillId mSaveTriggerId; /** set to true when onInvisibleForAutofill is called, used by onAuthenticationResult */ @GuardedBy("mLock") private boolean mOnInvisibleCalled; /** If set, session is commited when the activity is finished; otherwise session is canceled. */ @GuardedBy("mLock") private boolean mSaveOnFinish; /** @hide */ public interface AutofillClient { /** * Asks the client to start an authentication flow. * * @param authenticationId A unique id of the authentication operation. * @param intent The authentication intent. * @param fillInIntent The authentication fill-in intent. */ void autofillCallbackAuthenticate(int authenticationId, IntentSender intent, Intent fillInIntent); /** * Tells the client this manager has state to be reset. */ void autofillCallbackResetableStateAvailable(); /** * Request showing the autofill UI. * * @param anchor The real view the UI needs to anchor to. * @param width The width of the fill UI content. * @param height The height of the fill UI content. * @param virtualBounds The bounds of the virtual decendant of the anchor. * @param presenter The presenter that controls the fill UI window. * @return Whether the UI was shown. */ boolean autofillCallbackRequestShowFillUi(@NonNull View anchor, int width, int height, @Nullable Rect virtualBounds, IAutofillWindowPresenter presenter); /** * Request hiding the autofill UI. * * @return Whether the UI was hidden. */ boolean autofillCallbackRequestHideFillUi(); /** * Checks if views are currently attached and visible. * * @return And array with {@code true} iff the view is attached or visible */ @NonNull boolean[] getViewVisibility(@NonNull int[] viewId); /** * Checks is the client is currently visible as understood by autofill. * * @return {@code true} if the client is currently visible */ boolean isVisibleForAutofill(); /** * Client might disable enter/exit event e.g. when activity is paused. */ boolean isDisablingEnterExitEventForAutofill(); /** * Finds views by traversing the hierarchies of the client. * * @param viewIds The autofill ids of the views to find * * @return And array containing the views (empty if no views found). */ @NonNull View[] findViewsByAutofillIdTraversal(@NonNull int[] viewIds); /** * Finds a view by traversing the hierarchies of the client. * * @param viewId The autofill id of the views to find * * @return The view, or {@code null} if not found */ @Nullable View findViewByAutofillIdTraversal(int viewId); /** * Runs the specified action on the UI thread. */ void runOnUiThread(Runnable action); /** * Gets the complete component name of this client. */ ComponentName getComponentName(); } /** * @hide */ public AutofillManager(Context context, IAutoFillManager service) { mContext = Preconditions.checkNotNull(context, "context cannot be null"); mService = service; } /** * Restore state after activity lifecycle * * @param savedInstanceState The state to be restored * * {@hide} */ public void onCreate(Bundle savedInstanceState) { if (!hasAutofillFeature()) { return; } synchronized (mLock) { mLastAutofilledData = savedInstanceState.getParcelable(LAST_AUTOFILLED_DATA_TAG); if (isActiveLocked()) { Log.w(TAG, "New session was started before onCreate()"); return; } mSessionId = savedInstanceState.getInt(SESSION_ID_TAG, NO_SESSION); mState = savedInstanceState.getInt(STATE_TAG, STATE_UNKNOWN); if (mSessionId != NO_SESSION) { ensureServiceClientAddedIfNeededLocked(); final AutofillClient client = getClient(); if (client != null) { try { final boolean sessionWasRestored = mService.restoreSession(mSessionId, mContext.getActivityToken(), mServiceClient.asBinder()); if (!sessionWasRestored) { Log.w(TAG, "Session " + mSessionId + " could not be restored"); mSessionId = NO_SESSION; mState = STATE_UNKNOWN; } else { if (sDebug) { Log.d(TAG, "session " + mSessionId + " was restored"); } client.autofillCallbackResetableStateAvailable(); } } catch (RemoteException e) { Log.e(TAG, "Could not figure out if there was an autofill session", e); } } } } } /** * Called once the client becomes visible. * * @see AutofillClient#isVisibleForAutofill() * * {@hide} */ public void onVisibleForAutofill() { synchronized (mLock) { if (mEnabled && isActiveLocked() && mTrackedViews != null) { mTrackedViews.onVisibleForAutofillLocked(); } } } /** * Called once the client becomes invisible. * * @see AutofillClient#isVisibleForAutofill() * * {@hide} */ public void onInvisibleForAutofill() { synchronized (mLock) { mOnInvisibleCalled = true; } } /** * Save state before activity lifecycle * * @param outState Place to store the state * * {@hide} */ public void onSaveInstanceState(Bundle outState) { if (!hasAutofillFeature()) { return; } synchronized (mLock) { if (mSessionId != NO_SESSION) { outState.putInt(SESSION_ID_TAG, mSessionId); } if (mState != STATE_UNKNOWN) { outState.putInt(STATE_TAG, mState); } if (mLastAutofilledData != null) { outState.putParcelable(LAST_AUTOFILLED_DATA_TAG, mLastAutofilledData); } } } /** * Checks whether autofill is enabled for the current user. * *

Typically used to determine whether the option to explicitly request autofill should * be offered - see {@link #requestAutofill(View)}. * * @return whether autofill is enabled for the current user. */ public boolean isEnabled() { if (!hasAutofillFeature()) { return false; } synchronized (mLock) { if (isDisabledByServiceLocked()) { return false; } 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: * *

     *   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. * *

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

     *   int offset[] = new int[2];
     *   getLocationOnScreen(offset);
     *   Rect absBounds = new Rect(bounds.left + offset[0],
     *       bounds.top + offset[1],
     *       bounds.right + offset[0], bounds.bottom + offset[1]);
     * 
* * @param view the virtual view parent. * @param virtualId id identifying the virtual child inside the parent view. * @param absBounds absolute boundaries of the virtual view in the screen. */ public void notifyViewEntered(@NonNull View view, int virtualId, @NonNull Rect absBounds) { notifyViewEntered(view, virtualId, absBounds, 0); } private void notifyViewEntered(View view, int virtualId, Rect bounds, int flags) { if (!hasAutofillFeature()) { return; } AutofillCallback callback; synchronized (mLock) { callback = notifyViewEnteredLocked(view, 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. * *

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 getAvailableFieldClassificationAlgorithms() { final String[] algorithms; try { algorithms = mService.getAvailableFieldClassificationAlgorithms(); return algorithms != null ? Arrays.asList(algorithms) : Collections.emptyList(); } catch (RemoteException e) { e.rethrowFromSystemServer(); return null; } } /** * Returns {@code true} if autofill is supported by the current device and * is supported for this user. * *

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

    *
  1. Low-end devices. *
  2. Device policy rules that forbid its usage. *
*/ public boolean isAutofillSupported() { if (mService == null) return false; 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 ids, List values) { synchronized (mLock) { if (sessionId != mSessionId) { return; } final AutofillClient client = getClient(); if (client == null) { return; } final int itemCount = ids.size(); int numApplied = 0; ArrayMap> virtualValues = null; final View[] views = client.findViewsByAutofillIdTraversal(getViewIds(ids)); for (int i = 0; i < itemCount; i++) { final AutofillId id = ids.get(i); final AutofillValue value = values.get(i); final int viewId = id.getViewId(); final View view = views[i]; if (view == null) { Log.w(TAG, "autofill(): no View with id " + viewId); continue; } if (id.isVirtual()) { if (virtualValues == null) { // Most likely there will be just one view with virtual children. virtualValues = new ArrayMap<>(1); } SparseArray valuesByParent = virtualValues.get(view); if (valuesByParent == null) { // We don't know the size yet, but usually it will be just a few fields... valuesByParent = new SparseArray<>(5); virtualValues.put(view, valuesByParent); } valuesByParent.put(id.getVirtualChildId(), value); } else { // Mark the view as to be autofilled with 'value' if (mLastAutofilledData == null) { mLastAutofilledData = new ParcelableMap(itemCount - i); } mLastAutofilledData.put(id, value); view.autofill(value); // Set as autofilled if the values match now, e.g. when the value was updated // synchronously. // If autofill happens async, the view is set to autofilled in // notifyValueChanged. setAutofilledIfValuesIs(view, value); numApplied++; } } if (virtualValues != null) { for (int i = 0; i < virtualValues.size(); i++) { final View parent = virtualValues.keyAt(i); final SparseArray childrenValues = virtualValues.valueAt(i); parent.autofill(childrenValues); numApplied += childrenValues.size(); } } final LogMaker log = new LogMaker(MetricsEvent.AUTOFILL_DATASET_APPLIED) .setPackageName(mContext.getPackageName()) .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_VALUES, itemCount) .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_VIEWS_FILLED, numApplied); mMetricsLogger.write(log); } } /** * Set the tracked views. * * @param trackedIds The views to be tracked. * @param saveOnAllViewsInvisible Finish the session once all tracked views are invisible. * @param saveOnFinish Finish the session once the activity is finished. * @param fillableIds Views that might anchor FillUI. * @param saveTriggerId View that when clicked triggers commit(). */ private void setTrackedViews(int sessionId, @Nullable AutofillId[] trackedIds, boolean saveOnAllViewsInvisible, boolean saveOnFinish, @Nullable AutofillId[] fillableIds, @Nullable AutofillId saveTriggerId) { synchronized (mLock) { if (mEnabled && mSessionId == sessionId) { if (saveOnAllViewsInvisible) { mTrackedViews = new TrackedViews(trackedIds); } else { mTrackedViews = null; } mSaveOnFinish = saveOnFinish; if (fillableIds != null) { if (mFillableIds == null) { mFillableIds = new ArraySet<>(fillableIds.length); } for (AutofillId id : fillableIds) { mFillableIds.add(id); } if (sVerbose) { Log.v(TAG, "setTrackedViews(): fillableIds=" + fillableIds + ", mFillableIds" + mFillableIds); } } if (mSaveTriggerId != null && !mSaveTriggerId.equals(saveTriggerId)) { // Turn off trigger on previous view id. setNotifyOnClickLocked(mSaveTriggerId, false); } if (saveTriggerId != null && !saveTriggerId.equals(mSaveTriggerId)) { // Turn on trigger on new view id. mSaveTriggerId = saveTriggerId; setNotifyOnClickLocked(mSaveTriggerId, true); } } } } private void setNotifyOnClickLocked(@NonNull AutofillId id, boolean notify) { final View view = findView(id); if (view == null) { Log.w(TAG, "setNotifyOnClick(): invalid id: " + id); return; } view.setNotifyAutofillManagerOnClick(notify); } private void setSaveUiState(int sessionId, boolean shown) { if (sDebug) Log.d(TAG, "setSaveUiState(" + sessionId + "): " + shown); synchronized (mLock) { if (mSessionId != NO_SESSION) { // Race condition: app triggered a new session after the previous session was // finished but before server called setSaveUiState() - need to cancel the new // session to avoid further inconsistent behavior. Log.w(TAG, "setSaveUiState(" + sessionId + ", " + shown + ") called on existing session " + mSessionId + "; cancelling it"); cancelSessionLocked(); } if (shown) { mSessionId = sessionId; mState = STATE_SHOWING_SAVE_UI; } else { mSessionId = NO_SESSION; mState = STATE_UNKNOWN; } } } /** * Marks the state of the session as finished. * * @param newState {@link #STATE_FINISHED} (because the autofill service returned a {@code null} * FillResponse), {@link #STATE_UNKNOWN} (because the session was removed), or * {@link #STATE_DISABLED_BY_SERVICE} (because the autofill service disabled further autofill * requests for the activity). */ private void setSessionFinished(int newState) { synchronized (mLock) { if (sVerbose) Log.v(TAG, "setSessionFinished(): from " + mState + " to " + newState); resetSessionLocked(); mState = newState; } } private void requestHideFillUi(AutofillId id) { final View anchor = findView(id); if (sVerbose) Log.v(TAG, "requestHideFillUi(" + id + "): anchor = " + anchor); if (anchor == null) { 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.autofillCallbackRequestHideFillUi() && mCallback != null) { callback = mCallback; } } } if (callback != null) { if (id.isVirtual()) { callback.onAutofillEvent(anchor, id.getVirtualChildId(), AutofillCallback.EVENT_INPUT_HIDDEN); } else { callback.onAutofillEvent(anchor, AutofillCallback.EVENT_INPUT_HIDDEN); } } } private void notifyNoFillUi(int sessionId, AutofillId id, int sessionFinishedState) { if (sVerbose) { Log.v(TAG, "notifyNoFillUi(): sessionId=" + sessionId + ", autofillId=" + id + ", sessionFinishedState=" + sessionFinishedState); } 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.isVirtual()) { callback.onAutofillEvent(anchor, id.getVirtualChildId(), AutofillCallback.EVENT_INPUT_UNAVAILABLE); } else { callback.onAutofillEvent(anchor, AutofillCallback.EVENT_INPUT_UNAVAILABLE); } } if (sessionFinishedState != 0) { // Callback call was "hijacked" to also update the session state. setSessionFinished(sessionFinishedState); } } /** * Get an array of viewIds from a List of {@link AutofillId}. * * @param autofillIds The autofill ids to convert * * @return The array of viewIds. */ // TODO: move to Helper as static method @NonNull private int[] getViewIds(@NonNull AutofillId[] autofillIds) { final int numIds = autofillIds.length; final int[] viewIds = new int[numIds]; for (int i = 0; i < numIds; i++) { viewIds[i] = autofillIds[i].getViewId(); } return viewIds; } // TODO: move to Helper as static method @NonNull private int[] getViewIds(@NonNull List autofillIds) { final int numIds = autofillIds.size(); final int[] viewIds = new int[numIds]; for (int i = 0; i < numIds; i++) { viewIds[i] = autofillIds.get(i).getViewId(); } return viewIds; } /** * 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 null; } return client.findViewByAutofillIdTraversal(autofillId.getViewId()); } /** @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) { e.rethrowFromSystemServer(); } } } /** @hide */ public void dump(String outerPrefix, PrintWriter pw) { 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("client: "); pw.println(getClient()); pw.print(pfx); pw.print("enabled: "); pw.println(mEnabled); 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("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("save trigger id: "); pw.println(mSaveTriggerId); pw.print(pfx); pw.print("save on finish(): "); pw.println(mSaveOnFinish); pw.print(pfx); pw.print("debug: "); pw.print(sDebug); pw.print(" verbose: "); pw.println(sVerbose); } private String getStateAsStringLocked() { switch (mState) { case STATE_UNKNOWN: return "STATE_UNKNOWN"; case STATE_ACTIVE: return "STATE_ACTIVE"; case STATE_FINISHED: return "STATE_FINISHED"; case STATE_SHOWING_SAVE_UI: return "STATE_SHOWING_SAVE_UI"; case STATE_DISABLED_BY_SERVICE: return "STATE_DISABLED_BY_SERVICE"; default: return "INVALID:" + mState; } } private boolean isActiveLocked() { return mState == STATE_ACTIVE; } private boolean isDisabledByServiceLocked() { return mState == STATE_DISABLED_BY_SERVICE; } 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.runOnUiThread(runnable); } /** * View tracking information. Once all tracked views become invisible the session is finished. */ private class TrackedViews { /** Visible tracked views */ @Nullable private ArraySet mVisibleTrackedIds; /** Invisible tracked views */ @Nullable private ArraySet mInvisibleTrackedIds; /** * Check if set is null or value is in set. * * @param set The set or null (== empty set) * @param value The value that might be in the set * * @return {@code true} iff set is not empty and value is in set */ // TODO: move to Helper as static method private boolean isInSet(@Nullable ArraySet set, T value) { return set != null && set.contains(value); } /** * Add a value to a set. If set is null, create a new set. * * @param set The set or null (== empty set) * @param valueToAdd The value to add * * @return The set including the new value. If set was {@code null}, a set containing only * the new value. */ // TODO: move to Helper as static method @NonNull private ArraySet addToSet(@Nullable ArraySet set, T valueToAdd) { if (set == null) { set = new ArraySet<>(1); } set.add(valueToAdd); return set; } /** * Remove a value from a set. * * @param set The set or null (== empty set) * @param valueToRemove The value to remove * * @return The set without the removed value. {@code null} if set was null, or is empty * after removal. */ // TODO: move to Helper as static method @Nullable private ArraySet removeFromSet(@Nullable ArraySet set, T valueToRemove) { if (set == null) { return null; } set.remove(valueToRemove); if (set.isEmpty()) { return null; } return set; } /** * Set the tracked views. * * @param trackedIds The views to be tracked */ TrackedViews(@Nullable AutofillId[] trackedIds) { final AutofillClient client = getClient(); if (trackedIds != null && client != null) { final boolean[] isVisible; if (client.isVisibleForAutofill()) { isVisible = client.getViewVisibility(getViewIds(trackedIds)); } else { // All false isVisible = new boolean[trackedIds.length]; } final int numIds = trackedIds.length; for (int i = 0; i < numIds; i++) { final AutofillId id = trackedIds[i]; if (isVisible[i]) { mVisibleTrackedIds = addToSet(mVisibleTrackedIds, id); } else { mInvisibleTrackedIds = addToSet(mInvisibleTrackedIds, id); } } } if (sVerbose) { Log.v(TAG, "TrackedViews(trackedIds=" + trackedIds + "): " + " mVisibleTrackedIds=" + mVisibleTrackedIds + " mInvisibleTrackedIds=" + mInvisibleTrackedIds); } if (mVisibleTrackedIds == null) { finishSessionLocked(); } } /** * Called when a {@link View view's} visibility changes. * * @param id the id of the view/virtual view whose visibility changed. * @param isVisible visible if the view is visible in the view hierarchy. */ void notifyViewVisibilityChangedLocked(@NonNull AutofillId id, boolean isVisible) { if (sDebug) { Log.d(TAG, "notifyViewVisibilityChanged(): id=" + id + " isVisible=" + isVisible); } if (isClientVisibleForAutofillLocked()) { if (isVisible) { if (isInSet(mInvisibleTrackedIds, id)) { mInvisibleTrackedIds = removeFromSet(mInvisibleTrackedIds, id); mVisibleTrackedIds = addToSet(mVisibleTrackedIds, id); } } else { if (isInSet(mVisibleTrackedIds, id)) { mVisibleTrackedIds = removeFromSet(mVisibleTrackedIds, id); mInvisibleTrackedIds = addToSet(mInvisibleTrackedIds, id); } } } if (mVisibleTrackedIds == null) { if (sVerbose) { Log.v(TAG, "No more visible ids. Invisibile = " + mInvisibleTrackedIds); } finishSessionLocked(); } } /** * Called once the client becomes visible. * * @see AutofillClient#isVisibleForAutofill() */ void onVisibleForAutofillLocked() { // The visibility of the views might have changed while the client was not be visible, // hence update the visibility state for all views. AutofillClient client = getClient(); ArraySet updatedVisibleTrackedIds = null; ArraySet updatedInvisibleTrackedIds = null; if (client != null) { if (mInvisibleTrackedIds != null) { final ArrayList orderedInvisibleIds = new ArrayList<>(mInvisibleTrackedIds); final boolean[] isVisible = client.getViewVisibility( getViewIds(orderedInvisibleIds)); final int numInvisibleTrackedIds = orderedInvisibleIds.size(); for (int i = 0; i < numInvisibleTrackedIds; i++) { final AutofillId id = orderedInvisibleIds.get(i); if (isVisible[i]) { updatedVisibleTrackedIds = addToSet(updatedVisibleTrackedIds, id); if (sDebug) { Log.d(TAG, "onVisibleForAutofill() " + id + " became visible"); } } else { updatedInvisibleTrackedIds = addToSet(updatedInvisibleTrackedIds, id); } } } if (mVisibleTrackedIds != null) { final ArrayList orderedVisibleIds = new ArrayList<>(mVisibleTrackedIds); final boolean[] isVisible = client.getViewVisibility( getViewIds(orderedVisibleIds)); final int numVisibleTrackedIds = orderedVisibleIds.size(); for (int i = 0; i < numVisibleTrackedIds; i++) { final AutofillId id = orderedVisibleIds.get(i); if (isVisible[i]) { updatedVisibleTrackedIds = addToSet(updatedVisibleTrackedIds, id); } else { updatedInvisibleTrackedIds = addToSet(updatedInvisibleTrackedIds, id); if (sDebug) { Log.d(TAG, "onVisibleForAutofill() " + id + " became invisible"); } } } } mInvisibleTrackedIds = updatedInvisibleTrackedIds; mVisibleTrackedIds = updatedVisibleTrackedIds; } if (mVisibleTrackedIds == null) { finishSessionLocked(); } } } /** * Callback for autofill related events. * *

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

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

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

If the view provides its own auto-complete UI but was not displaying it * to avoid flickering, it could shown it upon receiving this event. */ public static final int EVENT_INPUT_UNAVAILABLE = 3; /** * Called after a change in the autofill state associated with a view. * * @param view view associated with the change. * * @param event currently either {@link #EVENT_INPUT_SHOWN} or {@link #EVENT_INPUT_HIDDEN}. */ public void onAutofillEvent(@NonNull View view, @AutofillEventType int event) { } /** * Called after a change in the autofill state associated with a virtual view. * * @param view parent view associated with the change. * @param virtualId id identifying the virtual child inside the parent view. * * @param event currently either {@link #EVENT_INPUT_SHOWN} or {@link #EVENT_INPUT_HIDDEN}. */ public void onAutofillEvent(@NonNull View view, int virtualId, @AutofillEventType int event) { } } private static final class AutofillManagerClient extends IAutoFillManagerClient.Stub { private final WeakReference mAfm; AutofillManagerClient(AutofillManager autofillManager) { mAfm = new WeakReference<>(autofillManager); } @Override public void setState(int flags) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.setState(flags)); } } @Override public void autofill(int sessionId, List ids, List values) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.autofill(sessionId, ids, values)); } } @Override public void authenticate(int sessionId, int authenticationId, IntentSender intent, Intent fillInIntent) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.authenticate(sessionId, authenticationId, intent, fillInIntent)); } } @Override public void requestShowFillUi(int sessionId, AutofillId id, int width, int height, Rect anchorBounds, IAutofillWindowPresenter presenter) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.requestShowFillUi(sessionId, id, width, height, anchorBounds, presenter)); } } @Override public void requestHideFillUi(int sessionId, AutofillId id) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.requestHideFillUi(id)); } } @Override public void notifyNoFillUi(int sessionId, AutofillId id, int sessionFinishedState) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.notifyNoFillUi(sessionId, id, sessionFinishedState)); } } @Override public void startIntentSender(IntentSender intentSender, Intent intent) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> { try { afm.mContext.startIntentSender(intentSender, intent, 0, 0, 0); } catch (IntentSender.SendIntentException e) { Log.e(TAG, "startIntentSender() failed for intent:" + intentSender, e); } }); } } @Override public void setTrackedViews(int sessionId, AutofillId[] ids, boolean saveOnAllViewsInvisible, boolean saveOnFinish, AutofillId[] fillableIds, AutofillId saveTriggerId) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.setTrackedViews(sessionId, ids, saveOnAllViewsInvisible, saveOnFinish, fillableIds, saveTriggerId)); } } @Override public void setSaveUiState(int sessionId, boolean shown) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.setSaveUiState(sessionId, shown)); } } @Override public void setSessionFinished(int newState) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.setSessionFinished(newState)); } } } }