chromium/content/public/android/java/src/org/chromium/content/browser/accessibility/WebContentsAccessibilityImpl.java

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.content.browser.accessibility;

import static androidx.core.view.accessibility.AccessibilityEventCompat.CONTENT_CHANGE_TYPE_PANE_APPEARED;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ARGUMENT_HTML_ELEMENT_STRING;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ARGUMENT_PROGRESS_VALUE;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SELECTION_END_INT;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SELECTION_START_INT;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_ACCESSIBILITY_FOCUS;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLEAR_FOCUS;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_COLLAPSE;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CONTEXT_CLICK;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_COPY;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CUT;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_EXPAND;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_FOCUS;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_IME_ENTER;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_LONG_CLICK;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_NEXT_AT_MOVEMENT_GRANULARITY;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_NEXT_HTML_ELEMENT;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_PAGE_DOWN;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_PAGE_LEFT;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_PAGE_RIGHT;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_PAGE_UP;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_PASTE;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_PREVIOUS_HTML_ELEMENT;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_BACKWARD;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_DOWN;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_FORWARD;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_LEFT;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_RIGHT;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_UP;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SET_PROGRESS;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SET_SELECTION;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SET_TEXT;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SHOW_ON_SCREEN;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_CHARACTER;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_LINE;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_PARAGRAPH;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_WORD;

import static org.chromium.content.browser.accessibility.AccessibilityNodeInfoBuilder.EXTRAS_DATA_REQUEST_IMAGE_DATA_KEY;
import static org.chromium.content.browser.accessibility.AccessibilityNodeInfoBuilder.EXTRAS_KEY_URL;
import static org.chromium.content_public.browser.ContentFeatureList.ACCESSIBILITY_MANAGE_BROADCAST_RECEIVER_ON_BACKGROUND;

import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ReceiverCallNotAllowedException;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.SparseArray;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.ViewStructure;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeProvider;
import android.view.autofill.AutofillManager;
import android.view.inputmethod.EditorInfo;

import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.core.view.accessibility.AccessibilityNodeProviderCompat;

import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;

import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.ResettersForTesting;
import org.chromium.base.StrictModeContext;
import org.chromium.base.TraceEvent;
import org.chromium.base.UserData;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskRunner;
import org.chromium.base.task.TaskTraits;
import org.chromium.build.BuildConfig;
import org.chromium.content.browser.WindowEventObserver;
import org.chromium.content.browser.WindowEventObserverManager;
import org.chromium.content.browser.accessibility.AccessibilityDelegate.AccessibilityCoordinates;
import org.chromium.content.browser.accessibility.AccessibilityNodeInfoBuilder.BuilderDelegate;
import org.chromium.content.browser.accessibility.AutoDisableAccessibilityHandler.Client;
import org.chromium.content.browser.accessibility.captioning.CaptioningController;
import org.chromium.content.browser.input.ImeAdapterImpl;
import org.chromium.content.browser.webcontents.WebContentsImpl;
import org.chromium.content.browser.webcontents.WebContentsImpl.UserDataFactory;
import org.chromium.content_public.browser.ContentFeatureList;
import org.chromium.content_public.browser.ContentFeatureMap;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsAccessibility;
import org.chromium.content_public.browser.WebContentsObserver;
import org.chromium.ui.accessibility.AccessibilityFeatures;
import org.chromium.ui.accessibility.AccessibilityFeaturesMap;
import org.chromium.ui.accessibility.AccessibilityState;
import org.chromium.ui.base.ViewAndroidDelegate;
import org.chromium.ui.base.WindowAndroid;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

/**
 * Implementation of {@link WebContentsAccessibility} interface. Native accessibility for a {@link
 * WebContents}. Actual native instance is created lazily upon the first request from Android
 * framework on {@link AccessibilityNodeProvider}, and shares the lifetime with {@link WebContents}.
 * Internally this class uses the {@link AccessibilityNodeProviderCompat} interface, and uses the
 * {@link AccessibilityNodeInfoCompat} object for the virtual tree, but will unwrap and surface the
 * non-Compat versions of these for any clients.
 */
