1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 package org.chromium.content.browser.accessibility;
7 import android.content.Context;
8 import android.graphics.Rect;
9 import android.os.Build;
10 import android.os.Bundle;
11 import android.view.MotionEvent;
12 import android.view.View;
13 import android.view.ViewParent;
14 import android.view.accessibility.AccessibilityEvent;
15 import android.view.accessibility.AccessibilityManager;
16 import android.view.accessibility.AccessibilityNodeInfo;
17 import android.view.accessibility.AccessibilityNodeProvider;
19 import org.chromium.base.CalledByNative;
20 import org.chromium.base.JNINamespace;
21 import org.chromium.content.browser.ContentViewCore;
22 import org.chromium.content.browser.RenderCoordinates;
24 import java.util.ArrayList;
25 import java.util.List;
28 * Native accessibility for a {@link ContentViewCore}.
30 * This class is safe to load on ICS and can be used to run tests, but
31 * only the subclass, JellyBeanBrowserAccessibilityManager, actually
32 * has a AccessibilityNodeProvider implementation needed for native
35 @JNINamespace("content")
36 public class BrowserAccessibilityManager {
37 private static final String TAG = "BrowserAccessibilityManager";
39 private ContentViewCore mContentViewCore;
40 private final AccessibilityManager mAccessibilityManager;
41 private final RenderCoordinates mRenderCoordinates;
42 private long mNativeObj;
43 private int mAccessibilityFocusId;
44 private int mCurrentHoverId;
45 private int mCurrentRootId;
46 private final int[] mTempLocation = new int[2];
47 private final View mView;
48 private boolean mUserHasTouchExplored;
49 private boolean mPendingScrollToMakeNodeVisible;
50 private boolean mFrameInfoInitialized;
53 * Create a BrowserAccessibilityManager object, which is owned by the C++
54 * BrowserAccessibilityManagerAndroid instance, and connects to the content view.
55 * @param nativeBrowserAccessibilityManagerAndroid A pointer to the counterpart native
56 * C++ object that owns this object.
57 * @param contentViewCore The content view that this object provides accessibility for.
60 private static BrowserAccessibilityManager create(long nativeBrowserAccessibilityManagerAndroid,
61 ContentViewCore contentViewCore) {
62 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
63 return new KitKatBrowserAccessibilityManager(
64 nativeBrowserAccessibilityManagerAndroid, contentViewCore);
65 } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
66 return new JellyBeanBrowserAccessibilityManager(
67 nativeBrowserAccessibilityManagerAndroid, contentViewCore);
69 return new BrowserAccessibilityManager(
70 nativeBrowserAccessibilityManagerAndroid, contentViewCore);
74 protected BrowserAccessibilityManager(long nativeBrowserAccessibilityManagerAndroid,
75 ContentViewCore contentViewCore) {
76 mNativeObj = nativeBrowserAccessibilityManagerAndroid;
77 mContentViewCore = contentViewCore;
78 mContentViewCore.setBrowserAccessibilityManager(this);
79 mAccessibilityFocusId = View.NO_ID;
80 mCurrentHoverId = View.NO_ID;
81 mCurrentRootId = View.NO_ID;
82 mView = mContentViewCore.getContainerView();
83 mRenderCoordinates = mContentViewCore.getRenderCoordinates();
84 mAccessibilityManager =
85 (AccessibilityManager) mContentViewCore.getContext()
86 .getSystemService(Context.ACCESSIBILITY_SERVICE);
90 private void onNativeObjectDestroyed() {
91 if (mContentViewCore.getBrowserAccessibilityManager() == this) {
92 mContentViewCore.setBrowserAccessibilityManager(null);
95 mContentViewCore = null;
99 * @return An AccessibilityNodeProvider on JellyBean, and null on previous versions.
101 public AccessibilityNodeProvider getAccessibilityNodeProvider() {
106 * @see AccessibilityNodeProvider#createAccessibilityNodeInfo(int)
108 protected AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
109 if (!mAccessibilityManager.isEnabled() || mNativeObj == 0) {
113 int rootId = nativeGetRootId(mNativeObj);
115 if (virtualViewId == View.NO_ID) {
116 return createNodeForHost(rootId);
119 if (!mFrameInfoInitialized) {
123 final AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(mView);
124 info.setPackageName(mContentViewCore.getContext().getPackageName());
125 info.setSource(mView, virtualViewId);
127 if (virtualViewId == rootId) {
128 info.setParent(mView);
131 if (nativePopulateAccessibilityNodeInfo(mNativeObj, info, virtualViewId)) {
140 * @see AccessibilityNodeProvider#findAccessibilityNodeInfosByText(String, int)
142 protected List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String text,
144 return new ArrayList<AccessibilityNodeInfo>();
148 * @see AccessibilityNodeProvider#performAction(int, int, Bundle)
150 protected boolean performAction(int virtualViewId, int action, Bundle arguments) {
151 // We don't support any actions on the host view or nodes
152 // that are not (any longer) in the tree.
153 if (!mAccessibilityManager.isEnabled() || mNativeObj == 0
154 || !nativeIsNodeValid(mNativeObj, virtualViewId)) {
159 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
160 if (mAccessibilityFocusId == virtualViewId) {
164 mAccessibilityFocusId = virtualViewId;
165 sendAccessibilityEvent(mAccessibilityFocusId,
166 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
167 if (mCurrentHoverId == View.NO_ID) {
168 nativeScrollToMakeNodeVisible(
169 mNativeObj, mAccessibilityFocusId);
171 mPendingScrollToMakeNodeVisible = true;
174 case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
175 if (mAccessibilityFocusId == virtualViewId) {
176 sendAccessibilityEvent(mAccessibilityFocusId,
177 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
178 mAccessibilityFocusId = View.NO_ID;
181 case AccessibilityNodeInfo.ACTION_CLICK:
182 nativeClick(mNativeObj, virtualViewId);
183 sendAccessibilityEvent(virtualViewId,
184 AccessibilityEvent.TYPE_VIEW_CLICKED);
186 case AccessibilityNodeInfo.ACTION_FOCUS:
187 nativeFocus(mNativeObj, virtualViewId);
189 case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS:
190 nativeBlur(mNativeObj);
199 * @see View#onHoverEvent(MotionEvent)
201 public boolean onHoverEvent(MotionEvent event) {
202 if (!mAccessibilityManager.isEnabled() || mNativeObj == 0) {
206 if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) {
207 if (mCurrentHoverId != View.NO_ID) {
208 sendAccessibilityEvent(mCurrentHoverId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
209 mCurrentHoverId = View.NO_ID;
211 if (mPendingScrollToMakeNodeVisible) {
212 nativeScrollToMakeNodeVisible(
213 mNativeObj, mAccessibilityFocusId);
215 mPendingScrollToMakeNodeVisible = false;
219 mUserHasTouchExplored = true;
220 float x = event.getX();
221 float y = event.getY();
223 // Convert to CSS coordinates.
224 int cssX = (int) (mRenderCoordinates.fromPixToLocalCss(x) +
225 mRenderCoordinates.getScrollX());
226 int cssY = (int) (mRenderCoordinates.fromPixToLocalCss(y) +
227 mRenderCoordinates.getScrollY());
228 int id = nativeHitTest(mNativeObj, cssX, cssY);
229 if (mCurrentHoverId != id) {
230 sendAccessibilityEvent(mCurrentHoverId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
231 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
232 mCurrentHoverId = id;
239 * Called by ContentViewCore to notify us when the frame info is initialized,
240 * the first time, since until that point, we can't use mRenderCoordinates to transform
241 * web coordinates to screen coordinates.
243 public void notifyFrameInfoInitialized() {
244 if (mFrameInfoInitialized) return;
246 mFrameInfoInitialized = true;
247 // Invalidate the host, since the chrome accessibility tree is now
248 // ready and listed as the child of the host.
249 mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
251 // (Re-) focus focused element, since we weren't able to create an
252 // AccessibilityNodeInfo for this element before.
253 if (mAccessibilityFocusId != View.NO_ID) {
254 sendAccessibilityEvent(mAccessibilityFocusId,
255 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
259 private void sendAccessibilityEvent(int virtualViewId, int eventType) {
260 // If mFrameInfoInitialized is false, then the virtual hierarchy
261 // doesn't exist in the view of the Android framework, so should
262 // never send any events.
263 if (!mAccessibilityManager.isEnabled() || mNativeObj == 0
264 || !mFrameInfoInitialized) {
268 final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
269 event.setPackageName(mContentViewCore.getContext().getPackageName());
270 event.setSource(mView, virtualViewId);
271 if (!nativePopulateAccessibilityEvent(mNativeObj, event, virtualViewId, eventType)) {
276 // This is currently needed if we want Android to draw the yellow box around
277 // the item that has accessibility focus. In practice, this doesn't seem to slow
278 // things down, because it's only called when the accessibility focus moves.
279 // TODO(dmazzoni): remove this if/when Android framework fixes bug.
280 mContentViewCore.getContainerView().postInvalidate();
282 mContentViewCore.getContainerView().requestSendAccessibilityEvent(mView, event);
285 private Bundle getOrCreateBundleForAccessibilityEvent(AccessibilityEvent event) {
286 Bundle bundle = (Bundle) event.getParcelableData();
287 if (bundle == null) {
288 bundle = new Bundle();
289 event.setParcelableData(bundle);
294 private AccessibilityNodeInfo createNodeForHost(int rootId) {
295 // Since we don't want the parent to be focusable, but we can't remove
296 // actions from a node, copy over the necessary fields.
297 final AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(mView);
298 final AccessibilityNodeInfo source = AccessibilityNodeInfo.obtain(mView);
299 mView.onInitializeAccessibilityNodeInfo(source);
301 // Copy over parent and screen bounds.
302 Rect rect = new Rect();
303 source.getBoundsInParent(rect);
304 result.setBoundsInParent(rect);
305 source.getBoundsInScreen(rect);
306 result.setBoundsInScreen(rect);
308 // Set up the parent view, if applicable.
309 final ViewParent parent = mView.getParentForAccessibility();
310 if (parent instanceof View) {
311 result.setParent((View) parent);
314 // Populate the minimum required fields.
315 result.setVisibleToUser(source.isVisibleToUser());
316 result.setEnabled(source.isEnabled());
317 result.setPackageName(source.getPackageName());
318 result.setClassName(source.getClassName());
320 // Add the Chrome root node.
321 if (mFrameInfoInitialized) {
322 result.addChild(mView, rootId);
329 private void handlePageLoaded(int id) {
330 if (mUserHasTouchExplored) return;
332 mAccessibilityFocusId = id;
333 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
337 private void handleFocusChanged(int id) {
338 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_FOCUSED);
340 // Update accessibility focus if not already set to this node.
341 if (mAccessibilityFocusId != id) {
342 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
343 mAccessibilityFocusId = id;
348 private void handleCheckStateChanged(int id) {
349 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_CLICKED);
353 private void handleTextSelectionChanged(int id) {
354 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
358 private void handleEditableTextChanged(int id) {
359 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
363 private void handleContentChanged(int id) {
364 int rootId = nativeGetRootId(mNativeObj);
365 if (rootId != mCurrentRootId) {
366 mCurrentRootId = rootId;
367 mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
369 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
374 private void handleNavigate() {
375 mAccessibilityFocusId = View.NO_ID;
376 mUserHasTouchExplored = false;
377 mFrameInfoInitialized = false;
378 // Invalidate the host, since its child is now gone.
379 mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
383 private void handleScrolledToAnchor(int id) {
384 if (mAccessibilityFocusId == id) {
388 mAccessibilityFocusId = id;
389 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
393 private void announceLiveRegionText(String text) {
394 mView.announceForAccessibility(text);
398 private void setAccessibilityNodeInfoParent(AccessibilityNodeInfo node, int parentId) {
399 node.setParent(mView, parentId);
403 private void addAccessibilityNodeInfoChild(AccessibilityNodeInfo node, int childId) {
404 node.addChild(mView, childId);
408 private void setAccessibilityNodeInfoBooleanAttributes(AccessibilityNodeInfo node,
409 int virtualViewId, boolean checkable, boolean checked, boolean clickable,
410 boolean enabled, boolean focusable, boolean focused, boolean password,
411 boolean scrollable, boolean selected, boolean visibleToUser) {
412 node.setCheckable(checkable);
413 node.setChecked(checked);
414 node.setClickable(clickable);
415 node.setEnabled(enabled);
416 node.setFocusable(focusable);
417 node.setFocused(focused);
418 node.setPassword(password);
419 node.setScrollable(scrollable);
420 node.setSelected(selected);
421 node.setVisibleToUser(visibleToUser);
425 node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_FOCUS);
427 node.addAction(AccessibilityNodeInfo.ACTION_FOCUS);
431 if (mAccessibilityFocusId == virtualViewId) {
432 node.setAccessibilityFocused(true);
433 node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
435 node.setAccessibilityFocused(false);
436 node.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
440 node.addAction(AccessibilityNodeInfo.ACTION_CLICK);
445 private void setAccessibilityNodeInfoStringAttributes(AccessibilityNodeInfo node,
446 String className, String contentDescription) {
447 node.setClassName(className);
448 node.setContentDescription(contentDescription);
452 private void setAccessibilityNodeInfoLocation(AccessibilityNodeInfo node,
453 int absoluteLeft, int absoluteTop, int parentRelativeLeft, int parentRelativeTop,
454 int width, int height, boolean isRootNode) {
455 // First set the bounds in parent.
456 Rect boundsInParent = new Rect(parentRelativeLeft, parentRelativeTop,
457 parentRelativeLeft + width, parentRelativeTop + height);
459 // Offset of the web content relative to the View.
460 boundsInParent.offset(0, (int) mRenderCoordinates.getContentOffsetYPix());
462 node.setBoundsInParent(boundsInParent);
464 // Now set the absolute rect, which requires several transformations.
465 Rect rect = new Rect(absoluteLeft, absoluteTop, absoluteLeft + width, absoluteTop + height);
467 // Offset by the scroll position.
468 rect.offset(-(int) mRenderCoordinates.getScrollX(),
469 -(int) mRenderCoordinates.getScrollY());
471 // Convert CSS (web) pixels to Android View pixels
472 rect.left = (int) mRenderCoordinates.fromLocalCssToPix(rect.left);
473 rect.top = (int) mRenderCoordinates.fromLocalCssToPix(rect.top);
474 rect.bottom = (int) mRenderCoordinates.fromLocalCssToPix(rect.bottom);
475 rect.right = (int) mRenderCoordinates.fromLocalCssToPix(rect.right);
477 // Offset by the location of the web content within the view.
479 (int) mRenderCoordinates.getContentOffsetYPix());
481 // Finally offset by the location of the view within the screen.
482 final int[] viewLocation = new int[2];
483 mView.getLocationOnScreen(viewLocation);
484 rect.offset(viewLocation[0], viewLocation[1]);
486 node.setBoundsInScreen(rect);
490 protected void setAccessibilityNodeInfoKitKatAttributes(AccessibilityNodeInfo node,
491 boolean canOpenPopup,
492 boolean contentInvalid,
497 // Requires KitKat or higher.
501 protected void setAccessibilityNodeInfoCollectionInfo(AccessibilityNodeInfo node,
502 int rowCount, int columnCount, boolean hierarchical) {
503 // Requires KitKat or higher.
507 protected void setAccessibilityNodeInfoCollectionItemInfo(AccessibilityNodeInfo node,
508 int rowIndex, int rowSpan, int columnIndex, int columnSpan, boolean heading) {
509 // Requires KitKat or higher.
513 protected void setAccessibilityNodeInfoRangeInfo(AccessibilityNodeInfo node,
514 int rangeType, float min, float max, float current) {
515 // Requires KitKat or higher.
519 private void setAccessibilityEventBooleanAttributes(AccessibilityEvent event,
520 boolean checked, boolean enabled, boolean password, boolean scrollable) {
521 event.setChecked(checked);
522 event.setEnabled(enabled);
523 event.setPassword(password);
524 event.setScrollable(scrollable);
528 private void setAccessibilityEventClassName(AccessibilityEvent event, String className) {
529 event.setClassName(className);
533 private void setAccessibilityEventListAttributes(AccessibilityEvent event,
534 int currentItemIndex, int itemCount) {
535 event.setCurrentItemIndex(currentItemIndex);
536 event.setItemCount(itemCount);
540 private void setAccessibilityEventScrollAttributes(AccessibilityEvent event,
541 int scrollX, int scrollY, int maxScrollX, int maxScrollY) {
542 event.setScrollX(scrollX);
543 event.setScrollY(scrollY);
544 event.setMaxScrollX(maxScrollX);
545 event.setMaxScrollY(maxScrollY);
549 private void setAccessibilityEventTextChangedAttrs(AccessibilityEvent event,
550 int fromIndex, int addedCount, int removedCount, String beforeText, String text) {
551 event.setFromIndex(fromIndex);
552 event.setAddedCount(addedCount);
553 event.setRemovedCount(removedCount);
554 event.setBeforeText(beforeText);
555 event.getText().add(text);
559 private void setAccessibilityEventSelectionAttrs(AccessibilityEvent event,
560 int fromIndex, int addedCount, int itemCount, String text) {
561 event.setFromIndex(fromIndex);
562 event.setAddedCount(addedCount);
563 event.setItemCount(itemCount);
564 event.getText().add(text);
568 protected void setAccessibilityEventKitKatAttributes(AccessibilityEvent event,
569 boolean canOpenPopup,
570 boolean contentInvalid,
575 // Backwards compatibility for KitKat AccessibilityNodeInfo fields.
576 Bundle bundle = getOrCreateBundleForAccessibilityEvent(event);
577 bundle.putBoolean("AccessibilityNodeInfo.canOpenPopup", canOpenPopup);
578 bundle.putBoolean("AccessibilityNodeInfo.contentInvalid", contentInvalid);
579 bundle.putBoolean("AccessibilityNodeInfo.dismissable", dismissable);
580 bundle.putBoolean("AccessibilityNodeInfo.multiLine", multiLine);
581 bundle.putInt("AccessibilityNodeInfo.inputType", inputType);
582 bundle.putInt("AccessibilityNodeInfo.liveRegion", liveRegion);
586 protected void setAccessibilityEventCollectionInfo(AccessibilityEvent event,
587 int rowCount, int columnCount, boolean hierarchical) {
588 // Backwards compatibility for KitKat AccessibilityNodeInfo fields.
589 Bundle bundle = getOrCreateBundleForAccessibilityEvent(event);
590 bundle.putInt("AccessibilityNodeInfo.CollectionInfo.rowCount", rowCount);
591 bundle.putInt("AccessibilityNodeInfo.CollectionInfo.columnCount", columnCount);
592 bundle.putBoolean("AccessibilityNodeInfo.CollectionInfo.hierarchical", hierarchical);
596 protected void setAccessibilityEventCollectionItemInfo(AccessibilityEvent event,
597 int rowIndex, int rowSpan, int columnIndex, int columnSpan, boolean heading) {
598 // Backwards compatibility for KitKat AccessibilityNodeInfo fields.
599 Bundle bundle = getOrCreateBundleForAccessibilityEvent(event);
600 bundle.putInt("AccessibilityNodeInfo.CollectionItemInfo.rowIndex", rowIndex);
601 bundle.putInt("AccessibilityNodeInfo.CollectionItemInfo.rowSpan", rowSpan);
602 bundle.putInt("AccessibilityNodeInfo.CollectionItemInfo.columnIndex", columnIndex);
603 bundle.putInt("AccessibilityNodeInfo.CollectionItemInfo.columnSpan", columnSpan);
604 bundle.putBoolean("AccessibilityNodeInfo.CollectionItemInfo.heading", heading);
608 protected void setAccessibilityEventRangeInfo(AccessibilityEvent event,
609 int rangeType, float min, float max, float current) {
610 // Backwards compatibility for KitKat AccessibilityNodeInfo fields.
611 Bundle bundle = getOrCreateBundleForAccessibilityEvent(event);
612 bundle.putInt("AccessibilityNodeInfo.RangeInfo.type", rangeType);
613 bundle.putFloat("AccessibilityNodeInfo.RangeInfo.min", min);
614 bundle.putFloat("AccessibilityNodeInfo.RangeInfo.max", max);
615 bundle.putFloat("AccessibilityNodeInfo.RangeInfo.current", current);
618 private native int nativeGetRootId(long nativeBrowserAccessibilityManagerAndroid);
619 private native boolean nativeIsNodeValid(long nativeBrowserAccessibilityManagerAndroid, int id);
620 private native int nativeHitTest(long nativeBrowserAccessibilityManagerAndroid, int x, int y);
621 private native boolean nativePopulateAccessibilityNodeInfo(
622 long nativeBrowserAccessibilityManagerAndroid, AccessibilityNodeInfo info, int id);
623 private native boolean nativePopulateAccessibilityEvent(
624 long nativeBrowserAccessibilityManagerAndroid, AccessibilityEvent event, int id,
626 private native void nativeClick(long nativeBrowserAccessibilityManagerAndroid, int id);
627 private native void nativeFocus(long nativeBrowserAccessibilityManagerAndroid, int id);
628 private native void nativeBlur(long nativeBrowserAccessibilityManagerAndroid);
629 private native void nativeScrollToMakeNodeVisible(
630 long nativeBrowserAccessibilityManagerAndroid, int id);