| Index: content/public/android/java/src/org/chromium/content/browser/accessibility/BrowserAccessibilityManager.java
|
| diff --git a/content/public/android/java/src/org/chromium/content/browser/accessibility/BrowserAccessibilityManager.java b/content/public/android/java/src/org/chromium/content/browser/accessibility/BrowserAccessibilityManager.java
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..f0e4c2cd11b0515e0d62c672d6fc9137d6d392df
|
| --- /dev/null
|
| +++ b/content/public/android/java/src/org/chromium/content/browser/accessibility/BrowserAccessibilityManager.java
|
| @@ -0,0 +1,468 @@
|
| +// Copyright (c) 2013 The Chromium Authors. All rights reserved.
|
| +// 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 android.content.Context;
|
| +import android.graphics.Rect;
|
| +import android.os.Bundle;
|
| +import android.view.MotionEvent;
|
| +import android.view.View;
|
| +import android.view.accessibility.AccessibilityEvent;
|
| +import android.view.accessibility.AccessibilityManager;
|
| +import android.view.accessibility.AccessibilityNodeInfo;
|
| +import android.view.accessibility.AccessibilityNodeProvider;
|
| +import android.view.inputmethod.InputMethodManager;
|
| +
|
| +import org.chromium.base.CalledByNative;
|
| +import org.chromium.base.JNINamespace;
|
| +import org.chromium.content.browser.ContentViewCore;
|
| +import org.chromium.content.browser.RenderCoordinates;
|
| +
|
| +import java.util.ArrayList;
|
| +import java.util.List;
|
| +
|
| +/**
|
| + * Native accessibility for a {@link ContentViewCore}.
|
| + */
|
| +@JNINamespace("content")
|
| +public class BrowserAccessibilityManager extends AccessibilityNodeProvider {
|
| + private static final String TAG = BrowserAccessibilityManager.class.getSimpleName();
|
| +
|
| + private ContentViewCore mContentViewCore;
|
| + private AccessibilityManager mAccessibilityManager;
|
| + private RenderCoordinates mRenderCoordinates;
|
| + private int mNativeObj;
|
| + private int mAccessibilityFocusId;
|
| + private final int[] mTempLocation = new int[2];
|
| + private View mView;
|
| + private boolean mUserHasTouchExplored;
|
| + private boolean mFrameInfoInitialized;
|
| +
|
| + // If this is true, enables an experimental feature that automatically focuses things
|
| + // as you explore them. This can be more efficient, but might cause the user to get stuck
|
| + // on a misbehaving page that keeps stealing focus.
|
| + private boolean mWebFocusFollowsAccessibilityFocus;
|
| +
|
| + // If this is true, enables an experimental feature that focuses the web page after it
|
| + // finishes loading. Disabled for now because it can be confusing if the user was
|
| + // trying to do something when this happens.
|
| + private boolean mFocusPageOnLoad;
|
| +
|
| + /**
|
| + * Create a BrowserAccessibilityManager object, which is owned by the C++
|
| + * BrowserAccessibilityManagerAndroid instance, and connects to the content view.
|
| + * @param nativeBrowserAccessibilityManagerAndroid A pointer to the counterpart native
|
| + * C++ object that owns this object.
|
| + * @param contentViewCore The content view that this object provides accessibility for.
|
| + */
|
| + @CalledByNative
|
| + private static BrowserAccessibilityManager create(int nativeBrowserAccessibilityManagerAndroid,
|
| + ContentViewCore contentViewCore) {
|
| + return new BrowserAccessibilityManager(
|
| + nativeBrowserAccessibilityManagerAndroid, contentViewCore);
|
| + }
|
| +
|
| + private BrowserAccessibilityManager(int nativeBrowserAccessibilityManagerAndroid,
|
| + ContentViewCore contentViewCore) {
|
| + mNativeObj = nativeBrowserAccessibilityManagerAndroid;
|
| + mContentViewCore = contentViewCore;
|
| + mContentViewCore.setBrowserAccessibilityManager(this);
|
| + mAccessibilityFocusId = View.NO_ID;
|
| + mView = mContentViewCore.getContainerView();
|
| + mRenderCoordinates = mContentViewCore.getRenderCoordinates();
|
| + mAccessibilityManager =
|
| + (AccessibilityManager) mContentViewCore.getContext()
|
| + .getSystemService(Context.ACCESSIBILITY_SERVICE);
|
| + }
|
| +
|
| + @CalledByNative
|
| + private void onNativeObjectDestroyed() {
|
| + if (mContentViewCore.getBrowserAccessibilityManager() == this) {
|
| + mContentViewCore.setBrowserAccessibilityManager(null);
|
| + }
|
| + mNativeObj = 0;
|
| + mContentViewCore = null;
|
| + }
|
| +
|
| + /** @inheritDoc */
|
| + public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
|
| + if (!mAccessibilityManager.isEnabled() || mNativeObj == 0 || !mFrameInfoInitialized) {
|
| + return null;
|
| + }
|
| +
|
| + int rootId = nativeGetRootId(mNativeObj);
|
| + if (virtualViewId == View.NO_ID) {
|
| + virtualViewId = rootId;
|
| + }
|
| + if (mAccessibilityFocusId == View.NO_ID) {
|
| + mAccessibilityFocusId = rootId;
|
| + }
|
| +
|
| + int nativeNode = nativeGetNativeNodeById(mNativeObj, virtualViewId);
|
| + if (nativeNode == 0) {
|
| + return null;
|
| + }
|
| +
|
| + final AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(mView);
|
| + info.setPackageName(mContentViewCore.getContext().getPackageName());
|
| + info.setClassName(BrowserAccessibility.nativeGetClassNameJNI(nativeNode));
|
| + info.setSource(mView, virtualViewId);
|
| + info.setCheckable(BrowserAccessibility.nativeIsCheckableJNI(nativeNode));
|
| + info.setChecked(BrowserAccessibility.nativeIsCheckedJNI(nativeNode));
|
| + info.setEnabled(BrowserAccessibility.nativeIsEnabledJNI(nativeNode));
|
| + info.setFocused(BrowserAccessibility.nativeIsFocusedJNI(nativeNode));
|
| + info.setPassword(BrowserAccessibility.nativeIsPasswordJNI(nativeNode));
|
| + info.setScrollable(BrowserAccessibility.nativeIsScrollableJNI(nativeNode));
|
| + info.setSelected(BrowserAccessibility.nativeIsSelectedJNI(nativeNode));
|
| + info.setVisibleToUser(BrowserAccessibility.nativeIsVisibleJNI(nativeNode));
|
| + info.setContentDescription(BrowserAccessibility.nativeGetNameJNI(nativeNode));
|
| +
|
| + boolean focusable = BrowserAccessibility.nativeIsFocusableJNI(nativeNode);
|
| + info.setFocusable(focusable);
|
| + if (focusable) {
|
| + if (BrowserAccessibility.nativeIsFocusedJNI(nativeNode))
|
| + info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_FOCUS);
|
| + else
|
| + info.addAction(AccessibilityNodeInfo.ACTION_FOCUS);
|
| + }
|
| +
|
| + if (mAccessibilityFocusId == virtualViewId) {
|
| + info.setAccessibilityFocused(true);
|
| + info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
|
| + } else {
|
| + info.setAccessibilityFocused(false);
|
| + info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
|
| + }
|
| +
|
| + boolean clickable = BrowserAccessibility.nativeGetClickableJNI(nativeNode);
|
| + if (clickable)
|
| + info.addAction(AccessibilityNodeInfo.ACTION_CLICK);
|
| +
|
| + int childCount = BrowserAccessibility.nativeGetChildCountJNI(nativeNode);
|
| + for (int i = 0; i < childCount; i++)
|
| + info.addChild(mView, BrowserAccessibility.nativeGetChildIdAtJNI(nativeNode, i));
|
| +
|
| +
|
| + if (virtualViewId != rootId)
|
| + info.setParent(mView, BrowserAccessibility.nativeGetParentJNI(nativeNode));
|
| +
|
| + Rect boundsInParent = (Rect) BrowserAccessibility.nativeGetRectInParentJNI(nativeNode);
|
| + if (virtualViewId == rootId) {
|
| + // Offset of the web content relative to the View.
|
| + boundsInParent.offset(0, (int) mRenderCoordinates.getContentOffsetYPix());
|
| + }
|
| + info.setBoundsInParent(boundsInParent);
|
| + Rect rect = (Rect) BrowserAccessibility.nativeGetAbsoluteRectJNI(nativeNode);
|
| +
|
| + // Offset by the scroll position.
|
| + rect.offset(-(int) mRenderCoordinates.getScrollX(),
|
| + -(int) mRenderCoordinates.getScrollY());
|
| +
|
| + // Convert CSS (web) pixels to Android View pixels
|
| + rect.left = (int) mRenderCoordinates.fromLocalCssToPix(rect.left);
|
| + rect.top = (int) mRenderCoordinates.fromLocalCssToPix(rect.top);
|
| + rect.bottom = (int) mRenderCoordinates.fromLocalCssToPix(rect.bottom);
|
| + rect.right = (int) mRenderCoordinates.fromLocalCssToPix(rect.right);
|
| +
|
| + // Offset by the location of the web content within the view.
|
| + rect.offset(0,
|
| + (int) mRenderCoordinates.getContentOffsetYPix());
|
| +
|
| + // Additionally offset by the location of the view within the screen.
|
| + final int[] viewLocation = new int[2];
|
| + mView.getLocationOnScreen(viewLocation);
|
| + rect.offset(viewLocation[0], viewLocation[1]);
|
| + info.setBoundsInScreen(rect);
|
| +
|
| + return info;
|
| + }
|
| +
|
| + /** @inheritDoc */
|
| + public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String text,
|
| + int virtualViewId) {
|
| + return new ArrayList<AccessibilityNodeInfo>();
|
| + }
|
| +
|
| + /** @inheritDoc */
|
| + public boolean performAction(int virtualViewId, int action, Bundle arguments) {
|
| + if (!mAccessibilityManager.isEnabled() || mNativeObj == 0) {
|
| + return false;
|
| + }
|
| +
|
| + int rootId = nativeGetRootId(mNativeObj);
|
| + int nativeRootNode = nativeGetNativeNodeById(mNativeObj, rootId);
|
| + int nativeNode = nativeGetNativeNodeById(mNativeObj, virtualViewId);
|
| + if (nativeNode == 0 || nativeRootNode == 0) {
|
| + return false;
|
| + }
|
| +
|
| + switch (action) {
|
| + case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
|
| + if (mAccessibilityFocusId == virtualViewId) {
|
| + return true;
|
| + }
|
| +
|
| + if (mAccessibilityFocusId != View.NO_ID) {
|
| + sendAccessibilityEvent(mAccessibilityFocusId,
|
| + AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
|
| + }
|
| + mAccessibilityFocusId = virtualViewId;
|
| + focusAndHideOrShowKeyboardIfNecessary(virtualViewId);
|
| + sendAccessibilityEvent(mAccessibilityFocusId,
|
| + AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
|
| + return true;
|
| + case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
|
| + if (mAccessibilityFocusId == virtualViewId) {
|
| + mAccessibilityFocusId = View.NO_ID;
|
| + }
|
| + return true;
|
| + case AccessibilityNodeInfo.ACTION_CLICK:
|
| + BrowserAccessibility.nativeClickJNI(nativeNode);
|
| + break;
|
| + case AccessibilityNodeInfo.ACTION_FOCUS:
|
| + BrowserAccessibility.nativeFocusJNI(nativeNode);
|
| + break;
|
| + case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS:
|
| + BrowserAccessibility.nativeFocusJNI(nativeRootNode);
|
| + break;
|
| + default:
|
| + break;
|
| + }
|
| + return false;
|
| + }
|
| +
|
| + /**
|
| + * @see View#dispatchHoverEvent(MotionEvent)
|
| + */
|
| + public boolean dispatchHoverEvent(MotionEvent event) {
|
| + if (!mAccessibilityManager.isEnabled() || mNativeObj == 0) {
|
| + return false;
|
| + }
|
| +
|
| + mUserHasTouchExplored = true;
|
| + float x = event.getX();
|
| + float y = event.getY();
|
| +
|
| + // Offset by the location of the web content within the view.
|
| + // Note: the MotionEvent has view-relative coordinates already,
|
| + // so we don't have to offset by the view relative to the screen.
|
| + y -= mRenderCoordinates.getContentOffsetYPix();
|
| +
|
| + // Convert to CSS coordinates.
|
| + int cssX = (int) (mRenderCoordinates.fromPixToLocalCss(x) +
|
| + mRenderCoordinates.getScrollX());
|
| + int cssY = (int) (mRenderCoordinates.fromPixToLocalCss(y) +
|
| + mRenderCoordinates.getScrollY());
|
| + int id = nativeHitTest(mNativeObj, cssX, cssY);
|
| + int nativeNode = nativeGetNativeNodeById(mNativeObj, id);
|
| + if (nativeNode == 0) {
|
| + return false;
|
| + }
|
| +
|
| + if (mAccessibilityFocusId != id) {
|
| + mAccessibilityFocusId = id;
|
| + sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
|
| + }
|
| +
|
| + // If this is a hover end event, web-focus the element and pop up the keyboard
|
| + // if necessary. TODO(aboxhall): figure out what happens when there's an
|
| + // ACTION_HOVER_EXIT because touch exploration exited the ContentViewCore.
|
| + if (event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT) {
|
| + focusAndHideOrShowKeyboardIfNecessary(id);
|
| + }
|
| +
|
| + return true;
|
| + }
|
| +
|
| + /**
|
| + * Called by ContentViewCore to notify us when the frame info is initialized,
|
| + * the first time, since until that point, we can't use mRenderCoordinates to transform
|
| + * web coordinates to screen coordinates.
|
| + */
|
| + public void notifyFrameInfoInitialized() {
|
| + if (mFrameInfoInitialized) return;
|
| +
|
| + mFrameInfoInitialized = true;
|
| + // (Re-) focus focused element, since we weren't able to create an
|
| + // AccessibilityNodeInfo for this element before.
|
| + if (mAccessibilityFocusId != View.NO_ID) {
|
| + sendAccessibilityEvent(mAccessibilityFocusId,
|
| + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
|
| + }
|
| + }
|
| +
|
| + private void focusAndHideOrShowKeyboardIfNecessary(int virtualViewId) {
|
| + // This is an experimental feature. Only do this if enabled.
|
| + if (!mWebFocusFollowsAccessibilityFocus) {
|
| + return;
|
| + }
|
| +
|
| + int nativeNode = nativeGetNativeNodeById(mNativeObj, virtualViewId);
|
| + if (nativeNode == 0) {
|
| + return;
|
| + }
|
| +
|
| + InputMethodManager imm =
|
| + (InputMethodManager) mContentViewCore.getContext().getSystemService(
|
| + Context.INPUT_METHOD_SERVICE);
|
| + if (!BrowserAccessibility.nativeIsFocusableJNI(nativeNode)) {
|
| + imm.hideSoftInputFromWindow(mView.getWindowToken(),
|
| + InputMethodManager.HIDE_IMPLICIT_ONLY);
|
| + return;
|
| + }
|
| +
|
| + if (BrowserAccessibility.nativeIsFocusedJNI(nativeNode)) {
|
| + return;
|
| + }
|
| +
|
| + BrowserAccessibility.nativeFocusJNI(nativeNode);
|
| + sendAccessibilityEvent(virtualViewId,
|
| + AccessibilityEvent.TYPE_VIEW_FOCUSED);
|
| + // Show/hide keyboard
|
| + if (BrowserAccessibility.nativeIsEditableTextJNI(nativeNode)) {
|
| + imm.showSoftInput(mView, InputMethodManager.SHOW_IMPLICIT);
|
| + } else {
|
| + imm.hideSoftInputFromWindow(mView.getWindowToken(),
|
| + InputMethodManager.HIDE_IMPLICIT_ONLY);
|
| + }
|
| + }
|
| +
|
| + private void sendAccessibilityEvent(int virtualViewId, int eventType) {
|
| + if (!mAccessibilityManager.isEnabled() || mNativeObj == 0) return;
|
| +
|
| + int nativeNode = nativeGetNativeNodeById(mNativeObj, virtualViewId);
|
| + if (nativeNode == 0) {
|
| + return;
|
| + }
|
| +
|
| + final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
|
| + event.setPackageName(mContentViewCore.getContext().getPackageName());
|
| + event.setClassName(BrowserAccessibility.nativeGetClassNameJNI(nativeNode));
|
| + event.setChecked(BrowserAccessibility.nativeIsCheckedJNI(nativeNode));
|
| + event.setEnabled(BrowserAccessibility.nativeIsEnabledJNI(nativeNode));
|
| + event.setPassword(BrowserAccessibility.nativeIsPasswordJNI(nativeNode));
|
| + event.setScrollable(BrowserAccessibility.nativeIsScrollableJNI(nativeNode));
|
| + event.setCurrentItemIndex(BrowserAccessibility.nativeGetItemIndexJNI(nativeNode));
|
| + event.setItemCount(BrowserAccessibility.nativeGetItemCountJNI(nativeNode));
|
| + event.setScrollX(BrowserAccessibility.nativeGetScrollXJNI(nativeNode));
|
| + event.setScrollY(BrowserAccessibility.nativeGetScrollYJNI(nativeNode));
|
| + event.setMaxScrollX(BrowserAccessibility.nativeGetScrollXJNI(nativeNode));
|
| + event.setMaxScrollY(BrowserAccessibility.nativeGetScrollYJNI(nativeNode));
|
| +
|
| + switch (eventType) {
|
| + case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED:
|
| + event.setFromIndex(
|
| + BrowserAccessibility.nativeGetTextChangeFromIndexJNI(nativeNode));
|
| + event.setAddedCount(
|
| + BrowserAccessibility.nativeGetTextChangeAddedCountJNI(nativeNode));
|
| + event.setRemovedCount(
|
| + BrowserAccessibility.nativeGetTextChangeRemovedCountJNI(nativeNode));
|
| + event.setBeforeText(
|
| + BrowserAccessibility.nativeGetTextChangeBeforeTextJNI(nativeNode));
|
| + event.getText().add(BrowserAccessibility.nativeGetNameJNI(nativeNode));
|
| + break;
|
| + case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED:
|
| + event.setFromIndex(BrowserAccessibility.nativeGetSelectionStartJNI(nativeNode));
|
| + event.setToIndex(BrowserAccessibility.nativeGetSelectionEndJNI(nativeNode));
|
| + event.setItemCount(BrowserAccessibility.nativeGetEditableTextLengthJNI(nativeNode));
|
| + event.getText().add(BrowserAccessibility.nativeGetNameJNI(nativeNode));
|
| + break;
|
| + default:
|
| + break;
|
| + }
|
| +
|
| + int rootId = nativeGetRootId(mNativeObj);
|
| + if (virtualViewId == rootId) {
|
| + virtualViewId = View.NO_ID;
|
| + }
|
| + event.setSource(mView, virtualViewId);
|
| +
|
| + // This is currently needed if we want Android to draw the yellow box around
|
| + // the item that has accessibility focus. In practice, this doesn't seem to slow
|
| + // things down, because it's only called when the accessibility focus moves.
|
| + // TODO(dmazzoni): remove this if/when Android framework fixes bug.
|
| + mContentViewCore.getContainerView().invalidate();
|
| +
|
| + mContentViewCore.getContainerView().requestSendAccessibilityEvent(mView, event);
|
| + }
|
| +
|
| + @CalledByNative
|
| + private void handlePageLoaded(int id) {
|
| + if (mUserHasTouchExplored) return;
|
| +
|
| + if (mFocusPageOnLoad) {
|
| + // Focus the natively focused node (usually document),
|
| + // if this feature is enabled.
|
| + mAccessibilityFocusId = id;
|
| + sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_FOCUSED);
|
| + }
|
| + }
|
| +
|
| + @CalledByNative
|
| + private void handleFocusChanged(int id) {
|
| + if (mAccessibilityFocusId == id) return;
|
| +
|
| + mAccessibilityFocusId = id;
|
| + sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_FOCUSED);
|
| + }
|
| +
|
| + @CalledByNative
|
| + private void handleCheckStateChanged(int id) {
|
| + sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_CLICKED);
|
| + }
|
| +
|
| + @CalledByNative
|
| + private void handleTextSelectionChanged(int id) {
|
| + sendAccessibilityEvent(id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
|
| + sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
|
| + }
|
| +
|
| + @CalledByNative
|
| + private void handleEditableTextChanged(int id) {
|
| + sendAccessibilityEvent(id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
|
| + sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
|
| + }
|
| +
|
| + @CalledByNative
|
| + private void handleContentChanged(int id) {
|
| + sendAccessibilityEvent(id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
|
| + }
|
| +
|
| + @CalledByNative
|
| + private void handleNavigate() {
|
| + mAccessibilityFocusId = View.NO_ID;
|
| + mUserHasTouchExplored = false;
|
| + mFrameInfoInitialized = false;
|
| + }
|
| +
|
| + @CalledByNative
|
| + private void handleScrolledToAnchor(int id) {
|
| + if (mAccessibilityFocusId == id) {
|
| + return;
|
| + }
|
| +
|
| + mAccessibilityFocusId = id;
|
| + sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
|
| + }
|
| +
|
| + @CalledByNative
|
| + private void announceObjectShow(int id, boolean forceAssertive) {
|
| + if (!mAccessibilityManager.isEnabled()) {
|
| + return;
|
| + }
|
| +
|
| + int nativeNode = nativeGetNativeNodeById(mNativeObj, id);
|
| + String nodeName = BrowserAccessibility.nativeGetNameJNI(nativeNode);
|
| +
|
| + // TODO(aboxhall): handle forceAssertive and other live region features
|
| + // once supported by framework.
|
| + mView.announceForAccessibility(nodeName);
|
| + }
|
| +
|
| + private native int nativeGetRootId(int nativeBrowserAccessibilityManagerAndroid);
|
| + private native int nativeHitTest(int nativeBrowserAccessibilityManagerAndroid, int x, int y);
|
| + private native int nativeGetNativeNodeById(
|
| + int nativeBrowserAccessibilityManagerAndroid, int id);
|
| +}
|
|
|