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