- add sources.
[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.Bundle;
10 import android.os.Build;
11 import android.view.MotionEvent;
12 import android.view.View;
13 import android.view.accessibility.AccessibilityEvent;
14 import android.view.accessibility.AccessibilityManager;
15 import android.view.accessibility.AccessibilityNodeInfo;
16 import android.view.accessibility.AccessibilityNodeProvider;
17 import android.view.inputmethod.InputMethodManager;
18
19 import org.chromium.base.CalledByNative;
20 import org.chromium.base.JNINamespace;
21 import org.chromium.content.browser.ContentViewCore;
22 import org.chromium.content.browser.RenderCoordinates;
23
24 import java.util.ArrayList;
25 import 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")
36 public class BrowserAccessibilityManager {
37     private static final String TAG = "BrowserAccessibilityManager";
38
39     private ContentViewCore mContentViewCore;
40     private AccessibilityManager mAccessibilityManager;
41     private RenderCoordinates mRenderCoordinates;
42     private int mNativeObj;
43     private int mAccessibilityFocusId;
44     private int mCurrentHoverId;
45     private final int[] mTempLocation = new int[2];
46     private View mView;
47     private boolean mUserHasTouchExplored;
48     private boolean mFrameInfoInitialized;
49
50     // If this is true, enables an experimental feature that focuses the web page after it
51     // finishes loading. Disabled for now because it can be confusing if the user was
52     // trying to do something when this happens.
53     private boolean mFocusPageOnLoad;
54
55     /**
56      * Create a BrowserAccessibilityManager object, which is owned by the C++
57      * BrowserAccessibilityManagerAndroid instance, and connects to the content view.
58      * @param nativeBrowserAccessibilityManagerAndroid A pointer to the counterpart native
59      *     C++ object that owns this object.
60      * @param contentViewCore The content view that this object provides accessibility for.
61      */
62     @CalledByNative
63     private static BrowserAccessibilityManager create(int nativeBrowserAccessibilityManagerAndroid,
64             ContentViewCore contentViewCore) {
65         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
66             return new JellyBeanBrowserAccessibilityManager(
67                     nativeBrowserAccessibilityManagerAndroid, contentViewCore);
68         } else {
69             return new BrowserAccessibilityManager(
70                     nativeBrowserAccessibilityManagerAndroid, contentViewCore);
71         }
72     }
73
74     protected BrowserAccessibilityManager(int nativeBrowserAccessibilityManagerAndroid,
75             ContentViewCore contentViewCore) {
76         mNativeObj = nativeBrowserAccessibilityManagerAndroid;
77         mContentViewCore = contentViewCore;
78         mContentViewCore.setBrowserAccessibilityManager(this);
79         mAccessibilityFocusId = View.NO_ID;
80         mCurrentHoverId = View.NO_ID;
81         mView = mContentViewCore.getContainerView();
82         mRenderCoordinates = mContentViewCore.getRenderCoordinates();
83         mAccessibilityManager =
84             (AccessibilityManager) mContentViewCore.getContext()
85             .getSystemService(Context.ACCESSIBILITY_SERVICE);
86     }
87
88     @CalledByNative
89     private void onNativeObjectDestroyed() {
90         if (mContentViewCore.getBrowserAccessibilityManager() == this) {
91             mContentViewCore.setBrowserAccessibilityManager(null);
92         }
93         mNativeObj = 0;
94         mContentViewCore = null;
95     }
96
97     /**
98      * @return An AccessibilityNodeProvider on JellyBean, and null on previous versions.
99      */
100     public AccessibilityNodeProvider getAccessibilityNodeProvider() {
101         return null;
102     }
103
104     /**
105      * @see AccessibilityNodeProvider#createAccessibilityNodeInfo(int)
106      */
107     protected AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
108         if (!mAccessibilityManager.isEnabled() || mNativeObj == 0 || !mFrameInfoInitialized) {
109             return null;
110         }
111
112         int rootId = nativeGetRootId(mNativeObj);
113         if (virtualViewId == View.NO_ID) {
114             virtualViewId = rootId;
115         }
116         if (mAccessibilityFocusId == View.NO_ID) {
117             mAccessibilityFocusId = rootId;
118         }
119
120         final AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(mView);
121         info.setPackageName(mContentViewCore.getContext().getPackageName());
122         info.setSource(mView, virtualViewId);
123
124         if (nativePopulateAccessibilityNodeInfo(mNativeObj, info, virtualViewId)) {
125             return info;
126         } else {
127             return null;
128         }
129     }
130
131     /**
132      * @see AccessibilityNodeProvider#findAccessibilityNodeInfosByText(String, int)
133      */
134     protected List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String text,
135             int virtualViewId) {
136         return new ArrayList<AccessibilityNodeInfo>();
137     }
138
139     /**
140      * @see AccessibilityNodeProvider#performAction(int, int, Bundle)
141      */
142     protected boolean performAction(int virtualViewId, int action, Bundle arguments) {
143         if (!mAccessibilityManager.isEnabled() || mNativeObj == 0) {
144             return false;
145         }
146
147         switch (action) {
148             case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
149                 if (mAccessibilityFocusId == virtualViewId) {
150                     return true;
151                 }
152
153                 mAccessibilityFocusId = virtualViewId;
154                 sendAccessibilityEvent(mAccessibilityFocusId,
155                         AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
156                 return true;
157             case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
158                 if (mAccessibilityFocusId == virtualViewId) {
159                     mAccessibilityFocusId = View.NO_ID;
160                 }
161                 return true;
162             case AccessibilityNodeInfo.ACTION_CLICK:
163                 nativeClick(mNativeObj, virtualViewId);
164                 break;
165             case AccessibilityNodeInfo.ACTION_FOCUS:
166                 nativeFocus(mNativeObj, virtualViewId);
167                 break;
168             case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS:
169                 nativeBlur(mNativeObj);
170                 break;
171             default:
172                 break;
173         }
174         return false;
175     }
176
177     /**
178      * @see View#onHoverEvent(MotionEvent)
179      */
180     public boolean onHoverEvent(MotionEvent event) {
181         if (!mAccessibilityManager.isEnabled() || mNativeObj == 0) {
182             return false;
183         }
184
185         if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) return true;
186
187         mUserHasTouchExplored = true;
188         float x = event.getX();
189         float y = event.getY();
190
191         // Convert to CSS coordinates.
192         int cssX = (int) (mRenderCoordinates.fromPixToLocalCss(x) +
193                           mRenderCoordinates.getScrollX());
194         int cssY = (int) (mRenderCoordinates.fromPixToLocalCss(y) +
195                           mRenderCoordinates.getScrollY());
196         int id = nativeHitTest(mNativeObj, cssX, cssY);
197         if (mCurrentHoverId != id) {
198             sendAccessibilityEvent(mCurrentHoverId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
199             sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
200             mCurrentHoverId = id;
201         }
202
203         return true;
204     }
205
206     /**
207      * Called by ContentViewCore to notify us when the frame info is initialized,
208      * the first time, since until that point, we can't use mRenderCoordinates to transform
209      * web coordinates to screen coordinates.
210      */
211     public void notifyFrameInfoInitialized() {
212         if (mFrameInfoInitialized) return;
213
214         mFrameInfoInitialized = true;
215         // (Re-) focus focused element, since we weren't able to create an
216         // AccessibilityNodeInfo for this element before.
217         if (mAccessibilityFocusId != View.NO_ID) {
218             sendAccessibilityEvent(mAccessibilityFocusId,
219                                    AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
220         }
221     }
222
223     private void sendAccessibilityEvent(int virtualViewId, int eventType) {
224         if (!mAccessibilityManager.isEnabled() || mNativeObj == 0) return;
225
226         final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
227         event.setPackageName(mContentViewCore.getContext().getPackageName());
228         int rootId = nativeGetRootId(mNativeObj);
229         if (virtualViewId == rootId) {
230             virtualViewId = View.NO_ID;
231         }
232         event.setSource(mView, virtualViewId);
233         if (!nativePopulateAccessibilityEvent(mNativeObj, event, virtualViewId, eventType)) return;
234
235         // This is currently needed if we want Android to draw the yellow box around
236         // the item that has accessibility focus. In practice, this doesn't seem to slow
237         // things down, because it's only called when the accessibility focus moves.
238         // TODO(dmazzoni): remove this if/when Android framework fixes bug.
239         mContentViewCore.getContainerView().postInvalidate();
240
241         mContentViewCore.getContainerView().requestSendAccessibilityEvent(mView, event);
242     }
243
244     @CalledByNative
245     private void handlePageLoaded(int id) {
246         if (mUserHasTouchExplored) return;
247
248         if (mFocusPageOnLoad) {
249             // Focus the natively focused node (usually document),
250             // if this feature is enabled.
251             mAccessibilityFocusId = id;
252             sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_FOCUSED);
253         }
254     }
255
256     @CalledByNative
257     private void handleFocusChanged(int id) {
258         if (mAccessibilityFocusId == id) return;
259
260         mAccessibilityFocusId = id;
261         sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_FOCUSED);
262     }
263
264     @CalledByNative
265     private void handleCheckStateChanged(int id) {
266         sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_CLICKED);
267     }
268
269     @CalledByNative
270     private void handleTextSelectionChanged(int id) {
271         sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
272     }
273
274     @CalledByNative
275     private void handleEditableTextChanged(int id) {
276         sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
277     }
278
279     @CalledByNative
280     private void handleContentChanged(int id) {
281         sendAccessibilityEvent(id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
282     }
283
284     @CalledByNative
285     private void handleNavigate() {
286         mAccessibilityFocusId = View.NO_ID;
287         mUserHasTouchExplored = false;
288         mFrameInfoInitialized = false;
289     }
290
291     @CalledByNative
292     private void handleScrolledToAnchor(int id) {
293         if (mAccessibilityFocusId == id) {
294             return;
295         }
296
297         mAccessibilityFocusId = id;
298         sendAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
299     }
300
301     @CalledByNative
302     private void announceLiveRegionText(String text) {
303         mView.announceForAccessibility(text);
304     }
305
306     @CalledByNative
307     private void setAccessibilityNodeInfoParent(AccessibilityNodeInfo node, int parentId) {
308         node.setParent(mView, parentId);
309     }
310
311     @CalledByNative
312     private void addAccessibilityNodeInfoChild(AccessibilityNodeInfo node, int child_id) {
313         node.addChild(mView, child_id);
314     }
315
316     @CalledByNative
317     private void setAccessibilityNodeInfoBooleanAttributes(AccessibilityNodeInfo node,
318             int virtualViewId, boolean checkable, boolean checked, boolean clickable,
319             boolean enabled, boolean focusable, boolean focused, boolean password,
320             boolean scrollable, boolean selected, boolean visibleToUser) {
321         node.setCheckable(checkable);
322         node.setChecked(checked);
323         node.setClickable(clickable);
324         node.setEnabled(enabled);
325         node.setFocusable(focusable);
326         node.setFocused(focused);
327         node.setPassword(password);
328         node.setScrollable(scrollable);
329         node.setSelected(selected);
330         node.setVisibleToUser(visibleToUser);
331
332         if (focusable) {
333             if (focused) {
334                 node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_FOCUS);
335             } else {
336                 node.addAction(AccessibilityNodeInfo.ACTION_FOCUS);
337             }
338         }
339
340         if (mAccessibilityFocusId == virtualViewId) {
341             node.setAccessibilityFocused(true);
342             node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
343         } else {
344             node.setAccessibilityFocused(false);
345             node.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
346         }
347
348         if (clickable) {
349             node.addAction(AccessibilityNodeInfo.ACTION_CLICK);
350         }
351     }
352
353     @CalledByNative
354     private void setAccessibilityNodeInfoStringAttributes(AccessibilityNodeInfo node,
355             String className, String contentDescription) {
356         node.setClassName(className);
357         node.setContentDescription(contentDescription);
358     }
359
360     @CalledByNative
361     private void setAccessibilityNodeInfoLocation(AccessibilityNodeInfo node,
362             int absoluteLeft, int absoluteTop, int parentRelativeLeft, int parentRelativeTop,
363             int width, int height, boolean isRootNode) {
364         // First set the bounds in parent.
365         Rect boundsInParent = new Rect(parentRelativeLeft, parentRelativeTop,
366                 parentRelativeLeft + width, parentRelativeTop + height);
367         if (isRootNode) {
368             // Offset of the web content relative to the View.
369             boundsInParent.offset(0, (int) mRenderCoordinates.getContentOffsetYPix());
370         }
371         node.setBoundsInParent(boundsInParent);
372
373         // Now set the absolute rect, which requires several transformations.
374         Rect rect = new Rect(absoluteLeft, absoluteTop, absoluteLeft + width, absoluteTop + height);
375
376         // Offset by the scroll position.
377         rect.offset(-(int) mRenderCoordinates.getScrollX(),
378                     -(int) mRenderCoordinates.getScrollY());
379
380         // Convert CSS (web) pixels to Android View pixels
381         rect.left = (int) mRenderCoordinates.fromLocalCssToPix(rect.left);
382         rect.top = (int) mRenderCoordinates.fromLocalCssToPix(rect.top);
383         rect.bottom = (int) mRenderCoordinates.fromLocalCssToPix(rect.bottom);
384         rect.right = (int) mRenderCoordinates.fromLocalCssToPix(rect.right);
385
386         // Offset by the location of the web content within the view.
387         rect.offset(0,
388                     (int) mRenderCoordinates.getContentOffsetYPix());
389
390         // Finally offset by the location of the view within the screen.
391         final int[] viewLocation = new int[2];
392         mView.getLocationOnScreen(viewLocation);
393         rect.offset(viewLocation[0], viewLocation[1]);
394
395         node.setBoundsInScreen(rect);
396     }
397
398     @CalledByNative
399     private void setAccessibilityEventBooleanAttributes(AccessibilityEvent event,
400             boolean checked, boolean enabled, boolean password, boolean scrollable) {
401         event.setChecked(checked);
402         event.setEnabled(enabled);
403         event.setPassword(password);
404         event.setScrollable(scrollable);
405     }
406
407     @CalledByNative
408     private void setAccessibilityEventClassName(AccessibilityEvent event, String className) {
409         event.setClassName(className);
410     }
411
412     @CalledByNative
413     private void setAccessibilityEventListAttributes(AccessibilityEvent event,
414             int currentItemIndex, int itemCount) {
415         event.setCurrentItemIndex(currentItemIndex);
416         event.setItemCount(itemCount);
417     }
418
419     @CalledByNative
420     private void setAccessibilityEventScrollAttributes(AccessibilityEvent event,
421             int scrollX, int scrollY, int maxScrollX, int maxScrollY) {
422         event.setScrollX(scrollX);
423         event.setScrollY(scrollY);
424         event.setMaxScrollX(maxScrollX);
425         event.setMaxScrollY(maxScrollY);
426     }
427
428     @CalledByNative
429     private void setAccessibilityEventTextChangedAttrs(AccessibilityEvent event,
430             int fromIndex, int addedCount, int removedCount, String beforeText, String text) {
431         event.setFromIndex(fromIndex);
432         event.setAddedCount(addedCount);
433         event.setRemovedCount(removedCount);
434         event.setBeforeText(beforeText);
435         event.getText().add(text);
436     }
437
438     @CalledByNative
439     private void setAccessibilityEventSelectionAttrs(AccessibilityEvent event,
440             int fromIndex, int addedCount, int itemCount, String text) {
441         event.setFromIndex(fromIndex);
442         event.setAddedCount(addedCount);
443         event.setItemCount(itemCount);
444         event.getText().add(text);
445     }
446
447     private native int nativeGetRootId(int nativeBrowserAccessibilityManagerAndroid);
448     private native int nativeHitTest(int nativeBrowserAccessibilityManagerAndroid, int x, int y);
449     private native boolean nativePopulateAccessibilityNodeInfo(
450         int nativeBrowserAccessibilityManagerAndroid, AccessibilityNodeInfo info, int id);
451     private native boolean nativePopulateAccessibilityEvent(
452         int nativeBrowserAccessibilityManagerAndroid, AccessibilityEvent event, int id,
453         int eventType);
454     private native void nativeClick(int nativeBrowserAccessibilityManagerAndroid, int id);
455     private native void nativeFocus(int nativeBrowserAccessibilityManagerAndroid, int id);
456     private native void nativeBlur(int nativeBrowserAccessibilityManagerAndroid);
457 }