Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(835)

Unified Diff: content/public/android/java/src/org/chromium/content/browser/accessibility/BrowserAccessibilityManager.java

Issue 15741009: Native Android accessibility. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Rebase Created 7 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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..9a8a04737021e2d78055a1ed25e59a4cbc9ac86d
--- /dev/null
+++ b/content/public/android/java/src/org/chromium/content/browser/accessibility/BrowserAccessibilityManager.java
@@ -0,0 +1,459 @@
+// 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.os.Build;
+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}.
+ *
+ * This class is safe to load on ICS and can be used to run tests, but
+ * only the subclass, JellyBeanBrowserAccessibilityManager, actually
+ * has a AccessibilityNodeProvider implementation needed for native
+ * accessibility.
+ */
+@JNINamespace("content")
+public class BrowserAccessibilityManager {
+ private static final String TAG = BrowserAccessibilityManager.class.getSimpleName();
+
+ private ContentViewCore mContentViewCore;
+ private AccessibilityManager mAccessibilityManager;
+ private RenderCoordinates mRenderCoordinates;
+ private int mNativeObj;
+ private int mAccessibilityFocusId;
+ private int mCurrentHoverId;
+ 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 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) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ return new JellyBeanBrowserAccessibilityManager(
+ nativeBrowserAccessibilityManagerAndroid, contentViewCore);
+ } else {
+ return new BrowserAccessibilityManager(
+ nativeBrowserAccessibilityManagerAndroid, contentViewCore);
+ }
+ }
+
+ protected BrowserAccessibilityManager(int nativeBrowserAccessibilityManagerAndroid,
+ ContentViewCore contentViewCore) {
+ mNativeObj = nativeBrowserAccessibilityManagerAndroid;
+ mContentViewCore = contentViewCore;
+ mContentViewCore.setBrowserAccessibilityManager(this);
+ mAccessibilityFocusId = View.NO_ID;
+ mCurrentHoverId = 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;
+ }
+
+ /**
+ * @return An AccessibilityNodeProvider on JellyBean, and null on previous versions.
+ */
+ public AccessibilityNodeProvider getAccessibilityNodeProvider() {
+ return null;
+ }
+
+ /**
+ * @see AccessibilityNodeProvider#createAccessibilityNodeInfo(int)
+ */
+ protected 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;
+ }
+
+ final AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(mView);
+ info.setPackageName(mContentViewCore.getContext().getPackageName());
+ info.setSource(mView, virtualViewId);
+
+ if (nativePopulateAccessibilityNodeInfo(mNativeObj, info, virtualViewId)) {
+ return info;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * @see AccessibilityNodeProvider#findAccessibilityNodeInfosByText(String, int)
+ */
+ protected List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String text,
+ int virtualViewId) {
+ return new ArrayList<AccessibilityNodeInfo>();
+ }
+
+ /**
+ * @see AccessibilityNodeProvider#performAction(int, int, Bundle)
+ */
+ protected boolean performAction(int virtualViewId, int action, Bundle arguments) {
+ if (!mAccessibilityManager.isEnabled() || mNativeObj == 0) {
+ return false;
+ }
+
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
+ if (mAccessibilityFocusId == virtualViewId) {
+ return true;
+ }
+
+ mAccessibilityFocusId = virtualViewId;
+ sendAccessibilityEvent(mAccessibilityFocusId,
+ AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
+ return true;
+ case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
+ if (mAccessibilityFocusId == virtualViewId) {
+ mAccessibilityFocusId = View.NO_ID;
+ }
+ return true;
+ case AccessibilityNodeInfo.ACTION_CLICK:
+ nativeClick(mNativeObj, virtualViewId);
+ break;
+ case AccessibilityNodeInfo.ACTION_FOCUS:
+ nativeFocus(mNativeObj, virtualViewId);
+ break;
+ case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS:
+ nativeBlur(mNativeObj);
+ break;
+ default:
+ break;
+ }
+ return false;
+ }
+
+ /**
+ * @see View#onHoverEvent(MotionEvent)
+ */
+ public boolean onHoverEvent(MotionEvent event) {
+ if (!mAccessibilityManager.isEnabled() || mNativeObj == 0) {
+ return false;
+ }
+
+ if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) return true;
+
+ mUserHasTouchExplored = true;
+ float x = event.getX();
+ float y = event.getY();
+
+ // 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);
+ if (mCurrentHoverId != id) {
+ sendAccessibilityEvent(mCurrentHoverId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
+ sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
+ mCurrentHoverId = 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 sendAccessibilityEvent(int virtualViewId, int eventType) {
+ if (!mAccessibilityManager.isEnabled() || mNativeObj == 0) return;
+
+ final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
+ event.setPackageName(mContentViewCore.getContext().getPackageName());
+ int rootId = nativeGetRootId(mNativeObj);
+ if (virtualViewId == rootId) {
+ virtualViewId = View.NO_ID;
+ }
+ event.setSource(mView, virtualViewId);
+ if (!nativePopulateAccessibilityEvent(mNativeObj, event, virtualViewId, eventType)) return;
+
+ // 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().postInvalidate();
+
+ 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 announceLiveRegionText(String text) {
+ mView.announceForAccessibility(text);
+ }
+
+ @CalledByNative
+ private void setAccessibilityNodeInfoParent(AccessibilityNodeInfo node, int parentId) {
+ node.setParent(mView, parentId);
+ }
+
+ @CalledByNative
+ private void addAccessibilityNodeInfoChild(AccessibilityNodeInfo node, int child_id) {
+ node.addChild(mView, child_id);
+ }
+
+ @CalledByNative
+ private void setAccessibilityNodeInfoBooleanAttributes(AccessibilityNodeInfo node,
+ int virtualViewId, boolean checkable, boolean checked, boolean clickable,
+ boolean enabled, boolean focusable, boolean focused, boolean password,
+ boolean scrollable, boolean selected, boolean visibleToUser) {
+ node.setCheckable(checkable);
+ node.setChecked(checked);
+ node.setClickable(clickable);
+ node.setEnabled(enabled);
+ node.setFocusable(focusable);
+ node.setFocused(focused);
+ node.setPassword(password);
+ node.setScrollable(scrollable);
+ node.setSelected(selected);
+ node.setVisibleToUser(visibleToUser);
+
+ if (focusable) {
+ if (focused) {
+ node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_FOCUS);
+ } else {
+ node.addAction(AccessibilityNodeInfo.ACTION_FOCUS);
+ }
+ }
+
+ if (mAccessibilityFocusId == virtualViewId) {
+ node.setAccessibilityFocused(true);
+ node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
+ } else {
+ node.setAccessibilityFocused(false);
+ node.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
+ }
+
+ if (clickable) {
+ node.addAction(AccessibilityNodeInfo.ACTION_CLICK);
+ }
+ }
+
+ @CalledByNative
+ private void setAccessibilityNodeInfoStringAttributes(AccessibilityNodeInfo node,
+ String className, String contentDescription) {
+ node.setClassName(className);
+ node.setContentDescription(contentDescription);
+ }
+
+ @CalledByNative
+ private void setAccessibilityNodeInfoLocation(AccessibilityNodeInfo node,
+ int absoluteLeft, int absoluteTop, int parentRelativeLeft, int parentRelativeTop,
+ int width, int height, boolean isRootNode) {
+ // First set the bounds in parent.
+ Rect boundsInParent = new Rect(parentRelativeLeft, parentRelativeTop,
+ parentRelativeLeft + width, parentRelativeTop + height);
+ if (isRootNode) {
+ // Offset of the web content relative to the View.
+ boundsInParent.offset(0, (int) mRenderCoordinates.getContentOffsetYPix());
+ }
+ node.setBoundsInParent(boundsInParent);
+
+ // Now set the absolute rect, which requires several transformations.
+ Rect rect = new Rect(absoluteLeft, absoluteTop, absoluteLeft + width, absoluteTop + height);
+
+ // 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());
+
+ // Finally 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]);
+
+ node.setBoundsInScreen(rect);
+ }
+
+ @CalledByNative
+ private void setAccessibilityEventBooleanAttributes(AccessibilityEvent event,
+ boolean checked, boolean enabled, boolean password, boolean scrollable) {
+ event.setChecked(checked);
+ event.setEnabled(enabled);
+ event.setPassword(password);
+ event.setScrollable(scrollable);
+ }
+
+ @CalledByNative
+ private void setAccessibilityEventClassName(AccessibilityEvent event, String className) {
+ event.setClassName(className);
+ }
+
+ @CalledByNative
+ private void setAccessibilityEventListAttributes(AccessibilityEvent event,
+ int currentItemIndex, int itemCount) {
+ event.setCurrentItemIndex(currentItemIndex);
+ event.setItemCount(itemCount);
+ }
+
+ @CalledByNative
+ private void setAccessibilityEventScrollAttributes(AccessibilityEvent event,
+ int scrollX, int scrollY, int maxScrollX, int maxScrollY) {
+ event.setScrollX(scrollX);
+ event.setScrollY(scrollY);
+ event.setMaxScrollX(maxScrollX);
+ event.setMaxScrollY(maxScrollY);
+ }
+
+ @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 addedCount, int itemCount, String text) {
+ event.setFromIndex(fromIndex);
+ event.setAddedCount(addedCount);
+ event.setItemCount(itemCount);
+ event.getText().add(text);
+ }
+
+ private native int nativeGetRootId(int nativeBrowserAccessibilityManagerAndroid);
+ private native int nativeHitTest(int nativeBrowserAccessibilityManagerAndroid, int x, int y);
+ private native boolean nativePopulateAccessibilityNodeInfo(
+ int nativeBrowserAccessibilityManagerAndroid, AccessibilityNodeInfo info, int id);
+ private native boolean nativePopulateAccessibilityEvent(
+ int nativeBrowserAccessibilityManagerAndroid, AccessibilityEvent event, int id,
+ int eventType);
+ private native void nativeClick(int nativeBrowserAccessibilityManagerAndroid, int id);
+ private native void nativeFocus(int nativeBrowserAccessibilityManagerAndroid, int id);
+ private native void nativeBlur(int nativeBrowserAccessibilityManagerAndroid);
+}

Powered by Google App Engine
This is Rietveld 408576698