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.provider.Settings;
12 import android.text.SpannableString;
13 import android.text.style.URLSpan;
14 import android.view.MotionEvent;
15 import android.view.View;
16 import android.view.ViewGroup;
17 import android.view.ViewParent;
18 import android.view.accessibility.AccessibilityEvent;
19 import android.view.accessibility.AccessibilityManager;
20 import android.view.accessibility.AccessibilityNodeInfo;
21 import android.view.accessibility.AccessibilityNodeProvider;
23 import org.chromium.base.CalledByNative;
24 import org.chromium.base.JNINamespace;
25 import org.chromium.content.browser.ContentViewCore;
26 import org.chromium.content.browser.RenderCoordinates;
28 import java.util.ArrayList;
29 import java.util.List;
30 import java.util.Locale;
33 * Native accessibility for a {@link ContentViewCore}.
35 * This class is safe to load on ICS and can be used to run tests, but
36 * only the subclass, JellyBeanBrowserAccessibilityManager, actually
37 * has a AccessibilityNodeProvider implementation needed for native
40 @JNINamespace("content")
41 public class BrowserAccessibilityManager {
42 private static final String TAG = "BrowserAccessibilityManager";
44 // Constants from AccessibilityNodeInfo defined in the L SDK.
45 private static final int ACTION_SET_TEXT = 0x200000;
46 private static final String ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE =
47 "ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE";
49 private ContentViewCore mContentViewCore;
50 private final AccessibilityManager mAccessibilityManager;
51 private final RenderCoordinates mRenderCoordinates;
52 private long mNativeObj;
53 private int mAccessibilityFocusId;
54 private Rect mAccessibilityFocusRect;
55 private boolean mIsHovering;
56 private int mLastHoverId = View.NO_ID;
57 private int mCurrentRootId;
58 private final int[] mTempLocation = new int[2];
59 private final ViewGroup mView;
60 private boolean mUserHasTouchExplored;
61 private boolean mPendingScrollToMakeNodeVisible;
62 private boolean mNotifyFrameInfoInitializedCalled;
63 private int mSelectionGranularity;
64 private int mSelectionStartIndex;
65 private int mSelectionEndIndex;
66 private boolean mVisible = true;
69 * Create a BrowserAccessibilityManager object, which is owned by the C++
70 * BrowserAccessibilityManagerAndroid instance, and connects to the content view.
71 * @param nativeBrowserAccessibilityManagerAndroid A pointer to the counterpart native
72 * C++ object that owns this object.
73 * @param contentViewCore The content view that this object provides accessibility for.
76 private static BrowserAccessibilityManager create(long nativeBrowserAccessibilityManagerAndroid,
77 ContentViewCore contentViewCore) {
78 // A bug in the KitKat framework prevents us from using these new APIs.
79 // http://crbug.com/348088/
80 // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
81 // return new KitKatBrowserAccessibilityManager(
82 // nativeBrowserAccessibilityManagerAndroid, contentViewCore);
84 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
85 return new JellyBeanBrowserAccessibilityManager(
86 nativeBrowserAccessibilityManagerAndroid, contentViewCore);
88 return new BrowserAccessibilityManager(
89 nativeBrowserAccessibilityManagerAndroid, contentViewCore);
93 protected BrowserAccessibilityManager(long nativeBrowserAccessibilityManagerAndroid,
94 ContentViewCore contentViewCore) {
95 mNativeObj = nativeBrowserAccessibilityManagerAndroid;
96 mContentViewCore = contentViewCore;
97 mContentViewCore.setBrowserAccessibilityManager(this);
98 mAccessibilityFocusId = View.NO_ID;
100 mCurrentRootId = View.NO_ID;
101 mView = mContentViewCore.getContainerView();
102 mRenderCoordinates = mContentViewCore.getRenderCoordinates();
103 mAccessibilityManager =
104 (AccessibilityManager) mContentViewCore.getContext()
105 .getSystemService(Context.ACCESSIBILITY_SERVICE);
109 private void onNativeObjectDestroyed() {
110 if (mContentViewCore.getBrowserAccessibilityManager() == this) {
111 mContentViewCore.setBrowserAccessibilityManager(null);
114 mContentViewCore = null;
118 * @return An AccessibilityNodeProvider on JellyBean, and null on previous versions.
120 public AccessibilityNodeProvider getAccessibilityNodeProvider() {
125 * Set whether the web content made accessible by this class is currently visible.
126 * Set it to false if the web view is still on the screen but it's obscured by a
127 * dialog or overlay. This will make every virtual view in the web hierarchy report
128 * that it's not visible, and not accessibility focusable.
130 * @param visible Whether the web content is currently visible and not obscured.
132 public void setVisible(boolean visible) {
133 if (visible == mVisible) return;
136 mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
140 * @see AccessibilityNodeProvider#createAccessibilityNodeInfo(int)
142 protected AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
143 if (!mAccessibilityManager.isEnabled() || mNativeObj == 0) {
147 int rootId = nativeGetRootId(mNativeObj);
149 if (virtualViewId == View.NO_ID) {
150 return createNodeForHost(rootId);
153 if (!isFrameInfoInitialized()) {
157 final AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(mView);
158 info.setPackageName(mContentViewCore.getContext().getPackageName());
159 info.setSource(mView, virtualViewId);
161 if (virtualViewId == rootId) {
162 info.setParent(mView);
165 if (nativePopulateAccessibilityNodeInfo(mNativeObj, info, virtualViewId)) {
174 * @see AccessibilityNodeProvider#findAccessibilityNodeInfosByText(String, int)
176 protected List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String text,
178 return new ArrayList<AccessibilityNodeInfo>();
181 protected static boolean isValidMovementGranularity(int granularity) {
182 switch (granularity) {
183 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER:
184 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD:
185 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE:
192 * @see AccessibilityNodeProvider#performAction(int, int, Bundle)
194 protected boolean performAction(int virtualViewId, int action, Bundle arguments) {
195 // We don't support any actions on the host view or nodes
196 // that are not (any longer) in the tree.
197 if (!mAccessibilityManager.isEnabled() || mNativeObj == 0
198 || !nativeIsNodeValid(mNativeObj, virtualViewId)) {
203 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
204 if (!moveAccessibilityFocusToId(virtualViewId)) return true;
207 nativeScrollToMakeNodeVisible(
208 mNativeObj, mAccessibilityFocusId);
210 mPendingScrollToMakeNodeVisible = true;
213 case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
214 if (mAccessibilityFocusId == virtualViewId) {
215 sendAccessibilityEvent(mAccessibilityFocusId,
216 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
217 mAccessibilityFocusId = View.NO_ID;
218 mAccessibilityFocusRect = null;
221 case AccessibilityNodeInfo.ACTION_CLICK:
222 nativeClick(mNativeObj, virtualViewId);
223 sendAccessibilityEvent(virtualViewId,
224 AccessibilityEvent.TYPE_VIEW_CLICKED);
226 case AccessibilityNodeInfo.ACTION_FOCUS:
227 nativeFocus(mNativeObj, virtualViewId);
229 case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS:
230 nativeBlur(mNativeObj);
232 case AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT: {
233 if (arguments == null) return false;
234 String elementType = arguments.getString(
235 AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING);
236 if (elementType == null) return false;
237 elementType = elementType.toUpperCase(Locale.US);
238 return jumpToElementType(elementType, true);
240 case AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT: {
241 if (arguments == null) return false;
242 String elementType = arguments.getString(
243 AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING);
244 if (elementType == null) return false;
245 elementType = elementType.toUpperCase(Locale.US);
246 return jumpToElementType(elementType, false);
248 case ACTION_SET_TEXT: {
249 if (!nativeIsEditableText(mNativeObj, virtualViewId)) return false;
250 if (arguments == null) return false;
251 String newText = arguments.getString(
252 ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE);
253 if (newText == null) return false;
254 nativeSetTextFieldValue(mNativeObj, virtualViewId, newText);
255 // Match Android framework and set the cursor to the end of the text field.
256 nativeSetSelection(mNativeObj, virtualViewId, newText.length(), newText.length());
259 case AccessibilityNodeInfo.ACTION_SET_SELECTION: {
260 if (!nativeIsEditableText(mNativeObj, virtualViewId)) return false;
261 int selectionStart = 0;
262 int selectionEnd = 0;
263 if (arguments != null) {
264 selectionStart = arguments.getInt(
265 AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT);
266 selectionEnd = arguments.getInt(
267 AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT);
269 nativeSetSelection(mNativeObj, virtualViewId, selectionStart, selectionEnd);
272 case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY: {
273 if (arguments == null) return false;
274 int granularity = arguments.getInt(
275 AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
276 boolean extend = arguments.getBoolean(
277 AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN);
278 if (!isValidMovementGranularity(granularity)) {
281 return nextAtGranularity(granularity, extend);
283 case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: {
284 if (arguments == null) return false;
285 int granularity = arguments.getInt(
286 AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
287 boolean extend = arguments.getBoolean(
288 AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN);
289 if (!isValidMovementGranularity(granularity)) {
292 return previousAtGranularity(granularity, extend);
294 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
295 return nativeAdjustSlider(mNativeObj, virtualViewId, true);
296 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
297 return nativeAdjustSlider(mNativeObj, virtualViewId, false);
305 * @see View#onHoverEvent(MotionEvent)
307 public boolean onHoverEvent(MotionEvent event) {
308 if (!mAccessibilityManager.isEnabled() || mNativeObj == 0) {
312 if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) {
314 if (mPendingScrollToMakeNodeVisible) {
315 nativeScrollToMakeNodeVisible(
316 mNativeObj, mAccessibilityFocusId);
318 mPendingScrollToMakeNodeVisible = false;
323 mUserHasTouchExplored = true;
324 float x = event.getX();
325 float y = event.getY();
327 // Convert to CSS coordinates.
328 int cssX = (int) (mRenderCoordinates.fromPixToLocalCss(x));
329 int cssY = (int) (mRenderCoordinates.fromPixToLocalCss(y));
331 // This sends an IPC to the render process to do the hit testing.
332 // The response is handled by handleHover.
333 nativeHitTest(mNativeObj, cssX, cssY);
338 * Called by ContentViewCore to notify us when the frame info is initialized,
339 * the first time, since until that point, we can't use mRenderCoordinates to transform
340 * web coordinates to screen coordinates.
342 public void notifyFrameInfoInitialized() {
343 if (mNotifyFrameInfoInitializedCalled) return;
345 mNotifyFrameInfoInitializedCalled = true;
347 // Invalidate the container view, since the chrome accessibility tree is now
348 // ready and listed as the child of the container view.
349 mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
351 // (Re-) focus focused element, since we weren't able to create an
352 // AccessibilityNodeInfo for this element before.
353 if (mAccessibilityFocusId != View.NO_ID) {
354 moveAccessibilityFocusToIdAndRefocusIfNeeded(mAccessibilityFocusId);
358 private boolean jumpToElementType(String elementType, boolean forwards) {
359 int id = nativeFindElementType(mNativeObj, mAccessibilityFocusId, elementType, forwards);
360 if (id == 0) return false;
362 moveAccessibilityFocusToId(id);
366 private void setGranularityAndUpdateSelection(int granularity) {
367 if (mSelectionGranularity == 0) {
368 mSelectionStartIndex = -1;
369 mSelectionEndIndex = -1;
371 mSelectionGranularity = granularity;
372 if (nativeIsEditableText(mNativeObj, mAccessibilityFocusId)) {
373 mSelectionStartIndex = nativeGetEditableTextSelectionStart(
374 mNativeObj, mAccessibilityFocusId);
375 mSelectionEndIndex = nativeGetEditableTextSelectionEnd(
376 mNativeObj, mAccessibilityFocusId);
380 private boolean nextAtGranularity(int granularity, boolean extendSelection) {
381 setGranularityAndUpdateSelection(granularity);
382 // This calls finishGranularityMove when it's done.
383 return nativeNextAtGranularity(mNativeObj, mSelectionGranularity, extendSelection,
384 mAccessibilityFocusId, mSelectionEndIndex);
387 private boolean previousAtGranularity(int granularity, boolean extendSelection) {
388 setGranularityAndUpdateSelection(granularity);
389 // This calls finishGranularityMove when it's done.
390 return nativePreviousAtGranularity(mNativeObj, mSelectionGranularity, extendSelection,
391 mAccessibilityFocusId, mSelectionEndIndex);
395 private void finishGranularityMove(String text, boolean extendSelection,
396 int itemStartIndex, int itemEndIndex, boolean forwards) {
397 // Prepare to send both a selection and a traversal event in sequence.
398 AccessibilityEvent selectionEvent = buildAccessibilityEvent(mAccessibilityFocusId,
399 AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
400 if (selectionEvent == null) return;
401 AccessibilityEvent traverseEvent = buildAccessibilityEvent(mAccessibilityFocusId,
402 AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY);
403 if (traverseEvent == null) {
404 selectionEvent.recycle();
408 // Update the cursor or selection based on the traversal. If it's an editable
409 // text node, set the real editing cursor too.
411 mSelectionEndIndex = itemEndIndex;
413 mSelectionEndIndex = itemStartIndex;
414 if (!extendSelection) {
415 mSelectionStartIndex = mSelectionEndIndex;
417 if (nativeIsEditableText(mNativeObj, mAccessibilityFocusId)) {
418 nativeSetSelection(mNativeObj, mAccessibilityFocusId,
419 mSelectionStartIndex, mSelectionEndIndex);
422 // The selection event's "from" and "to" indices are just a cursor at the focus
423 // end of the movement, or a selection if extendSelection is true.
424 selectionEvent.setFromIndex(mSelectionStartIndex);
425 selectionEvent.setToIndex(mSelectionStartIndex);
426 selectionEvent.setItemCount(text.length());
428 // The traverse event's "from" and "to" indices surround the item (e.g. the word,
429 // etc.) with no whitespace.
430 traverseEvent.setFromIndex(itemStartIndex);
431 traverseEvent.setToIndex(itemEndIndex);
432 traverseEvent.setItemCount(text.length());
433 traverseEvent.setMovementGranularity(mSelectionGranularity);
434 traverseEvent.setContentDescription(text);
436 // The traverse event needs to set its associated action that triggered it.
438 traverseEvent.setAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
440 traverseEvent.setAction(
441 AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
444 mView.requestSendAccessibilityEvent(mView, selectionEvent);
445 mView.requestSendAccessibilityEvent(mView, traverseEvent);
448 private boolean moveAccessibilityFocusToId(int newAccessibilityFocusId) {
449 if (newAccessibilityFocusId == mAccessibilityFocusId) return false;
451 mAccessibilityFocusId = newAccessibilityFocusId;
452 mAccessibilityFocusRect = null;
453 mSelectionGranularity = 0;
454 mSelectionStartIndex = 0;
455 mSelectionEndIndex = 0;
457 // Calling nativeSetAccessibilityFocus will asynchronously load inline text boxes for
458 // this node and its subtree. If accessibility focus is on anything other than
459 // the root, do it - otherwise set it to -1 so we don't load inline text boxes
460 // for the whole subtree of the root.
461 if (mAccessibilityFocusId == mCurrentRootId)
462 nativeSetAccessibilityFocus(mNativeObj, -1);
464 nativeSetAccessibilityFocus(mNativeObj, mAccessibilityFocusId);
466 sendAccessibilityEvent(mAccessibilityFocusId,
467 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
471 private void moveAccessibilityFocusToIdAndRefocusIfNeeded(int newAccessibilityFocusId) {
472 // Work around a bug in the Android framework where it doesn't fully update the object
473 // with accessibility focus even if you send it a WINDOW_CONTENT_CHANGED. To work around
474 // this, clear focus and then set focus again.
475 if (newAccessibilityFocusId == mAccessibilityFocusId) {
476 sendAccessibilityEvent(newAccessibilityFocusId,
477 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
478 mAccessibilityFocusId = View.NO_ID;
481 moveAccessibilityFocusToId(newAccessibilityFocusId);
484 private void sendAccessibilityEvent(int virtualViewId, int eventType) {
485 // The container view is indicated by a virtualViewId of NO_ID; post these events directly
486 // since there's no web-specific information to attach.
487 if (virtualViewId == View.NO_ID) {
488 mView.sendAccessibilityEvent(eventType);
492 AccessibilityEvent event = buildAccessibilityEvent(virtualViewId, eventType);
494 mView.requestSendAccessibilityEvent(mView, event);
498 private AccessibilityEvent buildAccessibilityEvent(int virtualViewId, int eventType) {
499 // If we don't have any frame info, then the virtual hierarchy
500 // doesn't exist in the view of the Android framework, so should
501 // never send any events.
502 if (!mAccessibilityManager.isEnabled() || mNativeObj == 0
503 || !isFrameInfoInitialized()) {
507 // This is currently needed if we want Android to visually highlight
508 // the item that has accessibility focus. In practice, this doesn't seem to slow
509 // things down, because it's only called when the accessibility focus moves.
510 // TODO(dmazzoni): remove this if/when Android framework fixes bug.
511 mView.postInvalidate();
513 final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
514 event.setPackageName(mContentViewCore.getContext().getPackageName());
515 event.setSource(mView, virtualViewId);
516 if (!nativePopulateAccessibilityEvent(mNativeObj, event, virtualViewId, eventType)) {
523 private Bundle getOrCreateBundleForAccessibilityEvent(AccessibilityEvent event) {
524 Bundle bundle = (Bundle) event.getParcelableData();
525 if (bundle == null) {
526 bundle = new Bundle();
527 event.setParcelableData(bundle);
532 private AccessibilityNodeInfo createNodeForHost(int rootId) {
533 // Since we don't want the parent to be focusable, but we can't remove
534 // actions from a node, copy over the necessary fields.
535 final AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(mView);
536 final AccessibilityNodeInfo source = AccessibilityNodeInfo.obtain(mView);
537 mView.onInitializeAccessibilityNodeInfo(source);
539 // Copy over parent and screen bounds.
540 Rect rect = new Rect();
541 source.getBoundsInParent(rect);
542 result.setBoundsInParent(rect);
543 source.getBoundsInScreen(rect);
544 result.setBoundsInScreen(rect);
546 // Set up the parent view, if applicable.
547 final ViewParent parent = mView.getParentForAccessibility();
548 if (parent instanceof View) {
549 result.setParent((View) parent);
552 // Populate the minimum required fields.
553 result.setVisibleToUser(source.isVisibleToUser() && mVisible);
554 result.setEnabled(source.isEnabled());
555 result.setPackageName(source.getPackageName());
556 result.setClassName(source.getClassName());
558 // Add the Chrome root node.
559 if (isFrameInfoInitialized()) {
560 result.addChild(mView, rootId);
567 * Returns whether or not the frame info is initialized, meaning we can safely
568 * convert web coordinates to screen coordinates. When this is first initialized,
569 * notifyFrameInfoInitialized is called - but we shouldn't check whether or not
570 * that method was called as a way to determine if frame info is valid because
571 * notifyFrameInfoInitialized might not be called at all if mRenderCoordinates
572 * gets initialized first.
574 private boolean isFrameInfoInitialized() {
575 return mRenderCoordinates.getContentWidthCss() != 0.0
576 || mRenderCoordinates.getContentHeightCss() != 0.0;
580 private void handlePageLoaded(int id) {
581 if (mUserHasTouchExplored) return;
583 if (mContentViewCore.shouldSetAccessibilityFocusOnPageLoad()) {
584 moveAccessibilityFocusToIdAndRefocusIfNeeded(id);
589 private void handleFocusChanged(int id) {
590 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_FOCUSED);
591 moveAccessibilityFocusToId(id);
595 private void handleCheckStateChanged(int id) {
596 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_CLICKED);
600 private void handleTextSelectionChanged(int id) {
601 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
605 private void handleEditableTextChanged(int id) {
606 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
610 private void handleSliderChanged(int id) {
611 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_SCROLLED);
615 private void handleContentChanged(int id) {
616 int rootId = nativeGetRootId(mNativeObj);
617 if (rootId != mCurrentRootId) {
618 mCurrentRootId = rootId;
619 mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
621 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
626 private void handleNavigate() {
627 mAccessibilityFocusId = View.NO_ID;
628 mAccessibilityFocusRect = null;
629 mUserHasTouchExplored = false;
630 // Invalidate the host, since its child is now gone.
631 mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
635 private void handleScrollPositionChanged(int id) {
636 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_SCROLLED);
640 private void handleScrolledToAnchor(int id) {
641 moveAccessibilityFocusToId(id);
645 private void handleHover(int id) {
646 if (mLastHoverId == id) return;
648 // Always send the ENTER and then the EXIT event, to match a standard Android View.
649 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
650 sendAccessibilityEvent(mLastHoverId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
655 private void announceLiveRegionText(String text) {
656 mView.announceForAccessibility(text);
660 private void setAccessibilityNodeInfoParent(AccessibilityNodeInfo node, int parentId) {
661 node.setParent(mView, parentId);
665 private void addAccessibilityNodeInfoChild(AccessibilityNodeInfo node, int childId) {
666 node.addChild(mView, childId);
670 private void setAccessibilityNodeInfoBooleanAttributes(AccessibilityNodeInfo node,
671 int virtualViewId, boolean canScrollForward, boolean canScrollBackward,
672 boolean checkable, boolean checked, boolean clickable, boolean editableText,
673 boolean enabled, boolean focusable, boolean focused, boolean password,
674 boolean scrollable, boolean selected, boolean visibleToUser) {
675 node.setCheckable(checkable);
676 node.setChecked(checked);
677 node.setClickable(clickable);
678 node.setEnabled(enabled);
679 node.setFocusable(focusable);
680 node.setFocused(focused);
681 node.setPassword(password);
682 node.setScrollable(scrollable);
683 node.setSelected(selected);
684 node.setVisibleToUser(visibleToUser && mVisible);
686 node.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
687 node.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT);
688 node.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
689 node.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
690 node.setMovementGranularities(
691 AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER
692 | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD
693 | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE);
695 if (editableText && enabled) {
696 node.addAction(ACTION_SET_TEXT);
697 node.addAction(AccessibilityNodeInfo.ACTION_SET_SELECTION);
700 if (canScrollForward) {
701 node.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
704 if (canScrollBackward) {
705 node.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
710 node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_FOCUS);
712 node.addAction(AccessibilityNodeInfo.ACTION_FOCUS);
716 if (mAccessibilityFocusId == virtualViewId) {
717 node.setAccessibilityFocused(true);
718 node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
719 } else if (mVisible) {
720 node.setAccessibilityFocused(false);
721 node.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
725 node.addAction(AccessibilityNodeInfo.ACTION_CLICK);
730 private void setAccessibilityNodeInfoClassName(AccessibilityNodeInfo node,
732 node.setClassName(className);
736 private void setAccessibilityNodeInfoContentDescription(
737 AccessibilityNodeInfo node, String contentDescription, boolean annotateAsLink) {
738 if (annotateAsLink) {
739 SpannableString spannable = new SpannableString(contentDescription);
740 spannable.setSpan(new URLSpan(""), 0, spannable.length(), 0);
741 node.setContentDescription(spannable);
743 node.setContentDescription(contentDescription);
748 private void setAccessibilityNodeInfoLocation(AccessibilityNodeInfo node,
749 final int virtualViewId,
750 int absoluteLeft, int absoluteTop, int parentRelativeLeft, int parentRelativeTop,
751 int width, int height, boolean isRootNode) {
752 // First set the bounds in parent.
753 Rect boundsInParent = new Rect(parentRelativeLeft, parentRelativeTop,
754 parentRelativeLeft + width, parentRelativeTop + height);
756 // Offset of the web content relative to the View.
757 boundsInParent.offset(0, (int) mRenderCoordinates.getContentOffsetYPix());
759 node.setBoundsInParent(boundsInParent);
761 // Now set the absolute rect, which requires several transformations.
762 Rect rect = new Rect(absoluteLeft, absoluteTop, absoluteLeft + width, absoluteTop + height);
764 // Offset by the scroll position.
765 rect.offset(-(int) mRenderCoordinates.getScrollX(),
766 -(int) mRenderCoordinates.getScrollY());
768 // Convert CSS (web) pixels to Android View pixels
769 rect.left = (int) mRenderCoordinates.fromLocalCssToPix(rect.left);
770 rect.top = (int) mRenderCoordinates.fromLocalCssToPix(rect.top);
771 rect.bottom = (int) mRenderCoordinates.fromLocalCssToPix(rect.bottom);
772 rect.right = (int) mRenderCoordinates.fromLocalCssToPix(rect.right);
774 // Offset by the location of the web content within the view.
776 (int) mRenderCoordinates.getContentOffsetYPix());
778 // Finally offset by the location of the view within the screen.
779 final int[] viewLocation = new int[2];
780 mView.getLocationOnScreen(viewLocation);
781 rect.offset(viewLocation[0], viewLocation[1]);
783 node.setBoundsInScreen(rect);
785 // Work around a bug in the Android framework where if the object with accessibility
786 // focus moves, the accessibility focus rect is not updated - both the visual highlight,
787 // and the location on the screen that's clicked if you double-tap. To work around this,
788 // when we know the object with accessibility focus moved, move focus away and then
789 // move focus right back to it, which tricks Android into updating its bounds.
790 if (virtualViewId == mAccessibilityFocusId && virtualViewId != mCurrentRootId) {
791 if (mAccessibilityFocusRect == null) {
792 mAccessibilityFocusRect = rect;
793 } else if (!mAccessibilityFocusRect.equals(rect)) {
794 mAccessibilityFocusRect = rect;
795 moveAccessibilityFocusToIdAndRefocusIfNeeded(virtualViewId);
801 protected void setAccessibilityNodeInfoKitKatAttributes(AccessibilityNodeInfo node,
802 boolean canOpenPopup,
803 boolean contentInvalid,
808 // Requires KitKat or higher.
812 protected void setAccessibilityNodeInfoCollectionInfo(AccessibilityNodeInfo node,
813 int rowCount, int columnCount, boolean hierarchical) {
814 // Requires KitKat or higher.
818 protected void setAccessibilityNodeInfoCollectionItemInfo(AccessibilityNodeInfo node,
819 int rowIndex, int rowSpan, int columnIndex, int columnSpan, boolean heading) {
820 // Requires KitKat or higher.
824 protected void setAccessibilityNodeInfoRangeInfo(AccessibilityNodeInfo node,
825 int rangeType, float min, float max, float current) {
826 // Requires KitKat or higher.
830 private void setAccessibilityEventBooleanAttributes(AccessibilityEvent event,
831 boolean checked, boolean enabled, boolean password, boolean scrollable) {
832 event.setChecked(checked);
833 event.setEnabled(enabled);
834 event.setPassword(password);
835 event.setScrollable(scrollable);
839 private void setAccessibilityEventClassName(AccessibilityEvent event, String className) {
840 event.setClassName(className);
844 private void setAccessibilityEventListAttributes(AccessibilityEvent event,
845 int currentItemIndex, int itemCount) {
846 event.setCurrentItemIndex(currentItemIndex);
847 event.setItemCount(itemCount);
851 private void setAccessibilityEventScrollAttributes(AccessibilityEvent event,
852 int scrollX, int scrollY, int maxScrollX, int maxScrollY) {
853 event.setScrollX(scrollX);
854 event.setScrollY(scrollY);
855 event.setMaxScrollX(maxScrollX);
856 event.setMaxScrollY(maxScrollY);
860 private void setAccessibilityEventTextChangedAttrs(AccessibilityEvent event,
861 int fromIndex, int addedCount, int removedCount, String beforeText, String text) {
862 event.setFromIndex(fromIndex);
863 event.setAddedCount(addedCount);
864 event.setRemovedCount(removedCount);
865 event.setBeforeText(beforeText);
866 event.getText().add(text);
870 private void setAccessibilityEventSelectionAttrs(AccessibilityEvent event,
871 int fromIndex, int toIndex, int itemCount, String text) {
872 event.setFromIndex(fromIndex);
873 event.setToIndex(toIndex);
874 event.setItemCount(itemCount);
875 event.getText().add(text);
879 protected void setAccessibilityEventKitKatAttributes(AccessibilityEvent event,
880 boolean canOpenPopup,
881 boolean contentInvalid,
886 // Backwards compatibility for KitKat AccessibilityNodeInfo fields.
887 Bundle bundle = getOrCreateBundleForAccessibilityEvent(event);
888 bundle.putBoolean("AccessibilityNodeInfo.canOpenPopup", canOpenPopup);
889 bundle.putBoolean("AccessibilityNodeInfo.contentInvalid", contentInvalid);
890 bundle.putBoolean("AccessibilityNodeInfo.dismissable", dismissable);
891 bundle.putBoolean("AccessibilityNodeInfo.multiLine", multiLine);
892 bundle.putInt("AccessibilityNodeInfo.inputType", inputType);
893 bundle.putInt("AccessibilityNodeInfo.liveRegion", liveRegion);
897 protected void setAccessibilityEventCollectionInfo(AccessibilityEvent event,
898 int rowCount, int columnCount, boolean hierarchical) {
899 // Backwards compatibility for KitKat AccessibilityNodeInfo fields.
900 Bundle bundle = getOrCreateBundleForAccessibilityEvent(event);
901 bundle.putInt("AccessibilityNodeInfo.CollectionInfo.rowCount", rowCount);
902 bundle.putInt("AccessibilityNodeInfo.CollectionInfo.columnCount", columnCount);
903 bundle.putBoolean("AccessibilityNodeInfo.CollectionInfo.hierarchical", hierarchical);
907 protected void setAccessibilityEventHeadingFlag(AccessibilityEvent event,
909 // Backwards compatibility for KitKat AccessibilityNodeInfo fields.
910 Bundle bundle = getOrCreateBundleForAccessibilityEvent(event);
911 bundle.putBoolean("AccessibilityNodeInfo.CollectionItemInfo.heading", heading);
915 protected void setAccessibilityEventCollectionItemInfo(AccessibilityEvent event,
916 int rowIndex, int rowSpan, int columnIndex, int columnSpan) {
917 // Backwards compatibility for KitKat AccessibilityNodeInfo fields.
918 Bundle bundle = getOrCreateBundleForAccessibilityEvent(event);
919 bundle.putInt("AccessibilityNodeInfo.CollectionItemInfo.rowIndex", rowIndex);
920 bundle.putInt("AccessibilityNodeInfo.CollectionItemInfo.rowSpan", rowSpan);
921 bundle.putInt("AccessibilityNodeInfo.CollectionItemInfo.columnIndex", columnIndex);
922 bundle.putInt("AccessibilityNodeInfo.CollectionItemInfo.columnSpan", columnSpan);
926 protected void setAccessibilityEventRangeInfo(AccessibilityEvent event,
927 int rangeType, float min, float max, float current) {
928 // Backwards compatibility for KitKat AccessibilityNodeInfo fields.
929 Bundle bundle = getOrCreateBundleForAccessibilityEvent(event);
930 bundle.putInt("AccessibilityNodeInfo.RangeInfo.type", rangeType);
931 bundle.putFloat("AccessibilityNodeInfo.RangeInfo.min", min);
932 bundle.putFloat("AccessibilityNodeInfo.RangeInfo.max", max);
933 bundle.putFloat("AccessibilityNodeInfo.RangeInfo.current", current);
937 boolean shouldExposePasswordText() {
938 return (Settings.Secure.getInt(
939 mContentViewCore.getContext().getContentResolver(),
940 Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD, 0) == 1);
943 private native int nativeGetRootId(long nativeBrowserAccessibilityManagerAndroid);
944 private native boolean nativeIsNodeValid(long nativeBrowserAccessibilityManagerAndroid, int id);
945 private native boolean nativeIsEditableText(
946 long nativeBrowserAccessibilityManagerAndroid, int id);
947 private native int nativeGetEditableTextSelectionStart(
948 long nativeBrowserAccessibilityManagerAndroid, int id);
949 private native int nativeGetEditableTextSelectionEnd(
950 long nativeBrowserAccessibilityManagerAndroid, int id);
951 private native void nativeHitTest(long nativeBrowserAccessibilityManagerAndroid, int x, int y);
952 private native boolean nativePopulateAccessibilityNodeInfo(
953 long nativeBrowserAccessibilityManagerAndroid, AccessibilityNodeInfo info, int id);
954 private native boolean nativePopulateAccessibilityEvent(
955 long nativeBrowserAccessibilityManagerAndroid, AccessibilityEvent event, int id,
957 private native void nativeClick(long nativeBrowserAccessibilityManagerAndroid, int id);
958 private native void nativeFocus(long nativeBrowserAccessibilityManagerAndroid, int id);
959 private native void nativeBlur(long nativeBrowserAccessibilityManagerAndroid);
960 private native void nativeScrollToMakeNodeVisible(
961 long nativeBrowserAccessibilityManagerAndroid, int id);
962 private native int nativeFindElementType(long nativeBrowserAccessibilityManagerAndroid,
963 int startId, String elementType, boolean forwards);
964 private native void nativeSetTextFieldValue(long nativeBrowserAccessibilityManagerAndroid,
965 int id, String newValue);
966 private native void nativeSetSelection(long nativeBrowserAccessibilityManagerAndroid,
967 int id, int start, int end);
968 private native boolean nativeNextAtGranularity(long nativeBrowserAccessibilityManagerAndroid,
969 int selectionGranularity, boolean extendSelection, int id, int cursorIndex);
970 private native boolean nativePreviousAtGranularity(
971 long nativeBrowserAccessibilityManagerAndroid,
972 int selectionGranularity, boolean extendSelection, int id, int cursorIndex);
973 private native boolean nativeAdjustSlider(
974 long nativeBrowserAccessibilityManagerAndroid, int id, boolean increment);
975 private native void nativeSetAccessibilityFocus(
976 long nativeBrowserAccessibilityManagerAndroid, int id);