Home | History | Annotate | Download | only in accessibility
      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 
      5 package org.chromium.content.browser.accessibility;
      6 
      7 import android.content.Context;
      8 import android.graphics.Rect;
      9 import android.os.Build;
     10 import android.os.Bundle;
     11 import android.text.SpannableString;
     12 import android.text.style.URLSpan;
     13 import android.view.MotionEvent;
     14 import android.view.View;
     15 import android.view.ViewGroup;
     16 import android.view.ViewParent;
     17 import android.view.accessibility.AccessibilityEvent;
     18 import android.view.accessibility.AccessibilityManager;
     19 import android.view.accessibility.AccessibilityNodeInfo;
     20 import android.view.accessibility.AccessibilityNodeProvider;
     21 
     22 import org.chromium.base.CalledByNative;
     23 import org.chromium.base.JNINamespace;
     24 import org.chromium.content.browser.ContentViewCore;
     25 import org.chromium.content.browser.RenderCoordinates;
     26 
     27 import java.util.ArrayList;
     28 import java.util.List;
     29 import java.util.Locale;
     30 
     31 /**
     32  * Native accessibility for a {@link ContentViewCore}.
     33  *
     34  * This class is safe to load on ICS and can be used to run tests, but
     35  * only the subclass, JellyBeanBrowserAccessibilityManager, actually
     36  * has a AccessibilityNodeProvider implementation needed for native
     37  * accessibility.
     38  */
     39 @JNINamespace("content")
     40 public class BrowserAccessibilityManager {
     41     private static final String TAG = "BrowserAccessibilityManager";
     42 
     43     private ContentViewCore mContentViewCore;
     44     private final AccessibilityManager mAccessibilityManager;
     45     private final RenderCoordinates mRenderCoordinates;
     46     private long mNativeObj;
     47     private int mAccessibilityFocusId;
     48     private Rect mAccessibilityFocusRect;
     49     private boolean mIsHovering;
     50     private int mLastHoverId = View.NO_ID;
     51     private int mCurrentRootId;
     52     private final int[] mTempLocation = new int[2];
     53     private final ViewGroup mView;
     54     private boolean mUserHasTouchExplored;
     55     private boolean mPendingScrollToMakeNodeVisible;
     56     private boolean mNotifyFrameInfoInitializedCalled;
     57 
     58     /**
     59      * Create a BrowserAccessibilityManager object, which is owned by the C++
     60      * BrowserAccessibilityManagerAndroid instance, and connects to the content view.
     61      * @param nativeBrowserAccessibilityManagerAndroid A pointer to the counterpart native
     62      *     C++ object that owns this object.
     63      * @param contentViewCore The content view that this object provides accessibility for.
     64      */
     65     @CalledByNative
     66     private static BrowserAccessibilityManager create(long nativeBrowserAccessibilityManagerAndroid,
     67             ContentViewCore contentViewCore) {
     68         // A bug in the KitKat framework prevents us from using these new APIs.
     69         // http://crbug.com/348088/
     70         // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
     71         //     return new KitKatBrowserAccessibilityManager(
     72         //             nativeBrowserAccessibilityManagerAndroid, contentViewCore);
     73 
     74         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
     75             return new JellyBeanBrowserAccessibilityManager(
     76                     nativeBrowserAccessibilityManagerAndroid, contentViewCore);
     77         } else {
     78             return new BrowserAccessibilityManager(
     79                     nativeBrowserAccessibilityManagerAndroid, contentViewCore);
     80         }
     81     }
     82 
     83     protected BrowserAccessibilityManager(long nativeBrowserAccessibilityManagerAndroid,
     84             ContentViewCore contentViewCore) {
     85         mNativeObj = nativeBrowserAccessibilityManagerAndroid;
     86         mContentViewCore = contentViewCore;
     87         mContentViewCore.setBrowserAccessibilityManager(this);
     88         mAccessibilityFocusId = View.NO_ID;
     89         mIsHovering = false;
     90         mCurrentRootId = View.NO_ID;
     91         mView = mContentViewCore.getContainerView();
     92         mRenderCoordinates = mContentViewCore.getRenderCoordinates();
     93         mAccessibilityManager =
     94             (AccessibilityManager) mContentViewCore.getContext()
     95             .getSystemService(Context.ACCESSIBILITY_SERVICE);
     96     }
     97 
     98     @CalledByNative
     99     private void onNativeObjectDestroyed() {
    100         if (mContentViewCore.getBrowserAccessibilityManager() == this) {
    101             mContentViewCore.setBrowserAccessibilityManager(null);
    102         }
    103         mNativeObj = 0;
    104         mContentViewCore = null;
    105     }
    106 
    107     /**
    108      * @return An AccessibilityNodeProvider on JellyBean, and null on previous versions.
    109      */
    110     public AccessibilityNodeProvider getAccessibilityNodeProvider() {
    111         return null;
    112     }
    113 
    114     /**
    115      * @see AccessibilityNodeProvider#createAccessibilityNodeInfo(int)
    116      */
    117     protected AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
    118         if (!mAccessibilityManager.isEnabled() || mNativeObj == 0) {
    119             return null;
    120         }
    121 
    122         int rootId = nativeGetRootId(mNativeObj);
    123 
    124         if (virtualViewId == View.NO_ID) {
    125             return createNodeForHost(rootId);
    126         }
    127 
    128         if (!isFrameInfoInitialized()) {
    129             return null;
    130         }
    131 
    132         final AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(mView);
    133         info.setPackageName(mContentViewCore.getContext().getPackageName());
    134         info.setSource(mView, virtualViewId);
    135 
    136         if (virtualViewId == rootId) {
    137             info.setParent(mView);
    138         }
    139 
    140         if (nativePopulateAccessibilityNodeInfo(mNativeObj, info, virtualViewId)) {
    141             return info;
    142         } else {
    143             info.recycle();
    144             return null;
    145         }
    146     }
    147 
    148     /**
    149      * @see AccessibilityNodeProvider#findAccessibilityNodeInfosByText(String, int)
    150      */
    151     protected List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String text,
    152             int virtualViewId) {
    153         return new ArrayList<AccessibilityNodeInfo>();
    154     }
    155 
    156     /**
    157      * @see AccessibilityNodeProvider#performAction(int, int, Bundle)
    158      */
    159     protected boolean performAction(int virtualViewId, int action, Bundle arguments) {
    160         // We don't support any actions on the host view or nodes
    161         // that are not (any longer) in the tree.
    162         if (!mAccessibilityManager.isEnabled() || mNativeObj == 0
    163                 || !nativeIsNodeValid(mNativeObj, virtualViewId)) {
    164             return false;
    165         }
    166 
    167         switch (action) {
    168             case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
    169                 if (!moveAccessibilityFocusToId(virtualViewId)) return true;
    170 
    171                 if (!mIsHovering) {
    172                     nativeScrollToMakeNodeVisible(
    173                             mNativeObj, mAccessibilityFocusId);
    174                 } else {
    175                     mPendingScrollToMakeNodeVisible = true;
    176                 }
    177                 return true;
    178             case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
    179                 if (mAccessibilityFocusId == virtualViewId) {
    180                     sendAccessibilityEvent(mAccessibilityFocusId,
    181                             AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
    182                     mAccessibilityFocusId = View.NO_ID;
    183                     mAccessibilityFocusRect = null;
    184                 }
    185                 return true;
    186             case AccessibilityNodeInfo.ACTION_CLICK:
    187                 nativeClick(mNativeObj, virtualViewId);
    188                 sendAccessibilityEvent(virtualViewId,
    189                         AccessibilityEvent.TYPE_VIEW_CLICKED);
    190                 return true;
    191             case AccessibilityNodeInfo.ACTION_FOCUS:
    192                 nativeFocus(mNativeObj, virtualViewId);
    193                 return true;
    194             case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS:
    195                 nativeBlur(mNativeObj);
    196                 return true;
    197 
    198             case AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT: {
    199                 if (arguments == null)
    200                     return false;
    201                 String elementType = arguments.getString(
    202                     AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING);
    203                 if (elementType == null)
    204                     return false;
    205                 elementType = elementType.toUpperCase(Locale.US);
    206                 return jumpToElementType(elementType, true);
    207             }
    208             case AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT: {
    209                 if (arguments == null)
    210                     return false;
    211                 String elementType = arguments.getString(
    212                     AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING);
    213                 if (elementType == null)
    214                     return false;
    215                 elementType = elementType.toUpperCase(Locale.US);
    216                 return jumpToElementType(elementType, false);
    217             }
    218 
    219             default:
    220                 break;
    221         }
    222         return false;
    223     }
    224 
    225     /**
    226      * @see View#onHoverEvent(MotionEvent)
    227      */
    228     public boolean onHoverEvent(MotionEvent event) {
    229         if (!mAccessibilityManager.isEnabled() || mNativeObj == 0) {
    230             return false;
    231         }
    232 
    233         if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) {
    234             mIsHovering = false;
    235             if (mPendingScrollToMakeNodeVisible) {
    236                 nativeScrollToMakeNodeVisible(
    237                         mNativeObj, mAccessibilityFocusId);
    238             }
    239             mPendingScrollToMakeNodeVisible = false;
    240             return true;
    241         }
    242 
    243         mIsHovering = true;
    244         mUserHasTouchExplored = true;
    245         float x = event.getX();
    246         float y = event.getY();
    247 
    248         // Convert to CSS coordinates.
    249         int cssX = (int) (mRenderCoordinates.fromPixToLocalCss(x));
    250         int cssY = (int) (mRenderCoordinates.fromPixToLocalCss(y));
    251 
    252         // This sends an IPC to the render process to do the hit testing.
    253         // The response is handled by handleHover.
    254         nativeHitTest(mNativeObj, cssX, cssY);
    255         return true;
    256     }
    257 
    258     /**
    259      * Called by ContentViewCore to notify us when the frame info is initialized,
    260      * the first time, since until that point, we can't use mRenderCoordinates to transform
    261      * web coordinates to screen coordinates.
    262      */
    263     public void notifyFrameInfoInitialized() {
    264         if (mNotifyFrameInfoInitializedCalled)
    265             return;
    266 
    267         mNotifyFrameInfoInitializedCalled = true;
    268 
    269         // Invalidate the container view, since the chrome accessibility tree is now
    270         // ready and listed as the child of the container view.
    271         mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
    272 
    273         // (Re-) focus focused element, since we weren't able to create an
    274         // AccessibilityNodeInfo for this element before.
    275         if (mAccessibilityFocusId != View.NO_ID) {
    276             moveAccessibilityFocusToIdAndRefocusIfNeeded(mAccessibilityFocusId);
    277         }
    278     }
    279 
    280     private boolean jumpToElementType(String elementType, boolean forwards) {
    281         int id = nativeFindElementType(mNativeObj, mAccessibilityFocusId, elementType, forwards);
    282         if (id == 0)
    283             return false;
    284 
    285         moveAccessibilityFocusToId(id);
    286         return true;
    287     }
    288 
    289     private boolean moveAccessibilityFocusToId(int newAccessibilityFocusId) {
    290         if (newAccessibilityFocusId == mAccessibilityFocusId)
    291             return false;
    292 
    293         mAccessibilityFocusId = newAccessibilityFocusId;
    294         mAccessibilityFocusRect = null;
    295         sendAccessibilityEvent(mAccessibilityFocusId,
    296                 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
    297         return true;
    298     }
    299 
    300     private void moveAccessibilityFocusToIdAndRefocusIfNeeded(int newAccessibilityFocusId) {
    301         // Work around a bug in the Android framework where it doesn't fully update the object
    302         // with accessibility focus even if you send it a WINDOW_CONTENT_CHANGED. To work around
    303         // this, clear focus and then set focus again.
    304         if (newAccessibilityFocusId == mAccessibilityFocusId) {
    305             sendAccessibilityEvent(newAccessibilityFocusId,
    306                     AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
    307             mAccessibilityFocusId = View.NO_ID;
    308         }
    309 
    310         moveAccessibilityFocusToId(newAccessibilityFocusId);
    311     }
    312 
    313     private void sendAccessibilityEvent(int virtualViewId, int eventType) {
    314         // If we don't have any frame info, then the virtual hierarchy
    315         // doesn't exist in the view of the Android framework, so should
    316         // never send any events.
    317         if (!mAccessibilityManager.isEnabled() || mNativeObj == 0
    318                 || !isFrameInfoInitialized()) {
    319             return;
    320         }
    321 
    322         // This is currently needed if we want Android to draw the yellow box around
    323         // the item that has accessibility focus. In practice, this doesn't seem to slow
    324         // things down, because it's only called when the accessibility focus moves.
    325         // TODO(dmazzoni): remove this if/when Android framework fixes bug.
    326         mView.postInvalidate();
    327 
    328         // The container view is indicated by a virtualViewId of NO_ID; post these events directly
    329         // since there's no web-specific information to attach.
    330         if (virtualViewId == View.NO_ID) {
    331             mView.sendAccessibilityEvent(eventType);
    332             return;
    333         }
    334 
    335         final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
    336         event.setPackageName(mContentViewCore.getContext().getPackageName());
    337         event.setSource(mView, virtualViewId);
    338         if (!nativePopulateAccessibilityEvent(mNativeObj, event, virtualViewId, eventType)) {
    339             event.recycle();
    340             return;
    341         }
    342 
    343         mView.requestSendAccessibilityEvent(mView, event);
    344     }
    345 
    346     private Bundle getOrCreateBundleForAccessibilityEvent(AccessibilityEvent event) {
    347         Bundle bundle = (Bundle) event.getParcelableData();
    348         if (bundle == null) {
    349             bundle = new Bundle();
    350             event.setParcelableData(bundle);
    351         }
    352         return bundle;
    353     }
    354 
    355     private AccessibilityNodeInfo createNodeForHost(int rootId) {
    356         // Since we don't want the parent to be focusable, but we can't remove
    357         // actions from a node, copy over the necessary fields.
    358         final AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(mView);
    359         final AccessibilityNodeInfo source = AccessibilityNodeInfo.obtain(mView);
    360         mView.onInitializeAccessibilityNodeInfo(source);
    361 
    362         // Copy over parent and screen bounds.
    363         Rect rect = new Rect();
    364         source.getBoundsInParent(rect);
    365         result.setBoundsInParent(rect);
    366         source.getBoundsInScreen(rect);
    367         result.setBoundsInScreen(rect);
    368 
    369         // Set up the parent view, if applicable.
    370         final ViewParent parent = mView.getParentForAccessibility();
    371         if (parent instanceof View) {
    372             result.setParent((View) parent);
    373         }
    374 
    375         // Populate the minimum required fields.
    376         result.setVisibleToUser(source.isVisibleToUser());
    377         result.setEnabled(source.isEnabled());
    378         result.setPackageName(source.getPackageName());
    379         result.setClassName(source.getClassName());
    380 
    381         // Add the Chrome root node.
    382         if (isFrameInfoInitialized()) {
    383             result.addChild(mView, rootId);
    384         }
    385 
    386         return result;
    387     }
    388 
    389     /**
    390      * Returns whether or not the frame info is initialized, meaning we can safely
    391      * convert web coordinates to screen coordinates. When this is first initialized,
    392      * notifyFrameInfoInitialized is called - but we shouldn't check whether or not
    393      * that method was called as a way to determine if frame info is valid because
    394      * notifyFrameInfoInitialized might not be called at all if mRenderCoordinates
    395      * gets initialized first.
    396      */
    397     private boolean isFrameInfoInitialized() {
    398         return mRenderCoordinates.getContentWidthCss() != 0.0 ||
    399                mRenderCoordinates.getContentHeightCss() != 0.0;
    400     }
    401 
    402     @CalledByNative
    403     private void handlePageLoaded(int id) {
    404         if (mUserHasTouchExplored) return;
    405 
    406         if (mContentViewCore.shouldSetAccessibilityFocusOnPageLoad()) {
    407             moveAccessibilityFocusToIdAndRefocusIfNeeded(id);
    408         }
    409     }
    410 
    411     @CalledByNative
    412     private void handleFocusChanged(int id) {
    413         sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_FOCUSED);
    414         moveAccessibilityFocusToId(id);
    415     }
    416 
    417     @CalledByNative
    418     private void handleCheckStateChanged(int id) {
    419         sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_CLICKED);
    420     }
    421 
    422     @CalledByNative
    423     private void handleTextSelectionChanged(int id) {
    424         sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
    425     }
    426 
    427     @CalledByNative
    428     private void handleEditableTextChanged(int id) {
    429         sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
    430     }
    431 
    432     @CalledByNative
    433     private void handleContentChanged(int id) {
    434         int rootId = nativeGetRootId(mNativeObj);
    435         if (rootId != mCurrentRootId) {
    436             mCurrentRootId = rootId;
    437             mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
    438         } else {
    439             sendAccessibilityEvent(id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
    440         }
    441     }
    442 
    443     @CalledByNative
    444     private void handleNavigate() {
    445         mAccessibilityFocusId = View.NO_ID;
    446         mAccessibilityFocusRect = null;
    447         mUserHasTouchExplored = false;
    448         // Invalidate the host, since its child is now gone.
    449         mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
    450     }
    451 
    452     @CalledByNative
    453     private void handleScrollPositionChanged(int id) {
    454         sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_SCROLLED);
    455     }
    456 
    457     @CalledByNative
    458     private void handleScrolledToAnchor(int id) {
    459         moveAccessibilityFocusToId(id);
    460     }
    461 
    462     @CalledByNative
    463     private void handleHover(int id) {
    464         if (mLastHoverId == id) return;
    465 
    466         // Always send the ENTER and then the EXIT event, to match a standard Android View.
    467         sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
    468         sendAccessibilityEvent(mLastHoverId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
    469         mLastHoverId = id;
    470     }
    471 
    472     @CalledByNative
    473     private void announceLiveRegionText(String text) {
    474         mView.announceForAccessibility(text);
    475     }
    476 
    477     @CalledByNative
    478     private void setAccessibilityNodeInfoParent(AccessibilityNodeInfo node, int parentId) {
    479         node.setParent(mView, parentId);
    480     }
    481 
    482     @CalledByNative
    483     private void addAccessibilityNodeInfoChild(AccessibilityNodeInfo node, int childId) {
    484         node.addChild(mView, childId);
    485     }
    486 
    487     @CalledByNative
    488     private void setAccessibilityNodeInfoBooleanAttributes(AccessibilityNodeInfo node,
    489             int virtualViewId, boolean checkable, boolean checked, boolean clickable,
    490             boolean enabled, boolean focusable, boolean focused, boolean password,
    491             boolean scrollable, boolean selected, boolean visibleToUser) {
    492         node.setCheckable(checkable);
    493         node.setChecked(checked);
    494         node.setClickable(clickable);
    495         node.setEnabled(enabled);
    496         node.setFocusable(focusable);
    497         node.setFocused(focused);
    498         node.setPassword(password);
    499         node.setScrollable(scrollable);
    500         node.setSelected(selected);
    501         node.setVisibleToUser(visibleToUser);
    502 
    503         node.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
    504         node.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT);
    505 
    506         if (focusable) {
    507             if (focused) {
    508                 node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_FOCUS);
    509             } else {
    510                 node.addAction(AccessibilityNodeInfo.ACTION_FOCUS);
    511             }
    512         }
    513 
    514         if (mAccessibilityFocusId == virtualViewId) {
    515             node.setAccessibilityFocused(true);
    516             node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
    517         } else {
    518             node.setAccessibilityFocused(false);
    519             node.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
    520         }
    521 
    522         if (clickable) {
    523             node.addAction(AccessibilityNodeInfo.ACTION_CLICK);
    524         }
    525     }
    526 
    527     @CalledByNative
    528     private void setAccessibilityNodeInfoClassName(AccessibilityNodeInfo node,
    529             String className) {
    530         node.setClassName(className);
    531     }
    532 
    533     @CalledByNative
    534     private void setAccessibilityNodeInfoContentDescription(
    535             AccessibilityNodeInfo node, String contentDescription, boolean annotateAsLink) {
    536         if (annotateAsLink) {
    537             SpannableString spannable = new SpannableString(contentDescription);
    538             spannable.setSpan(new URLSpan(""), 0, spannable.length(), 0);
    539             node.setContentDescription(spannable);
    540         } else {
    541             node.setContentDescription(contentDescription);
    542         }
    543     }
    544 
    545     @CalledByNative
    546     private void setAccessibilityNodeInfoLocation(AccessibilityNodeInfo node,
    547             final int virtualViewId,
    548             int absoluteLeft, int absoluteTop, int parentRelativeLeft, int parentRelativeTop,
    549             int width, int height, boolean isRootNode) {
    550         // First set the bounds in parent.
    551         Rect boundsInParent = new Rect(parentRelativeLeft, parentRelativeTop,
    552                 parentRelativeLeft + width, parentRelativeTop + height);
    553         if (isRootNode) {
    554             // Offset of the web content relative to the View.
    555             boundsInParent.offset(0, (int) mRenderCoordinates.getContentOffsetYPix());
    556         }
    557         node.setBoundsInParent(boundsInParent);
    558 
    559         // Now set the absolute rect, which requires several transformations.
    560         Rect rect = new Rect(absoluteLeft, absoluteTop, absoluteLeft + width, absoluteTop + height);
    561 
    562         // Offset by the scroll position.
    563         rect.offset(-(int) mRenderCoordinates.getScrollX(),
    564                     -(int) mRenderCoordinates.getScrollY());
    565 
    566         // Convert CSS (web) pixels to Android View pixels
    567         rect.left = (int) mRenderCoordinates.fromLocalCssToPix(rect.left);
    568         rect.top = (int) mRenderCoordinates.fromLocalCssToPix(rect.top);
    569         rect.bottom = (int) mRenderCoordinates.fromLocalCssToPix(rect.bottom);
    570         rect.right = (int) mRenderCoordinates.fromLocalCssToPix(rect.right);
    571 
    572         // Offset by the location of the web content within the view.
    573         rect.offset(0,
    574                     (int) mRenderCoordinates.getContentOffsetYPix());
    575 
    576         // Finally offset by the location of the view within the screen.
    577         final int[] viewLocation = new int[2];
    578         mView.getLocationOnScreen(viewLocation);
    579         rect.offset(viewLocation[0], viewLocation[1]);
    580 
    581         node.setBoundsInScreen(rect);
    582 
    583         // Work around a bug in the Android framework where if the object with accessibility
    584         // focus moves, the accessibility focus rect is not updated - both the visual highlight,
    585         // and the location on the screen that's clicked if you double-tap. To work around this,
    586         // when we know the object with accessibility focus moved, move focus away and then
    587         // move focus right back to it, which tricks Android into updating its bounds.
    588         if (virtualViewId == mAccessibilityFocusId && virtualViewId != mCurrentRootId) {
    589             if (mAccessibilityFocusRect == null) {
    590                 mAccessibilityFocusRect = rect;
    591             } else if (!mAccessibilityFocusRect.equals(rect)) {
    592                 mAccessibilityFocusRect = rect;
    593                 moveAccessibilityFocusToIdAndRefocusIfNeeded(virtualViewId);
    594             }
    595         }
    596     }
    597 
    598     @CalledByNative
    599     protected void setAccessibilityNodeInfoKitKatAttributes(AccessibilityNodeInfo node,
    600             boolean canOpenPopup,
    601             boolean contentInvalid,
    602             boolean dismissable,
    603             boolean multiLine,
    604             int inputType,
    605             int liveRegion) {
    606         // Requires KitKat or higher.
    607     }
    608 
    609     @CalledByNative
    610     protected void setAccessibilityNodeInfoCollectionInfo(AccessibilityNodeInfo node,
    611             int rowCount, int columnCount, boolean hierarchical) {
    612         // Requires KitKat or higher.
    613     }
    614 
    615     @CalledByNative
    616     protected void setAccessibilityNodeInfoCollectionItemInfo(AccessibilityNodeInfo node,
    617             int rowIndex, int rowSpan, int columnIndex, int columnSpan, boolean heading) {
    618         // Requires KitKat or higher.
    619     }
    620 
    621     @CalledByNative
    622     protected void setAccessibilityNodeInfoRangeInfo(AccessibilityNodeInfo node,
    623             int rangeType, float min, float max, float current) {
    624         // Requires KitKat or higher.
    625     }
    626 
    627     @CalledByNative
    628     private void setAccessibilityEventBooleanAttributes(AccessibilityEvent event,
    629             boolean checked, boolean enabled, boolean password, boolean scrollable) {
    630         event.setChecked(checked);
    631         event.setEnabled(enabled);
    632         event.setPassword(password);
    633         event.setScrollable(scrollable);
    634     }
    635 
    636     @CalledByNative
    637     private void setAccessibilityEventClassName(AccessibilityEvent event, String className) {
    638         event.setClassName(className);
    639     }
    640 
    641     @CalledByNative
    642     private void setAccessibilityEventListAttributes(AccessibilityEvent event,
    643             int currentItemIndex, int itemCount) {
    644         event.setCurrentItemIndex(currentItemIndex);
    645         event.setItemCount(itemCount);
    646     }
    647 
    648     @CalledByNative
    649     private void setAccessibilityEventScrollAttributes(AccessibilityEvent event,
    650             int scrollX, int scrollY, int maxScrollX, int maxScrollY) {
    651         event.setScrollX(scrollX);
    652         event.setScrollY(scrollY);
    653         event.setMaxScrollX(maxScrollX);
    654         event.setMaxScrollY(maxScrollY);
    655     }
    656 
    657     @CalledByNative
    658     private void setAccessibilityEventTextChangedAttrs(AccessibilityEvent event,
    659             int fromIndex, int addedCount, int removedCount, String beforeText, String text) {
    660         event.setFromIndex(fromIndex);
    661         event.setAddedCount(addedCount);
    662         event.setRemovedCount(removedCount);
    663         event.setBeforeText(beforeText);
    664         event.getText().add(text);
    665     }
    666 
    667     @CalledByNative
    668     private void setAccessibilityEventSelectionAttrs(AccessibilityEvent event,
    669             int fromIndex, int addedCount, int itemCount, String text) {
    670         event.setFromIndex(fromIndex);
    671         event.setAddedCount(addedCount);
    672         event.setItemCount(itemCount);
    673         event.getText().add(text);
    674     }
    675 
    676     @CalledByNative
    677     protected void setAccessibilityEventKitKatAttributes(AccessibilityEvent event,
    678             boolean canOpenPopup,
    679             boolean contentInvalid,
    680             boolean dismissable,
    681             boolean multiLine,
    682             int inputType,
    683             int liveRegion) {
    684         // Backwards compatibility for KitKat AccessibilityNodeInfo fields.
    685         Bundle bundle = getOrCreateBundleForAccessibilityEvent(event);
    686         bundle.putBoolean("AccessibilityNodeInfo.canOpenPopup", canOpenPopup);
    687         bundle.putBoolean("AccessibilityNodeInfo.contentInvalid", contentInvalid);
    688         bundle.putBoolean("AccessibilityNodeInfo.dismissable", dismissable);
    689         bundle.putBoolean("AccessibilityNodeInfo.multiLine", multiLine);
    690         bundle.putInt("AccessibilityNodeInfo.inputType", inputType);
    691         bundle.putInt("AccessibilityNodeInfo.liveRegion", liveRegion);
    692     }
    693 
    694     @CalledByNative
    695     protected void setAccessibilityEventCollectionInfo(AccessibilityEvent event,
    696             int rowCount, int columnCount, boolean hierarchical) {
    697         // Backwards compatibility for KitKat AccessibilityNodeInfo fields.
    698         Bundle bundle = getOrCreateBundleForAccessibilityEvent(event);
    699         bundle.putInt("AccessibilityNodeInfo.CollectionInfo.rowCount", rowCount);
    700         bundle.putInt("AccessibilityNodeInfo.CollectionInfo.columnCount", columnCount);
    701         bundle.putBoolean("AccessibilityNodeInfo.CollectionInfo.hierarchical", hierarchical);
    702     }
    703 
    704     @CalledByNative
    705     protected void setAccessibilityEventHeadingFlag(AccessibilityEvent event,
    706             boolean heading) {
    707         // Backwards compatibility for KitKat AccessibilityNodeInfo fields.
    708         Bundle bundle = getOrCreateBundleForAccessibilityEvent(event);
    709         bundle.putBoolean("AccessibilityNodeInfo.CollectionItemInfo.heading", heading);
    710     }
    711 
    712     @CalledByNative
    713     protected void setAccessibilityEventCollectionItemInfo(AccessibilityEvent event,
    714             int rowIndex, int rowSpan, int columnIndex, int columnSpan) {
    715         // Backwards compatibility for KitKat AccessibilityNodeInfo fields.
    716         Bundle bundle = getOrCreateBundleForAccessibilityEvent(event);
    717         bundle.putInt("AccessibilityNodeInfo.CollectionItemInfo.rowIndex", rowIndex);
    718         bundle.putInt("AccessibilityNodeInfo.CollectionItemInfo.rowSpan", rowSpan);
    719         bundle.putInt("AccessibilityNodeInfo.CollectionItemInfo.columnIndex", columnIndex);
    720         bundle.putInt("AccessibilityNodeInfo.CollectionItemInfo.columnSpan", columnSpan);
    721     }
    722 
    723     @CalledByNative
    724     protected void setAccessibilityEventRangeInfo(AccessibilityEvent event,
    725             int rangeType, float min, float max, float current) {
    726         // Backwards compatibility for KitKat AccessibilityNodeInfo fields.
    727         Bundle bundle = getOrCreateBundleForAccessibilityEvent(event);
    728         bundle.putInt("AccessibilityNodeInfo.RangeInfo.type", rangeType);
    729         bundle.putFloat("AccessibilityNodeInfo.RangeInfo.min", min);
    730         bundle.putFloat("AccessibilityNodeInfo.RangeInfo.max", max);
    731         bundle.putFloat("AccessibilityNodeInfo.RangeInfo.current", current);
    732     }
    733 
    734     private native int nativeGetRootId(long nativeBrowserAccessibilityManagerAndroid);
    735     private native boolean nativeIsNodeValid(long nativeBrowserAccessibilityManagerAndroid, int id);
    736     private native void nativeHitTest(long nativeBrowserAccessibilityManagerAndroid, int x, int y);
    737     private native boolean nativePopulateAccessibilityNodeInfo(
    738         long nativeBrowserAccessibilityManagerAndroid, AccessibilityNodeInfo info, int id);
    739     private native boolean nativePopulateAccessibilityEvent(
    740         long nativeBrowserAccessibilityManagerAndroid, AccessibilityEvent event, int id,
    741         int eventType);
    742     private native void nativeClick(long nativeBrowserAccessibilityManagerAndroid, int id);
    743     private native void nativeFocus(long nativeBrowserAccessibilityManagerAndroid, int id);
    744     private native void nativeBlur(long nativeBrowserAccessibilityManagerAndroid);
    745     private native void nativeScrollToMakeNodeVisible(
    746             long nativeBrowserAccessibilityManagerAndroid, int id);
    747     private native int nativeFindElementType(long nativeBrowserAccessibilityManagerAndroid,
    748             int startId, String elementType, boolean forwards);
    749 }
    750