/*
 * 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.accessibilityservice.AccessibilityServiceInfo;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresFeature;
import android.annotation.SystemService;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Rect;
import android.metrics.LogMaker;
import android.os.Build;
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.Slog;
import android.util.SparseArray;
import android.view.Choreographer;
import android.view.KeyEvent;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeProvider;
import android.view.accessibility.AccessibilityWindowInfo;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.os.IResultReceiver;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.Preconditions;

import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
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;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

//TODO: use java.lang.ref.Cleaner once Android supports Java 9
import sun.misc.Cleaner;

/**
 * <p>The {@link AutofillManager} class provides ways for apps and custom views to
 * integrate with the Autofill Framework lifecycle.
 *
 * <p>To learn about using Autofill in your app, read
 * the <a href="/guide/topics/text/autofill">Autofill Framework</a> guides.
 *
 * <h3 id="autofill-lifecycle">Autofill lifecycle</h3>
 *
 * <p>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:
 *
 * <ul>
 *   <li>{@link #notifyViewEntered(View)}
 *   <li>{@link #notifyViewEntered(View, int, Rect)}
 *   <li>{@link #requestAutofill(View)}
 * </ul>
 *
 * <p>Typically, the context is automatically created when the first view of the activity is
 * focused because {@code View.onFocusChanged()} indirectly calls
 * {@link #notifyViewEntered(View)}. App developers can call {@link #requestAutofill(View)} to
 * explicitly create it (for example, a custom view developer could offer a contextual menu action
 * in a text-field view to let users manually request autofill).
 *
 * <p>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.
 *
 * <p>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:
 *
 * <ul>
 *   <li>Datasets used to autofill subsets of views in the activity.
 *   <li>Id of views that the service can save their values for future autofilling.
 * </ul>
 *
 * <p>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)}.
 *
 * <p>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.
 *
 * <p>The context is then finished when one of the following occurs:
 *
 * <ul>
 *   <li>{@link #commit()} is called or all savable views are gone.
 *   <li>{@link #cancel()} is called.
 * </ul>
 *
 * <p>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.
 *
 * <h3 id="additional-notes">Additional notes</h3>
 *
 * <p>It is safe to call <code>AutofillManager</code> methods from any thread.
 */
@SystemService(Context.AUTOFILL_MANAGER_SERVICE)
@RequiresFeature(PackageManager.FEATURE_AUTOFILL)
public final class AutofillManager {

    private static final String TAG = "AutofillManager";

    /**
     * Intent extra: The assist structure which captures the filled screen.
     *
     * <p>
     * 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.
     *
     * <p>
     * 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}.
     *
     * <p>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.
     *
     * <p>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()}.
     *
     * <p>
     * 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";

    /**
     * Internal extra used to pass a binder to the {@link IAugmentedAutofillManagerClient}.
     *
     * @hide
     */
    public static final String EXTRA_AUGMENTED_AUTOFILL_CLIENT =
            "android.view.autofill.extra.AUGMENTED_AUTOFILL_CLIENT";

    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 NO_LOGGING = 0;
    /** @hide */ public static final int FLAG_ADD_CLIENT_ENABLED = 0x1;
    /** @hide */ public static final int FLAG_ADD_CLIENT_DEBUG = 0x2;
    /** @hide */ public static final int FLAG_ADD_CLIENT_VERBOSE = 0x4;
    /** @hide */
    public static final int DEFAULT_LOGGING_LEVEL = Build.IS_DEBUGGABLE
            ? AutofillManager.FLAG_ADD_CLIENT_DEBUG
            : AutofillManager.NO_LOGGING;

    /** @hide */
    public static final int DEFAULT_MAX_PARTITIONS_SIZE = 10;

    /** Which bits in an authentication id are used for the dataset id */
    private static final int AUTHENTICATION_ID_DATASET_ID_MASK = 0xFFFF;
    /** How many bits in an authentication id are used for the dataset id */
    private static final int AUTHENTICATION_ID_DATASET_ID_SHIFT = 16;
    /** @hide The index for an undefined data set */
    public static final int AUTHENTICATION_ID_DATASET_ID_UNDEFINED = 0xFFFF;

    /**
     * Used on {@link #onPendingSaveUi(int, IBinder)} to cancel the pending UI.
     *
     * @hide
     */
    public static final int PENDING_UI_OPERATION_CANCEL = 1;

    /**
     * Used on {@link #onPendingSaveUi(int, IBinder)} to restore the pending UI.
     *
     * @hide
     */
    public static final int PENDING_UI_OPERATION_RESTORE = 2;

    /**
     * Initial state of the autofill context, set when there is no session (i.e., when
     * {@link #mSessionId} is {@link #NO_SESSION}).
     *
     * <p>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.
     *
     * <p>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.
     *
     * <p>In this state, every call is ignored, even {@link #requestAutofill(View)}
     * (and {@link #requestAutofill(View, int, Rect)}).
     *
     * @hide
     */
    public static final int STATE_DISABLED_BY_SERVICE = 4;

    /**
     * Same as {@link #STATE_UNKNOWN}, but used on
     * {@link AutofillManagerClient#setSessionFinished(int)} when the session was finished because
     * the URL bar changed on client mode
     *
     * @hide
     */
    public static final int STATE_UNKNOWN_COMPAT_MODE = 5;

    /**
     * Same as {@link #STATE_UNKNOWN}, but used on
     * {@link AutofillManagerClient#setSessionFinished(int)} when the session was finished because
     * the service failed to fullfil a request.
     *
     * @hide
     */
    public static final int STATE_UNKNOWN_FAILED = 6;

    /**
     * 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 IAugmentedAutofillManagerClient mAugmentedAutofillServiceClient;

    @GuardedBy("mLock")
    private AutofillCallback mCallback;

    private final Context mContext;

    @GuardedBy("mLock")
    private int mSessionId = NO_SESSION;

    @GuardedBy("mLock")
    private int mState = STATE_UNKNOWN;

    @GuardedBy("mLock")
    private boolean mEnabled;

    /** If a view changes to this mapping the autofill operation was successful */
    @GuardedBy("mLock")
    @Nullable private ParcelableMap mLastAutofilledData;

    /** If view tracking is enabled, contains the tracking state */
    @GuardedBy("mLock")
    @Nullable private TrackedViews mTrackedViews;

    /** Views that are only tracked because they are fillable and could be anchoring the UI. */
    @GuardedBy("mLock")
    @Nullable private ArraySet<AutofillId> mFillableIds;

    /** id of last requested autofill ui */
    @Nullable private AutofillId mIdShownFillUi;

    /**
     * Views that were already "entered" - if they're entered again when the session is not active,
     * they're ignored
     * */
    @GuardedBy("mLock")
    @Nullable private ArraySet<AutofillId> mEnteredIds;

    /** If set, session is commited when the field is clicked. */
    @GuardedBy("mLock")
    @Nullable private AutofillId mSaveTriggerId;

    /** set to true when onInvisibleForAutofill is called, used by onAuthenticationResult */
    @GuardedBy("mLock")
    private boolean mOnInvisibleCalled;

    /** If set, session is commited when the activity is finished; otherwise session is canceled. */
    @GuardedBy("mLock")
    private boolean mSaveOnFinish;

    /** If compatibility mode is enabled - this is a bridge to interact with a11y */
    @GuardedBy("mLock")
    private CompatibilityBridge mCompatibilityBridge;

    /** @hide */
    public interface AutofillClient {
        /**
         * Asks the client to start an authentication flow.
         *
         * @param authenticationId A unique id of the authentication operation.
         * @param intent The authentication intent.
         * @param fillInIntent The authentication fill-in intent.
         */
        void autofillClientAuthenticate(int authenticationId, IntentSender intent,
                Intent fillInIntent);

        /**
         * Tells the client this manager has state to be reset.
         */
        void autofillClientResetableStateAvailable();

        /**
         * Request showing the autofill UI.
         *
         * @param anchor The real view the UI needs to anchor to.
         * @param width The width of the fill UI content.
         * @param height The height of the fill UI content.
         * @param virtualBounds The bounds of the virtual decendant of the anchor.
         * @param presenter The presenter that controls the fill UI window.
         * @return Whether the UI was shown.
         */
        boolean autofillClientRequestShowFillUi(@NonNull View anchor, int width, int height,
                @Nullable Rect virtualBounds, IAutofillWindowPresenter presenter);

