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_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 | } |