BrowserAccessibilityManager.java revision 9ab5563a3196760eb381d102cbb2bc0f7abc6a50
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. 4 5package org.chromium.content.browser.accessibility; 6 7import android.content.Context; 8import android.graphics.Rect; 9import android.os.Bundle; 10import android.os.Build; 11import android.view.MotionEvent; 12import android.view.View; 13import android.view.accessibility.AccessibilityEvent; 14import android.view.accessibility.AccessibilityManager; 15import android.view.accessibility.AccessibilityNodeInfo; 16import android.view.accessibility.AccessibilityNodeProvider; 17import android.view.inputmethod.InputMethodManager; 18 19import org.chromium.base.CalledByNative; 20import org.chromium.base.JNINamespace; 21import org.chromium.content.browser.ContentViewCore; 22import org.chromium.content.browser.RenderCoordinates; 23 24import java.util.ArrayList; 25import java.util.List; 26 27/** 28 * Native accessibility for a {@link ContentViewCore}. 29 * 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 33 * accessibility. 34 */ 35@JNINamespace("content") 36public class BrowserAccessibilityManager { 37 private static final String TAG = "BrowserAccessibilityManager"; 38 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]; 46 private View mView; 47 private boolean mUserHasTouchExplored; 48 private boolean mFrameInfoInitialized; 49 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; 54 55 /** 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. 61 */ 62 @CalledByNative 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); 68 } else { 69 return new BrowserAccessibilityManager( 70 nativeBrowserAccessibilityManagerAndroid, contentViewCore); 71 } 72 } 73 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); 86 } 87 88 @CalledByNative 89 private void onNativeObjectDestroyed() { 90 if (mContentViewCore.getBrowserAccessibilityManager() == this) { 91 mContentViewCore.setBrowserAccessibilityManager(null); 92 } 93 mNativeObj = 0; 94 mContentViewCore = null; 95 } 96 97 /** 98 * @return An AccessibilityNodeProvider on JellyBean, and null on previous versions. 99 */ 100 public AccessibilityNodeProvider getAccessibilityNodeProvider() { 101 return null; 102 } 103 104 /** 105 * @see AccessibilityNodeProvider#createAccessibilityNodeInfo(int) 106 */ 107 protected AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { 108 if (!mAccessibilityManager.isEnabled() || mNativeObj == 0 || !mFrameInfoInitialized) { 109 return null; 110 } 111 112 int rootId = nativeGetRootId(mNativeObj); 113 if (virtualViewId == View.NO_ID) { 114 virtualViewId = rootId; 115 } 116 if (mAccessibilityFocusId == View.NO_ID) { 117 mAccessibilityFocusId = rootId; 118 } 119 120 final AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(mView); 121 info.setPackageName(mContentViewCore.getContext().getPackageName()); 122 info.setSource(mView, virtualViewId); 123 124 if (nativePopulateAccessibilityNodeInfo(mNativeObj, info, virtualViewId)) { 125 return info; 126 } else { 127 return null; 128 } 129 } 130 131 /** 132 * @see AccessibilityNodeProvider#findAccessibilityNodeInfosByText(String, int) 133 */ 134 protected List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String text, 135 int virtualViewId) { 136 return new ArrayList<AccessibilityNodeInfo>(); 137 } 138 139 /** 140 * @see AccessibilityNodeProvider#performAction(int, int, Bundle) 141 */ 142 protected boolean performAction(int virtualViewId, int action, Bundle arguments) { 143 if (!mAccessibilityManager.isEnabled() || mNativeObj == 0) { 144 return false; 145 } 146 147 switch (action) { 148 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: 149 if (mAccessibilityFocusId == virtualViewId) { 150 return true; 151 } 152 153 mAccessibilityFocusId = virtualViewId; 154 sendAccessibilityEvent(mAccessibilityFocusId, 155 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); 156 return true; 157 case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: 158 if (mAccessibilityFocusId == virtualViewId) { 159 mAccessibilityFocusId = View.NO_ID; 160 } 161 return true; 162 case AccessibilityNodeInfo.ACTION_CLICK: 163 nativeClick(mNativeObj, virtualViewId); 164 break; 165 case AccessibilityNodeInfo.ACTION_FOCUS: 166 nativeFocus(mNativeObj, virtualViewId); 167 break; 168 case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS: 169 nativeBlur(mNativeObj); 170 break; 171 default: 172 break; 173 } 174 return false; 175 } 176 177 /** 178 * @see View#onHoverEvent(MotionEvent) 179 */ 180 public boolean onHoverEvent(MotionEvent event) { 181 if (!mAccessibilityManager.isEnabled() || mNativeObj == 0) { 182 return false; 183 } 184 185 if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) return true; 186 187 mUserHasTouchExplored = true; 188 float x = event.getX(); 189 float y = event.getY(); 190 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; 201 } 202 203 return true; 204 } 205 206 /** 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. 210 */ 211 public void notifyFrameInfoInitialized() { 212 if (mFrameInfoInitialized) return; 213 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); 220 } 221 } 222 223 private void sendAccessibilityEvent(int virtualViewId, int eventType) { 224 if (!mAccessibilityManager.isEnabled() || mNativeObj == 0) return; 225 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; 231 } 232 event.setSource(mView, virtualViewId); 233 if (!nativePopulateAccessibilityEvent(mNativeObj, event, virtualViewId, eventType)) return; 234 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(); 240 241 mContentViewCore.getContainerView().requestSendAccessibilityEvent(mView, event); 242 } 243 244 @CalledByNative 245 private void handlePageLoaded(int id) { 246 if (mUserHasTouchExplored) return; 247 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); 253 } 254 } 255 256 @CalledByNative 257 private void handleFocusChanged(int id) { 258 if (mAccessibilityFocusId == id) return; 259 260 mAccessibilityFocusId = id; 261 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_FOCUSED); 262 } 263 264 @CalledByNative 265 private void handleCheckStateChanged(int id) { 266 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_CLICKED); 267 } 268 269 @CalledByNative 270 private void handleTextSelectionChanged(int id) { 271 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 272 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED); 273 } 274 275 @CalledByNative 276 private void handleEditableTextChanged(int id) { 277 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 278 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); 279 } 280 281 @CalledByNative 282 private void handleContentChanged(int id) { 283 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 284 } 285 286 @CalledByNative 287 private void handleNavigate() { 288 mAccessibilityFocusId = View.NO_ID; 289 mUserHasTouchExplored = false; 290 mFrameInfoInitialized = false; 291 } 292 293 @CalledByNative 294 private void handleScrolledToAnchor(int id) { 295 if (mAccessibilityFocusId == id) { 296 return; 297 } 298 299 mAccessibilityFocusId = id; 300 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); 301 } 302 303 @CalledByNative 304 private void announceLiveRegionText(String text) { 305 mView.announceForAccessibility(text); 306 } 307 308 @CalledByNative 309 private void setAccessibilityNodeInfoParent(AccessibilityNodeInfo node, int parentId) { 310 node.setParent(mView, parentId); 311 } 312 313 @CalledByNative 314 private void addAccessibilityNodeInfoChild(AccessibilityNodeInfo node, int child_id) { 315 node.addChild(mView, child_id); 316 } 317 318 @CalledByNative 319 private void setAccessibilityNodeInfoBooleanAttributes(AccessibilityNodeInfo node, 320 int virtualViewId, boolean checkable, boolean checked, boolean clickable, 321 boolean enabled, boolean focusable, boolean focused, boolean password, 322 boolean scrollable, boolean selected, boolean visibleToUser) { 323 node.setCheckable(checkable); 324 node.setChecked(checked); 325 node.setClickable(clickable); 326 node.setEnabled(enabled); 327 node.setFocusable(focusable); 328 node.setFocused(focused); 329 node.setPassword(password); 330 node.setScrollable(scrollable); 331 node.setSelected(selected); 332 node.setVisibleToUser(visibleToUser); 333 334 if (focusable) { 335 if (focused) { 336 node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_FOCUS); 337 } else { 338 node.addAction(AccessibilityNodeInfo.ACTION_FOCUS); 339 } 340 } 341 342 if (mAccessibilityFocusId == virtualViewId) { 343 node.setAccessibilityFocused(true); 344 node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS); 345 } else { 346 node.setAccessibilityFocused(false); 347 node.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); 348 } 349 350 if (clickable) { 351 node.addAction(AccessibilityNodeInfo.ACTION_CLICK); 352 } 353 } 354 355 @CalledByNative 356 private void setAccessibilityNodeInfoStringAttributes(AccessibilityNodeInfo node, 357 String className, String contentDescription) { 358 node.setClassName(className); 359 node.setContentDescription(contentDescription); 360 } 361 362 @CalledByNative 363 private void setAccessibilityNodeInfoLocation(AccessibilityNodeInfo node, 364 int absoluteLeft, int absoluteTop, int parentRelativeLeft, int parentRelativeTop, 365 int width, int height, boolean isRootNode) { 366 // First set the bounds in parent. 367 Rect boundsInParent = new Rect(parentRelativeLeft, parentRelativeTop, 368 parentRelativeLeft + width, parentRelativeTop + height); 369 if (isRootNode) { 370 // Offset of the web content relative to the View. 371 boundsInParent.offset(0, (int) mRenderCoordinates.getContentOffsetYPix()); 372 } 373 node.setBoundsInParent(boundsInParent); 374 375 // Now set the absolute rect, which requires several transformations. 376 Rect rect = new Rect(absoluteLeft, absoluteTop, absoluteLeft + width, absoluteTop + height); 377 378 // Offset by the scroll position. 379 rect.offset(-(int) mRenderCoordinates.getScrollX(), 380 -(int) mRenderCoordinates.getScrollY()); 381 382 // Convert CSS (web) pixels to Android View pixels 383 rect.left = (int) mRenderCoordinates.fromLocalCssToPix(rect.left); 384 rect.top = (int) mRenderCoordinates.fromLocalCssToPix(rect.top); 385 rect.bottom = (int) mRenderCoordinates.fromLocalCssToPix(rect.bottom); 386 rect.right = (int) mRenderCoordinates.fromLocalCssToPix(rect.right); 387 388 // Offset by the location of the web content within the view. 389 rect.offset(0, 390 (int) mRenderCoordinates.getContentOffsetYPix()); 391 392 // Finally offset by the location of the view within the screen. 393 final int[] viewLocation = new int[2]; 394 mView.getLocationOnScreen(viewLocation); 395 rect.offset(viewLocation[0], viewLocation[1]); 396 397 node.setBoundsInScreen(rect); 398 } 399 400 @CalledByNative 401 private void setAccessibilityEventBooleanAttributes(AccessibilityEvent event, 402 boolean checked, boolean enabled, boolean password, boolean scrollable) { 403 event.setChecked(checked); 404 event.setEnabled(enabled); 405 event.setPassword(password); 406 event.setScrollable(scrollable); 407 } 408 409 @CalledByNative 410 private void setAccessibilityEventClassName(AccessibilityEvent event, String className) { 411 event.setClassName(className); 412 } 413 414 @CalledByNative 415 private void setAccessibilityEventListAttributes(AccessibilityEvent event, 416 int currentItemIndex, int itemCount) { 417 event.setCurrentItemIndex(currentItemIndex); 418 event.setItemCount(itemCount); 419 } 420 421 @CalledByNative 422 private void setAccessibilityEventScrollAttributes(AccessibilityEvent event, 423 int scrollX, int scrollY, int maxScrollX, int maxScrollY) { 424 event.setScrollX(scrollX); 425 event.setScrollY(scrollY); 426 event.setMaxScrollX(maxScrollX); 427 event.setMaxScrollY(maxScrollY); 428 } 429 430 @CalledByNative 431 private void setAccessibilityEventTextChangedAttrs(AccessibilityEvent event, 432 int fromIndex, int addedCount, int removedCount, String beforeText, String text) { 433 event.setFromIndex(fromIndex); 434 event.setAddedCount(addedCount); 435 event.setRemovedCount(removedCount); 436 event.setBeforeText(beforeText); 437 event.getText().add(text); 438 } 439 440 @CalledByNative 441 private void setAccessibilityEventSelectionAttrs(AccessibilityEvent event, 442 int fromIndex, int addedCount, int itemCount, String text) { 443 event.setFromIndex(fromIndex); 444 event.setAddedCount(addedCount); 445 event.setItemCount(itemCount); 446 event.getText().add(text); 447 } 448 449 private native int nativeGetRootId(int nativeBrowserAccessibilityManagerAndroid); 450 private native int nativeHitTest(int nativeBrowserAccessibilityManagerAndroid, int x, int y); 451 private native boolean nativePopulateAccessibilityNodeInfo( 452 int nativeBrowserAccessibilityManagerAndroid, AccessibilityNodeInfo info, int id); 453 private native boolean nativePopulateAccessibilityEvent( 454 int nativeBrowserAccessibilityManagerAndroid, AccessibilityEvent event, int id, 455 int eventType); 456 private native void nativeClick(int nativeBrowserAccessibilityManagerAndroid, int id); 457 private native void nativeFocus(int nativeBrowserAccessibilityManagerAndroid, int id); 458 private native void nativeBlur(int nativeBrowserAccessibilityManagerAndroid); 459} 460