        /**
         * Dispatch unhandled keyevent from Autofill window
         * @param anchor The real view the UI needs to anchor to.
         * @param keyEvent Unhandled KeyEvent from autofill window.
         */
        void autofillClientDispatchUnhandledKey(@NonNull View anchor, @NonNull KeyEvent keyEvent);

        /**
         * Request hiding the autofill UI.
         *
         * @return Whether the UI was hidden.
         */
        boolean autofillClientRequestHideFillUi();

        /**
         * Gets whether the fill UI is currenlty being shown.
         *
         * @return Whether the fill UI is currently being shown
         */
        boolean autofillClientIsFillUiShowing();

        /**
         * Checks if views are currently attached and visible.
         *
         * @return And array with {@code true} iff the view is attached or visible
         */
        @NonNull boolean[] autofillClientGetViewVisibility(@NonNull AutofillId[] autofillIds);

        /**
         * Checks is the client is currently visible as understood by autofill.
         *
         * @return {@code true} if the client is currently visible
         */
        boolean autofillClientIsVisibleForAutofill();

        /**
         * Client might disable enter/exit event e.g. when activity is paused.
         */
        boolean isDisablingEnterExitEventForAutofill();

        /**
         * Finds views by traversing the hierarchies of the client.
         *
         * @param autofillIds The autofill ids of the views to find
         *
         * @return And array containing the views (empty if no views found).
         */
        @NonNull View[] autofillClientFindViewsByAutofillIdTraversal(
                @NonNull AutofillId[] autofillIds);

        /**
         * Finds a view by traversing the hierarchies of the client.
         *
         * @param autofillId The autofill id of the views to find
         *
         * @return The view, or {@code null} if not found
         */
        @Nullable View autofillClientFindViewByAutofillIdTraversal(@NonNull AutofillId autofillId);

        /**
         * Finds a view by a11y id in a given client window.
         *
         * @param viewId The accessibility id of the views to find
         * @param windowId The accessibility window id where to search
         *
         * @return The view, or {@code null} if not found
         */
        @Nullable View autofillClientFindViewByAccessibilityIdTraversal(int viewId, int windowId);

        /**
         * Runs the specified action on the UI thread.
         */
        void autofillClientRunOnUiThread(Runnable action);

        /**
         * Gets the complete component name of this client.
         */
        ComponentName autofillClientGetComponentName();

        /**
         * Gets the activity token
         */
        @Nullable IBinder autofillClientGetActivityToken();

        /**
          * @return Whether compatibility mode is enabled.
          */
        boolean autofillClientIsCompatibilityModeEnabled();

        /**
         * Gets the next unique autofill ID.
         *
         * <p>Typically used to manage views whose content is recycled - see
         * {@link View#setAutofillId(AutofillId)} for more info.
         *
         * @return An ID that is unique in the activity.
         */
        @Nullable AutofillId autofillClientGetNextAutofillId();
    }

    /**
     * @hide
     */
    public AutofillManager(Context context, IAutoFillManager service) {
        mContext = Preconditions.checkNotNull(context, "context cannot be null");
        mService = service;
    }

    /**
     * @hide
     */
    public void enableCompatibilityMode() {
        synchronized (mLock) {
            // The accessibility manager is a singleton so we may need to plug
            // different bridge based on which activity is currently focused
            // in the current process. Since compat would be rarely used, just
            // create and register a new instance every time.
            if (sDebug) {
                Slog.d(TAG, "creating CompatibilityBridge for " + mContext);
            }
            mCompatibilityBridge = new CompatibilityBridge();
        }
    }

