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.Bundle;
10 import android.os.Build;
11 import android.view.MotionEvent;
12 import android.view.View;
13 import android.view.accessibility.AccessibilityEvent;
14 import android.view.accessibility.AccessibilityManager;
15 import android.view.accessibility.AccessibilityNodeInfo;
16 import android.view.accessibility.AccessibilityNodeProvider;
17 import android.view.inputmethod.InputMethodManager;
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 AccessibilityManager mAccessibilityManager;
41 private RenderCoordinates mRenderCoordinates;
42 private int mNativeObj;
43 private int mAccessibilityFocusId;
44 private int mCurrentHoverId;
45 private final int[] mTempLocation = new int[2];
47 private boolean mUserHasTouchExplored;
48 private boolean mFrameInfoInitialized;
50 // If this is true, enables an experimental feature that focuses the web page after it
51 // finishes loading. Disabled for now because it can be confusing if the user was
52 // trying to do something when this happens.
53 private boolean mFocusPageOnLoad;
56 * Create a BrowserAccessibilityManager object, which is owned by the C++
57 * BrowserAccessibilityManagerAndroid instance, and connects to the content view.
58 * @param nativeBrowserAccessibilityManagerAndroid A pointer to the counterpart native
59 * C++ object that owns this object.
60 * @param contentViewCore The content view that this object provides accessibility for.
63 private static BrowserAccessibilityManager create(int nativeBrowserAccessibilityManagerAndroid,
64 ContentViewCore contentViewCore) {
65 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(int 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 mView = mContentViewCore.getContainerView();
82 mRenderCoordinates = mContentViewCore.getRenderCoordinates();
83 mAccessibilityManager =
84 (AccessibilityManager) mContentViewCore.getContext()
85 .getSystemService(Context.ACCESSIBILITY_SERVICE);
89 private void onNativeObjectDestroyed() {
90 if (mContentViewCore.getBrowserAccessibilityManager() == this) {
91 mContentViewCore.setBrowserAccessibilityManager(null);
94 mContentViewCore = null;
98 * @return An AccessibilityNodeProvider on JellyBean, and null on previous versions.
100 public AccessibilityNodeProvider getAccessibilityNodeProvider() {
105 * @see AccessibilityNodeProvider#createAccessibilityNodeInfo(int)
107 protected AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
108 if (!mAccessibilityManager.isEnabled() || mNativeObj == 0 || !mFrameInfoInitialized) {
112 int rootId = nativeGetRootId(mNativeObj);
113 if (virtualViewId == View.NO_ID) {
114 virtualViewId = rootId;
116 if (mAccessibilityFocusId == View.NO_ID) {
117 mAccessibilityFocusId = rootId;
120 final AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(mView);
121 info.setPackageName(mContentViewCore.getContext().getPackageName());
122 info.setSource(mView, virtualViewId);
124 if (nativePopulateAccessibilityNodeInfo(mNativeObj, info, virtualViewId)) {
132 * @see AccessibilityNodeProvider#findAccessibilityNodeInfosByText(String, int)
134 protected List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String text,
136 return new ArrayList<AccessibilityNodeInfo>();
140 * @see AccessibilityNodeProvider#performAction(int, int, Bundle)
142 protected boolean performAction(int virtualViewId, int action, Bundle arguments) {
143 if (!mAccessibilityManager.isEnabled() || mNativeObj == 0) {
148 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
149 if (mAccessibilityFocusId == virtualViewId) {
153 mAccessibilityFocusId = virtualViewId;
154 sendAccessibilityEvent(mAccessibilityFocusId,
155 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
157 case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
158 if (mAccessibilityFocusId == virtualViewId) {
159 mAccessibilityFocusId = View.NO_ID;
162 case AccessibilityNodeInfo.ACTION_CLICK:
163 nativeClick(mNativeObj, virtualViewId);
165 case AccessibilityNodeInfo.ACTION_FOCUS:
166 nativeFocus(mNativeObj, virtualViewId);
168 case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS:
169 nativeBlur(mNativeObj);
178 * @see View#onHoverEvent(MotionEvent)
180 public boolean onHoverEvent(MotionEvent event) {
181 if (!mAccessibilityManager.isEnabled() || mNativeObj == 0) {
185 if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) return true;
187 mUserHasTouchExplored = true;
188 float x = event.getX();
189 float y = event.getY();
191 // Convert to CSS coordinates.
192 int cssX = (int) (mRenderCoordinates.fromPixToLocalCss(x) +
193 mRenderCoordinates.getScrollX());
194 int cssY = (int) (mRenderCoordinates.fromPixToLocalCss(y) +
195 mRenderCoordinates.getScrollY());
196 int id = nativeHitTest(mNativeObj, cssX, cssY);
197 if (mCurrentHoverId != id) {
198 sendAccessibilityEvent(mCurrentHoverId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
199 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
200 mCurrentHoverId = id;
207 * Called by ContentViewCore to notify us when the frame info is initialized,
208 * the first time, since until that point, we can't use mRenderCoordinates to transform
209 * web coordinates to screen coordinates.
211 public void notifyFrameInfoInitialized() {
212 if (mFrameInfoInitialized) return;
214 mFrameInfoInitialized = true;
215 // (Re-) focus focused element, since we weren't able to create an
216 // AccessibilityNodeInfo for this element before.
217 if (mAccessibilityFocusId != View.NO_ID) {
218 sendAccessibilityEvent(mAccessibilityFocusId,
219 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
223 private void sendAccessibilityEvent(int virtualViewId, int eventType) {
224 if (!mAccessibilityManager.isEnabled() || mNativeObj == 0) return;
226 final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
227 event.setPackageName(mContentViewCore.getContext().getPackageName());
228 int rootId = nativeGetRootId(mNativeObj);
229 if (virtualViewId == rootId) {
230 virtualViewId = View.NO_ID;
232 event.setSource(mView, virtualViewId);
233 if (!nativePopulateAccessibilityEvent(mNativeObj, event, virtualViewId, eventType)) return;
235 // This is currently needed if we want Android to draw the yellow box around
236 // the item that has accessibility focus. In practice, this doesn't seem to slow
237 // things down, because it's only called when the accessibility focus moves.
238 // TODO(dmazzoni): remove this if/when Android framework fixes bug.
239 mContentViewCore.getContainerView().postInvalidate();
241 mContentViewCore.getContainerView().requestSendAccessibilityEvent(mView, event);
245 private void handlePageLoaded(int id) {
246 if (mUserHasTouchExplored) return;
248 if (mFocusPageOnLoad) {
249 // Focus the natively focused node (usually document),
250 // if this feature is enabled.
251 mAccessibilityFocusId = id;
252 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_FOCUSED);
257 private void handleFocusChanged(int id) {
258 if (mAccessibilityFocusId == id) return;
260 mAccessibilityFocusId = id;
261 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_FOCUSED);
265 private void handleCheckStateChanged(int id) {
266 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_CLICKED);
270 private void handleTextSelectionChanged(int id) {
271 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
275 private void handleEditableTextChanged(int id) {
276 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
280 private void handleContentChanged(int id) {
281 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
285 private void handleNavigate() {
286 mAccessibilityFocusId = View.NO_ID;
287 mUserHasTouchExplored = false;
288 mFrameInfoInitialized = false;
292 private void handleScrolledToAnchor(int id) {
293 if (mAccessibilityFocusId == id) {
297 mAccessibilityFocusId = id;
298 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
302 private void announceLiveRegionText(String text) {
303 mView.announceForAccessibility(text);
307 private void setAccessibilityNodeInfoParent(AccessibilityNodeInfo node, int parentId) {
308 node.setParent(mView, parentId);
312 private void addAccessibilityNodeInfoChild(AccessibilityNodeInfo node, int child_id) {
313 node.addChild(mView, child_id);
317 private void setAccessibilityNodeInfoBooleanAttributes(AccessibilityNodeInfo node,
318 int virtualViewId, boolean checkable, boolean checked, boolean clickable,
319 boolean enabled, boolean focusable, boolean focused, boolean password,
320 boolean scrollable, boolean selected, boolean visibleToUser) {
321 node.setCheckable(checkable);
322 node.setChecked(checked);
323 node.setClickable(clickable);
324 node.setEnabled(enabled);
325 node.setFocusable(focusable);
326 node.setFocused(focused);
327 node.setPassword(password);
328 node.setScrollable(scrollable);
329 node.setSelected(selected);
330 node.setVisibleToUser(visibleToUser);
334 node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_FOCUS);
336 node.addAction(AccessibilityNodeInfo.ACTION_FOCUS);
340 if (mAccessibilityFocusId == virtualViewId) {
341 node.setAccessibilityFocused(true);
342 node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
344 node.setAccessibilityFocused(false);
345 node.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
349 node.addAction(AccessibilityNodeInfo.ACTION_CLICK);
354 private void setAccessibilityNodeInfoStringAttributes(AccessibilityNodeInfo node,
355 String className, String contentDescription) {
356 node.setClassName(className);
357 node.setContentDescription(contentDescription);
361 private void setAccessibilityNodeInfoLocation(AccessibilityNodeInfo node,
362 int absoluteLeft, int absoluteTop, int parentRelativeLeft, int parentRelativeTop,
363 int width, int height, boolean isRootNode) {
364 // First set the bounds in parent.
365 Rect boundsInParent = new Rect(parentRelativeLeft, parentRelativeTop,
366 parentRelativeLeft + width, parentRelativeTop + height);
368 // Offset of the web content relative to the View.
369 boundsInParent.offset(0, (int) mRenderCoordinates.getContentOffsetYPix());
371 node.setBoundsInParent(boundsInParent);
373 // Now set the absolute rect, which requires several transformations.
374 Rect rect = new Rect(absoluteLeft, absoluteTop, absoluteLeft + width, absoluteTop + height);
376 // Offset by the scroll position.
377 rect.offset(-(int) mRenderCoordinates.getScrollX(),
378 -(int) mRenderCoordinates.getScrollY());
380 // Convert CSS (web) pixels to Android View pixels
381 rect.left = (int) mRenderCoordinates.fromLocalCssToPix(rect.left);
382 rect.top = (int) mRenderCoordinates.fromLocalCssToPix(rect.top);
383 rect.bottom = (int) mRenderCoordinates.fromLocalCssToPix(rect.bottom);
384 rect.right = (int) mRenderCoordinates.fromLocalCssToPix(rect.right);
386 // Offset by the location of the web content within the view.
388 (int) mRenderCoordinates.getContentOffsetYPix());
390 // Finally offset by the location of the view within the screen.
391 final int[] viewLocation = new int[2];
392 mView.getLocationOnScreen(viewLocation);
393 rect.offset(viewLocation[0], viewLocation[1]);
395 node.setBoundsInScreen(rect);
399 private void setAccessibilityEventBooleanAttributes(AccessibilityEvent event,
400 boolean checked, boolean enabled, boolean password, boolean scrollable) {
401 event.setChecked(checked);
402 event.setEnabled(enabled);
403 event.setPassword(password);
404 event.setScrollable(scrollable);
408 private void setAccessibilityEventClassName(AccessibilityEvent event, String className) {
409 event.setClassName(className);
413 private void setAccessibilityEventListAttributes(AccessibilityEvent event,
414 int currentItemIndex, int itemCount) {
415 event.setCurrentItemIndex(currentItemIndex);
416 event.setItemCount(itemCount);
420 private void setAccessibilityEventScrollAttributes(AccessibilityEvent event,
421 int scrollX, int scrollY, int maxScrollX, int maxScrollY) {
422 event.setScrollX(scrollX);
423 event.setScrollY(scrollY);
424 event.setMaxScrollX(maxScrollX);
425 event.setMaxScrollY(maxScrollY);
429 private void setAccessibilityEventTextChangedAttrs(AccessibilityEvent event,
430 int fromIndex, int addedCount, int removedCount, String beforeText, String text) {
431 event.setFromIndex(fromIndex);
432 event.setAddedCount(addedCount);
433 event.setRemovedCount(removedCount);
434 event.setBeforeText(beforeText);
435 event.getText().add(text);
439 private void setAccessibilityEventSelectionAttrs(AccessibilityEvent event,
440 int fromIndex, int addedCount, int itemCount, String text) {
441 event.setFromIndex(fromIndex);
442 event.setAddedCount(addedCount);
443 event.setItemCount(itemCount);
444 event.getText().add(text);
447 private native int nativeGetRootId(int nativeBrowserAccessibilityManagerAndroid);
448 private native int nativeHitTest(int nativeBrowserAccessibilityManagerAndroid, int x, int y);
449 private native boolean nativePopulateAccessibilityNodeInfo(
450 int nativeBrowserAccessibilityManagerAndroid, AccessibilityNodeInfo info, int id);
451 private native boolean nativePopulateAccessibilityEvent(
452 int nativeBrowserAccessibilityManagerAndroid, AccessibilityEvent event, int id,
454 private native void nativeClick(int nativeBrowserAccessibilityManagerAndroid, int id);
455 private native void nativeFocus(int nativeBrowserAccessibilityManagerAndroid, int id);
456 private native void nativeBlur(int nativeBrowserAccessibilityManagerAndroid);