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_VIEW_TEXT_SELECTION_CHANGED); 272 } 273 274 @CalledByNative 275 private void handleEditableTextChanged(int id) { 276 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); 277 } 278 279 @CalledByNative 280 private void handleContentChanged(int id) { 281 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 282 } 283 284 @CalledByNative 285 private void handleNavigate() { 286 mAccessibilityFocusId = View.NO_ID; 287 mUserHasTouchExplored = false; 288 mFrameInfoInitialized = false; 289 } 290 291 @CalledByNative 292 private void handleScrolledToAnchor(int id) { 293 if (mAccessibilityFocusId == id) { 294 return; 295 } 296 297 mAccessibilityFocusId = id; 298 sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); 299 } 300 301 @CalledByNative 302 private void announceLiveRegionText(String text) { 303 mView.announceForAccessibility(text); 304 } 305 306 @CalledByNative 307 private void setAccessibilityNodeInfoParent(AccessibilityNodeInfo node, int parentId) { 308 node.setParent(mView, parentId); 309 } 310 311 @CalledByNative 312 private void addAccessibilityNodeInfoChild(AccessibilityNodeInfo node, int child_id) { 313 node.addChild(mView, child_id); 314 } 315 316 @CalledByNative 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); 331 332 if (focusable) { 333 if (focused) { 334 node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_FOCUS); 335 } else { 336 node.addAction(AccessibilityNodeInfo.ACTION_FOCUS); 337 } 338 } 339 340 if (mAccessibilityFocusId == virtualViewId) { 341 node.setAccessibilityFocused(true); 342 node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS); 343 } else { 344 node.setAccessibilityFocused(false); 345 node.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); 346 } 347 348 if (clickable) { 349 node.addAction(AccessibilityNodeInfo.ACTION_CLICK); 350 } 351 } 352 353 @CalledByNative 354 private void setAccessibilityNodeInfoStringAttributes(AccessibilityNodeInfo node, 355 String className, String contentDescription) { 356 node.setClassName(className); 357 node.setContentDescription(contentDescription); 358 } 359 360 @CalledByNative 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); 367 if (isRootNode) { 368 // Offset of the web content relative to the View. 369 boundsInParent.offset(0, (int) mRenderCoordinates.getContentOffsetYPix()); 370 } 371 node.setBoundsInParent(boundsInParent); 372 373 // Now set the absolute rect, which requires several transformations. 374 Rect rect = new Rect(absoluteLeft, absoluteTop, absoluteLeft + width, absoluteTop + height); 375 376 // Offset by the scroll position. 377 rect.offset(-(int) mRenderCoordinates.getScrollX(), 378 -(int) mRenderCoordinates.getScrollY()); 379 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); 385 386 // Offset by the location of the web content within the view. 387 rect.offset(0, 388 (int) mRenderCoordinates.getContentOffsetYPix()); 389 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]); 394 395 node.setBoundsInScreen(rect); 396 } 397 398 @CalledByNative 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); 405 } 406 407 @CalledByNative 408 private void setAccessibilityEventClassName(AccessibilityEvent event, String className) { 409 event.setClassName(className); 410 } 411 412 @CalledByNative 413 private void setAccessibilityEventListAttributes(AccessibilityEvent event, 414 int currentItemIndex, int itemCount) { 415 event.setCurrentItemIndex(currentItemIndex); 416 event.setItemCount(itemCount); 417 } 418 419 @CalledByNative 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); 426 } 427 428 @CalledByNative 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); 436 } 437 438 @CalledByNative 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); 445 } 446 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, 453 int eventType); 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); 457} 458