@JNINamespace("content")
public class WebContentsAccessibilityImpl extends AccessibilityNodeProviderCompat
        implements WebContentsAccessibility,
                WindowEventObserver,
                UserData,
                AccessibilityState.Listener,
                ViewAndroidDelegate.ContainerViewObserver {
    private static final String TAG = "A11yImpl";

    // Constant for paragraph predicate key from web_contents_accessibility_android.cc
    private static final String PARAGRAPH_ELEMENT_TYPE = "PARAGRAPH";

    // Constant for no granularity selected.
    private static final int NO_GRANULARITY_SELECTED = 0;

    // Delay times for throttling of successive AccessibilityEvents in milliseconds.
    private static final int ACCESSIBILITY_EVENT_DELAY_DEFAULT = 100;
    private static final int ACCESSIBILITY_EVENT_DELAY_HOVER = 50;

    // Delay time for disabling renderer accessibility when no services are enabled. Used to prevent
    // churn if an accessibility service is quickly disabled then re-enabled.
    private static final int NO_ACCESSIBILITY_SERVICES_ENABLED_DELAY_MS = 5 * 1000;

    // Maximum number of times that the auto-disable feature can affect |this|.
    private static final int AUTO_DISABLE_SINGLE_INSTANCE_TOGGLE_LIMIT = 3;

    private final AccessibilityDelegate mDelegate;
    protected AccessibilityManager mAccessibilityManager;
    protected Context mContext;
    private final String mProductVersion;
    protected long mNativeObj;
    protected long mNativeAssistDataObj;
    private boolean mIsHovering;
    private int mLastHoverId = View.NO_ID;
    private int mCurrentRootId;
    protected View mView;
    private boolean mPendingScrollToMakeNodeVisible;
    private boolean mNotifyFrameInfoInitializedCalled;
    private boolean mAccessibilityEnabledOverride;
    private int mSelectionGranularity;
    private int mAccessibilityFocusId;
    private int mLastAccessibilityFocusId = View.NO_ID;
    private int mSelectionNodeId;
    private View mAutofillPopupView;
    private CaptioningController mCaptioningController;
    private boolean mIsCurrentlyExtendingSelection;
    private int mSelectionStart;
    private int mCursorIndex;
    private String mSupportedHtmlElementTypes;
    private final AccessibilityNodeInfoBuilder mAccessibilityNodeInfoBuilder;
    private boolean mHasFinishedLatestAccessibilitySnapshot;

    // Observer for WebContents, used to update state when |this| is shown/hidden.
    private WebContentsObserver mWebContentsObserver;

    // Tracker for all actions performed and events sent by this instance, used for testing.
    private AccessibilityActionAndEventTracker mTracker;

    // Helper object to track and record values relevant to histograms.
    private final AccessibilityHistogramRecorder mHistogramRecorder;

    // Whether or not the next selection event should be fired. We only want to sent one traverse
    // and one selection event per granularity move, this ensures no double events while still
    // sending events when the user is using other assistive technology (e.g. external keyboard)
    private boolean mSuppressNextSelectionEvent;

    // Whether accessibility focus should be set to the page when it finishes loading.
    // This only applies if an accessibility service like TalkBack is running.
    // This is desirable behavior for a browser window, but not for an embedded
    // WebView.
    private boolean mShouldFocusOnPageLoad;

    // True if this instance is a candidate to have the image descriptions feature enabled. The
    // feature is dependent on embedder behavior and screen reader state. Default false.
    private boolean mIsImageDescriptionsCandidate;

    // If true, the web contents are obscured by another view and we will return a null
    // AccessibilityNodeProvider, and will not process touch exploration events or calls to
    // performAction. If false, all accessibility requests will be honored. When null, treat the
    // value as false, this is to differentiate between an initial value and a value set by a
    // client, since we assert the value is changed with each call to the setter. (Default: null).
    private Boolean mIsObscuredByAnotherView;

    // This array maps a given virtualViewId to an |AccessibilityNodeInfoCompat| for that view. We
    // use this to update a node quickly rather than building from one scratch each time.
    private final SparseArray<AccessibilityNodeInfoCompat> mNodeInfoCache = new SparseArray<>();

    // This handles the dispatching of accessibility events. It acts as an intermediary where we can
    // apply throttling rules, delay event construction, etc.
    private final AccessibilityEventDispatcher mEventDispatcher;
    private volatile String mSystemLanguageTag;
    private BroadcastReceiver mBroadcastReceiver;
    // Only un-register the broadcast receiver if this is true, otherwise it would result in a
    // crash.
    private volatile boolean mIsBroadcastReceiverRegistered;

    // Set of all nodes that have received a request to populate image data. The request only needs
    // to be run once per node, and it completes asynchronously. We track which nodes have already
    // started the async request so that if downstream apps request the same node multiple times
    // we can avoid doing the extra work.
    private final Set<Integer> mImageDataRequestedNodes = new HashSet<Integer>();

    // Handler for the "Auto Disable" accessibility feature and related state variables.
    private final AutoDisableAccessibilityHandler mAutoDisableAccessibilityHandler;
    private boolean mIsCurrentlyAutoDisabled;
    private int mAutoDisableUsageCounter;
    private boolean mIsAutoDisableAccessibilityCandidate;

    // To avoid any potential synchronization issues we post all broadcast receiver registration
    // actions to the same sequence to be run serially.
    private static final TaskRunner sSequencedTaskRunner =
            PostTask.createSequencedTaskRunner(TaskTraits.BEST_EFFORT_MAY_BLOCK);

    /** Create a WebContentsAccessibilityImpl object. */
    private static class Factory implements UserDataFactory<WebContentsAccessibilityImpl> {
        @Override
        public WebContentsAccessibilityImpl create(WebContents webContents) {
            return createForDelegate(new WebContentsAccessibilityDelegate(webContents));
        }
    }

    private static final class UserDataFactoryLazyHolder {
        private static final UserDataFactory<WebContentsAccessibilityImpl> INSTANCE = new Factory();
    }

    public static WebContentsAccessibilityImpl fromWebContents(WebContents webContents) {
        return ((WebContentsImpl) webContents)
                .getOrSetUserData(
                        WebContentsAccessibilityImpl.class, UserDataFactoryLazyHolder.INSTANCE);
    }

    public static WebContentsAccessibilityImpl fromDelegate(AccessibilityDelegate delegate) {
        // If WebContents exists, {@link #fromWebContents} should be used.
        assert delegate.getWebContents() == null;
        return createForDelegate(delegate);
    }

    private static WebContentsAccessibilityImpl createForDelegate(AccessibilityDelegate delegate) {
        return new WebContentsAccessibilityImpl(delegate);
    }

    protected WebContentsAccessibilityImpl(AccessibilityDelegate delegate) {
        TraceEvent.begin("WebContentsAccessibilityImpl.ctor");
        mDelegate = delegate;
        mView = mDelegate.getContainerView();
        mContext = mView.getContext();
        mProductVersion = mDelegate.getProductVersion();
        mAccessibilityManager =
                (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);

        // Need to be initialized before AXTreeUpdate initialization because updateMaxNodesInCache
        // gets called then. Also needs to be initialized before the WindowEventObserver is added,
        // which may call #onAttachedToWindow (or detached) if that is the current state.
        mHistogramRecorder = new AccessibilityHistogramRecorder();

        WebContents webContents = mDelegate.getWebContents();
        if (webContents != null) {
            mCaptioningController = new CaptioningController(webContents);
            WindowEventObserverManager.from(webContents).addObserver(this);
            webContents.getViewAndroidDelegate().addObserver(this);
        }
        mDelegate.setOnScrollPositionChangedCallback(
                () -> {
                    handleScrollPositionChanged(mAccessibilityFocusId);
                    moveAccessibilityFocusToId(mAccessibilityFocusId);
                });

        AccessibilityState.addListener(this);

        mAccessibilityNodeInfoBuilder =
                new AccessibilityNodeInfoBuilder(
                        new BuilderDelegate() {
                            @Override
                            public View getView() {
                                return mView;
                            }

                            @Override
                            public Context getContext() {
                                return mContext;
                            }

                            @Override
                            public int currentRootId() {
                                return mCurrentRootId;
                            }

                            @Override
                            public int currentAccessibilityFocusId() {
                                return mAccessibilityFocusId;
                            }

                            @Override
                            public String getLanguageTag() {
                                return mSystemLanguageTag;
                            }

                            @Override
                            public String getSupportedHtmlTags() {
                                return mSupportedHtmlElementTypes;
                            }

                            @Override
                            public AccessibilityCoordinates getAccessibilityCoordinates() {
                                return mDelegate.getAccessibilityCoordinates();
                            }
                        });

        mAutoDisableAccessibilityHandler =
                new AutoDisableAccessibilityHandler(
                        new Client() {
                            @Override
                            public View getView() {
                                return mView;
                            }

                            @Override
                            public void onDisabled() {
                                assert mNativeObj != 0
                                        : "Native code is not initialized, but disable was called.";
                                TraceEvent.begin(
                                        "WebContentsAccessibilityImpl.AutoDisableAccessibilityHandler.onDisabled");
                                mHistogramRecorder.onDisableCalled(mAutoDisableUsageCounter == 0);
                                // If the Auto-disable timer has expired, begin disabling the
                                // renderer, and clearing the Java-side caches. Changing AXModes
                                // must be done on the main thread.
                                WebContentsAccessibilityImplJni.get()
                                        .disableRendererAccessibility(mNativeObj);
                                mEventDispatcher.clearQueue();
                                mNodeInfoCache.clear();
                                mIsCurrentlyAutoDisabled = true;
                                TraceEvent.end(
                                        "WebContentsAccessibilityImpl.AutoDisableAccessibilityHandler.onDisabled");
                            }
                        });

        // Define our delays on a per event type basis.
        Map<Integer, Integer> eventThrottleDelays = new HashMap<Integer, Integer>();
        eventThrottleDelays.put(
                AccessibilityEvent.TYPE_VIEW_SCROLLED, ACCESSIBILITY_EVENT_DELAY_DEFAULT);
        eventThrottleDelays.put(
                AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED, ACCESSIBILITY_EVENT_DELAY_DEFAULT);
        eventThrottleDelays.put(
                AccessibilityEvent.TYPE_VIEW_HOVER_ENTER, ACCESSIBILITY_EVENT_DELAY_HOVER);

        // Define events to throttle without regard for |virtualViewId|.
        Set<Integer> viewIndependentEvents = new HashSet<Integer>();
        viewIndependentEvents.add(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);

        AccessibilityEventDispatcher.Client client =
                new AccessibilityEventDispatcher.Client() {
                    @Override
                    public void postRunnable(Runnable toPost, long delayInMilliseconds) {
                        mView.postDelayed(toPost, delayInMilliseconds);
                    }

                    @Override
                    public void removeRunnable(Runnable toRemove) {
                        mView.removeCallbacks(toRemove);
                    }

                    @Override
                    public boolean dispatchEvent(int virtualViewId, int eventType) {
                        AccessibilityEvent event =
                                buildAccessibilityEvent(virtualViewId, eventType);
                        if (event == null) return false;

                        requestSendAccessibilityEvent(event);

                        // Always send the ENTER and then the EXIT event, to match a
                        // standard Android View.
                        if (eventType == AccessibilityEvent.TYPE_VIEW_HOVER_ENTER) {
                            AccessibilityEvent exitEvent =
                                    buildAccessibilityEvent(
                                            mLastHoverId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
                            if (exitEvent != null) {
                                requestSendAccessibilityEvent(exitEvent);
                                mLastHoverId = virtualViewId;
                            } else if (virtualViewId != View.NO_ID
                                    && mLastHoverId != virtualViewId) {
                                // If IDs become mismatched, or on first hover, this will
                                // sync the values again so all further hovers have
                                // correct event pairing.
                                mLastHoverId = virtualViewId;
                            }
                        }

                        return true;
                    }
                };
        mEventDispatcher =
                new AccessibilityEventDispatcher(
                        client, eventThrottleDelays, viewIndependentEvents, new HashSet<Integer>());

        if (mDelegate.getNativeAXTree() != 0) {
            initializeNativeWithAXTreeUpdate(mDelegate.getNativeAXTree());
        }
        // If the AXTree is not provided, native is initialized lazily, when node provider is
        // actually requested.

        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O
                && !BuildConfig.IS_FOR_TEST) {
            // The system service call for AutofillManager can timeout and throws an Exception.
            // This is treated differently in each version of Android, so we must catch a
            // generic Exception. (refer to crbug.com/1186406 or AutofillManagerWrapper ctor).
            try {
                AutofillManager autofillManager = mContext.getSystemService(AutofillManager.class);
                if (autofillManager != null
                        && autofillManager.isEnabled()
                        && autofillManager.hasEnabledAutofillServices()) {
                    // Native accessibility is usually initialized when getAccessibilityNodeProvider
                    // is called, but the Autofill compatibility bridge only calls that method after
                    // it has received the first accessibility events. To solve the chicken-and-egg
                    // problem, always initialize the native parts when the user has an Autofill
                    // service enabled.
                    getAccessibilityNodeProvider();
                }
            } catch (Exception e) {
                Log.e(TAG, "AutofillManager did not resolve before time limit.");
            }
        }

        TraceEvent.end("WebContentsAccessibilityImpl.ctor");
    }

    /**
     * Called after the native a11y part is initialized. Overridable by subclasses
     * to do initialization that is not required until the native is set up.
     */
    protected void onNativeInit() {
        TraceEvent.begin("WebContentsAccessibilityImpl.onNativeInit");
        mHistogramRecorder.updateTimeOfNativeInitialization();
        mAccessibilityFocusId = View.NO_ID;
        mLastAccessibilityFocusId = View.NO_ID;
        mSelectionNodeId = View.NO_ID;
        mIsHovering = false;
        mCurrentRootId = View.NO_ID;

        mSupportedHtmlElementTypes =
                WebContentsAccessibilityImplJni.get().getSupportedHtmlElementTypes(mNativeObj);
        mBroadcastReceiver =
                new BroadcastReceiver() {
                    @Override
                    public void onReceive(Context context, Intent intent) {
                        mSystemLanguageTag = Locale.getDefault().toLanguageTag();
                    }
                };

        // Register a broadcast receiver for locale change.
        if (mView.isAttachedToWindow()) {
            if (ContentFeatureMap.isEnabled(
                    ACCESSIBILITY_MANAGE_BROADCAST_RECEIVER_ON_BACKGROUND)) {
                // To prevent having empty languageTag until this background task runs.
                mSystemLanguageTag = Locale.getDefault().toLanguageTag();
                sSequencedTaskRunner.postTask(this::registerLocaleChangeReceiver);
            } else {
                registerLocaleChangeReceiver();
            }
        }

        // Define a set of relevant AccessibilityEvents.
        Runnable serviceMaskRunnable =
                () -> {
                    mEventDispatcher.updateRelevantEventTypes(
                            AccessibilityState.relevantEventTypesForCurrentServices());
                };
        mView.post(serviceMaskRunnable);

        // Send state values set by embedders to native-side objects.
        refreshNativeState();

        TraceEvent.end("WebContentsAccessibilityImpl.onNativeInit");
    }

    @CalledByNative
    protected void onNativeObjectDestroyed() {
        mNativeObj = 0;
    }

    @Override
    public boolean isNativeInitialized() {
        return mNativeObj != 0;
    }

    private boolean isRootManagerConnected() {
        return isNativeInitialized()
                && WebContentsAccessibilityImplJni.get().isRootManagerConnected(mNativeObj);
    }

    public boolean isAccessibilityEnabled() {
        return isNativeInitialized()
                && (mAccessibilityEnabledOverride
                        || mAccessibilityManager.isEnabled()
                        || AccessibilityState.isAnyAccessibilityServiceEnabled());
    }

    public void addSpellingErrorForTesting(int virtualViewId, int startOffset, int endOffset) {
        WebContentsAccessibilityImplJni.get()
                .addSpellingErrorForTesting(mNativeObj, virtualViewId, startOffset, endOffset);
    }

    public void setMaxContentChangedEventsToFireForTesting(int maxEvents) {
        WebContentsAccessibilityImplJni.get()
                .setMaxContentChangedEventsToFireForTesting(mNativeObj, maxEvents);
    }

    public int getMaxContentChangedEventsToFireForTesting() {
        return WebContentsAccessibilityImplJni.get()
                .getMaxContentChangedEventsToFireForTesting(mNativeObj);
    }

    public void forceAutoDisableAccessibilityForTesting() {
        mAutoDisableAccessibilityHandler.notifyDisable();
    }

    public void setAccessibilityTrackerForTesting(AccessibilityActionAndEventTracker tracker) {
        mHistogramRecorder.updateTimeOfFirstShown();
        var oldValue = mTracker;
        mTracker = tracker;
        ResettersForTesting.register(() -> mTracker = oldValue);
    }

    public void setIsAutoDisableAccessibilityCandidateForTesting(
            boolean isAutoDisableAccessibilityCandidate) {
        mIsAutoDisableAccessibilityCandidate = isAutoDisableAccessibilityCandidate;
    }

    public boolean hasAnyPendingTimersForTesting() {
        return mAutoDisableAccessibilityHandler.hasPendingTimer();
    }

    public void signalEndOfTestForTesting() {
        WebContentsAccessibilityImplJni.get().signalEndOfTestForTesting(mNativeObj);
    }

    public void forceRecordUMAHistogramsForTesting() {
        mHistogramRecorder.recordEventsHistograms();
    }

    public void forceRecordCacheUMAHistogramsForTesting() {
        mHistogramRecorder.recordCacheHistograms();
    }

    public void forceRecordUsageUMAHistogramsForTesting() {
        mHistogramRecorder.recordAccessibilityUsageHistograms();
    }

    public boolean hasFinishedLatestAccessibilitySnapshotForTesting() {
        return mHasFinishedLatestAccessibilitySnapshot;
    }

    @CalledByNative
    public void handleEndOfTestSignal() {
        // We have received a signal that we have reached the end of a unit test. If we have a
        // tracker listening, set the test is complete.
        if (mTracker != null) {
            mTracker.signalEndOfTest();
        }
    }

    // WebContentsObserver

    private void registerWebContentsObserver(WebContents webContents) {
        if (mWebContentsObserver != null) return;

        mWebContentsObserver =
                new WebContentsObserver(webContents) {
                    @Override
                    public void wasShown() {
                        // The Tab holding |this| instance was shown, e.g. the user brings Chrome
                        // back to the foreground, switches to this Tab, etc.
                        super.wasShown();
                        mHistogramRecorder.updateTimeOfFirstShown();

                        // Accessibility state may have changed while |this| was not shown, so
                        // refresh.
                        refreshNativeState();
                        if (isNativeInitialized()) {
                            // When we are in an initialized state, accessibility may be disabled.
                            // In that case, we should not update the time of native
                            // initialization, and instead only update the time of the last
                            // disabled call so we don't count any time while this instance was
                            // hidden/backgrounded.
                            if (mIsCurrentlyAutoDisabled) {
                                mHistogramRecorder.showAutoDisabledInstance();
                            } else {
                                mHistogramRecorder.updateTimeOfNativeInitialization();
                            }
                        }
                    }

                    @Override
                    public void wasHidden() {
                        // The Tab holding |this| instance was hidden, e.g. a new Tab was opened,
                        // user has backgrounded Chrome, opened Settings, etc. Record usage times
                        // and reset state.
                        super.wasHidden();
                        mHistogramRecorder.recordAccessibilityUsageHistograms();

                        // When the native code was initialized, also record performance metrics.
                        if (isNativeInitialized()) {
                            mHistogramRecorder.recordAccessibilityPerformanceHistograms();
                            // When we are in an initialized state, accessibility may be disabled.
                            // In that case, we should keep an on-going sum of the time spent
                            // disabled (without counting time while hidden/backgrounded).
                            if (mIsCurrentlyAutoDisabled) {
                                mHistogramRecorder.hideAutoDisabledInstance();
                            }
                            mAutoDisableAccessibilityHandler.cancelDisableTimer();
                        }
                    }
                };
    }

    // WindowEventObserver

    @Override
    public void onDetachedFromWindow() {
        try (TraceEvent te =
                TraceEvent.scoped("WebContentsAccessibilityImpl.onDetachedFromWindow")) {
            mCaptioningController.stopListening();

            // Destroy the WebContentsObserver if |this| is no longer attached to a Window, but
            // first record whatever data we have collected since #wasHidden may not have been
            // called, for example when opening the Tab Switcher. Timers will restart during the
            // next onAttach.
            if (mWebContentsObserver != null) {
                mHistogramRecorder.recordAccessibilityUsageHistograms();
                mWebContentsObserver.destroy();
                mWebContentsObserver = null;
            }

            // When the native code was initialized, also record performance metrics unregister
            // our broadcast receiver.
            if (isNativeInitialized()) {
                if (mIsBroadcastReceiverRegistered) {
                    if (ContentFeatureMap.isEnabled(
                            ACCESSIBILITY_MANAGE_BROADCAST_RECEIVER_ON_BACKGROUND)) {
                        sSequencedTaskRunner.postTask(
                                () ->
                                        ContextUtils.getApplicationContext()
                                                .unregisterReceiver(mBroadcastReceiver));
                    } else {
                        ContextUtils.getApplicationContext().unregisterReceiver(mBroadcastReceiver);
                    }
                    mIsBroadcastReceiverRegistered = false;
                }
                mHistogramRecorder.recordAccessibilityPerformanceHistograms();
                // When we are in an initialized state, accessibility may be disabled. In that
                // case, we should keep an on-going sum of the time spent disabled (without
                // counting time while hidden/backgrounded).
                if (mIsCurrentlyAutoDisabled) {
                    mHistogramRecorder.hideAutoDisabledInstance();
                }
                mAutoDisableAccessibilityHandler.cancelDisableTimer();
            }
        }
    }

    @Override
    public void onAttachedToWindow() {
        TraceEvent.begin("WebContentsAccessibilityImpl.onAttachedToWindow");

        // When webContents is non-null (e.g. not a Paint Preview), we will track usage stats.
        if (mDelegate.getWebContents() != null) {
            registerWebContentsObserver(mDelegate.getWebContents());
            mWebContentsObserver.wasShown();
        }

        refreshNativeState();

        // Some devices (e.g. OnePlus) are enforcing a Strict Mode Violation in code outside Chrome,
        // which can result a crash when the listener starts.
        try (StrictModeContext ignored = StrictModeContext.allowDiskWrites()) {
            mCaptioningController.startListening();
        }

        if (isNativeInitialized()) {
            if (ContentFeatureMap.isEnabled(
                    ACCESSIBILITY_MANAGE_BROADCAST_RECEIVER_ON_BACKGROUND)) {
                // To prevent having empty languageTag until this background task runs.
                mSystemLanguageTag = Locale.getDefault().toLanguageTag();
                sSequencedTaskRunner.postTask(this::registerLocaleChangeReceiver);
            } else {
                registerLocaleChangeReceiver();
            }
        }
        TraceEvent.end("WebContentsAccessibilityImpl.onAttachedToWindow");
    }

    private void registerLocaleChangeReceiver() {
        try {
            IntentFilter filter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
            ContextUtils.registerProtectedBroadcastReceiver(
                    ContextUtils.getApplicationContext(), mBroadcastReceiver, filter);
            mIsBroadcastReceiverRegistered = true;
        } catch (ReceiverCallNotAllowedException e) {
            // WebView may be running inside a BroadcastReceiver, in which case registerReceiver is
            // not allowed.
        }
        mSystemLanguageTag = Locale.getDefault().toLanguageTag();
    }

    @Override
    public void onWindowAndroidChanged(WindowAndroid windowAndroid) {
        TraceEvent.begin("WebContentsAccessibilityImpl.onWindowAndroidChanged");
        // When the WindowAndroid changes, we must update our Context reference to the new value.
        // We also need to remove all references to the previous context, which in this case would
        // be the reference in any existing SuggestionSpans. To remove these, clear our cache to
        // recycle all nodes. Any other AccessibilityNodeInfo objects that were created would have
        // been passed to the Framework, which can handle clean-up on its end. We do not want to
        // delete |this| because the object is (largely) not WindowAndroid dependent.
        mNodeInfoCache.clear();
        if (windowAndroid != null && windowAndroid.getContext().get() != null) {
            mContext = windowAndroid.getContext().get();
        }

        TraceEvent.end("WebContentsAccessibilityImpl.onWindowAndroidChanged");
    }

    @Override
    public void onUpdateContainerView(ViewGroup view) {
        // When the ContainerView is updated, we must update the |mView| variable and remove all
        // previous references to it. We clear the AccessibilityEventDispatcher queue, which may
        // have posted Runnable(s) to the old view. We also clear the AccessibilityNodeInfo cache
        // since some objects may still be referencing the old view as their parent or source. We
        // do not want to delete |this| because the object is (largely) not ContainerView dependent.
        mEventDispatcher.clearQueue();
        mNodeInfoCache.clear();
        mView = view;
    }

    @Override
    public void destroy() {
        TraceEvent.begin("WebContentsAccessibilityImpl.destroy");
        mNodeInfoCache.clear();
        mEventDispatcher.clearQueue();
        mAutoDisableAccessibilityHandler.cancelDisableTimer();
        if (mDelegate.getWebContents() == null) {
            deleteEarly();
        } else {
            if (mWebContentsObserver != null) mWebContentsObserver.destroy();
            WindowEventObserverManager.from(mDelegate.getWebContents()).removeObserver(this);
            ((WebContentsImpl) mDelegate.getWebContents())
                    .removeUserData(WebContentsAccessibilityImpl.class);
        }
        TraceEvent.end("WebContentsAccessibilityImpl.destroy");
    }

    protected void deleteEarly() {
        if (mNativeObj != 0) {
            TraceEvent.begin("WebContentsAccessibilityImpl.deleteEarly");
            WebContentsAccessibilityImplJni.get().deleteEarly(mNativeObj);
            assert mNativeObj == 0;
            TraceEvent.end("WebContentsAccessibilityImpl.deleteEarly");
        }
    }

    private void refreshNativeState() {
        try (TraceEvent te = TraceEvent.scoped("WebContentsAccessibilityImpl.refreshNativeState")) {
            if (!isNativeInitialized()) return;

            // Update the browser-level AXMode based on running applications.
            WebContentsAccessibilityImplJni.get()
                    .setBrowserAXMode(
                            WebContentsAccessibilityImpl.this,
                            AccessibilityState.isScreenReaderEnabled(),
                            AccessibilityState.isOnlyPasswordManagersEnabled(),
                            /* isAccessibilityEnabled= */ true);

            // Update the state of enabling/disabling the image descriptions feature. To enable the
            // feature, this instance must be a candidate and a screen reader must be enabled.
            WebContentsAccessibilityImplJni.get()
                    .setAllowImageDescriptions(
                            mNativeObj,
                            mIsImageDescriptionsCandidate
                                    && AccessibilityState.isScreenReaderEnabled());

            // Update the list of events we dispatch to enabled services.
            mEventDispatcher.updateRelevantEventTypes(
                    AccessibilityState.relevantEventTypesForCurrentServices());

            // When no accessibility services are running, disable renderer accessibility and tear
            // down objects. If we have disabled then re-enabled the renderer accessibility multiple
            // times for this instance, return early and keep enabled to prevent further churn.
            if (mAutoDisableUsageCounter >= AUTO_DISABLE_SINGLE_INSTANCE_TOGGLE_LIMIT
                    || !mIsAutoDisableAccessibilityCandidate) {
                mAutoDisableAccessibilityHandler.cancelDisableTimer();
                return;
            }

            // The C++ and Java instances are not fully connected until the root manager has
            // been connected, which will happen asynchronously. Accessibility cannot be auto
            // disabled and re-enabled when there is no root manager. See note in
            // {@link web_contents_accessibility_android.h}.
            if (!isRootManagerConnected()) return;

            // If accessibility was auto-disabled, then we do not want to restart a new timer.
            if (mIsCurrentlyAutoDisabled) return;

            if (!AccessibilityState.isAnyAccessibilityServiceEnabled()) {
                mAutoDisableAccessibilityHandler.cancelDisableTimer();
                mAutoDisableAccessibilityHandler.startDisableTimer(
                        NO_ACCESSIBILITY_SERVICES_ENABLED_DELAY_MS);
            } else {
                mAutoDisableAccessibilityHandler.cancelDisableTimer();
            }
        }
    }

    // AccessibilityNodeProvider

    @Override
    public AccessibilityNodeProvider getAccessibilityNodeProvider() {
        // The |WebContentsAccessibilityImpl| class will rely on the Compat library, but we will
        // not require other parts of Chrome to do the same for simplicity, so unwrap the
        // |AccessibilityNodeProvider| object before returning.
        AccessibilityNodeProviderCompat anpc = getAccessibilityNodeProviderCompat();
        if (anpc == null) return null;

        return (AccessibilityNodeProvider) anpc.getProvider();
    }

    /**
     * Allows clients to get an |AccessibilityNodeProviderCompat| instance if they do not want
     * the unwrapped version that is available with getAccessibilityNodeProvider above.
     *
     * @return AccessibilityNodeProviderCompat (this)
     */
    public AccessibilityNodeProviderCompat getAccessibilityNodeProviderCompat() {
        if (shouldPreventNativeEngineUse()) return null;

        // If the Auto-Disable feature is on, and accessibility has been disabled, when the
        // Android Framework calls this method, it is a signal to re-enable renderer accessibility.
        // This must be done before we try to verify/reconnect the root manager, since doing so
        // requires a reference to the webContents.
        if (mIsCurrentlyAutoDisabled) {
            TraceEvent.begin("WebContentsAccessibilityImpl.reEnableRendererAccessibility");
            mHistogramRecorder.onReEnableCalled(mAutoDisableUsageCounter == 0);
            WebContentsAccessibilityImplJni.get()
                    .reEnableRendererAccessibility(mNativeObj, mDelegate.getWebContents());
            mIsCurrentlyAutoDisabled = false;
            mAutoDisableUsageCounter++;
            TraceEvent.end("WebContentsAccessibilityImpl.reEnableRendererAccessibility");
        }

        if (!isNativeInitialized()) {
            assert mDelegate.getWebContents() != null
                    : "WebContentsAccessibility with no webContents should not be initialized, or"
                            + " it should be initialized during constructor with an AXTreeUpdate.";

            mNativeObj =
                    WebContentsAccessibilityImplJni.get()
                            .init(
                                    WebContentsAccessibilityImpl.this,
                                    mDelegate.getWebContents(),
                                    mAccessibilityNodeInfoBuilder);
            onNativeInit();
        }

        if (!isRootManagerConnected()) {
            WebContentsAccessibilityImplJni.get().connectInstanceToRootManager(mNativeObj);
            return null;
        }

        return this;
    }

    protected void initializeNativeWithAXTreeUpdate(long nativeAxTree) {
        assert !isNativeInitialized();

        mNativeObj =
                WebContentsAccessibilityImplJni.get()
                        .initWithAXTree(
                                WebContentsAccessibilityImpl.this,
                                nativeAxTree,
                                mAccessibilityNodeInfoBuilder);
        onNativeInit();
    }

    @CalledByNative
    public String generateAccessibilityNodeInfoString(int virtualViewId) {
        // If accessibility isn't enabled, all the AccessibilityNodeInfoCompat objects will be null,
        // so temporarily set the |mAccessibilityEnabledOverride| flag to true, then disable it.
        mAccessibilityEnabledOverride = true;
        String returnString =
                AccessibilityNodeInfoUtils.toString(
                        createAccessibilityNodeInfo(virtualViewId), true);
        mAccessibilityEnabledOverride = false;
        return returnString;
    }

    @CalledByNative
    public void updateMaxNodesInCache() {
        mHistogramRecorder.updateMaxNodesInCache(mNodeInfoCache.size());
    }

    @CalledByNative
    public void clearNodeInfoCacheForGivenId(int virtualViewId) {
        // Recycle and remove the element in our cache for this |virtualViewId|.
        if (mNodeInfoCache.get(virtualViewId) != null) {
            mNodeInfoCache.get(virtualViewId).recycle();
            mNodeInfoCache.remove(virtualViewId);
        }
        // Remove this node from requested image data nodes in case data changed with update.
        mImageDataRequestedNodes.remove(virtualViewId);
    }

    @Override
    public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(int virtualViewId) {
        if (!isAccessibilityEnabled()) {
            return null;
        }
        if (mCurrentRootId == View.NO_ID) {
            mCurrentRootId = WebContentsAccessibilityImplJni.get().getRootId(mNativeObj);
        }

        if (virtualViewId == View.NO_ID) {
            return createNodeForHost(mCurrentRootId);
        }

        if (!isFrameInfoInitialized()) {
            return null;
        }

        // We need to create an |AccessibilityNodeInfoCompat| object for this |virtualViewId|. If we
        // have one in our cache, then communicate this so web_contents_accessibility_android.cc
        // will update a fraction of the object and for the rest leverage what is already there.
        if (mNodeInfoCache.get(virtualViewId) != null) {
            AccessibilityNodeInfoCompat cachedNode =
                    AccessibilityNodeInfoCompat.obtain(mNodeInfoCache.get(virtualViewId));

            // Always update the source node id to prevent potential infinite loop in framework.
            cachedNode.setSource(mView, virtualViewId);

            if (WebContentsAccessibilityImplJni.get()
                    .updateCachedAccessibilityNodeInfo(mNativeObj, cachedNode, virtualViewId)) {
                // After successfully re-populating this cached node, update the accessibility
                // focus since this would not be included in the update call, and set the
                // available actions accordingly, then return result.
                cachedNode.setAccessibilityFocused(mAccessibilityFocusId == virtualViewId);

                if (mAccessibilityFocusId == virtualViewId) {
                    cachedNode.addAction(ACTION_CLEAR_ACCESSIBILITY_FOCUS);
                    cachedNode.removeAction(ACTION_ACCESSIBILITY_FOCUS);
                } else {
                    cachedNode.removeAction(ACTION_CLEAR_ACCESSIBILITY_FOCUS);
                    cachedNode.addAction(ACTION_ACCESSIBILITY_FOCUS);
                }

                mHistogramRecorder.incrementNodeWasReturnedFromCache();
                return cachedNode;
            } else {
                // If the node is no longer valid, wipe it from the cache and return null
                mNodeInfoCache.get(virtualViewId).recycle();
                mNodeInfoCache.remove(virtualViewId);
                return null;
            }

        } else {
            // If we have no copy of this node in our cache, build a new one from scratch.
            final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain(mView);
            info.setPackageName(mContext.getPackageName());
            info.setSource(mView, virtualViewId);

            if (virtualViewId == mCurrentRootId) {
                info.setParent(mView);
            }

            if (WebContentsAccessibilityImplJni.get()
                    .populateAccessibilityNodeInfo(mNativeObj, info, virtualViewId)) {
                // After successfully populating this node, add it to our cache then return.
                mNodeInfoCache.put(virtualViewId, AccessibilityNodeInfoCompat.obtain(info));
                mHistogramRecorder.incrementNodeWasCreatedFromScratch();
                return info;
            } else {
                info.recycle();
                return null;
            }
        }
    }

    @Override
    public List<AccessibilityNodeInfoCompat> findAccessibilityNodeInfosByText(
            String text, int virtualViewId) {
        return new ArrayList<AccessibilityNodeInfoCompat>();
    }

    private static boolean isValidMovementGranularity(int granularity) {
        switch (granularity) {
            case MOVEMENT_GRANULARITY_CHARACTER:
            case MOVEMENT_GRANULARITY_WORD:
            case MOVEMENT_GRANULARITY_LINE:
            case MOVEMENT_GRANULARITY_PARAGRAPH:
                return true;
        }
        return false;
    }

    // BrowserAccessibilityStateListener

    @Override
    public void onAccessibilityStateChanged(
            AccessibilityState.State oldAccessibilityState,
            AccessibilityState.State newAccessibilityState) {
        refreshNativeState();
    }

    // WebContentsAccessibility

    @Override
    public void setObscuredByAnotherView(boolean isObscured) {
        assert mIsObscuredByAnotherView == null || isObscured != mIsObscuredByAnotherView
                : "Two clients are both trying to obscure web contents accessibility. These are "
                        + "duplicate requests, or prone to error.";
        mIsObscuredByAnotherView = isObscured;
        sendAccessibilityEvent(View.NO_ID, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
    }

    private boolean shouldPreventNativeEngineUse() {
        return mIsObscuredByAnotherView != null && mIsObscuredByAnotherView;
    }

    @Override
    public void setShouldFocusOnPageLoad(boolean on) {
        mShouldFocusOnPageLoad = on;
    }

    @Override
    public void setIsImageDescriptionsCandidate(boolean isImageDescriptionsCandidate) {
        mIsImageDescriptionsCandidate = isImageDescriptionsCandidate;
    }

    @Override
    public void setIsAutoDisableAccessibilityCandidate(
            boolean isAutoDisableAccessibilityCandidate) {
        mIsAutoDisableAccessibilityCandidate = isAutoDisableAccessibilityCandidate;
    }

    @Override
    public void onProvideVirtualStructure(
            final ViewStructure structure, final boolean ignoreScrollOffset) {
        // Do not collect accessibility tree in incognito mode
        if (mDelegate.isIncognito()) {
            structure.setChildCount(0);
            return;
        }
        structure.setChildCount(1);
        final ViewStructure viewRoot = structure.asyncNewChild(0);
        viewRoot.setClassName("");
        viewRoot.setHint(mProductVersion);

        WebContents webContents = mDelegate.getWebContents();
        if (webContents != null && !webContents.isDestroyed()) {
            Bundle extras = viewRoot.getExtras();
            extras.putCharSequence(EXTRAS_KEY_URL, webContents.getVisibleUrl().getSpec());
        }

        mHasFinishedLatestAccessibilitySnapshot = false;
        long beforeSnapshotTimeMs = SystemClock.elapsedRealtime();

        if (ContentFeatureMap.isEnabled(ContentFeatureList.ACCESSIBILITY_UNIFIED_SNAPSHOTS)) {
            mNativeAssistDataObj =
                    WebContentsAccessibilityImplJni.get()
                            .initForAssistData(
                                    WebContentsAccessibilityImpl.this,
                                    webContents,
                                    new AssistDataBuilder());

            WebContentsAccessibilityImplJni.get()
                    .requestAccessibilityTreeSnapshot(
                            mNativeAssistDataObj,
                            viewRoot,
                            mDelegate.getAccessibilityCoordinates(),
                            mView,
                            () -> onSnapshotDoneCallback(viewRoot, beforeSnapshotTimeMs));
        } else {
            mDelegate.requestAccessibilitySnapshot(
                    viewRoot, () -> onSnapshotDoneCallback(viewRoot, beforeSnapshotTimeMs));
        }
    }

    private void onSnapshotDoneCallback(ViewStructure viewRoot, long beforeSnapshotTimeMs) {
        viewRoot.asyncCommit();
        mHasFinishedLatestAccessibilitySnapshot = true;

        if (AccessibilityFeaturesMap.isEnabled(
                AccessibilityFeatures.ACCESSIBILITY_SNAPSHOT_STRESS_TESTS)) {
            long snapshotRuntimeMs = SystemClock.elapsedRealtime() - beforeSnapshotTimeMs;
            RecordHistogram.recordLinearCountHistogram(
                    "Accessibility.AXTreeSnapshotter.Snapshot.EndToEndRuntime",
                    (int) snapshotRuntimeMs,
                    1,
                    5 * 1000,
                    100);
        }

        if (ContentFeatureMap.isEnabled(ContentFeatureList.ACCESSIBILITY_UNIFIED_SNAPSHOTS)) {
            // In some cases (e.g. testing) the full engine may also be running, so don't delete.
            if (!isNativeInitialized()) {
                WebContentsAccessibilityImplJni.get().deleteEarly(mNativeAssistDataObj);
                mNativeAssistDataObj = 0;
            }
        }
    }

    @Override
    public boolean performAction(int virtualViewId, int action, Bundle arguments) {
        // We don't support any actions on the host view or nodes
        // that are not (any longer) in the tree.
        if (!isAccessibilityEnabled()
                || shouldPreventNativeEngineUse()
                || !WebContentsAccessibilityImplJni.get().isNodeValid(mNativeObj, virtualViewId)) {
            return false;
        }

        if (mTracker != null) mTracker.addAction(action, arguments);

        // Constant expressions are required for switches. To avoid duplicating aspects of the
        // framework, or adding an enum or IntDef to the codebase, we opt for an if/else-if
        // approach. The benefits of using the Compat library makes up for the messier code.
        if (action == ACTION_ACCESSIBILITY_FOCUS.getId()) {
            if (!moveAccessibilityFocusToId(virtualViewId)) return true;
            if (!mIsHovering) {
                scrollToMakeNodeVisible(mAccessibilityFocusId);
            } else {
                mPendingScrollToMakeNodeVisible = true;
            }
            return true;
        } else if (action == ACTION_CLEAR_ACCESSIBILITY_FOCUS.getId()) {
            // ALWAYS respond with TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED whether we thought
            // it had focus or not, so that the Android framework cache is correct.
            sendAccessibilityEvent(
                    virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
            if (mAccessibilityFocusId == virtualViewId) {
                WebContentsAccessibilityImplJni.get()
                        .moveAccessibilityFocus(mNativeObj, mAccessibilityFocusId, View.NO_ID);
                mAccessibilityFocusId = View.NO_ID;
            }
            if (mLastHoverId == virtualViewId) {
                sendAccessibilityEvent(mLastHoverId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
                mLastHoverId = View.NO_ID;
            }
            return true;
        } else if (action == ACTION_CLICK.getId()) {
            if (!mView.hasFocus()) mView.requestFocus();
            performClick(virtualViewId);
            return true;
        } else if (action == ACTION_FOCUS.getId()) {
            if (!mView.hasFocus()) mView.requestFocus();
            WebContentsAccessibilityImplJni.get().focus(mNativeObj, virtualViewId);
            return true;
        } else if (action == ACTION_CLEAR_FOCUS.getId()) {
            WebContentsAccessibilityImplJni.get().blur(mNativeObj);
            return true;
        } else if (action == ACTION_NEXT_HTML_ELEMENT.getId()) {
            if (arguments == null) return false;
            String elementType = arguments.getString(ACTION_ARGUMENT_HTML_ELEMENT_STRING);
            if (elementType == null) return false;
            elementType = elementType.toUpperCase(Locale.US);
            return jumpToElementType(
                    virtualViewId, elementType, /* forwards= */ true, /* canWrap= */ false);
        } else if (action == ACTION_PREVIOUS_HTML_ELEMENT.getId()) {
            if (arguments == null) return false;
            String elementType = arguments.getString(ACTION_ARGUMENT_HTML_ELEMENT_STRING);
            if (elementType == null) return false;
            elementType = elementType.toUpperCase(Locale.US);
            return jumpToElementType(
                    virtualViewId,
                    elementType,
                    /* forwards= */ false,
                    /* canWrap= */ virtualViewId == mCurrentRootId);
        } else if (action == ACTION_SET_TEXT.getId()) {
            if (!WebContentsAccessibilityImplJni.get().isEditableText(mNativeObj, virtualViewId)) {
                return false;
            }
            if (arguments == null) return false;
            CharSequence bundleText =
                    arguments.getCharSequence(ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE);
            if (bundleText == null) return false;
            String newText = bundleText.toString();
            WebContentsAccessibilityImplJni.get()
                    .setTextFieldValue(mNativeObj, virtualViewId, newText);
            // Match Android framework and set the cursor to the end of the text field.
            WebContentsAccessibilityImplJni.get()
                    .setSelection(mNativeObj, virtualViewId, newText.length(), newText.length());
            return true;
        } else if (action == ACTION_SET_SELECTION.getId()) {
            if (!WebContentsAccessibilityImplJni.get().isEditableText(mNativeObj, virtualViewId)) {
                return false;
            }
            int selectionStart = 0;
            int selectionEnd = 0;
            if (arguments != null) {
                selectionStart = arguments.getInt(ACTION_ARGUMENT_SELECTION_START_INT);
                selectionEnd = arguments.getInt(ACTION_ARGUMENT_SELECTION_END_INT);
            }
            WebContentsAccessibilityImplJni.get()
                    .setSelection(mNativeObj, virtualViewId, selectionStart, selectionEnd);
            return true;
        } else if (action == ACTION_NEXT_AT_MOVEMENT_GRANULARITY.getId()) {
            if (arguments == null) return false;
            int granularity = arguments.getInt(ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
            boolean extend = arguments.getBoolean(ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN);
            if (!isValidMovementGranularity(granularity)) {
                return false;
                // ATs view paragraphs as a granularity rather than an element type to jump between.
                // As a stopgap until we implement an actual paragraph granularity, we can send
                // these movements to jumpToElementType instead to allow AT users to at least
                // navigate backward and forward by paragraph
                // TODO(jacklynch): Implement paragraph granularity and remove this block
            } else if (granularity == MOVEMENT_GRANULARITY_PARAGRAPH) {
                return jumpToElementType(
                        virtualViewId,
                        PARAGRAPH_ELEMENT_TYPE,
                        /* forwards= */ true,
                        /* canWrap= */ false);
            }
            return nextAtGranularity(granularity, extend, virtualViewId);
        } else if (action == ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY.getId()) {
            if (arguments == null) return false;
            int granularity = arguments.getInt(ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
            boolean extend = arguments.getBoolean(ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN);
            if (!isValidMovementGranularity(granularity)) {
                return false;
                // ATs view paragraphs as a granularity rather than an element type to jump between.
                // As a stopgap until we implement an actual paragraph granularity, we can send
                // these movements to jumpToElementType instead to allow AT users to at least
                // navigate backward and forward by paragraph
                // TODO(jacklynch): Implement paragraph granularity and remove this block
            } else if (granularity == MOVEMENT_GRANULARITY_PARAGRAPH) {
                return jumpToElementType(
                        virtualViewId,
                        PARAGRAPH_ELEMENT_TYPE,
                        /* forwards= */ false,
                        /* canWrap= */ virtualViewId == mCurrentRootId);
            }
            return previousAtGranularity(granularity, extend, virtualViewId);
        } else if (action == ACTION_SCROLL_FORWARD.getId()) {
            return scrollForward(virtualViewId);
        } else if (action == ACTION_SCROLL_BACKWARD.getId()) {
            return scrollBackward(virtualViewId);
        } else if (action == ACTION_CUT.getId()) {
            if (mDelegate.getWebContents() != null) {
                ((WebContentsImpl) mDelegate.getWebContents()).cut();
                return true;
            }
            return false;
        } else if (action == ACTION_COPY.getId()) {
            if (mDelegate.getWebContents() != null) {
                ((WebContentsImpl) mDelegate.getWebContents()).copy();
                return true;
            }
            return false;
        } else if (action == ACTION_PASTE.getId()) {
            if (mDelegate.getWebContents() != null) {
                ((WebContentsImpl) mDelegate.getWebContents()).paste();
                return true;
            }
            return false;
        } else if (action == ACTION_COLLAPSE.getId() || action == ACTION_EXPAND.getId()) {
            // If something is collapsible or expandable, just activate it to toggle.
            performClick(virtualViewId);
            return true;
        } else if (action == ACTION_SHOW_ON_SCREEN.getId()) {
            scrollToMakeNodeVisible(virtualViewId);
            return true;
        } else if (action == ACTION_CONTEXT_CLICK.getId() || action == ACTION_LONG_CLICK.getId()) {
            WebContentsAccessibilityImplJni.get().showContextMenu(mNativeObj, virtualViewId);
            return true;
        } else if (action == ACTION_SCROLL_UP.getId() || action == ACTION_PAGE_UP.getId()) {
            return WebContentsAccessibilityImplJni.get()
                    .scroll(
                            mNativeObj,
                            virtualViewId,
                            ScrollDirection.UP,
                            action == ACTION_PAGE_UP.getId());
        } else if (action == ACTION_SCROLL_DOWN.getId() || action == ACTION_PAGE_DOWN.getId()) {
            return WebContentsAccessibilityImplJni.get()
                    .scroll(
                            mNativeObj,
                            virtualViewId,
                            ScrollDirection.DOWN,
                            action == ACTION_PAGE_DOWN.getId());
        } else if (action == ACTION_SCROLL_LEFT.getId() || action == ACTION_PAGE_LEFT.getId()) {
            return WebContentsAccessibilityImplJni.get()
                    .scroll(
                            mNativeObj,
                            virtualViewId,
                            ScrollDirection.LEFT,
                            action == ACTION_PAGE_LEFT.getId());
        } else if (action == ACTION_SCROLL_RIGHT.getId() || action == ACTION_PAGE_RIGHT.getId()) {
            return WebContentsAccessibilityImplJni.get()
                    .scroll(
                            mNativeObj,
                            virtualViewId,
                            ScrollDirection.RIGHT,
                            action == ACTION_PAGE_RIGHT.getId());
        } else if (action == ACTION_SET_PROGRESS.getId()) {
            if (arguments == null) return false;
            if (!arguments.containsKey(ACTION_ARGUMENT_PROGRESS_VALUE)) return false;
            return WebContentsAccessibilityImplJni.get()
                    .setRangeValue(
                            mNativeObj,
                            virtualViewId,
                            arguments.getFloat(ACTION_ARGUMENT_PROGRESS_VALUE));
        } else if (action == ACTION_IME_ENTER.getId()) {
            if (mDelegate.getWebContents() != null) {
                if (ImeAdapterImpl.fromWebContents(mDelegate.getWebContents()) != null) {
                    // We send an unspecified action to ensure Enter key is hit
                    return ImeAdapterImpl.fromWebContents(mDelegate.getWebContents())
                            .performEditorAction(EditorInfo.IME_ACTION_UNSPECIFIED);
                }
            }
            return false;
        } else {
            // This should never be hit, so do the equivalent of NOTREACHED;
            assert false : "AccessibilityNodeProvider called performAction with unexpected action.";
        }

        return false;
    }

    @Override
    public void onAutofillPopupDisplayed(View autofillPopupView) {
        if (isAccessibilityEnabled()) {
            mAutofillPopupView = autofillPopupView;
            WebContentsAccessibilityImplJni.get().onAutofillPopupDisplayed(mNativeObj);
        }
    }

    @Override
    public void onAutofillPopupDismissed() {
        if (isAccessibilityEnabled()) {
            WebContentsAccessibilityImplJni.get().onAutofillPopupDismissed(mNativeObj);
            mAutofillPopupView = null;
        }
    }

    @Override
    public void onAutofillPopupAccessibilityFocusCleared() {
        if (isAccessibilityEnabled()) {
            int id =
                    WebContentsAccessibilityImplJni.get()
                            .getIdForElementAfterElementHostingAutofillPopup(mNativeObj);
            if (id == 0) return;

            moveAccessibilityFocusToId(id);
            scrollToMakeNodeVisible(mAccessibilityFocusId);
        }
    }

    // Returns true if the hover event is to be consumed by accessibility feature.
    @CalledByNative
    private boolean onHoverEvent(int action) {
        if (!isAccessibilityEnabled()) {
            return false;
        }

        if (action == MotionEvent.ACTION_HOVER_EXIT) {
            mIsHovering = false;
            return true;
        }

        mIsHovering = true;
        return true;
    }

    @Override
    public boolean onHoverEventNoRenderer(MotionEvent event) {
        if (!onHoverEvent(event.getAction())) return false;

        float x = event.getX() + mDelegate.getAccessibilityCoordinates().getScrollX();
        float y = event.getY() + mDelegate.getAccessibilityCoordinates().getScrollY();
        return WebContentsAccessibilityImplJni.get().onHoverEventNoRenderer(mNativeObj, x, y);
    }

    @Override
    public void resetFocus() {
        if (mNativeObj == 0) return;

        // Reset accessibility focus.
        WebContentsAccessibilityImplJni.get()
                .moveAccessibilityFocus(mNativeObj, mAccessibilityFocusId, View.NO_ID);
        mAccessibilityFocusId = View.NO_ID;

        sendAccessibilityEvent(mLastHoverId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
        mLastHoverId = View.NO_ID;
    }

    @Override
    public void restoreFocus() {
        if (isAccessibilityEnabled() && mLastAccessibilityFocusId != View.NO_ID) {
            moveAccessibilityFocusToId(mLastAccessibilityFocusId);
            scrollToMakeNodeVisible(mLastAccessibilityFocusId);
        }
    }

    /**
     * Notify us when the frame info is initialized,
     * the first time, since until that point, we can't use AccessibilityCoordinates to transform
     * web coordinates to screen coordinates.
     */
    @CalledByNative
    private void notifyFrameInfoInitialized() {
        if (mNotifyFrameInfoInitializedCalled) return;

        mNotifyFrameInfoInitializedCalled = true;

        // Invalidate the container view, since the chrome accessibility tree is now
        // ready and listed as the child of the container view.
        sendAccessibilityEvent(View.NO_ID, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);

        // (Re-) focus focused element, since we weren't able to create an
        // AccessibilityNodeInfoCompat for this element before.
        if (!mShouldFocusOnPageLoad) return;
        if (mAccessibilityFocusId != View.NO_ID) {
            moveAccessibilityFocusToId(mAccessibilityFocusId);
        }
    }

    private boolean jumpToElementType(
            int virtualViewId, String elementType, boolean forwards, boolean canWrap) {
        int id =
                WebContentsAccessibilityImplJni.get()
                        .findElementType(
                                mNativeObj,
                                virtualViewId,
                                elementType,
                                forwards,
                                canWrap,
                                elementType.isEmpty());
        if (id == 0) return false;

        moveAccessibilityFocusToId(id);
        scrollToMakeNodeVisible(mAccessibilityFocusId);
        return true;
    }

    private void setGranularityAndUpdateSelection(int granularity) {
        mSelectionGranularity = granularity;

        if (WebContentsAccessibilityImplJni.get().isEditableText(mNativeObj, mAccessibilityFocusId)
                && WebContentsAccessibilityImplJni.get()
                        .isFocused(mNativeObj, mAccessibilityFocusId)) {
            // If selection/cursor are "unassigned" (e.g. first user swipe), then assign as needed
            if (mSelectionStart == -1) {
                mSelectionStart =
                        WebContentsAccessibilityImplJni.get()
                                .getEditableTextSelectionStart(mNativeObj, mAccessibilityFocusId);
            }
            if (mCursorIndex == -1) {
                mCursorIndex =
                        WebContentsAccessibilityImplJni.get()
                                .getEditableTextSelectionEnd(mNativeObj, mAccessibilityFocusId);
            }
        }
    }

    private boolean nextAtGranularity(int granularity, boolean extendSelection, int virtualViewId) {
        if (virtualViewId != mSelectionNodeId) return false;
        setGranularityAndUpdateSelection(granularity);

        // This calls finishGranularityMoveNext when it's done.
        // If we are extending or starting a selection, pass the current cursor index, otherwise
        // default to selection start, which will be the position at the end of the last move
        if (extendSelection && mIsCurrentlyExtendingSelection) {
            return WebContentsAccessibilityImplJni.get()
                    .nextAtGranularity(
                            mNativeObj,
                            mSelectionGranularity,
                            extendSelection,
                            virtualViewId,
                            mCursorIndex);
        } else {
            return WebContentsAccessibilityImplJni.get()
                    .nextAtGranularity(
                            mNativeObj,
                            mSelectionGranularity,
                            extendSelection,
                            virtualViewId,
                            mSelectionStart);
        }
    }

    private boolean previousAtGranularity(
            int granularity, boolean extendSelection, int virtualViewId) {
        if (virtualViewId != mSelectionNodeId) return false;
        setGranularityAndUpdateSelection(granularity);

        // This calls finishGranularityMovePrevious when it's done.
        return WebContentsAccessibilityImplJni.get()
                .previousAtGranularity(
                        mNativeObj,
                        mSelectionGranularity,
                        extendSelection,
                        virtualViewId,
                        mCursorIndex);
    }

    @CalledByNative
    private void finishGranularityMoveNext(
            String text, boolean extendSelection, int itemStartIndex, int itemEndIndex) {
        // Prepare to send both a selection and a traversal event in sequence.
        AccessibilityEvent selectionEvent =
                buildAccessibilityEvent(
                        mSelectionNodeId, AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
        if (selectionEvent == null) return;

        AccessibilityEvent traverseEvent =
                buildAccessibilityEvent(
                        mSelectionNodeId,
                        AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY);
        if (traverseEvent == null) {
            selectionEvent.recycle();
            return;
        }

        // Build selection event dependent on whether user is extending selection or not
        if (extendSelection) {
            // User started selecting, set the selection start point (only set once per selection)
            if (!mIsCurrentlyExtendingSelection) {
                mIsCurrentlyExtendingSelection = true;
                mSelectionStart = itemStartIndex;
            }

            selectionEvent.setFromIndex(mSelectionStart);
            selectionEvent.setToIndex(itemEndIndex);

        } else {
            // User is no longer selecting, or wasn't originally, reset values
            mIsCurrentlyExtendingSelection = false;
            mSelectionStart = itemEndIndex;

            // Set selection to/from indices to new cursor position, itemEndIndex with forwards nav
            selectionEvent.setFromIndex(itemEndIndex);
            selectionEvent.setToIndex(itemEndIndex);
        }

        // Moving forwards, cursor is now at end of granularity move (itemEndIndex)
        mCursorIndex = itemEndIndex;
        selectionEvent.setItemCount(text.length());

        // Call back to native code to update selection
        setSelection(selectionEvent);

        // Build traverse event, set appropriate action
        traverseEvent.setFromIndex(itemStartIndex);
        traverseEvent.setToIndex(itemEndIndex);
        traverseEvent.setItemCount(text.length());
        traverseEvent.setMovementGranularity(mSelectionGranularity);
        traverseEvent.setContentDescription(text);
        traverseEvent.setAction(ACTION_NEXT_AT_MOVEMENT_GRANULARITY.getId());

        requestSendAccessibilityEvent(selectionEvent);
        requestSendAccessibilityEvent(traverseEvent);

        // Suppress the next event since we have already sent traverse and selection for this move
        mSuppressNextSelectionEvent = true;
    }

    @CalledByNative
    private void finishGranularityMovePrevious(
            String text, boolean extendSelection, int itemStartIndex, int itemEndIndex) {
        // Prepare to send both a selection and a traversal event in sequence.
        AccessibilityEvent selectionEvent =
                buildAccessibilityEvent(
                        mSelectionNodeId, AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
        if (selectionEvent == null) return;

        AccessibilityEvent traverseEvent =
                buildAccessibilityEvent(
                        mSelectionNodeId,
                        AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY);
        if (traverseEvent == null) {
            selectionEvent.recycle();
            return;
        }

        // Build selection event dependent on whether user is extending selection or not
        if (extendSelection) {
            // User started selecting, set the selection start point (only set once per selection)
            if (!mIsCurrentlyExtendingSelection) {
                mIsCurrentlyExtendingSelection = true;
                mSelectionStart = itemEndIndex;
            }

            selectionEvent.setFromIndex(mSelectionStart);
            selectionEvent.setToIndex(itemStartIndex);

        } else {
            // User is no longer selecting, or wasn't originally, reset values
            mIsCurrentlyExtendingSelection = false;
            mSelectionStart = itemStartIndex;

            // Set selection to/from indices to new cursor position, itemStartIndex with back nav
            selectionEvent.setFromIndex(itemStartIndex);
            selectionEvent.setToIndex(itemStartIndex);
        }

        // Moving backwards, cursor is now at the start of the granularity move (itemStartIndex)
        mCursorIndex = itemStartIndex;
        selectionEvent.setItemCount(text.length());

        // Call back to native code to update selection
        setSelection(selectionEvent);

        // Build traverse event, set appropriate action
        traverseEvent.setFromIndex(itemStartIndex);
        traverseEvent.setToIndex(itemEndIndex);
        traverseEvent.setItemCount(text.length());
        traverseEvent.setMovementGranularity(mSelectionGranularity);
        traverseEvent.setContentDescription(text);
        traverseEvent.setAction(ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY.getId());

        requestSendAccessibilityEvent(selectionEvent);
        requestSendAccessibilityEvent(traverseEvent);

        // Suppress the next event since we have already sent traverse and selection for this move
        mSuppressNextSelectionEvent = true;
    }

    private void scrollToMakeNodeVisible(int virtualViewId) {
        if (mDelegate.getNativeAXTree() != 0) {
            mDelegate.scrollToMakeNodeVisible(getAbsolutePositionForNode(virtualViewId));
        } else {
            mPendingScrollToMakeNodeVisible = true;
            WebContentsAccessibilityImplJni.get()
                    .scrollToMakeNodeVisible(mNativeObj, virtualViewId);
        }
    }

    private void performClick(int virtualViewId) {
        if (mDelegate.getNativeAXTree() != 0) {
            mDelegate.performClick(getAbsolutePositionForNode(virtualViewId));
        } else {
            WebContentsAccessibilityImplJni.get().click(mNativeObj, virtualViewId);
        }
    }

    private void setSelection(AccessibilityEvent selectionEvent) {
        if (WebContentsAccessibilityImplJni.get().isEditableText(mNativeObj, mSelectionNodeId)
                && WebContentsAccessibilityImplJni.get().isFocused(mNativeObj, mSelectionNodeId)) {
            WebContentsAccessibilityImplJni.get()
                    .setSelection(
                            mNativeObj,
                            mSelectionNodeId,
                            selectionEvent.getFromIndex(),
                            selectionEvent.getToIndex());
        }
    }

    private boolean scrollForward(int virtualViewId) {
        if (WebContentsAccessibilityImplJni.get().isSlider(mNativeObj, virtualViewId)) {
            return WebContentsAccessibilityImplJni.get()
                    .adjustSlider(mNativeObj, virtualViewId, true);
        } else {
            return WebContentsAccessibilityImplJni.get()
                    .scroll(mNativeObj, virtualViewId, ScrollDirection.FORWARD, false);
        }
    }

    private boolean scrollBackward(int virtualViewId) {
        if (WebContentsAccessibilityImplJni.get().isSlider(mNativeObj, virtualViewId)) {
            return WebContentsAccessibilityImplJni.get()
                    .adjustSlider(mNativeObj, virtualViewId, false);
        } else {
            return WebContentsAccessibilityImplJni.get()
                    .scroll(mNativeObj, virtualViewId, ScrollDirection.BACKWARD, false);
        }
    }

    private boolean moveAccessibilityFocusToId(int newAccessibilityFocusId) {
        if (newAccessibilityFocusId == mAccessibilityFocusId) return false;

        if (newAccessibilityFocusId != View.NO_ID) {
            mLastAccessibilityFocusId = newAccessibilityFocusId;
        }

        WebContentsAccessibilityImplJni.get()
                .moveAccessibilityFocus(mNativeObj, mAccessibilityFocusId, newAccessibilityFocusId);

        mAccessibilityFocusId = newAccessibilityFocusId;
        // Used to store the node (edit text field) that has input focus but not a11y focus.
        // Usually while the user is typing in an edit text field, a11y is on the IME and input
        // focus is on the edit field. Granularity move needs to know where the input focus is.
        mSelectionNodeId = mAccessibilityFocusId;
        mSelectionGranularity = NO_GRANULARITY_SELECTED;
        mIsCurrentlyExtendingSelection = false;
        mSelectionStart = -1;
        mCursorIndex =
                WebContentsAccessibilityImplJni.get()
                        .getTextLength(mNativeObj, mAccessibilityFocusId);
        mSuppressNextSelectionEvent = false;

        if (WebContentsAccessibilityImplJni.get()
                .isAutofillPopupNode(mNativeObj, mAccessibilityFocusId)) {
            mAutofillPopupView.requestFocus();
        }

        // Android has a bug that can lead to the a11y focus not being rendered: b/264356970
        // The reason is that this event alone is not enough to rerender, this line works it
        // around by adding the rerender trigger via the underlying view.
        // TODO(b/264356970): Remove when all supported platforms have this bug fixed.
        mView.invalidate();

        sendAccessibilityEvent(
                mAccessibilityFocusId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
        return true;
    }

    /**
     * Send a WINDOW_CONTENT_CHANGED event after a short delay. This helps throttle such
     * events from firing too quickly during animations, for example.
     */
    @CalledByNative
    private void sendDelayedWindowContentChangedEvent() {
        sendAccessibilityEvent(View.NO_ID, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
    }

    private void sendAccessibilityEvent(int virtualViewId, int eventType) {
        // The container view is indicated by a virtualViewId of NO_ID; post these events directly
        // since there's no web-specific information to attach.
        if (virtualViewId == View.NO_ID) {
            mView.sendAccessibilityEvent(eventType);
            return;
        }

        // Do not send an event when we want to suppress this event, update flag for next event
        if (mSuppressNextSelectionEvent
                && eventType == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED) {
            mSuppressNextSelectionEvent = false;
            return;
        }

        mHistogramRecorder.incrementEnqueuedEvents();
        mEventDispatcher.enqueueEvent(virtualViewId, eventType);
    }

    private AccessibilityEvent buildAccessibilityEvent(int virtualViewId, int eventType) {
        // If accessibility is disabled, node is invalid, or we don't have any frame info,
        // then the virtual hierarchy doesn't exist in the view of the Android framework,
        // so should never send any events.
        if (!isAccessibilityEnabled()
                || !isFrameInfoInitialized()
                || !WebContentsAccessibilityImplJni.get().isNodeValid(mNativeObj, virtualViewId)) {
            return null;
        }

        final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
        event.setPackageName(mContext.getPackageName());
        event.setSource(mView, virtualViewId);
        if (eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
            event.setContentChangeTypes(AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
        }
        if (!WebContentsAccessibilityImplJni.get()
                .populateAccessibilityEvent(mNativeObj, event, virtualViewId, eventType)) {
            event.recycle();
            return null;
        }
        return event;
    }

    private AccessibilityNodeInfoCompat createNodeForHost(int rootId) {
        // Since we don't want the parent to be focusable, but we can't remove
        // actions from a node, copy over the necessary fields.
        final AccessibilityNodeInfoCompat result = AccessibilityNodeInfoCompat.obtain(mView);
        // mView requires an |AccessibilityNodeInfo| object here, so we keep the |source| as the
        // non-Compat type rather than unwrapping an |AccessibilityNodeInfoCompat| object.
        final AccessibilityNodeInfo source = AccessibilityNodeInfo.obtain(mView);
        mView.onInitializeAccessibilityNodeInfo(source);

        // Copy over parent and screen bounds.
        Rect rect = new Rect();
        source.getBoundsInParent(rect);
        result.setBoundsInParent(rect);
        source.getBoundsInScreen(rect);
        result.setBoundsInScreen(rect);

        // Set up the parent view, if applicable.
        final ViewParent parent = mView.getParentForAccessibility();
        if (parent instanceof View) {
            result.setParent((View) parent);
        }

        // Populate the minimum required fields.
        result.setVisibleToUser(source.isVisibleToUser());
        result.setEnabled(source.isEnabled());
        result.setPackageName(source.getPackageName());
        result.setClassName(source.getClassName());

        // Add the Chrome root node.
        if (isFrameInfoInitialized()) {
            result.addChild(mView, rootId);
        }

        return result;
    }

    /**
     * Returns whether or not the frame info is initialized, meaning we can safely
     * convert web coordinates to screen coordinates. When this is first initialized,
     * notifyFrameInfoInitialized is called - but we shouldn't check whether or not
     * that method was called as a way to determine if frame info is valid because
     * notifyFrameInfoInitialized might not be called at all if AccessibilityCoordinates
     * gets initialized first.
     */
    private boolean isFrameInfoInitialized() {
        if (mDelegate.getWebContents() == null && mNativeObj == 0) {
            // We already got frame info since WebContents finished its lifecycle.
            return true;
        }

        AccessibilityCoordinates ac = mDelegate.getAccessibilityCoordinates();
        return ac.getContentWidthCss() != 0.0 || ac.getContentHeightCss() != 0.0;
    }

    @CalledByNative
    private void handleFocusChanged(int id) {
        // If |mShouldFocusOnPageLoad| is false, that means this is a WebView and
        // we should avoid moving accessibility focus when the page loads, but more
        // generally we should avoid moving accessibility focus whenever it's not
        // already within this WebView.
        if (!mShouldFocusOnPageLoad && mAccessibilityFocusId == View.NO_ID) return;

        sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_FOCUSED);
        moveAccessibilityFocusToId(id);
    }

    @CalledByNative
    private void handleCheckStateChanged(int id) {
        // If the node has accessibility focus, fire TYPE_VIEW_CLICKED event. This check ensures
        // only necessary announcements are made (e.g. changing a radio group selection
        // would erroneously announce "checked not checked" without this check)
        if (mAccessibilityFocusId == id) {
            sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_CLICKED);
        }
    }

    @CalledByNative
    private void handleStateDescriptionChanged(int id) {
        if (isAccessibilityEnabled()) {
            AccessibilityEvent event =
                    AccessibilityEvent.obtain(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
            if (event == null) return;

            event.setContentChangeTypes(AccessibilityEvent.CONTENT_CHANGE_TYPE_STATE_DESCRIPTION);
            event.setSource(mView, id);
            requestSendAccessibilityEvent(event);
        }
    }

    @CalledByNative
    private void handleClicked(int id) {
        sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_CLICKED);
    }

    @CalledByNative
    private void handleTextSelectionChanged(int id) {
        sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
    }

    @CalledByNative
    private void handleTextContentChanged(int id) {
        AccessibilityEvent event =
                buildAccessibilityEvent(id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
        if (event != null) {
            event.setContentChangeTypes(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);
            requestSendAccessibilityEvent(event);
        }
    }

    @CalledByNative
    private void handleEditableTextChanged(int id) {
        sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
    }

    @CalledByNative
    private void handleSliderChanged(int id) {
        // If the node has accessibility focus, fire TYPE_VIEW_SELECTED, which triggers
        // TalkBack to announce the change. If not, fire TYPE_VIEW_SCROLLED, which
        // does not trigger an immediate announcement but still ensures some event is fired.
        if (mAccessibilityFocusId == id) {
            sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_SELECTED);
        } else {
            sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_SCROLLED);
        }
    }

    @CalledByNative
    private void handleContentChanged(int id) {
        sendAccessibilityEvent(id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
    }

    @CalledByNative
    private void handleNavigate(int newRootId) {
        mAccessibilityFocusId = View.NO_ID;
        mLastAccessibilityFocusId = View.NO_ID;
        mCurrentRootId = newRootId;
        // Invalidate the host, since its child is now gone.
        sendAccessibilityEvent(View.NO_ID, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
    }

    @CalledByNative
    protected void handleScrollPositionChanged(int id) {
        sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_SCROLLED);
        if (mPendingScrollToMakeNodeVisible) {
            sendAccessibilityEvent(id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
            mPendingScrollToMakeNodeVisible = false;
        }
    }

    @CalledByNative
    private void handleScrolledToAnchor(int id) {
        moveAccessibilityFocusToId(id);
    }

    @CalledByNative
    private void handleHover(int id) {
        if (mLastHoverId == id) return;
        if (!mIsHovering) return;

        sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
        // The above call doesn't work reliably for nodes that weren't in the viewport when
        // using an AXTree that was cached.
        if (mDelegate.getNativeAXTree() != 0) {
            // As a workaround force the node into focus when a paint preview is showing.
            moveAccessibilityFocusToId(id);
        }
    }

    @CalledByNative
    @SuppressLint("WrongConstant")
    protected void handleDialogModalOpened(int virtualViewId) {
        if (isAccessibilityEnabled()) {
            AccessibilityEvent event =
                    AccessibilityEvent.obtain(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
            if (event == null) return;

            event.setContentChangeTypes(CONTENT_CHANGE_TYPE_PANE_APPEARED);
            event.setSource(mView, virtualViewId);
            requestSendAccessibilityEvent(event);
        }
    }

    @CalledByNative
    private void announceLiveRegionText(String text) {
        if (isAccessibilityEnabled()) {
            AccessibilityEvent event =
                    AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT);
            if (event == null) return;

            event.getText().add(text);
            event.setContentDescription(null);
            requestSendAccessibilityEvent(event);
        }
    }

    protected boolean areInlineTextBoxesLoaded(int virtualViewId) {
        return WebContentsAccessibilityImplJni.get()
                .areInlineTextBoxesLoaded(mNativeObj, virtualViewId);
    }

    protected void loadInlineTextBoxes(int virtualViewId) {
        WebContentsAccessibilityImplJni.get().loadInlineTextBoxes(mNativeObj, virtualViewId);
    }

    protected int[] getCharacterBoundingBoxes(
            int virtualViewId, int positionInfoStartIndex, int positionInfoLength) {
        return WebContentsAccessibilityImplJni.get()
                .getCharacterBoundingBoxes(
                        mNativeObj, virtualViewId, positionInfoStartIndex, positionInfoLength);
    }

    protected void requestSendAccessibilityEvent(AccessibilityEvent event) {
        // If there is no parent, then the event can be ignored. In general the parent is only
        // transiently null (such as during teardown, switching tabs...). Also ensure that
        // accessibility is still enabled, throttling may result in events sent late.
        if (mView.getParent() != null && isAccessibilityEnabled()) {
            mHistogramRecorder.incrementDispatchedEvents();
            if (mTracker != null) mTracker.addEvent(event);
            try {
                mView.getParent().requestSendAccessibilityEvent(mView, event);
            } catch (IllegalStateException ignored) {
                // During boot-up of some content shell tests, events will erroneously be sent even
                // though the AccessibilityManager is not enabled, resulting in a crash.
                // TODO(mschillaci): Address flakiness to remove this try/catch, crbug.com/1186376.
            }
        }
    }

    private Rect getAbsolutePositionForNode(int virtualViewId) {
        int[] coords =
                WebContentsAccessibilityImplJni.get()
                        .getAbsolutePositionForNode(mNativeObj, virtualViewId);
        if (coords == null) return null;

        return new Rect(coords[0], coords[1], coords[2], coords[3]);
    }

    @CalledByNative
    private void setAccessibilityEventBaseAttributes(
            AccessibilityEvent event,
            boolean checked,
            boolean enabled,
            boolean password,
            boolean scrollable,
            int currentItemIndex,
            int itemCount,
            int scrollX,
            int scrollY,
            int maxScrollX,
            int maxScrollY,
            String className) {
        event.setChecked(checked);
        event.setEnabled(enabled);
        event.setPassword(password);
        event.setScrollable(scrollable);
        event.setCurrentItemIndex(currentItemIndex);
        event.setItemCount(itemCount);
        event.setScrollX(scrollX);
        event.setScrollY(scrollY);
        event.setMaxScrollX(maxScrollX);
        event.setMaxScrollY(maxScrollY);
        event.setClassName(className);
    }

    @CalledByNative
    private void setAccessibilityEventTextChangedAttrs(
            AccessibilityEvent event,
            int fromIndex,
            int addedCount,
            int removedCount,
            String beforeText,
            String text) {
        event.setFromIndex(fromIndex);
        event.setAddedCount(addedCount);
        event.setRemovedCount(removedCount);
        event.setBeforeText(beforeText);
        event.getText().add(text);
    }

    @CalledByNative
    private void setAccessibilityEventSelectionAttrs(
            AccessibilityEvent event, int fromIndex, int toIndex, int itemCount, String text) {
        event.setFromIndex(fromIndex);
        event.setToIndex(toIndex);
        event.setItemCount(itemCount);
        event.getText().add(text);
    }

    @Override
    public void addExtraDataToAccessibilityNodeInfo(
            int virtualViewId,
            AccessibilityNodeInfoCompat info,
            String extraDataKey,
            Bundle arguments) {
        switch (extraDataKey) {
            case EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY:
                getExtraDataTextCharacterLocations(virtualViewId, info, arguments);
                break;
            case EXTRAS_DATA_REQUEST_IMAGE_DATA_KEY:
                getImageData(virtualViewId, info);
                break;
        }
    }

    private void getExtraDataTextCharacterLocations(
            int virtualViewId, AccessibilityNodeInfoCompat info, Bundle arguments) {
        // Arguments must be provided, but some debug tools may not so guard against this.
        if (arguments == null) return;

        if (!areInlineTextBoxesLoaded(virtualViewId)) {
            loadInlineTextBoxes(virtualViewId);
        }

        int positionInfoStartIndex =
                arguments.getInt(EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX, -1);
        int positionInfoLength =
                arguments.getInt(EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH, -1);
        if (positionInfoLength <= 0 || positionInfoStartIndex < 0) return;

        int[] coords =
                getCharacterBoundingBoxes(
                        virtualViewId, positionInfoStartIndex, positionInfoLength);
        if (coords == null) return;
        assert coords.length == positionInfoLength * 4;

        RectF[] boundingRects = new RectF[positionInfoLength];
        for (int i = 0; i < positionInfoLength; i++) {
            Rect rect =
                    new Rect(
                            coords[4 * i + 0],
                            coords[4 * i + 1],
                            coords[4 * i + 2],
                            coords[4 * i + 3]);
            AccessibilityNodeInfoBuilder.convertWebRectToAndroidCoordinates(
                    rect, info.getExtras(), mDelegate.getAccessibilityCoordinates(), mView);
            boundingRects[i] = new RectF(rect);
        }

        info.getExtras().putParcelableArray(EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY, boundingRects);
    }

    private void getImageData(int virtualViewId, AccessibilityNodeInfoCompat info) {
        boolean hasSentPreviousRequest = mImageDataRequestedNodes.contains(virtualViewId);
        // If the below call returns true, then image data has been set on the node.
        if (!WebContentsAccessibilityImplJni.get()
                .getImageData(mNativeObj, info, virtualViewId, hasSentPreviousRequest)) {
            // If the above call returns false, then the data was missing. The native-side code
            // will have started the asynchronous process to populate the image data if no previous
            // request has been sent. Add this |virtualViewId| to the list of requested nodes.
            mImageDataRequestedNodes.add(virtualViewId);
        }
    }

    @NativeMethods
    interface Natives {
        long init(
                WebContentsAccessibilityImpl caller,
                WebContents webContents,
                AccessibilityNodeInfoBuilder builder);

        long initWithAXTree(
                WebContentsAccessibilityImpl caller,
                long axTreePtr,
                AccessibilityNodeInfoBuilder builder);

        // These two methods are only used for one-off accessibility tree snapshots.
        long initForAssistData(
                WebContentsAccessibilityImpl caller,
                WebContents webContents,
                AssistDataBuilder builder);

        void requestAccessibilityTreeSnapshot(
                long nativeWebContentsAccessibilityAndroid,
                ViewStructure viewRoot,
                AccessibilityDelegate.AccessibilityCoordinates accessibilityCoordinates,
                View view,
                Runnable onDoneCallback);

        void connectInstanceToRootManager(long nativeWebContentsAccessibilityAndroid);

        void setBrowserAXMode(
                WebContentsAccessibilityImpl caller,
                boolean screenReaderMode,
                boolean formControlsMode,
                boolean isAccessibilityEnabled);

        void disableRendererAccessibility(long nativeWebContentsAccessibilityAndroid);

        void reEnableRendererAccessibility(
                long nativeWebContentsAccessibilityAndroid, WebContents webContents);

        void deleteEarly(long nativeWebContentsAccessibilityAndroid);

        void onAutofillPopupDisplayed(long nativeWebContentsAccessibilityAndroid);

        void onAutofillPopupDismissed(long nativeWebContentsAccessibilityAndroid);

        int getIdForElementAfterElementHostingAutofillPopup(
                long nativeWebContentsAccessibilityAndroid);

        int getRootId(long nativeWebContentsAccessibilityAndroid);

        boolean isNodeValid(long nativeWebContentsAccessibilityAndroid, int id);

        boolean isAutofillPopupNode(long nativeWebContentsAccessibilityAndroid, int id);

        boolean isEditableText(long nativeWebContentsAccessibilityAndroid, int id);

        boolean isFocused(long nativeWebContentsAccessibilityAndroid, int id);

        int getEditableTextSelectionStart(long nativeWebContentsAccessibilityAndroid, int id);

        int getEditableTextSelectionEnd(long nativeWebContentsAccessibilityAndroid, int id);

        int[] getAbsolutePositionForNode(long nativeWebContentsAccessibilityAndroid, int id);

        boolean updateCachedAccessibilityNodeInfo(
                long nativeWebContentsAccessibilityAndroid,
                AccessibilityNodeInfoCompat info,
                int id);

        boolean populateAccessibilityNodeInfo(
                long nativeWebContentsAccessibilityAndroid,
                AccessibilityNodeInfoCompat info,
                int id);

        boolean populateAccessibilityEvent(
                long nativeWebContentsAccessibilityAndroid,
                AccessibilityEvent event,
                int id,
                int eventType);

        void click(long nativeWebContentsAccessibilityAndroid, int id);

        void focus(long nativeWebContentsAccessibilityAndroid, int id);

        void blur(long nativeWebContentsAccessibilityAndroid);

        void scrollToMakeNodeVisible(long nativeWebContentsAccessibilityAndroid, int id);

        int findElementType(
                long nativeWebContentsAccessibilityAndroid,
                int startId,
                String elementType,
                boolean forwards,
                boolean canWrapToLastElement,
                boolean useDefaultPredicate);

        void setTextFieldValue(long nativeWebContentsAccessibilityAndroid, int id, String newValue);

        void setSelection(long nativeWebContentsAccessibilityAndroid, int id, int start, int end);

        boolean nextAtGranularity(
                long nativeWebContentsAccessibilityAndroid,
                int selectionGranularity,
                boolean extendSelection,
                int id,
                int cursorIndex);

        boolean previousAtGranularity(
                long nativeWebContentsAccessibilityAndroid,
                int selectionGranularity,
                boolean extendSelection,
                int id,
                int cursorIndex);

        boolean adjustSlider(long nativeWebContentsAccessibilityAndroid, int id, boolean increment);

        void moveAccessibilityFocus(
                long nativeWebContentsAccessibilityAndroid, int oldId, int newId);

        boolean isSlider(long nativeWebContentsAccessibilityAndroid, int id);

        boolean scroll(
                long nativeWebContentsAccessibilityAndroid,
                int id,
                int direction,
                boolean pageScroll);

        boolean setRangeValue(long nativeWebContentsAccessibilityAndroid, int id, float value);

        String getSupportedHtmlElementTypes(long nativeWebContentsAccessibilityAndroid);

        void showContextMenu(long nativeWebContentsAccessibilityAndroid, int id);

        boolean isRootManagerConnected(long nativeWebContentsAccessibilityAndroid);

        boolean areInlineTextBoxesLoaded(long nativeWebContentsAccessibilityAndroid, int id);

        void loadInlineTextBoxes(long nativeWebContentsAccessibilityAndroid, int id);

        int[] getCharacterBoundingBoxes(
                long nativeWebContentsAccessibilityAndroid, int id, int start, int len);

        int getTextLength(long nativeWebContentsAccessibilityAndroid, int id);

        void addSpellingErrorForTesting(
                long nativeWebContentsAccessibilityAndroid, int id, int startOffset, int endOffset);

        void setMaxContentChangedEventsToFireForTesting(
                long nativeWebContentsAccessibilityAndroid, int maxEvents);

        int getMaxContentChangedEventsToFireForTesting(long nativeWebContentsAccessibilityAndroid);

        void signalEndOfTestForTesting(long nativeWebContentsAccessibilityAndroid);

        void setAllowImageDescriptions(
                long nativeWebContentsAccessibilityAndroid, boolean allowImageDescriptions);

        boolean onHoverEventNoRenderer(
                long nativeWebContentsAccessibilityAndroid, float x, float y);

        boolean getImageData(
                long nativeWebContentsAccessibilityAndroid,
                AccessibilityNodeInfoCompat info,
                int id,
                boolean hasSentPreviousRequest);
    }
}