Qt 6.x
The Qt SDK
Loading...
Searching...
No Matches
QtAccessibilityDelegate.java
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4
5package org.qtproject.qt.android.accessibility;
6
7import android.accessibilityservice.AccessibilityService;
8import android.app.Activity;
9import android.graphics.Rect;
10import android.os.Build;
11import android.os.Bundle;
12import android.util.Log;
13import android.view.View;
14import android.view.ViewGroup;
15import android.view.ViewParent;
16import android.text.TextUtils;
17
18import android.view.accessibility.*;
19import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo;
20import android.view.MotionEvent;
21import android.view.View.OnHoverListener;
22
23import android.content.Context;
24import android.system.Os;
25
26import java.util.LinkedList;
27import java.util.List;
28
30
31public class QtAccessibilityDelegate extends View.AccessibilityDelegate
32{
33 private static final String TAG = "Qt A11Y";
34
35 // Qt uses the upper half of the unsiged integers
36 // all low positive ints should be fine.
37 public static final int INVALID_ID = 333; // half evil
38
39 // The platform might ask for the class implementing the "view".
40 // Pretend to be an inner class of the QtSurface.
41 private static final String DEFAULT_CLASS_NAME = "$VirtualChild";
42
43 private View m_view = null;
44 private AccessibilityManager m_manager;
45 private QtActivityDelegate m_activityDelegate;
46 private Activity m_activity;
47 private ViewGroup m_layout;
48
49 // The accessible object that currently has the "accessibility focus"
50 // usually indicated by a yellow rectangle on screen.
51 private int m_focusedVirtualViewId = INVALID_ID;
52 // When exploring the screen by touch, the item "hovered" by the finger.
53 private int m_hoveredVirtualViewId = INVALID_ID;
54
55 // Cache coordinates of the view to know the global offset
56 // this is because the Android platform window does not take
57 // the offset of the view on screen into account (eg status bar on top)
58 private final int[] m_globalOffset = new int[2];
59 private int m_oldOffsetX = 0;
60 private int m_oldOffsetY = 0;
61
62 private class HoverEventListener implements View.OnHoverListener
63 {
64 @Override
65 public boolean onHover(View v, MotionEvent event)
66 {
67 return dispatchHoverEvent(event);
68 }
69 }
70
71 public QtAccessibilityDelegate(Activity activity, ViewGroup layout, QtActivityDelegate activityDelegate)
72 {
73 m_activity = activity;
74 m_layout = layout;
75 m_activityDelegate = activityDelegate;
76
77 m_manager = (AccessibilityManager) m_activity.getSystemService(Context.ACCESSIBILITY_SERVICE);
78 if (m_manager != null) {
79 AccessibilityManagerListener accServiceListener = new AccessibilityManagerListener();
80 if (!m_manager.addAccessibilityStateChangeListener(accServiceListener))
81 Log.w("Qt A11y", "Could not register a11y state change listener");
82 if (m_manager.isEnabled())
83 accServiceListener.onAccessibilityStateChanged(true);
84 }
85 }
86
87 private class AccessibilityManagerListener implements AccessibilityManager.AccessibilityStateChangeListener
88 {
89 @Override
90 public void onAccessibilityStateChanged(boolean enabled)
91 {
92 if (Os.getenv("QT_ANDROID_DISABLE_ACCESSIBILITY") != null)
93 return;
94 if (enabled) {
95 try {
96 View view = m_view;
97 if (view == null) {
98 view = new View(m_activity);
99 view.setId(View.NO_ID);
100 }
101
102 // ### Keep this for debugging for a while. It allows us to visually see that our View
103 // ### is on top of the surface(s)
104 // ColorDrawable color = new ColorDrawable(0x80ff8080); //0xAARRGGBB
105 // view.setBackground(color);
106 view.setAccessibilityDelegate(QtAccessibilityDelegate.this);
107
108 // if all is fine, add it to the layout
109 if (m_view == null) {
110 //m_layout.addAccessibilityView(view);
111 m_layout.addView(view, m_activityDelegate.getSurfaceCount(),
112 new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
113 }
114 m_view = view;
115
116 m_view.setOnHoverListener(new HoverEventListener());
117 } catch (Exception e) {
118 // Unknown exception means something went wrong.
119 Log.w("Qt A11y", "Unknown exception: " + e.toString());
120 }
121 } else {
122 if (m_view != null) {
123 m_layout.removeView(m_view);
124 m_view = null;
125 }
126 }
127
128 QtNativeAccessibility.setActive(enabled);
129 }
130 }
131
132
133 @Override
134 public AccessibilityNodeProvider getAccessibilityNodeProvider(View host)
135 {
136 return m_nodeProvider;
137 }
138
139 // For "explore by touch" we need all movement events here first
140 // (user moves finger over screen to discover items on screen).
141 private boolean dispatchHoverEvent(MotionEvent event)
142 {
143 if (!m_manager.isTouchExplorationEnabled()) {
144 return false;
145 }
146
147 int virtualViewId = QtNativeAccessibility.hitTest(event.getX(), event.getY());
148 if (virtualViewId == INVALID_ID) {
149 virtualViewId = View.NO_ID;
150 }
151
152 switch (event.getAction()) {
153 case MotionEvent.ACTION_HOVER_ENTER:
154 case MotionEvent.ACTION_HOVER_MOVE:
155 setHoveredVirtualViewId(virtualViewId);
156 break;
157 case MotionEvent.ACTION_HOVER_EXIT:
158 setHoveredVirtualViewId(virtualViewId);
159 break;
160 }
161
162 return true;
163 }
164
165 public void notifyScrolledEvent(int viewId)
166 {
167 sendEventForVirtualViewId(viewId, AccessibilityEvent.TYPE_VIEW_SCROLLED);
168 }
169
170 public void notifyLocationChange(int viewId)
171 {
172 if (m_focusedVirtualViewId == viewId)
173 invalidateVirtualViewId(m_focusedVirtualViewId);
174 }
175
176 public void notifyObjectHide(int viewId, int parentId)
177 {
178 // If the object had accessibility focus, we need to clear it.
179 // Note: This code is mostly copied from
180 // AccessibilityNodeProvider::performAction, but we remove the
181 // focus only if the focused view id matches the one that was hidden.
182 if (m_focusedVirtualViewId == viewId) {
183 m_focusedVirtualViewId = INVALID_ID;
184 m_view.invalidate();
186 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
187 }
188 // When the object is hidden, we need to notify its parent about
189 // content change, not the hidden object itself
190 invalidateVirtualViewId(parentId);
191 }
192
193 public void notifyObjectFocus(int viewId)
194 {
195 if (m_view == null)
196 return;
197 m_focusedVirtualViewId = viewId;
198 m_view.invalidate();
200 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
201 }
202
203 public void notifyValueChanged(int viewId, String value)
204 {
205 // Send a TYPE_ANNOUNCEMENT event with the new value
206
207 if ((viewId == INVALID_ID) || !m_manager.isEnabled()) {
208 Log.w(TAG, "notifyValueChanged() for invalid view");
209 return;
210 }
211
212 final ViewGroup group = (ViewGroup)m_view.getParent();
213 if (group == null) {
214 Log.w(TAG, "Could not announce value because ViewGroup was null.");
215 return;
216 }
217
218 final AccessibilityEvent event =
219 AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT);
220
221 event.setEnabled(true);
222 event.setClassName(m_view.getClass().getName() + DEFAULT_CLASS_NAME);
223
224 event.setContentDescription(value);
225
226 if (event.getText().isEmpty() && TextUtils.isEmpty(event.getContentDescription())) {
227 Log.w(TAG, "No value to announce for " + event.getClassName());
228 return;
229 }
230
231 event.setPackageName(m_view.getContext().getPackageName());
232 event.setSource(m_view, viewId);
233
234 if (!group.requestSendAccessibilityEvent(m_view, event))
235 Log.w(TAG, "Failed to send value change announcement for " + event.getClassName());
236 }
237
238 public boolean sendEventForVirtualViewId(int virtualViewId, int eventType)
239 {
240 final AccessibilityEvent event = getEventForVirtualViewId(virtualViewId, eventType);
242 }
243
244 public boolean sendAccessibilityEvent(AccessibilityEvent event)
245 {
246 if (event == null)
247 return false;
248
249 final ViewGroup group = (ViewGroup) m_view.getParent();
250 if (group == null) {
251 Log.w(TAG, "Could not send AccessibilityEvent because group was null. This should really not happen.");
252 return false;
253 }
254
255 return group.requestSendAccessibilityEvent(m_view, event);
256 }
257
258 public void invalidateVirtualViewId(int virtualViewId)
259 {
260 final AccessibilityEvent event = getEventForVirtualViewId(virtualViewId, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
261
262 if (event == null)
263 return;
264
265 event.setContentChangeTypes(AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
267 }
268
269 private void setHoveredVirtualViewId(int virtualViewId)
270 {
271 if (m_hoveredVirtualViewId == virtualViewId) {
272 return;
273 }
274
275 final int previousVirtualViewId = m_hoveredVirtualViewId;
276 m_hoveredVirtualViewId = virtualViewId;
277 sendEventForVirtualViewId(virtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
278 sendEventForVirtualViewId(previousVirtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
279 }
280
281 private AccessibilityEvent getEventForVirtualViewId(int virtualViewId, int eventType)
282 {
283 if ((virtualViewId == INVALID_ID) || !m_manager.isEnabled()) {
284 Log.w(TAG, "getEventForVirtualViewId for invalid view");
285 return null;
286 }
287
288 if (m_activityDelegate.getSurfaceCount() == 0)
289 return null;
290
291 final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
292
293 event.setEnabled(true);
294 event.setClassName(m_view.getClass().getName() + DEFAULT_CLASS_NAME);
295
296 event.setContentDescription(QtNativeAccessibility.descriptionForAccessibleObject(virtualViewId));
297 if (event.getText().isEmpty() && TextUtils.isEmpty(event.getContentDescription()))
298 Log.w(TAG, "AccessibilityEvent with empty description");
299
300 event.setPackageName(m_view.getContext().getPackageName());
301 event.setSource(m_view, virtualViewId);
302 return event;
303 }
304
305 private void dumpNodes(int parentId)
306 {
307 Log.i(TAG, "A11Y hierarchy: " + parentId + " parent: " + QtNativeAccessibility.parentId(parentId));
308 Log.i(TAG, " desc: " + QtNativeAccessibility.descriptionForAccessibleObject(parentId) + " rect: " + QtNativeAccessibility.screenRect(parentId));
309 Log.i(TAG, " NODE: " + getNodeForVirtualViewId(parentId));
310 int[] ids = QtNativeAccessibility.childIdListForAccessibleObject(parentId);
311 for (int i = 0; i < ids.length; ++i) {
312 Log.i(TAG, parentId + " has child: " + ids[i]);
313 dumpNodes(ids[i]);
314 }
315 }
316
317 private AccessibilityNodeInfo getNodeForView()
318 {
319 // Since we don't want the parent to be focusable, but we can't remove
320 // actions from a node, copy over the necessary fields.
321 final AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(m_view);
322 final AccessibilityNodeInfo source = AccessibilityNodeInfo.obtain(m_view);
323 m_view.onInitializeAccessibilityNodeInfo(source);
324
325 // Get the actual position on screen, taking the status bar into account.
326 m_view.getLocationOnScreen(m_globalOffset);
327 final int offsetX = m_globalOffset[0];
328 final int offsetY = m_globalOffset[1];
329
330 // Copy over parent and screen bounds.
331 final Rect m_tempParentRect = new Rect();
332 source.getBoundsInParent(m_tempParentRect);
333 result.setBoundsInParent(m_tempParentRect);
334
335 final Rect m_tempScreenRect = new Rect();
336 source.getBoundsInScreen(m_tempScreenRect);
337 m_tempScreenRect.offset(offsetX, offsetY);
338 result.setBoundsInScreen(m_tempScreenRect);
339
340 // Set up the parent view, if applicable.
341 final ViewParent parent = m_view.getParent();
342 if (parent instanceof View) {
343 result.setParent((View) parent);
344 }
345
346 result.setVisibleToUser(source.isVisibleToUser());
347 result.setPackageName(source.getPackageName());
348 result.setClassName(source.getClassName());
349
350// Spit out the entire hierarchy for debugging purposes
351// dumpNodes(-1);
352
353 if (m_activityDelegate.getSurfaceCount() != 0) {
354 int[] ids = QtNativeAccessibility.childIdListForAccessibleObject(-1);
355 for (int i = 0; i < ids.length; ++i)
356 result.addChild(m_view, ids[i]);
357 }
358
359 // The offset values have changed, so we need to re-focus the
360 // currently focused item, otherwise it will have an incorrect
361 // focus frame
362 if ((m_oldOffsetX != offsetX) || (m_oldOffsetY != offsetY)) {
363 m_oldOffsetX = offsetX;
364 m_oldOffsetY = offsetY;
365 if (m_focusedVirtualViewId != INVALID_ID) {
366 m_nodeProvider.performAction(m_focusedVirtualViewId,
367 AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS,
368 new Bundle());
369 m_nodeProvider.performAction(m_focusedVirtualViewId,
370 AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS,
371 new Bundle());
372 }
373 }
374
375 return result;
376 }
377
378 private AccessibilityNodeInfo getNodeForVirtualViewId(int virtualViewId)
379 {
380 final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain();
381
382 node.setClassName(m_view.getClass().getName() + DEFAULT_CLASS_NAME);
383 node.setPackageName(m_view.getContext().getPackageName());
384
385 if (m_activityDelegate.getSurfaceCount() == 0 || !QtNativeAccessibility.populateNode(virtualViewId, node)) {
386 return node;
387 }
388
389 // set only if valid, otherwise we return a node that is invalid and will crash when accessed
390 node.setSource(m_view, virtualViewId);
391
392 if (TextUtils.isEmpty(node.getText()) && TextUtils.isEmpty(node.getContentDescription()))
393 Log.w(TAG, "AccessibilityNodeInfo with empty contentDescription: " + virtualViewId);
394
395 int parentId = QtNativeAccessibility.parentId(virtualViewId);
396 node.setParent(m_view, parentId);
397
398 Rect screenRect = QtNativeAccessibility.screenRect(virtualViewId);
399 final int offsetX = m_globalOffset[0];
400 final int offsetY = m_globalOffset[1];
401 screenRect.offset(offsetX, offsetY);
402 node.setBoundsInScreen(screenRect);
403
404 Rect rectInParent = screenRect;
405 Rect parentScreenRect = QtNativeAccessibility.screenRect(parentId);
406 rectInParent.offset(-parentScreenRect.left, -parentScreenRect.top);
407 node.setBoundsInParent(rectInParent);
408
409 // Manage internal accessibility focus state.
410 if (m_focusedVirtualViewId == virtualViewId) {
411 node.setAccessibilityFocused(true);
412 node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
413 } else {
414 node.setAccessibilityFocused(false);
415 node.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
416 }
417
418 int[] ids = QtNativeAccessibility.childIdListForAccessibleObject(virtualViewId);
419 for (int i = 0; i < ids.length; ++i)
420 node.addChild(m_view, ids[i]);
421 if (node.isScrollable()) {
422 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
423 node.setCollectionInfo(new CollectionInfo(ids.length, 1, false));
424 } else {
425 node.setCollectionInfo(CollectionInfo.obtain(ids.length, 1, false));
426 }
427 }
428
429 return node;
430 }
431
432 private AccessibilityNodeProvider m_nodeProvider = new AccessibilityNodeProvider()
433 {
434 @Override
435 public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId)
436 {
437 if (virtualViewId == View.NO_ID || m_activityDelegate.getSurfaceCount() == 0) {
438 return getNodeForView();
439 }
440 return getNodeForVirtualViewId(virtualViewId);
441 }
442
443 @Override
444 public boolean performAction(int virtualViewId, int action, Bundle arguments)
445 {
446 boolean handled = false;
447 //Log.i(TAG, "PERFORM ACTION: " + action + " on " + virtualViewId);
448 switch (action) {
449 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
450 // Only handle the FOCUS action if it's placing focus on
451 // a different view that was previously focused.
452 if (m_focusedVirtualViewId != virtualViewId) {
453 m_focusedVirtualViewId = virtualViewId;
454 m_view.invalidate();
455 sendEventForVirtualViewId(virtualViewId,
456 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
457 handled = true;
458 }
459 break;
460 case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
461 if (m_focusedVirtualViewId == virtualViewId) {
462 m_focusedVirtualViewId = INVALID_ID;
463 }
464 // Since we're managing focus at the parent level, we are
465 // likely to receive a FOCUS action before a CLEAR_FOCUS
466 // action. We'll give the benefit of the doubt to the
467 // framework and always handle FOCUS_CLEARED.
468 m_view.invalidate();
469 sendEventForVirtualViewId(virtualViewId,
470 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
471 handled = true;
472 break;
473 default:
474 // Let the node provider handle focus for the view node.
475 if (virtualViewId == View.NO_ID) {
476 return m_view.performAccessibilityAction(action, arguments);
477 }
478 }
479 handled |= performActionForVirtualViewId(virtualViewId, action, arguments);
480
481 return handled;
482 }
483 };
484
485 protected boolean performActionForVirtualViewId(int virtualViewId, int action, Bundle arguments)
486 {
487// Log.i(TAG, "ACTION " + action + " on " + virtualViewId);
488// dumpNodes(virtualViewId);
489 boolean success = false;
490 switch (action) {
491 case AccessibilityNodeInfo.ACTION_CLICK:
492 success = QtNativeAccessibility.clickAction(virtualViewId);
493 if (success)
494 sendEventForVirtualViewId(virtualViewId, AccessibilityEvent.TYPE_VIEW_CLICKED);
495 break;
496 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
497 success = QtNativeAccessibility.scrollForward(virtualViewId);
498 if (success)
499 sendEventForVirtualViewId(virtualViewId, AccessibilityEvent.TYPE_VIEW_SCROLLED);
500 break;
501 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
502 success = QtNativeAccessibility.scrollBackward(virtualViewId);
503 if (success)
504 sendEventForVirtualViewId(virtualViewId, AccessibilityEvent.TYPE_VIEW_SCROLLED);
505 break;
506 }
507 return success;
508 }
509}
boolean performActionForVirtualViewId(int virtualViewId, int action, Bundle arguments)
QtAccessibilityDelegate(Activity activity, ViewGroup layout, QtActivityDelegate activityDelegate)
double e
QList< QVariant > arguments
EGLOutputLayerEXT EGLint EGLAttrib value
[5]
#define TAG(x)
GLsizei const GLfloat * v
[13]
GLenum GLenum GLsizei const GLuint * ids
GLenum GLenum GLsizei const GLuint GLboolean enabled
GLboolean GLuint group
GLsizei GLsizei GLchar * source
struct _cl_event * event
GLuint64EXT * result
[6]
QVBoxLayout * layout
QQuickView * view
[0]
IUIAutomationTreeWalker __RPC__deref_out_opt IUIAutomationElement ** parent