    /**
     * Restore state after activity lifecycle
     *
     * @param savedInstanceState The state to be restored
     *
     * {@hide}
     */
    public void onCreate(Bundle savedInstanceState) {
        if (!hasAutofillFeature()) {
            return;
        }
        synchronized (mLock) {
            mLastAutofilledData = savedInstanceState.getParcelable(LAST_AUTOFILLED_DATA_TAG);

            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) {
                    final SyncResultReceiver receiver = new SyncResultReceiver();
                    try {
                        mService.restoreSession(mSessionId, client.autofillClientGetActivityToken(),
                                mServiceClient.asBinder(), receiver);
                        final boolean sessionWasRestored = receiver.getIntResult() == 1;

                        if (!sessionWasRestored) {
                            Log.w(TAG, "Session " + mSessionId + " could not be restored");
                            mSessionId = NO_SESSION;
                            mState = STATE_UNKNOWN;
                        } else {
                            if (sDebug) {
                                Log.d(TAG, "session " + mSessionId + " was restored");
                            }

                            client.autofillClientResetableStateAvailable();
                        }
                    } catch (RemoteException e) {
                        Log.e(TAG, "Could not figure out if there was an autofill session", e);
                    }
                }
            }
        }
    }

    /**
     * Called once the client becomes visible.
     *
     * @see AutofillClient#autofillClientIsVisibleForAutofill()
     *
     * {@hide}
     */
    public void onVisibleForAutofill() {
        // This gets called when the client just got visible at which point the visibility
        // of the tracked views may not have been computed (due to a pending layout, etc).
        // While generally we have no way to know when the UI has settled. We will evaluate
        // the tracked views state at the end of next frame to guarantee that everything
        // that may need to be laid out is laid out.
        Choreographer.getInstance().postCallback(Choreographer.CALLBACK_COMMIT, () -> {
            synchronized (mLock) {
                if (mEnabled && isActiveLocked() && mTrackedViews != null) {
                    mTrackedViews.onVisibleForAutofillChangedLocked();
                }
            }
        }, null);
    }

    /**
     * Called once the client becomes invisible.
     *
     * @see AutofillClient#autofillClientIsVisibleForAutofill()
     *
     * {@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);
            }
        }
    }

    /**
     * @hide
     */
    @GuardedBy("mLock")
    public boolean isCompatibilityModeEnabledLocked() {
        return mCompatibilityBridge != null;
    }

    /**
     * Checks whether autofill is enabled for the current user.
     *
     * <p>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 {
            final SyncResultReceiver receiver = new SyncResultReceiver();
            mService.getFillEventHistory(receiver);
            return receiver.getObjectResult(SyncResultReceiver.TYPE_PARCELABLE);
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
            return null;
        }
    }

    /**
     * Explicitly requests a new autofill context.
     *
     * <p>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.
     *
     * <p>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.
     *
     * <p>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:
     *
     * <pre class="prettyprint">
     *   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]);
     * </pre>
     *
     * @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);
    }

    @GuardedBy("mLock")
    private boolean shouldIgnoreViewEnteredLocked(@NonNull AutofillId id, int flags) {
        if (isDisabledByServiceLocked()) {
            if (sVerbose) {
                Log.v(TAG, "ignoring notifyViewEntered(flags=" + flags + ", view=" + id
                        + ") on state " + getStateAsStringLocked() + " because disabled by svc");
            }
            return true;
        }
        if (isFinishedLocked()) {
            // Session already finished: ignore if automatic request and view already entered
            if ((flags & FLAG_MANUAL_REQUEST) == 0 && mEnteredIds != null
                    && mEnteredIds.contains(id)) {
                if (sVerbose) {
                    Log.v(TAG, "ignoring notifyViewEntered(flags=" + flags + ", view=" + id
                            + ") on state " + getStateAsStringLocked()
                            + " because view was already entered: " + mEnteredIds);
                }
                return true;
            }
        }
        return false;
    }

    private boolean isClientVisibleForAutofillLocked() {
        final AutofillClient client = getClient();
        return client != null && client.autofillClientIsVisibleForAutofill();
    }

    private boolean isClientDisablingEnterExitEvent() {
        final AutofillClient client = getClient();
        return client != null && client.isDisablingEnterExitEventForAutofill();
    }

    private void notifyViewEntered(@NonNull View view, int flags) {
        if (!hasAutofillFeature()) {
            return;
        }
        AutofillCallback callback;
        synchronized (mLock) {
            callback = notifyViewEnteredLocked(view, flags);
        }

        if (callback != null) {
            mCallback.onAutofillEvent(view, AutofillCallback.EVENT_INPUT_UNAVAILABLE);
        }
    }

    /** Returns AutofillCallback if need fire EVENT_INPUT_UNAVAILABLE */
    @GuardedBy("mLock")
    private AutofillCallback notifyViewEnteredLocked(@NonNull View view, int flags) {
        final AutofillId id = view.getAutofillId();
        if (shouldIgnoreViewEnteredLocked(id, flags)) return null;

        AutofillCallback callback = null;

        ensureServiceClientAddedIfNeededLocked();

        if (!mEnabled) {
            if (sVerbose) {
                Log.v(TAG, "ignoring notifyViewEntered(" + id + "): disabled");
            }
            if (mCallback != null) {
                callback = mCallback;
            }
        } else {
            // don't notify entered when Activity is already in background
            if (!isClientDisablingEnterExitEvent()) {
                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);
                }
                addEnteredIdLocked(id);
            }
        }
        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);
        }
    }

    @GuardedBy("mLock")
    void notifyViewExitedLocked(@NonNull View view) {
        ensureServiceClientAddedIfNeededLocked();

        if (mEnabled && isActiveLocked()) {
            // dont notify exited when Activity is already in background
            if (!isClientDisablingEnterExitEvent()) {
                final AutofillId id = view.getAutofillId();

                // Update focus on existing session.
                updateSessionLocked(id, null, null, ACTION_VIEW_EXITED, 0);
            }
        }
    }

    /**
     * Called when a {@link View view's} visibility changed.
     *
     * @param view {@link View} that was exited.
     * @param isVisible visible if the view is visible in the view hierarchy.
     */
    public void notifyViewVisibilityChanged(@NonNull View view, boolean isVisible) {
        notifyViewVisibilityChangedInternal(view, 0, isVisible, false);
    }

    /**
     * Called when a virtual view's visibility changed.
     *
     * @param view {@link View} that was exited.
     * @param virtualId id identifying the virtual child inside the parent view.
     * @param isVisible visible if the view is visible in the view hierarchy.
     */
    public void notifyViewVisibilityChanged(@NonNull View view, int virtualId, boolean isVisible) {
        notifyViewVisibilityChangedInternal(view, virtualId, isVisible, true);
    }

    /**
     * Called when a view/virtual view's visibility changed.
     *
     * @param view {@link View} that was exited.
     * @param virtualId id identifying the virtual child inside the parent view.
     * @param isVisible visible if the view is visible in the view hierarchy.
     * @param virtual Whether the view is virtual.
     */
    private void notifyViewVisibilityChangedInternal(@NonNull View view, int virtualId,
            boolean isVisible, boolean virtual) {
        synchronized (mLock) {
            if (mEnabled && isActiveLocked()) {
                final AutofillId id = virtual ? getAutofillId(view, virtualId)
                        : view.getAutofillId();
                if (sVerbose) Log.v(TAG, "visibility changed for " + id + ": " + isVisible);
                if (!isVisible && mFillableIds != null) {
                    if (mFillableIds.contains(id)) {
                        if (sDebug) Log.d(TAG, "Hidding UI when view " + id + " became invisible");
                        requestHideFillUi(id, view);
                    }
                }
                if (mTrackedViews != null) {
                    mTrackedViews.notifyViewVisibilityChangedLocked(id, isVisible);
                } else if (sVerbose) {
                    Log.v(TAG, "Ignoring visibility change on " + id + ": no tracked views");
                }
            }
        }
    }

    /**
     * Called when a virtual view that supports autofill is entered.
     *
     * <p>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:
     *
     * <pre class="prettyprint">
     *   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]);
     * </pre>
     *
     * @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 */
    @GuardedBy("mLock")
    private AutofillCallback notifyViewEnteredLocked(View view, int virtualId, Rect bounds,
                                                     int flags) {
        final AutofillId id = getAutofillId(view, virtualId);
        AutofillCallback callback = null;
        if (shouldIgnoreViewEnteredLocked(id, flags)) return callback;

        ensureServiceClientAddedIfNeededLocked();

        if (!mEnabled) {
            if (sVerbose) {
                Log.v(TAG, "ignoring notifyViewEntered(" + id + "): disabled");
            }
            if (mCallback != null) {
                callback = mCallback;
            }
        } else {
            // don't notify entered when Activity is already in background
            if (!isClientDisablingEnterExitEvent()) {
                if (!isActiveLocked()) {
                    // Starts new session.
                    startSessionLocked(id, bounds, null, flags);
                } else {
                    // Update focus on existing session.
                    updateSessionLocked(id, bounds, null, ACTION_VIEW_ENTERED, flags);
                }
                addEnteredIdLocked(id);
            }
        }
        return callback;
    }

    @GuardedBy("mLock")
    private void addEnteredIdLocked(@NonNull AutofillId id) {
        if (mEnteredIds == null) {
            mEnteredIds = new ArraySet<>(1);
        }
        mEnteredIds.add(id);
    }

    /**
     * Called when a virtual view that supports autofill is exited.
     *
     * @param view the virtual view parent.
     * @param virtualId id identifying the virtual child inside the parent view.
     */
    public void notifyViewExited(@NonNull View view, int virtualId) {
        if (sVerbose) Log.v(TAG, "notifyViewExited(" + view.getAutofillId() + ", " + virtualId);
        if (!hasAutofillFeature()) {
            return;
        }
        synchronized (mLock) {
            notifyViewExitedLocked(view, virtualId);
        }
    }

    @GuardedBy("mLock")
    private void notifyViewExitedLocked(@NonNull View view, int virtualId) {
        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 = view.getAutofillId();
                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) {
                    Log.v(TAG, "notifyValueChanged(" + view.getAutofillId()
                            + "): ignoring on state " + getStateAsStringLocked());
                }
                return;
            }

            if (id == null) {
                id = view.getAutofillId();
            }

            if (!valueWasRead) {
                value = view.getAutofillValue();
            }

            updateSessionLocked(id, null, value, ACTION_VALUE_CHANGED, 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()) {
                if (sVerbose) {
                    Log.v(TAG, "notifyValueChanged(" + view.getAutofillId() + ":" + virtualId
                            + "): ignoring on state " + getStateAsStringLocked());
                }
                return;
            }

            final AutofillId id = getAutofillId(view, virtualId);
            updateSessionLocked(id, null, value, ACTION_VALUE_CHANGED, 0);
        }
    }

    /**
     * Called to indicate a {@link View} is clicked.
     *
     * @param view view that has been clicked.
     */
    public void notifyViewClicked(@NonNull View view) {
        notifyViewClicked(view.getAutofillId());
    }

    /**
     * Called to indicate a virtual view has been clicked.
     *
     * @param view the virtual view parent.
     * @param virtualId id identifying the virtual child inside the parent view.
     */
    public void notifyViewClicked(@NonNull View view, int virtualId) {
        notifyViewClicked(getAutofillId(view, virtualId));
    }

    private void notifyViewClicked(AutofillId id) {
        if (!hasAutofillFeature()) {
            return;
        }
        if (sVerbose) Log.v(TAG, "notifyViewClicked(): id=" + id + ", trigger=" + mSaveTriggerId);

        synchronized (mLock) {
            if (!mEnabled || !isActiveLocked()) {
                return;
            }
            if (mSaveTriggerId != null && mSaveTriggerId.equals(id)) {
                if (sDebug) Log.d(TAG, "triggering commit by click of " + id);
                commitLocked();
                mMetricsLogger.write(newLog(MetricsEvent.AUTOFILL_SAVE_EXPLICITLY_TRIGGERED));
            }
        }
    }

    /**
     * Called by {@link android.app.Activity} to commit or cancel the session on finish.
     *
     * @hide
     */
    public void onActivityFinishing() {
        if (!hasAutofillFeature()) {
            return;
        }
        synchronized (mLock) {
            if (mSaveOnFinish) {
                if (sDebug) Log.d(TAG, "onActivityFinishing(): calling commitLocked()");
                commitLocked();
            } else {
                if (sDebug) Log.d(TAG, "onActivityFinishing(): calling cancelLocked()");
                cancelLocked();
            }
        }
    }

    /**
     * Called to indicate the current autofill context should be commited.
     *
     * <p>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.
     *
     * <p><b>Note:</b> This method does not need to be called on regular application lifecycle
     * methods such as {@link android.app.Activity#finish()}.
     */
    public void commit() {
        if (!hasAutofillFeature()) {
            return;
        }
        if (sVerbose) Log.v(TAG, "commit() called by app");
        synchronized (mLock) {
            commitLocked();
        }
    }

    @GuardedBy("mLock")
    private void commitLocked() {
        if (!mEnabled && !isActiveLocked()) {
            return;
        }
        finishSessionLocked();
    }

    /**
     * Called to indicate the current autofill context should be cancelled.
     *
     * <p>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.
     *
     * <p><b>Note:</b> 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();
        }
    }

    @GuardedBy("mLock")
    private void cancelLocked() {
        if (!mEnabled && !isActiveLocked()) {
            return;
        }
        cancelSessionLocked();
    }

    /** @hide */
    public void disableOwnedAutofillServices() {
        disableAutofillServices();
    }

    /**
     * If the app calling this API has enabled autofill services they
     * will be disabled.
     */
    public void disableAutofillServices() {
        if (!hasAutofillFeature()) {
            return;
        }
        try {
            mService.disableOwnedAutofillServices(mContext.getUserId());
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Returns {@code true} if the calling application provides a {@link AutofillService} that is
     * enabled for the current user, or {@code false} otherwise.
     */
    public boolean hasEnabledAutofillServices() {
        if (mService == null) return false;

        final SyncResultReceiver receiver = new SyncResultReceiver();
        try {
            mService.isServiceEnabled(mContext.getUserId(), mContext.getPackageName(), receiver);
            return receiver.getIntResult() == 1;
        } 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;

        final SyncResultReceiver receiver = new SyncResultReceiver();
        try {
            mService.getAutofillServiceComponentName(receiver);
            return receiver.getObjectResult(SyncResultReceiver.TYPE_PARCELABLE);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Gets the id of the {@link UserData} used for
     * <a href="AutofillService.html#FieldClassification">field classification</a>.
     *
     * <p>This method is useful when the service must check the status of the {@link UserData} in
     * the device without fetching the whole object.
     *
     * <p><b>Note:</b> This method should only be called by an app providing an autofill service,
     * and it's ignored if the caller currently doesn't have an enabled autofill service for
     * the user.
     *
     * @return id of the {@link UserData} previously set by {@link #setUserData(UserData)}
     * or {@code null} if it was reset or if the caller currently does not have an enabled autofill
     * service for the user.
     */
    @Nullable public String getUserDataId() {
        try {
            final SyncResultReceiver receiver = new SyncResultReceiver();
            mService.getUserDataId(receiver);
            return receiver.getObjectResult(SyncResultReceiver.TYPE_STRING);
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
            return null;
        }
    }

    /**
     * Gets the user data used for
     * <a href="AutofillService.html#FieldClassification">field classification</a>.
     *
     * <p><b>Note:</b> This method should only be called by an app providing an autofill service,
     * and it's ignored if the caller currently doesn't have an enabled autofill service for
     * the user.
     *
     * @return value previously set by {@link #setUserData(UserData)} or {@code null} if it was
     * reset or if the caller currently does not have an enabled autofill service for the user.
     */
    @Nullable public UserData getUserData() {
        try {
            final SyncResultReceiver receiver = new SyncResultReceiver();
            mService.getUserData(receiver);
            return receiver.getObjectResult(SyncResultReceiver.TYPE_PARCELABLE);
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
            return null;
        }
    }

    /**
     * Sets the {@link UserData} used for
     * <a href="AutofillService.html#FieldClassification">field classification</a>
     *
     * <p><b>Note:</b> 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 <a href="AutofillService.html#FieldClassification">field classification</a> is
     * enabled.
     *
     * <p>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).
     *
     * <p><b>Note:</b> This method should only be called by an app providing an autofill service,
     * and it's ignored if the caller currently doesn't have an enabled autofill service for
     * the user.
     */
    public boolean isFieldClassificationEnabled() {
        final SyncResultReceiver receiver = new SyncResultReceiver();
        try {
            mService.isFieldClassificationEnabled(receiver);
            return receiver.getIntResult() == 1;
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
            return false;
        }
    }

    /**
     * Gets the name of the default algorithm used for
     * <a href="AutofillService.html#FieldClassification">field classification</a>.
     *
     * <p>The default algorithm is used when the algorithm on {@link UserData} is invalid or not
     * set.
     *
     * <p><b>Note:</b> This method should only be called by an app providing an autofill service,
     * and it's ignored if the caller currently doesn't have an enabled autofill service for
     * the user.
     */
    @Nullable
    public String getDefaultFieldClassificationAlgorithm() {
        final SyncResultReceiver receiver = new SyncResultReceiver();
        try {
            mService.getDefaultFieldClassificationAlgorithm(receiver);
            return receiver.getObjectResult(SyncResultReceiver.TYPE_STRING);
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
            return null;
        }
    }

    /**
     * Gets the name of all algorithms currently available for
     * <a href="AutofillService.html#FieldClassification">field classification</a>.
     *
     * <p><b>Note:</b> 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<String> getAvailableFieldClassificationAlgorithms() {
        final SyncResultReceiver receiver = new SyncResultReceiver();
        try {
            mService.getAvailableFieldClassificationAlgorithms(receiver);
            final String[] algorithms = receiver
                .getObjectResult(SyncResultReceiver.TYPE_STRING_ARRAY);
            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.
     *
     * <p>Autofill is typically supported, but it could be unsupported in cases like:
     * <ol>
     *     <li>Low-end devices.
     *     <li>Device policy rules that forbid its usage.
     * </ol>
     */
    public boolean isAutofillSupported() {
        if (mService == null) return false;

        final SyncResultReceiver receiver = new SyncResultReceiver();
        try {
            mService.isServiceSupported(mContext.getUserId(), receiver);
            return receiver.getIntResult() == 1;
        } 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;
    }

    /**
     * Check if autofill ui is showing, must be called on UI thread.
     * @hide
     */
    public boolean isAutofillUiShowing() {
        final AutofillClient client = mContext.getAutofillClient();
        return client != null && client.autofillClientIsFillUiShowing();
    }

    /** @hide */
    public void onAuthenticationResult(int authenticationId, Intent data, View focusView) {
        if (!hasAutofillFeature()) {
            return;
        }
        // TODO: the result code is being ignored, so this method is not reliably
        // handling the cases where it's not RESULT_OK: it works fine if the service does not
        // set the EXTRA_AUTHENTICATION_RESULT extra, but it could cause weird results if the
        // service set the extra and returned RESULT_CANCELED...

        if (sDebug) {
            Log.d(TAG, "onAuthenticationResult(): id= " + authenticationId + ", data=" + data);
        }

        synchronized (mLock) {
            if (!isActiveLocked()) {
                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);
            }
        }
    }

    /**
     * Gets the next unique autofill ID for the activity context.
     *
     * <p>Typically used to manage views whose content is recycled - see
     * {@link View#setAutofillId(AutofillId)} for more info.
     *
     * @return An ID that is unique in the activity, or {@code null} if autofill is not supported in
     * the {@link Context} associated with this {@link AutofillManager}.
     */
    @Nullable
    public AutofillId getNextAutofillId() {
        final AutofillClient client = getClient();
        if (client == null) return null;

        final AutofillId id = client.autofillClientGetNextAutofillId();

        if (id == null && sDebug) {
            Log.d(TAG, "getNextAutofillId(): client " + client + " returned null");
        }

        return id;
    }

    private static AutofillId getAutofillId(View parent, int virtualId) {
        return new AutofillId(parent.getAutofillViewId(), virtualId);
    }

    @GuardedBy("mLock")
    private void startSessionLocked(@NonNull AutofillId id, @NonNull Rect bounds,
            @NonNull AutofillValue value, int flags) {
        if (sVerbose) {
            Log.v(TAG, "startSessionLocked(): id=" + id + ", bounds=" + bounds + ", value=" + value
                    + ", flags=" + flags + ", state=" + getStateAsStringLocked()
                    + ", compatMode=" + isCompatibilityModeEnabledLocked()
                    + ", enteredIds=" + mEnteredIds);
        }
        if (mState != STATE_UNKNOWN && !isFinishedLocked() && (flags & FLAG_MANUAL_REQUEST) == 0) {
            if (sVerbose) {
                Log.v(TAG, "not automatically starting session for " + id
                        + " on state " + getStateAsStringLocked() + " and flags " + flags);
            }
            return;
        }
        try {
            final AutofillClient client = getClient();
            if (client == null) return; // NOTE: getClient() already logged it..

            final SyncResultReceiver receiver = new SyncResultReceiver();
            mService.startSession(client.autofillClientGetActivityToken(),
                    mServiceClient.asBinder(), id, bounds, value, mContext.getUserId(),
                    mCallback != null, flags, client.autofillClientGetComponentName(),
                    isCompatibilityModeEnabledLocked(), receiver);
            mSessionId = receiver.getIntResult();
            if (mSessionId != NO_SESSION) {
                mState = STATE_ACTIVE;
            }
            client.autofillClientResetableStateAvailable();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @GuardedBy("mLock")
    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(/* resetEnteredIds= */ true);
    }

    @GuardedBy("mLock")
    private void cancelSessionLocked() {
        if (sVerbose) Log.v(TAG, "cancelSessionLocked(): " + getStateAsStringLocked());

        if (!isActiveLocked()) return;

        try {
            mService.cancelSession(mSessionId, mContext.getUserId());
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }

        resetSessionLocked(/* resetEnteredIds= */ true);
    }

    @GuardedBy("mLock")
    private void resetSessionLocked(boolean resetEnteredIds) {
        mSessionId = NO_SESSION;
        mState = STATE_UNKNOWN;
        mTrackedViews = null;
        mFillableIds = null;
        mSaveTriggerId = null;
        mIdShownFillUi = null;
        if (resetEnteredIds) {
            mEnteredIds = null;
        }
    }

    @GuardedBy("mLock")
    private void updateSessionLocked(AutofillId id, Rect bounds, AutofillValue value, int action,
            int flags) {
        if (sVerbose) {
            Log.v(TAG, "updateSessionLocked(): id=" + id + ", bounds=" + bounds
                    + ", value=" + value + ", action=" + action + ", flags=" + flags);
        }
        try {
            mService.updateSession(mSessionId, id, bounds, value, action, flags,
                    mContext.getUserId());
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @GuardedBy("mLock")
    private void ensureServiceClientAddedIfNeededLocked() {
        if (getClient() == null) {
            return;
        }

        if (mServiceClient == null) {
            mServiceClient = new AutofillManagerClient(this);
            try {
                final int userId = mContext.getUserId();
                final SyncResultReceiver receiver = new SyncResultReceiver();
                mService.addClient(mServiceClient, userId, receiver);
                final int flags = receiver.getIntResult();
                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, () -> {
                    // TODO(b/111330312): call service to also remove reference to
                    // mAugmentedAutofillServiceClient
                    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.autofillClientRequestShowFillUi(anchor, width, height,
                            anchorBounds, presenter)) {
                        callback = mCallback;
                        mIdShownFillUi = id;
                    }
                }
            }
        }

        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.autofillClientAuthenticate(authenticationId, intent, fillInIntent);
                }
            }
        }
    }

    private void dispatchUnhandledKey(int sessionId, AutofillId id, KeyEvent keyEvent) {
        final View anchor = findView(id);
        if (anchor == null) {
            return;
        }

        synchronized (mLock) {
            if (mSessionId == sessionId) {
                AutofillClient client = getClient();

                if (client != null) {
                    client.autofillClientDispatchUnhandledKey(anchor, keyEvent);
                }
            }
        }
    }

    /** @hide */
    public static final int SET_STATE_FLAG_ENABLED = 0x01;
    /** @hide */
    public static final int SET_STATE_FLAG_RESET_SESSION = 0x02;
    /** @hide */
    public static final int SET_STATE_FLAG_RESET_CLIENT = 0x04;
    /** @hide */
    public static final int SET_STATE_FLAG_DEBUG = 0x08;
    /** @hide */
    public static final int SET_STATE_FLAG_VERBOSE = 0x10;

    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(/* resetEnteredIds= */ true);
            }
            if ((flags & SET_STATE_FLAG_RESET_CLIENT) != 0) {
                // Reset connection to system
                mServiceClient = null;
                mAugmentedAutofillServiceClient = null;
                if (mServiceClientCleaner != null) {
                    mServiceClientCleaner.clean();
                    mServiceClientCleaner = null;
                }
            }
        }
        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(view.getAutofillId(), targetValue);
            }
            view.setAutofilled(true);
        }
    }

    private void autofill(int sessionId, List<AutofillId> ids, List<AutofillValue> values) {
        synchronized (mLock) {
            if (sessionId != mSessionId) {
                return;
            }

            final AutofillClient client = getClient();
            if (client == null) {
                return;
            }

            final int itemCount = ids.size();
            int numApplied = 0;
            ArrayMap<View, SparseArray<AutofillValue>> virtualValues = null;
            final View[] views = client.autofillClientFindViewsByAutofillIdTraversal(
                    Helper.toArray(ids));

            ArrayList<AutofillId> failedIds = null;

            for (int i = 0; i < itemCount; i++) {
                final AutofillId id = ids.get(i);
                final AutofillValue value = values.get(i);
                final View view = views[i];
                if (view == null) {
                    // Most likely view has been removed after the initial request was sent to the
                    // the service; this is fine, but we need to update the view status in the
                    // server side so it can be triggered again.
                    Log.d(TAG, "autofill(): no View with id " + id);
                    if (failedIds == null) {
                        failedIds = new ArrayList<>();
                    }
                    failedIds.add(id);
                    continue;
                }
                if (id.isVirtual()) {
                    if (virtualValues == null) {
                        // Most likely there will be just one view with virtual children.
                        virtualValues = new ArrayMap<>(1);
                    }
                    SparseArray<AutofillValue> 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 (failedIds != null) {
                if (sVerbose) {
                    Log.v(TAG, "autofill(): total failed views: " + failedIds);
                }
                try {
                    mService.setAutofillFailure(mSessionId, failedIds, mContext.getUserId());
                } catch (RemoteException e) {
                    // In theory, we could ignore this error since it's not a big deal, but
                    // in reality, we rather crash the app anyways, as the failure could be
                    // a consequence of something going wrong on the server side...
                    e.rethrowFromSystemServer();
                }
            }

            if (virtualValues != null) {
                for (int i = 0; i < virtualValues.size(); i++) {
                    final View parent = virtualValues.keyAt(i);
                    final SparseArray<AutofillValue> childrenValues = virtualValues.valueAt(i);
                    parent.autofill(childrenValues);
                    numApplied += childrenValues.size();
                    // TODO: we should provide a callback so the parent can call failures; something
                    // like notifyAutofillFailed(View view, int[] childrenIds);
                }
            }

            mMetricsLogger.write(newLog(MetricsEvent.AUTOFILL_DATASET_APPLIED)
                    .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_VALUES, itemCount)
                    .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_VIEWS_FILLED, numApplied));
        }
    }

    private LogMaker newLog(int category) {
        final LogMaker log = new LogMaker(category)
                .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SESSION_ID, mSessionId);

        if (isCompatibilityModeEnabledLocked()) {
            log.addTaggedData(MetricsEvent.FIELD_AUTOFILL_COMPAT_MODE, 1);
        }
        final AutofillClient client = getClient();
        if (client == null) {
            // Client should never be null here, but it doesn't hurt to check...
            log.setPackageName(mContext.getPackageName());
        } else {
            log.setComponentName(client.autofillClientGetComponentName());
        }
        return log;
    }

    /**
     *  Set the tracked views.
     *
     * @param trackedIds The views to be tracked.
     * @param saveOnAllViewsInvisible Finish the session once all tracked views are invisible.
     * @param saveOnFinish Finish the session once the activity is finished.
     * @param fillableIds Views that might anchor FillUI.
     * @param saveTriggerId View that when clicked triggers commit().
     */
    private void setTrackedViews(int sessionId, @Nullable AutofillId[] trackedIds,
            boolean saveOnAllViewsInvisible, boolean saveOnFinish,
            @Nullable AutofillId[] fillableIds, @Nullable AutofillId saveTriggerId) {
        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=" + Arrays.toString(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),
     *  {@link #STATE_UNKNOWN_COMPAT_MODE} (beucase the session was finished when the URL bar
     *  changed on compat mode), {@link #STATE_UNKNOWN_FAILED} (because the session was finished
     *  when the service failed to fullfil the request, or {@link #STATE_DISABLED_BY_SERVICE}
     *  (because the autofill service 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 " + getStateAsStringLocked() + " to "
                        + getStateAsString(newState));
            }
            if (newState == STATE_UNKNOWN_COMPAT_MODE || newState == STATE_UNKNOWN_FAILED) {
                resetSessionLocked(/* resetEnteredIds= */ true);
                mState = STATE_UNKNOWN;
            } else {
                resetSessionLocked(/* resetEnteredIds= */ false);
                mState = newState;
            }
        }
    }

    /**
     * Gets a {@link AugmentedAutofillManagerClient} for this {@link AutofillManagerClient}.
     *
     * <p>These are 2 distinct objects because we need to restrict what the Augmented Autofill
     * service can do (which is defined by {@code IAugmentedAutofillManagerClient.aidl}).
     */
    private void getAugmentedAutofillClient(@NonNull IResultReceiver result) {
        synchronized (mLock) {
            if (mAugmentedAutofillServiceClient == null) {
                mAugmentedAutofillServiceClient = new AugmentedAutofillManagerClient(this);
            }
            final Bundle resultData = new Bundle();
            resultData.putBinder(EXTRA_AUGMENTED_AUTOFILL_CLIENT,
                    mAugmentedAutofillServiceClient.asBinder());

            try {
                result.send(0, resultData);
            } catch (RemoteException e) {
                Log.w(TAG, "Could not send AugmentedAutofillClient back: " + e);
            }
        }
    }

    /** @hide */
    public void requestHideFillUi() {
        requestHideFillUi(mIdShownFillUi, true);
    }

    private void requestHideFillUi(AutofillId id, boolean force) {
        final View anchor = id == null ? null : findView(id);
        if (sVerbose) Log.v(TAG, "requestHideFillUi(" + id + "): anchor = " + anchor);
        if (anchor == null) {
            if (force) {
                // When user taps outside autofill window, force to close fill ui even id does
                // not match.
                AutofillClient client = getClient();
                if (client != null) {
                    client.autofillClientRequestHideFillUi();
                }
            }
            return;
        }
        requestHideFillUi(id, anchor);
    }

    private void requestHideFillUi(AutofillId id, View anchor) {

        AutofillCallback callback = null;
        synchronized (mLock) {
            // We do not check the session id for two reasons:
            // 1. If local and remote session id are off sync the UI would be stuck shown
            // 2. There is a race between the user state being destroyed due the fill
            //    service being uninstalled and the UI being dismissed.
            AutofillClient client = getClient();
            if (client != null) {
                if (client.autofillClientRequestHideFillUi()) {
                    mIdShownFillUi = null;
                    callback = mCallback;
                }
            }
        }

        if (callback != null) {
            if (id.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);
        }
    }

    /**
     * Find a single view by its id.
     *
     * @param autofillId The autofill id of the view
     *
     * @return The view or {@code null} if view was not found
     */
    private View findView(@NonNull AutofillId autofillId) {
        final AutofillClient client = getClient();
        if (client != null) {
            return client.autofillClientFindViewByAutofillIdTraversal(autofillId);
        }
        return null;
    }

    /** @hide */
    public boolean hasAutofillFeature() {
        return mService != null;
    }

    /** @hide */
    public void onPendingSaveUi(int operation, IBinder token) {
        if (sVerbose) Log.v(TAG, "onPendingSaveUi(" + operation + "): " + token);

        synchronized (mLock) {
            try {
                mService.onPendingSaveUi(operation, token);
            } catch (RemoteException e) {
                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);
        final AutofillClient client = getClient();
        if (client != null) {
            pw.print(pfx); pw.print("client: "); pw.print(client);
            pw.print(" ("); pw.print(client.autofillClientGetActivityToken()); pw.println(')');
        }
        pw.print(pfx); pw.print("enabled: "); pw.println(mEnabled);
        pw.print(pfx); pw.print("hasService: "); pw.println(mService != null);
        pw.print(pfx); pw.print("hasCallback: "); pw.println(mCallback != null);
        pw.print(pfx); pw.print("onInvisibleCalled "); pw.println(mOnInvisibleCalled);
        pw.print(pfx); pw.print("last autofilled data: "); pw.println(mLastAutofilledData);
        pw.print(pfx); pw.print("id of last fill UI shown: "); pw.println(mIdShownFillUi);
        pw.print(pfx); pw.print("tracked views: ");
        if (mTrackedViews == null) {
            pw.println("null");
        } else {
            final String pfx2 = pfx + "  ";
            pw.println();
            pw.print(pfx2); pw.print("visible:"); pw.println(mTrackedViews.mVisibleTrackedIds);
            pw.print(pfx2); pw.print("invisible:"); pw.println(mTrackedViews.mInvisibleTrackedIds);
        }
        pw.print(pfx); pw.print("fillable ids: "); pw.println(mFillableIds);
        pw.print(pfx); pw.print("entered ids: "); pw.println(mEnteredIds);
        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("compat mode enabled: ");
        synchronized (mLock) {
            if (mCompatibilityBridge != null) {
                final String pfx2 = pfx + "  ";
                pw.println("true");
                pw.print(pfx2); pw.print("windowId: ");
                pw.println(mCompatibilityBridge.mFocusedWindowId);
                pw.print(pfx2); pw.print("nodeId: ");
                pw.println(mCompatibilityBridge.mFocusedNodeId);
                pw.print(pfx2); pw.print("virtualId: ");
                pw.println(AccessibilityNodeInfo
                        .getVirtualDescendantId(mCompatibilityBridge.mFocusedNodeId));
                pw.print(pfx2); pw.print("focusedBounds: ");
                pw.println(mCompatibilityBridge.mFocusedBounds);
            } else {
                pw.println("false");
            }
        }
        pw.print(pfx); pw.print("debug: "); pw.print(sDebug);
        pw.print(" verbose: "); pw.println(sVerbose);
    }

    @GuardedBy("mLock")
    private String getStateAsStringLocked() {
        return getStateAsString(mState);
    }

    @NonNull
    private static String getStateAsString(int state) {
        switch (state) {
            case STATE_UNKNOWN:
                return "UNKNOWN";
            case STATE_ACTIVE:
                return "ACTIVE";
            case STATE_FINISHED:
                return "FINISHED";
            case STATE_SHOWING_SAVE_UI:
                return "SHOWING_SAVE_UI";
            case STATE_DISABLED_BY_SERVICE:
                return "DISABLED_BY_SERVICE";
            case STATE_UNKNOWN_COMPAT_MODE:
                return "UNKNOWN_COMPAT_MODE";
            case STATE_UNKNOWN_FAILED:
                return "UNKNOWN_FAILED";
            default:
                return "INVALID:" + state;
        }
    }

    @GuardedBy("mLock")
    private boolean isActiveLocked() {
        return mState == STATE_ACTIVE;
    }

    @GuardedBy("mLock")
    private boolean isDisabledByServiceLocked() {
        return mState == STATE_DISABLED_BY_SERVICE;
    }

    @GuardedBy("mLock")
    private boolean isFinishedLocked() {
        return mState == STATE_FINISHED;
    }

    private void post(Runnable runnable) {
        final AutofillClient client = getClient();
        if (client == null) {
            if (sVerbose) Log.v(TAG, "ignoring post() because client is null");
            return;
        }
        client.autofillClientRunOnUiThread(runnable);
    }

    /**
     * Implementation of the accessibility based compatibility.
     */
    private final class CompatibilityBridge implements AccessibilityManager.AccessibilityPolicy {
        @GuardedBy("mLock")
        private final Rect mFocusedBounds = new Rect();
        @GuardedBy("mLock")
        private final Rect mTempBounds = new Rect();

        @GuardedBy("mLock")
        private int mFocusedWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
        @GuardedBy("mLock")
        private long mFocusedNodeId = AccessibilityNodeInfo.UNDEFINED_NODE_ID;

        // Need to report a fake service in case a11y clients check the service list
        @NonNull
        @GuardedBy("mLock")
        AccessibilityServiceInfo mCompatServiceInfo;

        CompatibilityBridge() {
            final AccessibilityManager am = AccessibilityManager.getInstance(mContext);
            am.setAccessibilityPolicy(this);
        }

        private AccessibilityServiceInfo getCompatServiceInfo() {
            synchronized (mLock) {
                if (mCompatServiceInfo != null) {
                    return mCompatServiceInfo;
                }
                final Intent intent = new Intent();
                intent.setComponent(new ComponentName("android",
                        "com.android.server.autofill.AutofillCompatAccessibilityService"));
                final ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(
                        intent, PackageManager.MATCH_SYSTEM_ONLY | PackageManager.GET_META_DATA);
                try {
                    mCompatServiceInfo = new AccessibilityServiceInfo(resolveInfo, mContext);
                } catch (XmlPullParserException | IOException e) {
                    Log.e(TAG, "Cannot find compat autofill service:" + intent);
                    throw new IllegalStateException("Cannot find compat autofill service");
                }
                return mCompatServiceInfo;
            }
        }

        @Override
        public boolean isEnabled(boolean accessibilityEnabled) {
            return true;
        }

        @Override
        public int getRelevantEventTypes(int relevantEventTypes) {
            return relevantEventTypes | AccessibilityEvent.TYPE_VIEW_FOCUSED
                    | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED
                    | AccessibilityEvent.TYPE_VIEW_CLICKED
                    | AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
        }

        @Override
        public List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList(
               List<AccessibilityServiceInfo> installedServices) {
            if (installedServices == null) {
                installedServices = new ArrayList<>();
            }
            installedServices.add(getCompatServiceInfo());
            return installedServices;
        }

        @Override
        public List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList(
                int feedbackTypeFlags, List<AccessibilityServiceInfo> enabledService) {
            if (enabledService == null) {
                enabledService = new ArrayList<>();
            }
            enabledService.add(getCompatServiceInfo());
            return enabledService;
        }

        @Override
        public AccessibilityEvent onAccessibilityEvent(AccessibilityEvent event,
                boolean accessibilityEnabled, int relevantEventTypes) {
            final int type = event.getEventType();
            if (sVerbose) {
                // NOTE: this is waaay spammy, but that's life.
                Log.v(TAG, "onAccessibilityEvent(" + AccessibilityEvent.eventTypeToString(type)
                        + "): virtualId="
                        + AccessibilityNodeInfo.getVirtualDescendantId(event.getSourceNodeId())
                        + ", client=" + getClient());
            }
            switch (type) {
                case AccessibilityEvent.TYPE_VIEW_FOCUSED: {
                    synchronized (mLock) {
                        if (mFocusedWindowId == event.getWindowId()
                                && mFocusedNodeId == event.getSourceNodeId()) {
                            return event;
                        }
                        if (mFocusedWindowId != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID
                                && mFocusedNodeId != AccessibilityNodeInfo.UNDEFINED_NODE_ID) {
                            notifyViewExited(mFocusedWindowId, mFocusedNodeId);
                            mFocusedWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
                            mFocusedNodeId = AccessibilityNodeInfo.UNDEFINED_NODE_ID;
                            mFocusedBounds.set(0, 0, 0, 0);
                        }
                        final int windowId = event.getWindowId();
                        final long nodeId = event.getSourceNodeId();
                        if (notifyViewEntered(windowId, nodeId, mFocusedBounds)) {
                            mFocusedWindowId = windowId;
                            mFocusedNodeId = nodeId;
                        }
                    }
                } break;

                case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED: {
                    synchronized (mLock) {
                        if (mFocusedWindowId == event.getWindowId()
                                && mFocusedNodeId == event.getSourceNodeId()) {
                            notifyValueChanged(event.getWindowId(), event.getSourceNodeId());
                        }
                    }
                } break;

                case AccessibilityEvent.TYPE_VIEW_CLICKED: {
                    synchronized (mLock) {
                        notifyViewClicked(event.getWindowId(), event.getSourceNodeId());
                    }
                } break;

                case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: {
                    final AutofillClient client = getClient();
                    if (client != null) {
                        synchronized (mLock) {
                            if (client.autofillClientIsFillUiShowing()) {
                                notifyViewEntered(mFocusedWindowId, mFocusedNodeId, mFocusedBounds);
                            }
                            updateTrackedViewsLocked();
                        }
                    }
                } break;
            }

            return accessibilityEnabled ? event : null;
        }

        private boolean notifyViewEntered(int windowId, long nodeId, Rect focusedBounds) {
            final int virtualId = AccessibilityNodeInfo.getVirtualDescendantId(nodeId);
            if (!isVirtualNode(virtualId)) {
                return false;
            }
            final View view = findViewByAccessibilityId(windowId, nodeId);
            if (view == null) {
                return false;
            }
            final AccessibilityNodeInfo node = findVirtualNodeByAccessibilityId(view, virtualId);
            if (node == null) {
                return false;
            }
            if (!node.isEditable()) {
                return false;
            }
            final Rect newBounds = mTempBounds;
            node.getBoundsInScreen(newBounds);
            if (newBounds.equals(focusedBounds)) {
                return false;
            }
            focusedBounds.set(newBounds);
            AutofillManager.this.notifyViewEntered(view, virtualId, newBounds);
            return true;
        }

        private void notifyViewExited(int windowId, long nodeId) {
            final int virtualId = AccessibilityNodeInfo.getVirtualDescendantId(nodeId);
            if (!isVirtualNode(virtualId)) {
                return;
            }
            final View view = findViewByAccessibilityId(windowId, nodeId);
            if (view == null) {
                return;
            }
            AutofillManager.this.notifyViewExited(view, virtualId);
        }

        private void notifyValueChanged(int windowId, long nodeId) {
            final int virtualId = AccessibilityNodeInfo.getVirtualDescendantId(nodeId);
            if (!isVirtualNode(virtualId)) {
                return;
            }
            final View view = findViewByAccessibilityId(windowId, nodeId);
            if (view == null) {
                return;
            }
            final AccessibilityNodeInfo node = findVirtualNodeByAccessibilityId(view, virtualId);
            if (node == null) {
                return;
            }
            AutofillManager.this.notifyValueChanged(view, virtualId,
                    AutofillValue.forText(node.getText()));
        }

        private void notifyViewClicked(int windowId, long nodeId) {
            final int virtualId = AccessibilityNodeInfo.getVirtualDescendantId(nodeId);
            if (!isVirtualNode(virtualId)) {
                return;
            }
            final View view = findViewByAccessibilityId(windowId, nodeId);
            if (view == null) {
                return;
            }
            final AccessibilityNodeInfo node = findVirtualNodeByAccessibilityId(view, virtualId);
            if (node == null) {
                return;
            }
            AutofillManager.this.notifyViewClicked(view, virtualId);
        }

        @GuardedBy("mLock")
        private void updateTrackedViewsLocked() {
            if (mTrackedViews != null) {
                mTrackedViews.onVisibleForAutofillChangedLocked();
            }
        }

        private View findViewByAccessibilityId(int windowId, long nodeId) {
            final AutofillClient client = getClient();
            if (client == null) {
                return null;
            }
            final int viewId = AccessibilityNodeInfo.getAccessibilityViewId(nodeId);
            return client.autofillClientFindViewByAccessibilityIdTraversal(viewId, windowId);
        }

        private AccessibilityNodeInfo findVirtualNodeByAccessibilityId(View view, int virtualId) {
            final AccessibilityNodeProvider provider = view.getAccessibilityNodeProvider();
            if (provider == null) {
                return null;
            }
            return provider.createAccessibilityNodeInfo(virtualId);
        }

        private boolean isVirtualNode(int nodeId) {
            return nodeId != AccessibilityNodeProvider.HOST_VIEW_ID
                    && nodeId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
        }
    }

    /**
     * View tracking information. Once all tracked views become invisible the session is finished.
     */
    private class TrackedViews {
        /** Visible tracked views */
        @Nullable private ArraySet<AutofillId> mVisibleTrackedIds;

        /** Invisible tracked views */
        @Nullable private ArraySet<AutofillId> 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 <T> boolean isInSet(@Nullable ArraySet<T> 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 <T> ArraySet<T> addToSet(@Nullable ArraySet<T> 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 <T> ArraySet<T> removeFromSet(@Nullable ArraySet<T> 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 (!ArrayUtils.isEmpty(trackedIds) && client != null) {
                final boolean[] isVisible;

                if (client.autofillClientIsVisibleForAutofill()) {
                    if (sVerbose) Log.v(TAG, "client is visible, check tracked ids");
                    isVisible = client.autofillClientGetViewVisibility(trackedIds);
                } else {
                    // All false
                    isVisible = new boolean[trackedIds.length];
                }

                final int numIds = trackedIds.length;
                for (int i = 0; i < numIds; i++) {
                    final AutofillId id = trackedIds[i];

                    if (isVisible[i]) {
                        mVisibleTrackedIds = addToSet(mVisibleTrackedIds, id);
                    } else {
                        mInvisibleTrackedIds = addToSet(mInvisibleTrackedIds, id);
                    }
                }
            }

            if (sVerbose) {
                Log.v(TAG, "TrackedViews(trackedIds=" + Arrays.toString(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.
         */
        @GuardedBy("mLock")
        void notifyViewVisibilityChangedLocked(@NonNull AutofillId id, boolean isVisible) {
            if (sDebug) {
                Log.d(TAG, "notifyViewVisibilityChangedLocked(): id=" + id + " isVisible="
                        + isVisible);
            }

            if (isClientVisibleForAutofillLocked()) {
                if (isVisible) {
                    if (isInSet(mInvisibleTrackedIds, id)) {
                        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#autofillClientIsVisibleForAutofill()
         */
        @GuardedBy("mLock")
        void onVisibleForAutofillChangedLocked() {
            // The visibility of the views might have changed while the client was not be visible,
            // hence update the visibility state for all views.
            AutofillClient client = getClient();
            ArraySet<AutofillId> updatedVisibleTrackedIds = null;
            ArraySet<AutofillId> updatedInvisibleTrackedIds = null;
            if (client != null) {
                if (sVerbose) {
                    Log.v(TAG, "onVisibleForAutofillChangedLocked(): inv= " + mInvisibleTrackedIds
                            + " vis=" + mVisibleTrackedIds);
                }
                if (mInvisibleTrackedIds != null) {
                    final ArrayList<AutofillId> orderedInvisibleIds =
                            new ArrayList<>(mInvisibleTrackedIds);
                    final boolean[] isVisible = client.autofillClientGetViewVisibility(
                            Helper.toArray(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<AutofillId> orderedVisibleIds =
                            new ArrayList<>(mVisibleTrackedIds);
                    final boolean[] isVisible = client.autofillClientGetViewVisibility(
                            Helper.toArray(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) {
                if (sVerbose) {
                    Log.v(TAG, "onVisibleForAutofillChangedLocked(): no more visible ids");
                }
                finishSessionLocked();
            }
        }
    }

    /**
     * Callback for autofill related events.
     *
     * <p>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.
         *
         * <p>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.
         *
         * <p>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.
         *
         * <p>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<AutofillManager> mAfm;

        private AutofillManagerClient(AutofillManager autofillManager) {
            mAfm = new WeakReference<>(autofillManager);
        }

        @Override
        public void setState(int flags) {
            final AutofillManager afm = mAfm.get();
            if (afm != null) {
                afm.post(() -> afm.setState(flags));
            }
        }

        @Override
        public void autofill(int sessionId, List<AutofillId> ids, List<AutofillValue> 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, false));
            }
        }

        @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 dispatchUnhandledKey(int sessionId, AutofillId id, KeyEvent fullScreen) {
            final AutofillManager afm = mAfm.get();
            if (afm != null) {
                afm.post(() -> afm.dispatchUnhandledKey(sessionId, id, fullScreen));
            }
        }

        @Override
        public void startIntentSender(IntentSender intentSender, Intent intent) {
            final AutofillManager afm = mAfm.get();
            if (afm != null) {
                afm.post(() -> {
                    try {
                        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));
            }
        }

        @Override
        public void getAugmentedAutofillClient(IResultReceiver result) {
            final AutofillManager afm = mAfm.get();
            if (afm != null) {
                afm.post(() -> afm.getAugmentedAutofillClient(result));
            }
        }
    }

    private static final class AugmentedAutofillManagerClient
            extends IAugmentedAutofillManagerClient.Stub {
        private final WeakReference<AutofillManager> mAfm;

        private AugmentedAutofillManagerClient(AutofillManager autofillManager) {
            mAfm = new WeakReference<>(autofillManager);
        }

        @Override
        public Rect getViewCoordinates(@NonNull AutofillId id) {
            // TODO(b/111330312): use handler / callback?
            final AutofillManager afm = mAfm.get();
            if (afm == null) return null;

            final View view = afm.getClient().autofillClientFindViewByAutofillIdTraversal(id);
            // TODO(b/111330312): optimize (for example, use temp rect from attach info) and
            // fix (for example, take system status bar height into account) logic below
            final int[] location = new int[2];
            view.getLocationOnScreen(location);
            final Rect rect = new Rect(location[0], location[1], location[0] + view.getWidth(),
                    location[1] + view.getHeight());
            if (sVerbose) {
                Log.v(TAG, "Coordinates for " + id + ": " + rect);
            }
            return rect;
        }

        @Override
        public void autofill(int sessionId, List<AutofillId> ids, List<AutofillValue> values) {
            final AutofillManager afm = mAfm.get();
            if (afm != null) {
                afm.post(() -> afm.autofill(sessionId, ids, values));
            }
        }
    }

    /**
     * @hide
     */
    public static final class SyncResultReceiver extends IResultReceiver.Stub {

        private static final String EXTRA = "EXTRA";

        /**
         * How long to block waiting for {@link IResultReceiver} callbacks when calling server.
         */
        private static final long BINDER_TIMEOUT_MS = 5000;

        private static final int TYPE_STRING = 0;
        private static final int TYPE_STRING_ARRAY = 1;
        private static final int TYPE_PARCELABLE = 2;

        private final CountDownLatch mLatch  = new CountDownLatch(1);
        private int mResult;
        private Bundle mBundle;

        private void waitResult() {
            try {
                if (!mLatch.await(BINDER_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
                    throw new IllegalStateException("Not called in " + BINDER_TIMEOUT_MS + "ms");
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

        /**
         * Gets the result from an operation that returns an {@code int}.
         */
        int getIntResult() {
            waitResult();
            return mResult;
        }

        /**
         * Gets the result from an operation that returns an {@code Object}.
         *
         * @param type type of expected object.
         */
        @Nullable
        @SuppressWarnings("unchecked")
        <T> T getObjectResult(int type) {
            waitResult();
            if (mBundle == null) {
                return null;
            }
            switch (type) {
                case TYPE_STRING:
                    return (T) mBundle.getString(EXTRA);
                case TYPE_STRING_ARRAY:
                    return (T) mBundle.getStringArray(EXTRA);
                case TYPE_PARCELABLE:
                    return (T) mBundle.getParcelable(EXTRA);
                default:
                    throw new IllegalArgumentException("unsupported type: " + type);
            }
        }

        @Override
        public void send(int resultCode, Bundle resultData) {
            mResult = resultCode;
            mBundle = resultData;
            mLatch.countDown();
        }

        /**
         * Creates a bundle for a {@code String} value.
         */
        @NonNull
        public static Bundle bundleFor(@Nullable String value) {
            final Bundle bundle = new Bundle();
            bundle.putString(EXTRA, value);
            return bundle;
        }

        /**
         * Creates a bundle for a {@code String[]} value.
         */
        @NonNull
        public static Bundle bundleFor(@Nullable String[] value) {
            final Bundle bundle = new Bundle();
            bundle.putStringArray(EXTRA, value);
            return bundle;
        }

        /**
         * Creates a bundle for a {@code Parcelable} value.
         */
        @NonNull
        public static Bundle bundleFor(@Nullable Parcelable value) {
            final Bundle bundle = new Bundle();
            bundle.putParcelable(EXTRA, value);
            return bundle;
        }
    }
}
