Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / content / public / android / java / src / org / chromium / content / browser / accessibility / BrowserAccessibilityManager.java
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.provider.Settings;
12 import android.text.SpannableString;
13 import android.text.style.URLSpan;
14 import android.view.MotionEvent;
15 import android.view.View;
16 import android.view.ViewGroup;
17 import android.view.ViewParent;
18 import android.view.accessibility.AccessibilityEvent;
19 import android.view.accessibility.AccessibilityManager;
20 import android.view.accessibility.AccessibilityNodeInfo;
21 import android.view.accessibility.AccessibilityNodeProvider;
22
23 import org.chromium.base.CalledByNative;
24 import org.chromium.base.JNINamespace;
25 import org.chromium.content.browser.ContentViewCore;
26 import org.chromium.content.browser.RenderCoordinates;
27
28 import java.util.ArrayList;
29 import java.util.List;
30 import java.util.Locale;
31
32 /**
33  * Native accessibility for a {@link ContentViewCore}.
34  *
35  * This class is safe to load on ICS and can be used to run tests, but
36  * only the subclass, JellyBeanBrowserAccessibilityManager, actually
37  * has a AccessibilityNodeProvider implementation needed for native
38  * accessibility.
39  */
40 @JNINamespace("content")
41 public class BrowserAccessibilityManager {
42     private static final String TAG = "BrowserAccessibilityManager";
43
44     // Constants from AccessibilityNodeInfo defined in the L SDK.
45     private static final int ACTION_SET_TEXT = 0x200000;
46     private static final String ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE =
47             "ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE";
48
49     private ContentViewCore mContentViewCore;
50     private final AccessibilityManager mAccessibilityManager;
51     private final RenderCoordinates mRenderCoordinates;
52     private long mNativeObj;
53     private int mAccessibilityFocusId;
54     private Rect mAccessibilityFocusRect;
55     private boolean mIsHovering;
56     private int mLastHoverId = View.NO_ID;
57     private int mCurrentRootId;
58     private final int[] mTempLocation = new int[2];
59     private final ViewGroup mView;
60     private boolean mUserHasTouchExplored;
61     private boolean mPendingScrollToMakeNodeVisible;
62     private boolean mNotifyFrameInfoInitializedCalled;
63     private int mSelectionGranularity;
64     private int mSelectionStartIndex;
65     private int mSelectionEndIndex;
66     private boolean mVisible = true;
67
68     /**
69      * Create a BrowserAccessibilityManager object, which is owned by the C++
70      * BrowserAccessibilityManagerAndroid instance, and connects to the content view.
71      * @param nativeBrowserAccessibilityManagerAndroid A pointer to the counterpart native
72      *     C++ object that owns this object.
73      * @param contentViewCore The content view that this object provides accessibility for.
74      */
75     @CalledByNative
76     private static BrowserAccessibilityManager create(long nativeBrowserAccessibilityManagerAndroid,
77             ContentViewCore contentViewCore) {
78         // A bug in the KitKat framework prevents us from using these new APIs.
79         // http://crbug.com/348088/
80         // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
81         //     return new KitKatBrowserAccessibilityManager(
82         //             nativeBrowserAccessibilityManagerAndroid, contentViewCore);
83
84         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
85             return new JellyBeanBrowserAccessibilityManager(
86                     nativeBrowserAccessibilityManagerAndroid, contentViewCore);
87         } else {
88             return new BrowserAccessibilityManager(
89                     nativeBrowserAccessibilityManagerAndroid, contentViewCore);
90         }
91     }
92
93     protected BrowserAccessibilityManager(long nativeBrowserAccessibilityManagerAndroid,
94             ContentViewCore contentViewCore) {
95         mNativeObj = nativeBrowserAccessibilityManagerAndroid;
96         mContentViewCore = contentViewCore;
97         mContentViewCore.setBrowserAccessibilityManager(this);
98         mAccessibilityFocusId = View.NO_ID;
99         mIsHovering = false;
100         mCurrentRootId = View.NO_ID;
101         mView = mContentViewCore.getContainerView();
102         mRenderCoordinates = mContentViewCore.getRenderCoordinates();
103         mAccessibilityManager =
104             (AccessibilityManager) mContentViewCore.getContext()
105             .getSystemService(Context.ACCESSIBILITY_SERVICE);
106     }
107
108     @CalledByNative
109     private void onNativeObjectDestroyed() {
110         if (mContentViewCore.getBrowserAccessibilityManager() == this) {
111             mContentViewCore.setBrowserAccessibilityManager(null);
112         }
113         mNativeObj = 0;
114         mContentViewCore = null;
115     }
116
117     /**
118      * @return An AccessibilityNodeProvider on JellyBean, and null on previous versions.
119      */
120     public AccessibilityNodeProvider getAccessibilityNodeProvider() {
121         return null;
122     }
123
124     /**
125      * Set whether the web content made accessible by this class is currently visible.
126      * Set it to false if the web view is still on the screen but it's obscured by a
127      * dialog or overlay. This will make every virtual view in the web hierarchy report
128      * that it's not visible, and not accessibility focusable.
129      *
130      * @param visible Whether the web content is currently visible and not obscured.
131      */
132     public void setVisible(boolean visible) {
133         if (visible == mVisible) return;
134
135         mVisible = visible;
136         mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
137     }
138
139     /**
140      * @see AccessibilityNodeProvider#createAccessibilityNodeInfo(int)
141      */
142     protected AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
143         if (!mAccessibilityManager.isEnabled() || mNativeObj == 0) {
144             return null;
145         }
146
147         int rootId = nativeGetRootId(mNativeObj);
148
149         if (virtualViewId == View.NO_ID) {
150             return createNodeForHost(rootId);
151         }
152
153         if (!isFrameInfoInitialized()) {
154             return null;
155         }
156
157         final AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(mView);
158         info.setPackageName(mContentViewCore.getContext().getPackageName());
159         info.setSource(mView, virtualViewId);
160
161         if (virtualViewId == rootId) {
162             info.setParent(mView);
163         }
164
165         if (nativePopulateAccessibilityNodeInfo(mNativeObj, info, virtualViewId)) {
166             return info;
167         } else {
168             info.recycle();
169             return null;
170         }
171     }
172
173     /**
174      * @see AccessibilityNodeProvider#findAccessibilityNodeInfosByText(String, int)
175      */
176     protected List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String text,
177             int virtualViewId) {
178         return new ArrayList<AccessibilityNodeInfo>();
179     }
180
181     protected static boolean isValidMovementGranularity(int granularity) {
182         switch (granularity) {
183             case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER:
184             case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD:
185             case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE:
186                 return true;
187         }
188         return false;
189     }
190
191     /**
192      * @see AccessibilityNodeProvider#performAction(int, int, Bundle)
193      */
194     protected boolean performAction(int virtualViewId, int action, Bundle arguments) {
195         // We don't support any actions on the host view or nodes
196         // that are not (any longer) in the tree.
197         if (!mAccessibilityManager.isEnabled() || mNativeObj == 0
198                 || !nativeIsNodeValid(mNativeObj, virtualViewId)) {
199             return false;
200         }
201
202         switch (action) {
203             case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
204                 if (!moveAccessibilityFocusToId(virtualViewId)) return true;
205
206                 if (!mIsHovering) {
207                     nativeScrollToMakeNodeVisible(
208                             mNativeObj, mAccessibilityFocusId);
209                 } else {
210                     mPendingScrollToMakeNodeVisible = true;
211                 }
212                 return true;
213             case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
214                 if (mAccessibilityFocusId == virtualViewId) {
215                     sendAccessibilityEvent(mAccessibilityFocusId,
216                             AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
217                     mAccessibilityFocusId = View.NO_ID;
218                     mAccessibilityFocusRect = null;
219                 }
220                 return true;
221             case AccessibilityNodeInfo.ACTION_CLICK:
222                 nativeClick(mNativeObj, virtualViewId);
223                 sendAccessibilityEvent(virtualViewId,
224                         AccessibilityEvent.TYPE_VIEW_CLICKED);
225                 return true;
226             case AccessibilityNodeInfo.ACTION_FOCUS:
227                 nativeFocus(mNativeObj, virtualViewId);
228                 return true;
229             case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS:
230                 nativeBlur(mNativeObj);
231                 return true;
232             case AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT: {
233                 if (arguments == null) return false;
234                 String elementType = arguments.getString(
235                         AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING);
236                 if (elementType == null) return false;
237                 elementType = elementType.toUpperCase(Locale.US);
238                 return jumpToElementType(elementType, true);
239             }
240             case AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT: {
241                 if (arguments == null) return false;
242                 String elementType = arguments.getString(
243                         AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING);
244                 if (elementType == null) return false;
245                 elementType = elementType.toUpperCase(Locale.US);
246                 return jumpToElementType(elementType, false);
247             }
248             case ACTION_SET_TEXT: {
249                 if (!nativeIsEditableText(mNativeObj, virtualViewId)) return false;
250                 if (arguments == null) return false;
251                 String newText = arguments.getString(
252                         ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE);
253                 if (newText == null) return false;
254                 nativeSetTextFieldValue(mNativeObj, virtualViewId, newText);
255                 // Match Android framework and set the cursor to the end of the text field.
256                 nativeSetSelection(mNativeObj, virtualViewId, newText.length(), newText.length());
257                 return true;
258             }
259             case AccessibilityNodeInfo.ACTION_SET_SELECTION: {
260                 if (!nativeIsEditableText(mNativeObj, virtualViewId)) return false;
261                 int selectionStart = 0;
262                 int selectionEnd = 0;
263                 if (arguments != null) {
264                     selectionStart = arguments.getInt(
265                             AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT);
266                     selectionEnd = arguments.getInt(
267                             AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT);
268                 }
269                 nativeSetSelection(mNativeObj, virtualViewId, selectionStart, selectionEnd);
270                 return true;
271             }
272             case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY: {
273                 if (arguments == null) return false;
274                 int granularity = arguments.getInt(
275                         AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
276                 boolean extend = arguments.getBoolean(
277                         AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN);
278                 if (!isValidMovementGranularity(granularity)) {
279                     return false;
280                 }
281                 return nextAtGranularity(granularity, extend);
282             }
283             case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: {
284                 if (arguments == null) return false;
285                 int granularity = arguments.getInt(
286                         AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
287                 boolean extend = arguments.getBoolean(
288                         AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN);
289                 if (!isValidMovementGranularity(granularity)) {
290                     return false;
291                 }
292                 return previousAtGranularity(granularity, extend);
293             }
294             case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
295                 return nativeAdjustSlider(mNativeObj, virtualViewId, true);
296             case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
297                 return nativeAdjustSlider(mNativeObj, virtualViewId, false);
298             default:
299                 break;
300         }
301         return false;
302     }
303
304     /**
305      * @see View#onHoverEvent(MotionEvent)
306      */
307     public boolean onHoverEvent(MotionEvent event) {
308         if (!mAccessibilityManager.isEnabled() || mNativeObj == 0) {
309             return false;
310         }
311
312         if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) {
313             mIsHovering = false;
314             if (mPendingScrollToMakeNodeVisible) {
315                 nativeScrollToMakeNodeVisible(
316                         mNativeObj, mAccessibilityFocusId);
317             }
318             mPendingScrollToMakeNodeVisible = false;
319             return true;
320         }
321
322         mIsHovering = true;
323         mUserHasTouchExplored = true;
324         float x = event.getX();
325         float y = event.getY();
326
327         // Convert to CSS coordinates.
328         int cssX = (int) (mRenderCoordinates.fromPixToLocalCss(x));
329         int cssY = (int) (mRenderCoordinates.fromPixToLocalCss(y));
330
331         // This sends an IPC to the render process to do the hit testing.
332         // The response is handled by handleHover.
333         nativeHitTest(mNativeObj, cssX, cssY);
334         return true;
335     }
336
337     /**
338      * Called by ContentViewCore to notify us when the frame info is initialized,
339      * the first time, since until that point, we can't use mRenderCoordinates to transform
340      * web coordinates to screen coordinates.
341      */
342     public void notifyFrameInfoInitialized() {
343         if (mNotifyFrameInfoInitializedCalled) return;
344
345         mNotifyFrameInfoInitializedCalled = true;
346
347         // Invalidate the container view, since the chrome accessibility tree is now
348         // ready and listed as the child of the container view.
349         mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
350
351         // (Re-) focus focused element, since we weren't able to create an
352         // AccessibilityNodeInfo for this element before.
353         if (mAccessibilityFocusId != View.NO_ID) {
354             moveAccessibilityFocusToIdAndRefocusIfNeeded(mAccessibilityFocusId);
355         }
356     }
357
358     private boolean jumpToElementType(String elementType, boolean forwards) {
359         int id = nativeFindElementType(mNativeObj, mAccessibilityFocusId, elementType, forwards);
360         if (id == 0) return false;
361
362         moveAccessibilityFocusToId(id);
363         return true;
364     }
365
366     private void setGranularityAndUpdateSelection(int granularity) {
367         if (mSelectionGranularity == 0) {
368             mSelectionStartIndex = -1;
369             mSelectionEndIndex = -1;
370         }
371         mSelectionGranularity = granularity;
372         if (nativeIsEditableText(mNativeObj, mAccessibilityFocusId)) {
373             mSelectionStartIndex = nativeGetEditableTextSelectionStart(
374                     mNativeObj, mAccessibilityFocusId);
375             mSelectionEndIndex = nativeGetEditableTextSelectionEnd(
376                     mNativeObj, mAccessibilityFocusId);
377         }
378     }
379
380     private boolean nextAtGranularity(int granularity, boolean extendSelection) {
381         setGranularityAndUpdateSelection(granularity);
382         // This calls finishGranularityMove when it's done.
383         return nativeNextAtGranularity(mNativeObj, mSelectionGranularity, extendSelection,
384                 mAccessibilityFocusId, mSelectionEndIndex);
385     }
386
387     private boolean previousAtGranularity(int granularity, boolean extendSelection) {
388         setGranularityAndUpdateSelection(granularity);
389         // This calls finishGranularityMove when it's done.
390         return nativePreviousAtGranularity(mNativeObj, mSelectionGranularity, extendSelection,
391                 mAccessibilityFocusId, mSelectionEndIndex);
392     }
393
394     @CalledByNative
395     private void finishGranularityMove(String text, boolean extendSelection,
396             int itemStartIndex, int itemEndIndex, boolean forwards) {
397         // Prepare to send both a selection and a traversal event in sequence.
398         AccessibilityEvent selectionEvent = buildAccessibilityEvent(mAccessibilityFocusId,
399                 AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
400         if (selectionEvent == null) return;
401         AccessibilityEvent traverseEvent = buildAccessibilityEvent(mAccessibilityFocusId,
402                 AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY);
403         if (traverseEvent == null) {
404             selectionEvent.recycle();
405             return;
406         }
407
408         // Update the cursor or selection based on the traversal. If it's an editable
409         // text node, set the real editing cursor too.
410         if (forwards)
411             mSelectionEndIndex = itemEndIndex;
412         else
413             mSelectionEndIndex = itemStartIndex;
414         if (!extendSelection) {
415             mSelectionStartIndex = mSelectionEndIndex;
416         }
417         if (nativeIsEditableText(mNativeObj, mAccessibilityFocusId)) {
418             nativeSetSelection(mNativeObj, mAccessibilityFocusId,
419                     mSelectionStartIndex, mSelectionEndIndex);
420         }
421
422         // The selection event's "from" and "to" indices are just a cursor at the focus
423         // end of the movement, or a selection if extendSelection is true.
424         selectionEvent.setFromIndex(mSelectionStartIndex);
425         selectionEvent.setToIndex(mSelectionStartIndex);
426         selectionEvent.setItemCount(text.length());
427
428         // The traverse event's "from" and "to" indices surround the item (e.g. the word,
429         // etc.) with no whitespace.
430         traverseEvent.setFromIndex(itemStartIndex);
431         traverseEvent.setToIndex(itemEndIndex);
432         traverseEvent.setItemCount(text.length());
433         traverseEvent.setMovementGranularity(mSelectionGranularity);
434         traverseEvent.setContentDescription(text);
435
436         // The traverse event needs to set its associated action that triggered it.
437         if (forwards) {
438             traverseEvent.setAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
439         } else {
440             traverseEvent.setAction(
441                     AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
442         }
443
444         mView.requestSendAccessibilityEvent(mView, selectionEvent);
445         mView.requestSendAccessibilityEvent(mView, traverseEvent);
446     }
447
448     private boolean moveAccessibilityFocusToId(int newAccessibilityFocusId) {
449         if (newAccessibilityFocusId == mAccessibilityFocusId) return false;
450
451         mAccessibilityFocusId = newAccessibilityFocusId;
452         mAccessibilityFocusRect = null;
453         mSelectionGranularity = 0;
454         mSelectionStartIndex = 0;
455         mSelectionEndIndex = 0;
456
457         // Calling nativeSetAccessibilityFocus will asynchronously load inline text boxes for
458         // this node and its subtree. If accessibility focus is on anything other than
459         // the root, do it - otherwise set it to -1 so we don't load inline text boxes
460         // for the whole subtree of the root.
461         if (mAccessibilityFocusId == mCurrentRootId)
462             nativeSetAccessibilityFocus(mNativeObj, -1);
463         else
464             nativeSetAccessibilityFocus(mNativeObj, mAccessibilityFocusId);
465
466         sendAccessibilityEvent(mAccessibilityFocusId,
467                 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
468         return true;
469     }
470
471     private void moveAccessibilityFocusToIdAndRefocusIfNeeded(int newAccessibilityFocusId) {
472         // Work around a bug in the Android framework where it doesn't fully update the object
473         // with accessibility focus even if you send it a WINDOW_CONTENT_CHANGED. To work around
474         // this, clear focus and then set focus again.
475         if (newAccessibilityFocusId == mAccessibilityFocusId) {
476             sendAccessibilityEvent(newAccessibilityFocusId,
477                     AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
478             mAccessibilityFocusId = View.NO_ID;
479         }
480
481         moveAccessibilityFocusToId(newAccessibilityFocusId);
482     }
483
484     private void sendAccessibilityEvent(int virtualViewId, int eventType) {
485         // The container view is indicated by a virtualViewId of NO_ID; post these events directly
486         // since there's no web-specific information to attach.
487         if (virtualViewId == View.NO_ID) {
488             mView.sendAccessibilityEvent(eventType);
489             return;
490         }
491
492         AccessibilityEvent event = buildAccessibilityEvent(virtualViewId, eventType);
493         if (event != null) {
494             mView.requestSendAccessibilityEvent(mView, event);
495         }
496     }
497
498     private AccessibilityEvent buildAccessibilityEvent(int virtualViewId, int eventType) {
499         // If we don't have any frame info, then the virtual hierarchy
500         // doesn't exist in the view of the Android framework, so should
501         // never send any events.
502         if (!mAccessibilityManager.isEnabled() || mNativeObj == 0
503                 || !isFrameInfoInitialized()) {
504             return null;
505         }
506
507         // This is currently needed if we want Android to visually highlight
508         // the item that has accessibility focus. In practice, this doesn't seem to slow
509         // things down, because it's only called when the accessibility focus moves.
510         // TODO(dmazzoni): remove this if/when Android framework fixes bug.
511         mView.postInvalidate();
512
513         final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
514         event.setPackageName(mContentViewCore.getContext().getPackageName());
515         event.setSource(mView, virtualViewId);
516         if (!nativePopulateAccessibilityEvent(mNativeObj, event, virtualViewId, eventType)) {
517             event.recycle();
518             return null;
519         }
520         return event;
521     }
522
523     private Bundle getOrCreateBundleForAccessibilityEvent(AccessibilityEvent event) {
524         Bundle bundle = (Bundle) event.getParcelableData();
525         if (bundle == null) {
526             bundle = new Bundle();
527             event.setParcelableData(bundle);
528         }
529         return bundle;
530     }
531
532     private AccessibilityNodeInfo createNodeForHost(int rootId) {
533         // Since we don't want the parent to be focusable, but we can't remove
534         // actions from a node, copy over the necessary fields.
535         final AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(mView);
536         final AccessibilityNodeInfo source = AccessibilityNodeInfo.obtain(mView);
537         mView.onInitializeAccessibilityNodeInfo(source);
538
539         // Copy over parent and screen bounds.
540         Rect rect = new Rect();
541         source.getBoundsInParent(rect);
542         result.setBoundsInParent(rect);
543         source.getBoundsInScreen(rect);
544         result.setBoundsInScreen(rect);
545
546         // Set up the parent view, if applicable.
547         final ViewParent parent = mView.getParentForAccessibility();
548         if (parent instanceof View) {
549             result.setParent((View) parent);
550         }
551
552         // Populate the minimum required fields.
553         result.setVisibleToUser(source.isVisibleToUser() && mVisible);
554         result.setEnabled(source.isEnabled());
555         result.setPackageName(source.getPackageName());
556         result.setClassName(source.getClassName());
557
558         // Add the Chrome root node.
559         if (isFrameInfoInitialized()) {
560             result.addChild(mView, rootId);
561         }
562
563         return result;
564     }
565
566     /**
567      * Returns whether or not the frame info is initialized, meaning we can safely
568      * convert web coordinates to screen coordinates. When this is first initialized,
569      * notifyFrameInfoInitialized is called - but we shouldn't check whether or not
570      * that method was called as a way to determine if frame info is valid because
571      * notifyFrameInfoInitialized might not be called at all if mRenderCoordinates
572      * gets initialized first.
573      */
574     private boolean isFrameInfoInitialized() {
575         return mRenderCoordinates.getContentWidthCss() != 0.0
576                 || mRenderCoordinates.getContentHeightCss() != 0.0;
577     }
578
579     @CalledByNative
580     private void handlePageLoaded(int id) {
581         if (mUserHasTouchExplored) return;
582
583         if (mContentViewCore.shouldSetAccessibilityFocusOnPageLoad()) {
584             moveAccessibilityFocusToIdAndRefocusIfNeeded(id);
585         }
586     }
587
588     @CalledByNative
589     private void handleFocusChanged(int id) {
590         sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_FOCUSED);
591         moveAccessibilityFocusToId(id);
592     }
593
594     @CalledByNative
595     private void handleCheckStateChanged(int id) {
596         sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_CLICKED);
597     }
598
599     @CalledByNative
600     private void handleTextSelectionChanged(int id) {
601         sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
602     }
603
604     @CalledByNative
605     private void handleEditableTextChanged(int id) {
606         sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
607     }
608
609     @CalledByNative
610     private void handleSliderChanged(int id) {
611         sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_SCROLLED);
612     }
613
614     @CalledByNative
615     private void handleContentChanged(int id) {
616         int rootId = nativeGetRootId(mNativeObj);
617         if (rootId != mCurrentRootId) {
618             mCurrentRootId = rootId;
619             mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
620         } else {
621             sendAccessibilityEvent(id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
622         }
623     }
624
625     @CalledByNative
626     private void handleNavigate() {
627         mAccessibilityFocusId = View.NO_ID;
628         mAccessibilityFocusRect = null;
629         mUserHasTouchExplored = false;
630         // Invalidate the host, since its child is now gone.
631         mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
632     }
633
634     @CalledByNative
635     private void handleScrollPositionChanged(int id) {
636         sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_SCROLLED);
637     }
638
639     @CalledByNative
640     private void handleScrolledToAnchor(int id) {
641         moveAccessibilityFocusToId(id);
642     }
643
644     @CalledByNative
645     private void handleHover(int id) {
646         if (mLastHoverId == id) return;
647
648         // Always send the ENTER and then the EXIT event, to match a standard Android View.
649         sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
650         sendAccessibilityEvent(mLastHoverId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
651         mLastHoverId = id;
652     }
653
654     @CalledByNative
655     private void announceLiveRegionText(String text) {
656         mView.announceForAccessibility(text);
657     }
658
659     @CalledByNative
660     private void setAccessibilityNodeInfoParent(AccessibilityNodeInfo node, int parentId) {
661         node.setParent(mView, parentId);
662     }
663
664     @CalledByNative
665     private void addAccessibilityNodeInfoChild(AccessibilityNodeInfo node, int childId) {
666         node.addChild(mView, childId);
667     }
668
669     @CalledByNative
670     private void setAccessibilityNodeInfoBooleanAttributes(AccessibilityNodeInfo node,
671             int virtualViewId, boolean canScrollForward, boolean canScrollBackward,
672             boolean checkable, boolean checked, boolean clickable, boolean editableText,
673             boolean enabled, boolean focusable, boolean focused, boolean password,
674             boolean scrollable, boolean selected, boolean visibleToUser) {
675         node.setCheckable(checkable);
676         node.setChecked(checked);
677         node.setClickable(clickable);
678         node.setEnabled(enabled);
679         node.setFocusable(focusable);
680         node.setFocused(focused);
681         node.setPassword(password);
682         node.setScrollable(scrollable);
683         node.setSelected(selected);
684         node.setVisibleToUser(visibleToUser && mVisible);
685
686         node.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
687         node.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT);
688         node.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
689         node.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
690         node.setMovementGranularities(
691                 AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER
692                 | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD
693                 | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE);
694
695         if (editableText && enabled) {
696             node.addAction(ACTION_SET_TEXT);
697             node.addAction(AccessibilityNodeInfo.ACTION_SET_SELECTION);
698         }
699
700         if (canScrollForward) {
701             node.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
702         }
703
704         if (canScrollBackward) {
705             node.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
706         }
707
708         if (focusable) {
709             if (focused) {
710                 node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_FOCUS);
711             } else {
712                 node.addAction(AccessibilityNodeInfo.ACTION_FOCUS);
713             }
714         }
715
716         if (mAccessibilityFocusId == virtualViewId) {
717             node.setAccessibilityFocused(true);
718             node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
719         } else if (mVisible) {
720             node.setAccessibilityFocused(false);
721             node.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
722         }
723
724         if (clickable) {
725             node.addAction(AccessibilityNodeInfo.ACTION_CLICK);
726         }
727     }
728
729     @CalledByNative
730     private void setAccessibilityNodeInfoClassName(AccessibilityNodeInfo node,
731             String className) {
732         node.setClassName(className);
733     }
734
735     @CalledByNative
736     private void setAccessibilityNodeInfoContentDescription(
737             AccessibilityNodeInfo node, String contentDescription, boolean annotateAsLink) {
738         if (annotateAsLink) {
739             SpannableString spannable = new SpannableString(contentDescription);
740             spannable.setSpan(new URLSpan(""), 0, spannable.length(), 0);
741             node.setContentDescription(spannable);
742         } else {
743             node.setContentDescription(contentDescription);
744         }
745     }
746
747     @CalledByNative
748     private void setAccessibilityNodeInfoLocation(AccessibilityNodeInfo node,
749             final int virtualViewId,
750             int absoluteLeft, int absoluteTop, int parentRelativeLeft, int parentRelativeTop,
751             int width, int height, boolean isRootNode) {
752         // First set the bounds in parent.
753         Rect boundsInParent = new Rect(parentRelativeLeft, parentRelativeTop,
754                 parentRelativeLeft + width, parentRelativeTop + height);
755         if (isRootNode) {
756             // Offset of the web content relative to the View.
757             boundsInParent.offset(0, (int) mRenderCoordinates.getContentOffsetYPix());
758         }
759         node.setBoundsInParent(boundsInParent);
760
761         // Now set the absolute rect, which requires several transformations.
762         Rect rect = new Rect(absoluteLeft, absoluteTop, absoluteLeft + width, absoluteTop + height);
763
764         // Offset by the scroll position.
765         rect.offset(-(int) mRenderCoordinates.getScrollX(),
766                     -(int) mRenderCoordinates.getScrollY());
767
768         // Convert CSS (web) pixels to Android View pixels
769         rect.left = (int) mRenderCoordinates.fromLocalCssToPix(rect.left);
770         rect.top = (int) mRenderCoordinates.fromLocalCssToPix(rect.top);
771         rect.bottom = (int) mRenderCoordinates.fromLocalCssToPix(rect.bottom);
772         rect.right = (int) mRenderCoordinates.fromLocalCssToPix(rect.right);
773
774         // Offset by the location of the web content within the view.
775         rect.offset(0,
776                     (int) mRenderCoordinates.getContentOffsetYPix());
777
778         // Finally offset by the location of the view within the screen.
779         final int[] viewLocation = new int[2];
780         mView.getLocationOnScreen(viewLocation);
781         rect.offset(viewLocation[0], viewLocation[1]);
782
783         node.setBoundsInScreen(rect);
784
785         // Work around a bug in the Android framework where if the object with accessibility
786         // focus moves, the accessibility focus rect is not updated - both the visual highlight,
787         // and the location on the screen that's clicked if you double-tap. To work around this,
788         // when we know the object with accessibility focus moved, move focus away and then
789         // move focus right back to it, which tricks Android into updating its bounds.
790         if (virtualViewId == mAccessibilityFocusId && virtualViewId != mCurrentRootId) {
791             if (mAccessibilityFocusRect == null) {
792                 mAccessibilityFocusRect = rect;
793             } else if (!mAccessibilityFocusRect.equals(rect)) {
794                 mAccessibilityFocusRect = rect;
795                 moveAccessibilityFocusToIdAndRefocusIfNeeded(virtualViewId);
796             }
797         }
798     }
799
800     @CalledByNative
801     protected void setAccessibilityNodeInfoKitKatAttributes(AccessibilityNodeInfo node,
802             boolean canOpenPopup,
803             boolean contentInvalid,
804             boolean dismissable,
805             boolean multiLine,
806             int inputType,
807             int liveRegion) {
808         // Requires KitKat or higher.
809     }
810
811     @CalledByNative
812     protected void setAccessibilityNodeInfoCollectionInfo(AccessibilityNodeInfo node,
813             int rowCount, int columnCount, boolean hierarchical) {
814         // Requires KitKat or higher.
815     }
816
817     @CalledByNative
818     protected void setAccessibilityNodeInfoCollectionItemInfo(AccessibilityNodeInfo node,
819             int rowIndex, int rowSpan, int columnIndex, int columnSpan, boolean heading) {
820         // Requires KitKat or higher.
821     }
822
823     @CalledByNative
824     protected void setAccessibilityNodeInfoRangeInfo(AccessibilityNodeInfo node,
825             int rangeType, float min, float max, float current) {
826         // Requires KitKat or higher.
827     }
828
829     @CalledByNative
830     private void setAccessibilityEventBooleanAttributes(AccessibilityEvent event,
831             boolean checked, boolean enabled, boolean password, boolean scrollable) {
832         event.setChecked(checked);
833         event.setEnabled(enabled);
834         event.setPassword(password);
835         event.setScrollable(scrollable);
836     }
837
838     @CalledByNative
839     private void setAccessibilityEventClassName(AccessibilityEvent event, String className) {
840         event.setClassName(className);
841     }
842
843     @CalledByNative
844     private void setAccessibilityEventListAttributes(AccessibilityEvent event,
845             int currentItemIndex, int itemCount) {
846         event.setCurrentItemIndex(currentItemIndex);
847         event.setItemCount(itemCount);
848     }
849
850     @CalledByNative
851     private void setAccessibilityEventScrollAttributes(AccessibilityEvent event,
852             int scrollX, int scrollY, int maxScrollX, int maxScrollY) {
853         event.setScrollX(scrollX);
854         event.setScrollY(scrollY);
855         event.setMaxScrollX(maxScrollX);
856         event.setMaxScrollY(maxScrollY);
857     }
858
859     @CalledByNative
860     private void setAccessibilityEventTextChangedAttrs(AccessibilityEvent event,
861             int fromIndex, int addedCount, int removedCount, String beforeText, String text) {
862         event.setFromIndex(fromIndex);
863         event.setAddedCount(addedCount);
864         event.setRemovedCount(removedCount);
865         event.setBeforeText(beforeText);
866         event.getText().add(text);
867     }
868
869     @CalledByNative
870     private void setAccessibilityEventSelectionAttrs(AccessibilityEvent event,
871             int fromIndex, int toIndex, int itemCount, String text) {
872         event.setFromIndex(fromIndex);
873         event.setToIndex(toIndex);
874         event.setItemCount(itemCount);
875         event.getText().add(text);
876     }
877
878     @CalledByNative
879     protected void setAccessibilityEventKitKatAttributes(AccessibilityEvent event,
880             boolean canOpenPopup,
881             boolean contentInvalid,
882             boolean dismissable,
883             boolean multiLine,
884             int inputType,
885             int liveRegion) {
886         // Backwards compatibility for KitKat AccessibilityNodeInfo fields.
887         Bundle bundle = getOrCreateBundleForAccessibilityEvent(event);
888         bundle.putBoolean("AccessibilityNodeInfo.canOpenPopup", canOpenPopup);
889         bundle.putBoolean("AccessibilityNodeInfo.contentInvalid", contentInvalid);
890         bundle.putBoolean("AccessibilityNodeInfo.dismissable", dismissable);
891         bundle.putBoolean("AccessibilityNodeInfo.multiLine", multiLine);
892         bundle.putInt("AccessibilityNodeInfo.inputType", inputType);
893         bundle.putInt("AccessibilityNodeInfo.liveRegion", liveRegion);
894     }
895
896     @CalledByNative
897     protected void setAccessibilityEventCollectionInfo(AccessibilityEvent event,
898             int rowCount, int columnCount, boolean hierarchical) {
899         // Backwards compatibility for KitKat AccessibilityNodeInfo fields.
900         Bundle bundle = getOrCreateBundleForAccessibilityEvent(event);
901         bundle.putInt("AccessibilityNodeInfo.CollectionInfo.rowCount", rowCount);
902         bundle.putInt("AccessibilityNodeInfo.CollectionInfo.columnCount", columnCount);
903         bundle.putBoolean("AccessibilityNodeInfo.CollectionInfo.hierarchical", hierarchical);
904     }
905
906     @CalledByNative
907     protected void setAccessibilityEventHeadingFlag(AccessibilityEvent event,
908             boolean heading) {
909         // Backwards compatibility for KitKat AccessibilityNodeInfo fields.
910         Bundle bundle = getOrCreateBundleForAccessibilityEvent(event);
911         bundle.putBoolean("AccessibilityNodeInfo.CollectionItemInfo.heading", heading);
912     }
913
914     @CalledByNative
915     protected void setAccessibilityEventCollectionItemInfo(AccessibilityEvent event,
916             int rowIndex, int rowSpan, int columnIndex, int columnSpan) {
917         // Backwards compatibility for KitKat AccessibilityNodeInfo fields.
918         Bundle bundle = getOrCreateBundleForAccessibilityEvent(event);
919         bundle.putInt("AccessibilityNodeInfo.CollectionItemInfo.rowIndex", rowIndex);
920         bundle.putInt("AccessibilityNodeInfo.CollectionItemInfo.rowSpan", rowSpan);
921         bundle.putInt("AccessibilityNodeInfo.CollectionItemInfo.columnIndex", columnIndex);
922         bundle.putInt("AccessibilityNodeInfo.CollectionItemInfo.columnSpan", columnSpan);
923     }
924
925     @CalledByNative
926     protected void setAccessibilityEventRangeInfo(AccessibilityEvent event,
927             int rangeType, float min, float max, float current) {
928         // Backwards compatibility for KitKat AccessibilityNodeInfo fields.
929         Bundle bundle = getOrCreateBundleForAccessibilityEvent(event);
930         bundle.putInt("AccessibilityNodeInfo.RangeInfo.type", rangeType);
931         bundle.putFloat("AccessibilityNodeInfo.RangeInfo.min", min);
932         bundle.putFloat("AccessibilityNodeInfo.RangeInfo.max", max);
933         bundle.putFloat("AccessibilityNodeInfo.RangeInfo.current", current);
934     }
935
936     @CalledByNative
937     boolean shouldExposePasswordText() {
938         return (Settings.Secure.getInt(
939                         mContentViewCore.getContext().getContentResolver(),
940                         Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD, 0) == 1);
941     }
942
943     private native int nativeGetRootId(long nativeBrowserAccessibilityManagerAndroid);
944     private native boolean nativeIsNodeValid(long nativeBrowserAccessibilityManagerAndroid, int id);
945     private native boolean nativeIsEditableText(
946             long nativeBrowserAccessibilityManagerAndroid, int id);
947     private native int nativeGetEditableTextSelectionStart(
948             long nativeBrowserAccessibilityManagerAndroid, int id);
949     private native int nativeGetEditableTextSelectionEnd(
950             long nativeBrowserAccessibilityManagerAndroid, int id);
951     private native void nativeHitTest(long nativeBrowserAccessibilityManagerAndroid, int x, int y);
952     private native boolean nativePopulateAccessibilityNodeInfo(
953             long nativeBrowserAccessibilityManagerAndroid, AccessibilityNodeInfo info, int id);
954     private native boolean nativePopulateAccessibilityEvent(
955             long nativeBrowserAccessibilityManagerAndroid, AccessibilityEvent event, int id,
956             int eventType);
957     private native void nativeClick(long nativeBrowserAccessibilityManagerAndroid, int id);
958     private native void nativeFocus(long nativeBrowserAccessibilityManagerAndroid, int id);
959     private native void nativeBlur(long nativeBrowserAccessibilityManagerAndroid);
960     private native void nativeScrollToMakeNodeVisible(
961             long nativeBrowserAccessibilityManagerAndroid, int id);
962     private native int nativeFindElementType(long nativeBrowserAccessibilityManagerAndroid,
963             int startId, String elementType, boolean forwards);
964     private native void nativeSetTextFieldValue(long nativeBrowserAccessibilityManagerAndroid,
965             int id, String newValue);
966     private native void nativeSetSelection(long nativeBrowserAccessibilityManagerAndroid,
967             int id, int start, int end);
968     private native boolean nativeNextAtGranularity(long nativeBrowserAccessibilityManagerAndroid,
969             int selectionGranularity, boolean extendSelection, int id, int cursorIndex);
970     private native boolean nativePreviousAtGranularity(
971             long nativeBrowserAccessibilityManagerAndroid,
972             int selectionGranularity, boolean extendSelection, int id, int cursorIndex);
973     private native boolean nativeAdjustSlider(
974             long nativeBrowserAccessibilityManagerAndroid, int id, boolean increment);
975     private native void nativeSetAccessibilityFocus(
976             long nativeBrowserAccessibilityManagerAndroid, int id);
977 }