Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
androidjniaccessibility.cpp
Go to the documentation of this file.
1// Copyright (C) 2021 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
7#include "qpa/qplatformaccessibility.h"
8#include <QtGui/private/qaccessiblebridgeutils_p.h>
10#include "qwindow.h"
11#include "qrect.h"
12#include "QtGui/qaccessible.h"
13#include <QtCore/qmath.h>
14#include <QtCore/private/qjnihelpers_p.h>
15#include <QtCore/QJniObject>
16#include <QtGui/private/qhighdpiscaling_p.h>
17
18#include <QtCore/QObject>
19#include <QtCore/qpointer.h>
20#include <QtCore/qvarlengtharray.h>
21
22static const char m_qtTag[] = "Qt A11Y";
23
24QT_BEGIN_NAMESPACE
25
26using namespace Qt::StringLiterals;
27
29{
45
46 static bool m_accessibilityActivated = false;
47
48 // This object is needed to schedule the execution of the code that
49 // deals with accessibility instances to the Qt main thread.
50 // Because of that almost every method here is split into two parts.
51 // The _helper part is executed in the context of m_accessibilityContext
52 // on the main thread. The other part is executed in Java thread.
54
55 // This method is called from the Qt main thread, and normally a
56 // QGuiApplication instance will be used as a parent.
58 {
59 if (m_accessibilityContext)
60 m_accessibilityContext->deleteLater();
61 m_accessibilityContext = new QObject(parent);
62 }
63
64 template <typename Func, typename Ret>
65 void runInObjectContext(QObject *context, Func &&func, Ret *retVal)
66 {
67 QtAndroidPrivate::AndroidDeadlockProtector protector(
68 u"QtAndroidAccessibility::runInObjectContext()"_s);
69 if (!protector.acquire()) {
70 __android_log_print(ANDROID_LOG_WARN, m_qtTag,
71 "Could not run accessibility call in object context, accessing "
72 "main thread could lead to deadlock");
73 return;
74 }
75
76 if (!QtAndroid::blockEventLoopsWhenSuspended()
77 || QGuiApplication::applicationState() != Qt::ApplicationSuspended) {
78 QMetaObject::invokeMethod(context, func, Qt::BlockingQueuedConnection, retVal);
79 } else {
80 __android_log_print(ANDROID_LOG_WARN, m_qtTag,
81 "Could not run accessibility call in object context, event loop suspended.");
82 }
83 }
84
85 bool isActive()
86 {
88 }
89
90 static void setActive(JNIEnv */*env*/, jobject /*thiz*/, jboolean active)
91 {
92 QMutexLocker lock(QtAndroid::platformInterfaceMutex());
95 if (platformIntegration) {
96 platformIntegration->accessibility()->setActive(active);
97 } else {
98 __android_log_print(ANDROID_LOG_DEBUG, m_qtTag,
99 "Android platform integration is not ready, accessibility activation deferred.");
100 }
101 }
102
103 QAccessibleInterface *interfaceFromId(jint objectId)
104 {
105 QAccessibleInterface *iface = nullptr;
106 if (objectId == -1) {
107 QWindow *win = qApp->focusWindow();
108 if (win)
109 iface = win->accessibleRoot();
110 } else {
111 iface = QAccessible::accessibleInterface(objectId);
112 }
113 return iface;
114 }
115
116 void notifyLocationChange(uint accessibilityObjectId)
117 {
118 QtAndroid::notifyAccessibilityLocationChange(accessibilityObjectId);
119 }
120
121 static int parentId_helper(int objectId); // forward declaration
122
123 void notifyObjectHide(uint accessibilityObjectId)
124 {
125 const auto parentObjectId = parentId_helper(accessibilityObjectId);
126 QtAndroid::notifyObjectHide(accessibilityObjectId, parentObjectId);
127 }
128
129 void notifyObjectShow(uint accessibilityObjectId)
130 {
131 const auto parentObjectId = parentId_helper(accessibilityObjectId);
132 QtAndroid::notifyObjectShow(parentObjectId);
133 }
134
135 void notifyObjectFocus(uint accessibilityObjectId)
136 {
137 QtAndroid::notifyObjectFocus(accessibilityObjectId);
138 }
139
140 static jstring jvalueForAccessibleObject(int objectId); // forward declaration
141
142 void notifyValueChanged(uint accessibilityObjectId)
143 {
144 jstring value = jvalueForAccessibleObject(accessibilityObjectId);
145 QtAndroid::notifyValueChanged(accessibilityObjectId, value);
146 }
147
148 // Forward declaration
149 static QString descriptionForInterface(QAccessibleInterface *iface);
150
151 void notifyDescriptionOrNameChanged(uint accessibilityObjectId)
152 {
153 QAccessibleInterface *iface = interfaceFromId(accessibilityObjectId);
154 if (iface && iface->isValid()) {
155 const QString value = descriptionForInterface(iface);
156 QtAndroid::notifyDescriptionOrNameChanged(accessibilityObjectId, value);
157 }
158 }
159
160 void notifyScrolledEvent(uint accessiblityObjectId)
161 {
162 QtAndroid::notifyScrolledEvent(accessiblityObjectId);
163 }
164
165 void notifyAnnouncementEvent(uint accessibilityObjectId, const QString &message)
166 {
167 QtAndroid::notifyAnnouncementEvent(accessibilityObjectId, message);
168 }
169
171 {
172 QAccessibleInterface *iface = interfaceFromId(objectId);
173 if (iface && iface->isValid()) {
174 const int childCount = iface->childCount();
175 QVarLengthArray<jint, 8> ifaceIdArray;
176 ifaceIdArray.reserve(childCount);
177 for (int i = 0; i < childCount; ++i) {
178 QAccessibleInterface *child = iface->child(i);
179 if (child && child->isValid())
180 ifaceIdArray.append(QAccessible::uniqueId(child));
181 }
182 return ifaceIdArray;
183 }
184 return {};
185 }
186
187 static jintArray childIdListForAccessibleObject(JNIEnv *env, jobject /*thiz*/, jint objectId)
188 {
189 if (m_accessibilityContext) {
190 QVarLengthArray<jint, 8> ifaceIdArray;
191 runInObjectContext(m_accessibilityContext, [objectId]() {
192 return childIdListForAccessibleObject_helper(objectId);
193 }, &ifaceIdArray);
194 jintArray jArray = env->NewIntArray(jsize(ifaceIdArray.count()));
195 env->SetIntArrayRegion(jArray, 0, ifaceIdArray.count(), ifaceIdArray.data());
196 return jArray;
197 }
198
199 return env->NewIntArray(jsize(0));
200 }
201
202 static int parentId_helper(int objectId)
203 {
204 QAccessibleInterface *iface = interfaceFromId(objectId);
205 if (iface && iface->isValid()) {
206 QAccessibleInterface *parent = iface->parent();
207 if (parent && parent->isValid()) {
208 if (parent->role() == QAccessible::Application)
209 return -1;
210 return QAccessible::uniqueId(parent);
211 }
212 }
213 return -1;
214 }
215
216 static jint parentId(JNIEnv */*env*/, jobject /*thiz*/, jint objectId)
217 {
218 jint result = -1;
219 if (m_accessibilityContext) {
220 runInObjectContext(m_accessibilityContext, [objectId]() {
221 return parentId_helper(objectId);
222 }, &result);
223 }
224 return result;
225 }
226
227 static QRect screenRect_helper(int objectId, bool clip = true)
228 {
229 QRect rect;
230 QAccessibleInterface *iface = interfaceFromId(objectId);
231 if (iface && iface->isValid()) {
232 rect = QHighDpi::toNativePixels(iface->rect(), iface->window());
233 }
234 // If the widget is not fully in-bound in its parent then we have to clip the rectangle to draw
235 if (clip && iface && iface->parent() && iface->parent()->isValid()) {
236 const auto parentRect = QHighDpi::toNativePixels(iface->parent()->rect(), iface->parent()->window());
237 rect = rect.intersected(parentRect);
238 }
239 return rect;
240 }
241
242 static jobject screenRect(JNIEnv *env, jobject /*thiz*/, jint objectId)
243 {
244 QRect rect;
245 if (m_accessibilityContext) {
246 runInObjectContext(m_accessibilityContext, [objectId]() {
247 return screenRect_helper(objectId);
248 }, &rect);
249 }
250 jclass rectClass = env->FindClass("android/graphics/Rect");
251 jmethodID ctor = env->GetMethodID(rectClass, "<init>", "(IIII)V");
252 jobject jrect = env->NewObject(rectClass, ctor, rect.left(), rect.top(), rect.right(), rect.bottom());
253 return jrect;
254 }
255
256 static int hitTest_helper(float x, float y)
257 {
258 QAccessibleInterface *root = interfaceFromId(-1);
259 if (root && root->isValid()) {
260 QPoint pos = QHighDpi::fromNativePixels(QPoint(int(x), int(y)), root->window());
261
262 QAccessibleInterface *child = root->childAt(pos.x(), pos.y());
263 QAccessibleInterface *lastChild = nullptr;
264 while (child && (child != lastChild)) {
265 lastChild = child;
266 child = child->childAt(pos.x(), pos.y());
267 }
268 if (lastChild)
269 return QAccessible::uniqueId(lastChild);
270 }
271 return -1;
272 }
273
274 static jint hitTest(JNIEnv */*env*/, jobject /*thiz*/, jfloat x, jfloat y)
275 {
276 jint result = -1;
277 if (m_accessibilityContext) {
278 runInObjectContext(m_accessibilityContext, [x, y]() {
279 return hitTest_helper(x, y);
280 }, &result);
281 }
282 return result;
283 }
284
285 static void invokeActionOnInterfaceInMainThread(QAccessibleActionInterface* actionInterface,
286 const QString& action)
287 {
288 // Queue the action and return back to Java thread, so that we do not
289 // block it for too long
290 QMetaObject::invokeMethod(qApp, [actionInterface, action]() {
291 actionInterface->doAction(action);
292 }, Qt::QueuedConnection);
293 }
294
295 static bool clickAction_helper(int objectId)
296 {
297 QAccessibleInterface *iface = interfaceFromId(objectId);
298 if (!iface || !iface->isValid() || !iface->actionInterface())
299 return false;
300
301 const auto& actionNames = iface->actionInterface()->actionNames();
302
303 if (actionNames.contains(QAccessibleActionInterface::pressAction())) {
304 invokeActionOnInterfaceInMainThread(iface->actionInterface(),
305 QAccessibleActionInterface::pressAction());
306 } else if (actionNames.contains(QAccessibleActionInterface::toggleAction())) {
307 invokeActionOnInterfaceInMainThread(iface->actionInterface(),
308 QAccessibleActionInterface::toggleAction());
309 } else {
310 return false;
311 }
312 return true;
313 }
314
315 static bool focusAction_helper(int objectId)
316 {
317 QAccessibleInterface *iface = interfaceFromId(objectId);
318 if (!iface || !iface->isValid() || !iface->actionInterface())
319 return false;
320
321 const auto& actionNames = iface->actionInterface()->actionNames();
322
323 if (actionNames.contains(QAccessibleActionInterface::setFocusAction())) {
324 invokeActionOnInterfaceInMainThread(iface->actionInterface(),
325 QAccessibleActionInterface::setFocusAction());
326 return true;
327 }
328 return false;
329 }
330
331 static jboolean clickAction(JNIEnv */*env*/, jobject /*thiz*/, jint objectId)
332 {
333 bool result = false;
334 if (m_accessibilityContext) {
335 runInObjectContext(m_accessibilityContext, [objectId]() {
336 return clickAction_helper(objectId);
337 }, &result);
338 }
339 return result;
340 }
341
342 static jboolean focusAction(JNIEnv */*env*/, jobject /*thiz*/, jint objectId)
343 {
344 bool result = false;
345 if (m_accessibilityContext) {
346 runInObjectContext(m_accessibilityContext, [objectId]() {
347 return focusAction_helper(objectId);
348 }, &result);
349 }
350 return result;
351 }
352
353 static bool scroll_helper(int objectId, const QString &actionName)
354 {
355 QAccessibleInterface *iface = interfaceFromId(objectId);
356 if (iface && iface->isValid())
357 return QAccessibleBridgeUtils::performEffectiveAction(iface, actionName);
358 return false;
359 }
360
361 static jboolean scrollForward(JNIEnv */*env*/, jobject /*thiz*/, jint objectId)
362 {
363 bool result = false;
364
365 const auto& ids = childIdListForAccessibleObject_helper(objectId);
366 if (ids.isEmpty())
367 return false;
368
369 const int firstChildId = ids.first();
370 const QRect oldPosition = screenRect_helper(firstChildId, false);
371
372 if (m_accessibilityContext) {
373 runInObjectContext(m_accessibilityContext, [objectId]() {
374 return scroll_helper(objectId, QAccessibleActionInterface::increaseAction());
375 }, &result);
376 }
377
378 // Don't check for position change if the call was not successful
379 return result && oldPosition != screenRect_helper(firstChildId, false);
380 }
381
382 static jboolean scrollBackward(JNIEnv */*env*/, jobject /*thiz*/, jint objectId)
383 {
384 bool result = false;
385
386 const auto& ids = childIdListForAccessibleObject_helper(objectId);
387 if (ids.isEmpty())
388 return false;
389
390 const int firstChildId = ids.first();
391 const QRect oldPosition = screenRect_helper(firstChildId, false);
392
393 if (m_accessibilityContext) {
394 runInObjectContext(m_accessibilityContext, [objectId]() {
395 return scroll_helper(objectId, QAccessibleActionInterface::decreaseAction());
396 }, &result);
397 }
398
399 // Don't check for position change if the call was not successful
400 return result && oldPosition != screenRect_helper(firstChildId, false);
401 }
402
403 static QString textFromValue(QAccessibleInterface *iface)
404 {
405 QString valueStr;
406 QAccessibleValueInterface *valueIface = iface->valueInterface();
407 if (valueIface) {
408 const QVariant valueVar = valueIface->currentValue();
409 const auto type = valueVar.typeId();
410 if (type == QMetaType::Double || type == QMetaType::Float) {
411 // QVariant's toString() formats floating-point values with
412 // FloatingPointShortest, which is not an accessible
413 // representation; nor, in many cases, is it suitable to the UI
414 // element whose value we're looking at. So roll our own
415 // A11Y-friendly conversion to string.
416 const double val = valueVar.toDouble();
417 // Try to use minimumStepSize() to determine precision
418 bool stepIsValid = false;
419 const double step = qAbs(valueIface->minimumStepSize().toDouble(&stepIsValid));
420 if (!stepIsValid || qFuzzyIsNull(step)) {
421 // Ignore step, use default precision
422 valueStr = qFuzzyIsNull(val) ? u"0"_s : QString::number(val, 'f');
423 } else {
424 const int precision = [](double s) {
425 int count = 0;
426 while (s < 1. && !qFuzzyCompare(s, 1.)) {
427 ++count;
428 s *= 10;
429 }
430 // If s is now 1.25, we want to show some more digits,
431 // but don't want to get silly with a step like 1./7;
432 // so only include a few extra digits.
433 const int stop = count + 3;
434 const auto fractional = [](double v) {
435 double whole = 0.0;
436 std::modf(v + 0.5, &whole);
437 return qAbs(v - whole);
438 };
439 s = fractional(s);
440 while (count < stop && !qFuzzyIsNull(s)) {
441 ++count;
442 s = fractional(s * 10);
443 }
444 return count;
445 }(step);
446 valueStr = qFuzzyIsNull(val / step) ? u"0"_s
447 : QString::number(val, 'f', precision);
448 }
449 } else {
450 valueStr = valueVar.toString();
451 }
452 }
453 return valueStr;
454 }
455
457 {
458 QAccessibleInterface *iface = interfaceFromId(objectId);
459 const QString value = textFromValue(iface);
460 QJniEnvironment env;
461 jstring jstr = env->NewString((jchar*)value.constData(), (jsize)value.size());
462 if (env.checkAndClearExceptions())
463 __android_log_print(ANDROID_LOG_WARN, m_qtTag, "Failed to create jstring");
464 return jstr;
465 }
466
467 static QString classNameForRole(QAccessible::Role role, QAccessible::State state) {
468 switch (role) {
469 case QAccessible::Role::Button:
470 case QAccessible::Role::Link:
471 {
472 if (state.checkable)
473 // There is also a android.widget.Switch for which we have no match.
474 return QStringLiteral("android.widget.ToggleButton");
475 return QStringLiteral("android.widget.Button");
476 }
477 case QAccessible::Role::CheckBox:
478 // As of android/accessibility/utils/Role.java::getRole a CheckBox
479 // is NOT android.widget.CheckBox
480 return QStringLiteral("android.widget.CompoundButton");
481 case QAccessible::Role::Clock:
482 return QStringLiteral("android.widget.TextClock");
483 case QAccessible::Role::ComboBox:
484 return QStringLiteral("android.widget.Spinner");
485 case QAccessible::Role::Graphic:
486 // QQuickImage does not provide this role it inherits Client from QQuickItem
487 return QStringLiteral("android.widget.ImageView");
488 case QAccessible::Role::Grouping:
489 return QStringLiteral("android.view.ViewGroup");
490 case QAccessible::Role::List:
491 // As of android/accessibility/utils/Role.java::getRole a List
492 // is NOT android.widget.ListView
493 return QStringLiteral("android.widget.AbsListView");
494 case QAccessible::Role::MenuItem:
495 return QStringLiteral("android.view.MenuItem");
496 case QAccessible::Role::PopupMenu:
497 return QStringLiteral("android.widget.PopupMenu");
498 case QAccessible::Role::Separator:
499 return QStringLiteral("android.widget.Space");
500 case QAccessible::Role::ToolBar:
501 return QStringLiteral("android.view.Toolbar");
502 case QAccessible::Role::Heading: [[fallthrough]];
503 case QAccessible::Role::StaticText:
504 // Heading vs. regular Text is finally determined by AccessibilityNodeInfo.isHeading()
505 return QStringLiteral("android.widget.TextView");
506 case QAccessible::Role::EditableText:
507 return QStringLiteral("android.widget.EditText");
508 case QAccessible::Role::RadioButton:
509 return QStringLiteral("android.widget.RadioButton");
510 case QAccessible::Role::ProgressBar:
511 return QStringLiteral("android.widget.ProgressBar");
512 // Range information need to be filled to announce percentages
513 case QAccessible::Role::SpinBox:
514 return QStringLiteral("android.widget.NumberPicker");
515 case QAccessible::Role::WebDocument:
516 return QStringLiteral("android.webkit.WebView");
517 case QAccessible::Role::Dialog:
518 return QStringLiteral("android.app.AlertDialog");
519 case QAccessible::Role::PageTab:
520 return QStringLiteral("android.app.ActionBar.Tab");
521 case QAccessible::Role::PageTabList:
522 return QStringLiteral("android.widget.TabWidget");
523 case QAccessible::Role::ScrollBar:
524 return QStringLiteral("android.widget.Scroller");
525 case QAccessible::Role::Slider:
526 return QStringLiteral("com.google.android.material.slider.Slider");
527 case QAccessible::Role::Table:
528 // #TODO Evaluate the usage of AccessibleNodeInfo.setCollectionItemInfo() to provide
529 // infos about colums, rows und items.
530 return QStringLiteral("android.widget.GridView");
531 case QAccessible::Role::Pane:
532 // #TODO QQuickScrollView, QQuickListView (see QTBUG-137806)
533 return QStringLiteral("android.view.ViewGroup");
534 case QAccessible::Role::AlertMessage:
535 case QAccessible::Role::Animation:
536 case QAccessible::Role::Application:
537 case QAccessible::Role::Assistant:
538 case QAccessible::Role::BlockQuote:
539 case QAccessible::Role::Border:
540 case QAccessible::Role::ButtonDropGrid:
541 case QAccessible::Role::ButtonDropDown:
542 case QAccessible::Role::ButtonMenu:
543 case QAccessible::Role::Canvas:
544 case QAccessible::Role::Caret:
545 case QAccessible::Role::Cell:
546 case QAccessible::Role::Chart:
547 case QAccessible::Role::Client:
548 case QAccessible::Role::ColorChooser:
549 case QAccessible::Role::Column:
550 case QAccessible::Role::ColumnHeader:
551 case QAccessible::Role::ComplementaryContent:
552 case QAccessible::Role::Cursor:
553 case QAccessible::Role::Desktop:
554 case QAccessible::Role::Dial:
555 case QAccessible::Role::Document:
556 case QAccessible::Role::Equation:
557 case QAccessible::Role::Footer:
558 case QAccessible::Role::Form:
559 case QAccessible::Role::Grip:
560 case QAccessible::Role::HelpBalloon:
561 case QAccessible::Role::HotkeyField:
562 case QAccessible::Role::Indicator:
563 case QAccessible::Role::LayeredPane:
564 case QAccessible::Role::ListItem:
565 case QAccessible::Role::MenuBar:
566 case QAccessible::Role::NoRole:
567 case QAccessible::Role::Note:
568 case QAccessible::Role::Notification:
569 case QAccessible::Role::Paragraph:
570 case QAccessible::Role::PropertyPage:
571 case QAccessible::Role::Row:
572 case QAccessible::Role::RowHeader:
573 case QAccessible::Role::Section:
574 case QAccessible::Role::Sound:
575 case QAccessible::Role::Splitter:
576 case QAccessible::Role::StatusBar:
577 case QAccessible::Role::Terminal:
578 case QAccessible::Role::TitleBar:
579 case QAccessible::Role::ToolTip:
580 case QAccessible::Role::Tree:
581 case QAccessible::Role::TreeItem:
582 case QAccessible::Role::UserRole:
583 case QAccessible::Role::Whitespace:
584 case QAccessible::Role::Window:
585 // If unsure, every visible or interactive element in Android
586 // inherits android.view.View and by many extends also TextView.
587 // Android itself does a similar thing e.g. in its Settings-App.
588 return QStringLiteral("android.view.TextView");
589 }
590 }
591
592 static QString descriptionForInterface(QAccessibleInterface *iface)
593 {
594 QString desc;
595 if (iface && iface->isValid()) {
596 bool hasValue = false;
597 desc = iface->text(QAccessible::Name);
598 const QString descStr = iface->text(QAccessible::Description);
599 if (!descStr.isEmpty()) {
600 if (!desc.isEmpty())
601 desc.append(QStringLiteral(", "));
602 desc.append(descStr);
603 }
604 if (desc.isEmpty()) {
605 desc = iface->text(QAccessible::Value);
606 hasValue = !desc.isEmpty();
607 }
608 if (!hasValue && iface->valueInterface()) {
609 const QString valueStr = textFromValue(iface);
610 if (!valueStr.isEmpty()) {
611 if (!desc.isEmpty())
612 desc.append(QChar(QChar::Space));
613 desc.append(valueStr);
614 }
615 }
616 }
617 return desc;
618 }
619
620 static QString descriptionForAccessibleObject_helper(int objectId)
621 {
622 QAccessibleInterface *iface = interfaceFromId(objectId);
623 return descriptionForInterface(iface);
624 }
625
626 static jstring descriptionForAccessibleObject(JNIEnv *env, jobject /*thiz*/, jint objectId)
627 {
628 QString desc;
629 if (m_accessibilityContext) {
630 runInObjectContext(m_accessibilityContext, [objectId]() {
631 return descriptionForAccessibleObject_helper(objectId);
632 }, &desc);
633 }
634 return env->NewString((jchar*) desc.constData(), (jsize) desc.size());
635 }
636
637
655
656 static NodeInfo populateNode_helper(int objectId)
657 {
658 NodeInfo info;
659 QAccessibleInterface *iface = interfaceFromId(objectId);
660 if (iface && iface->isValid()) {
661 info.valid = true;
662 info.state = iface->state();
663 info.role = iface->role();
664 info.actions = QAccessibleBridgeUtils::effectiveActionNames(iface);
665 info.description = descriptionForInterface(iface);
666 info.identifier = QAccessibleBridgeUtils::accessibleId(iface);
667 QAccessibleTextInterface *textIface = iface->textInterface();
668 if (textIface && (textIface->selectionCount() > 0)) {
669 info.hasTextSelection = true;
670 textIface->selection(0, &info.selectionStart, &info.selectionEnd);
671 }
672 QAccessibleValueInterface *valueInterface = iface->valueInterface();
673 if (valueInterface) {
674 info.hasValue = true;
675 info.minValue = valueInterface->minimumValue();
676 info.maxValue = valueInterface->maximumValue();
677 info.currentValue = valueInterface->currentValue();
678 info.valueStepSize = valueInterface->minimumStepSize();
679 }
680 }
681 return info;
682 }
683
684 static jboolean populateNode(JNIEnv *env, jobject /*thiz*/, jint objectId, jobject node)
685 {
686 NodeInfo info;
687 if (m_accessibilityContext) {
688 runInObjectContext(m_accessibilityContext, [objectId]() {
689 return populateNode_helper(objectId);
690 }, &info);
691 }
692 if (!info.valid) {
693 __android_log_print(ANDROID_LOG_WARN, m_qtTag, "Accessibility: populateNode for Invalid ID");
694 return false;
695 }
696
697 const QString role = classNameForRole(info.role, info.state);
698 jstring jrole = env->NewString((jchar*)role.constData(), (jsize)role.size());
699 env->CallVoidMethod(node, m_setClassNameMethodID, jrole);
700
701 const bool hasClickableAction =
702 (info.actions.contains(QAccessibleActionInterface::pressAction())
703 || info.actions.contains(QAccessibleActionInterface::toggleAction()))
704 && !(info.role == QAccessible::StaticText || info.role == QAccessible::Heading);
705 const bool hasIncreaseAction =
706 info.actions.contains(QAccessibleActionInterface::increaseAction());
707 const bool hasDecreaseAction =
708 info.actions.contains(QAccessibleActionInterface::decreaseAction());
709
710 if (info.hasTextSelection && m_setTextSelectionMethodID) {
711 env->CallVoidMethod(node, m_setTextSelectionMethodID, info.selectionStart,
712 info.selectionEnd);
713 }
714
715 if (info.hasValue && m_setRangeInfoMethodID) {
716 int valueType = info.currentValue.typeId();
717 jint rangeType = 3; // RANGE_TYPE_INDETERMINATE
718 switch (valueType) {
719 case QMetaType::Float:
720 case QMetaType::Double:
721 rangeType = 1; // RANGE_TYPE_FLOAT
722 break;
723 case QMetaType::Int:
724 rangeType = 0; // RANGE_TYPE_INT
725 break;
726 }
727
728 QJniObject rangeInfo("android/view/accessibility/AccessibilityNodeInfo$RangeInfo",
729 "(IFFF)V", rangeType, info.minValue.toFloat(),
730 info.maxValue.toFloat(), info.currentValue.toFloat());
731
732 if (rangeInfo.isValid()) {
733 env->CallVoidMethod(node, m_setRangeInfoMethodID, rangeInfo.object());
734 }
735 }
736
737 env->CallVoidMethod(node, m_setCheckableMethodID, (bool)info.state.checkable);
738 env->CallVoidMethod(node, m_setCheckedMethodID, (bool)info.state.checked);
739 env->CallVoidMethod(node, m_setEditableMethodID, info.state.editable);
740 env->CallVoidMethod(node, m_setEnabledMethodID, !info.state.disabled);
741 env->CallVoidMethod(node, m_setFocusableMethodID, (bool)info.state.focusable);
742 env->CallVoidMethod(node, m_setFocusedMethodID, (bool)info.state.focused);
743 if (m_setHeadingMethodID)
744 env->CallVoidMethod(node, m_setHeadingMethodID, info.role == QAccessible::Heading);
745 env->CallVoidMethod(node, m_setVisibleToUserMethodID, !info.state.invisible);
746 env->CallVoidMethod(node, m_setScrollableMethodID, hasIncreaseAction || hasDecreaseAction);
747 env->CallVoidMethod(node, m_setClickableMethodID, hasClickableAction || info.role == QAccessible::Link);
748
749 // Add ACTION_CLICK
750 if (hasClickableAction)
751 env->CallVoidMethod(node, m_addActionMethodID, (int)0x00000010); // ACTION_CLICK defined in AccessibilityNodeInfo
752
753 // Add ACTION_SCROLL_FORWARD
754 if (hasIncreaseAction)
755 env->CallVoidMethod(node, m_addActionMethodID, (int)0x00001000); // ACTION_SCROLL_FORWARD defined in AccessibilityNodeInfo
756
757 // Add ACTION_SCROLL_BACKWARD
758 if (hasDecreaseAction)
759 env->CallVoidMethod(node, m_addActionMethodID, (int)0x00002000); // ACTION_SCROLL_BACKWARD defined in AccessibilityNodeInfo
760
761 // try to fill in the text property, this is what the screen reader reads
762 jstring jdesc = env->NewString((jchar*)info.description.constData(),
763 (jsize)info.description.size());
764 //CALL_METHOD(node, "setText", "(Ljava/lang/CharSequence;)V", jdesc)
765 env->CallVoidMethod(node, m_setContentDescriptionMethodID, jdesc);
766
767 QJniObject(node).callMethod<void>("setViewIdResourceName", info.identifier);
768
769 return true;
770 }
771
772 static const JNINativeMethod methods[] = {
773 {"setActive","(Z)V",(void*)setActive},
774 {"childIdListForAccessibleObject", "(I)[I", (jintArray)childIdListForAccessibleObject},
775 {"parentId", "(I)I", (void*)parentId},
776 {"descriptionForAccessibleObject", "(I)Ljava/lang/String;", (jstring)descriptionForAccessibleObject},
777 {"screenRect", "(I)Landroid/graphics/Rect;", (jobject)screenRect},
778 {"hitTest", "(FF)I", (void*)hitTest},
779 {"populateNode", "(ILandroid/view/accessibility/AccessibilityNodeInfo;)Z", (void*)populateNode},
780 {"clickAction", "(I)Z", (void*)clickAction},
781 {"focusAction", "(I)Z", (void*)focusAction},
782 {"scrollForward", "(I)Z", (void*)scrollForward},
783 {"scrollBackward", "(I)Z", (void*)scrollBackward},
784 };
785
786#define GET_AND_CHECK_STATIC_METHOD(VAR, CLASS, METHOD_NAME, METHOD_SIGNATURE)
787 VAR = env->GetMethodID(CLASS, METHOD_NAME, METHOD_SIGNATURE);
788 if (!VAR) {
789 __android_log_print(ANDROID_LOG_FATAL, QtAndroid::qtTagText(), QtAndroid::methodErrorMsgFmt(), METHOD_NAME, METHOD_SIGNATURE);
790 return false;
791 }
792
793 bool registerNatives(QJniEnvironment &env)
794 {
795 if (!env.registerNativeMethods("org/qtproject/qt/android/QtNativeAccessibility",
796 methods, sizeof(methods) / sizeof(methods[0]))) {
797 __android_log_print(ANDROID_LOG_FATAL,"Qt A11y", "RegisterNatives failed");
798 return false;
799 }
800
801 jclass nodeInfoClass = env->FindClass("android/view/accessibility/AccessibilityNodeInfo");
802 GET_AND_CHECK_STATIC_METHOD(m_setClassNameMethodID, nodeInfoClass, "setClassName", "(Ljava/lang/CharSequence;)V");
803 GET_AND_CHECK_STATIC_METHOD(m_addActionMethodID, nodeInfoClass, "addAction", "(I)V");
804 GET_AND_CHECK_STATIC_METHOD(m_setCheckableMethodID, nodeInfoClass, "setCheckable", "(Z)V");
805 GET_AND_CHECK_STATIC_METHOD(m_setCheckedMethodID, nodeInfoClass, "setChecked", "(Z)V");
806 GET_AND_CHECK_STATIC_METHOD(m_setClickableMethodID, nodeInfoClass, "setClickable", "(Z)V");
807 GET_AND_CHECK_STATIC_METHOD(m_setContentDescriptionMethodID, nodeInfoClass, "setContentDescription", "(Ljava/lang/CharSequence;)V");
808 GET_AND_CHECK_STATIC_METHOD(m_setEditableMethodID, nodeInfoClass, "setEditable", "(Z)V");
809 GET_AND_CHECK_STATIC_METHOD(m_setEnabledMethodID, nodeInfoClass, "setEnabled", "(Z)V");
810 GET_AND_CHECK_STATIC_METHOD(m_setFocusableMethodID, nodeInfoClass, "setFocusable", "(Z)V");
811 GET_AND_CHECK_STATIC_METHOD(m_setFocusedMethodID, nodeInfoClass, "setFocused", "(Z)V");
812 if (QtAndroidPrivate::androidSdkVersion() >= 28) {
813 GET_AND_CHECK_STATIC_METHOD(m_setHeadingMethodID, nodeInfoClass, "setHeading", "(Z)V");
814 }
815 GET_AND_CHECK_STATIC_METHOD(m_setScrollableMethodID, nodeInfoClass, "setScrollable", "(Z)V");
816 GET_AND_CHECK_STATIC_METHOD(m_setVisibleToUserMethodID, nodeInfoClass, "setVisibleToUser", "(Z)V");
817 GET_AND_CHECK_STATIC_METHOD(m_setTextSelectionMethodID, nodeInfoClass, "setTextSelection", "(II)V");
819 m_setRangeInfoMethodID, nodeInfoClass, "setRangeInfo",
820 "(Landroid/view/accessibility/AccessibilityNodeInfo$RangeInfo;)V");
821
822 return true;
823 }
824}
825
826QT_END_NAMESPACE
static const char m_qtTag[]
#define GET_AND_CHECK_STATIC_METHOD(VAR, CLASS, METHOD_NAME, METHOD_SIGNATURE)
\inmodule QtCore\reentrant
Definition qpoint.h:29
void notifyDescriptionOrNameChanged(uint accessibilityObjectId)
void notifyObjectShow(uint accessibilityObjectId)
static bool clickAction_helper(int objectId)
static const JNINativeMethod methods[]
void notifyLocationChange(uint accessibilityObjectId)
void runInObjectContext(QObject *context, Func &&func, Ret *retVal)
static jboolean scrollForward(JNIEnv *, jobject, jint objectId)
void notifyObjectFocus(uint accessibilityObjectId)
static jboolean scrollBackward(JNIEnv *, jobject, jint objectId)
static QString descriptionForInterface(QAccessibleInterface *iface)
static int hitTest_helper(float x, float y)
static jstring descriptionForAccessibleObject(JNIEnv *env, jobject, jint objectId)
static bool scroll_helper(int objectId, const QString &actionName)
static QString classNameForRole(QAccessible::Role role, QAccessible::State state)
static jmethodID m_setContentDescriptionMethodID
static QString textFromValue(QAccessibleInterface *iface)
void createAccessibilityContextObject(QObject *parent)
static QVarLengthArray< int, 8 > childIdListForAccessibleObject_helper(int objectId)
static NodeInfo populateNode_helper(int objectId)
void notifyObjectHide(uint accessibilityObjectId)
static jboolean focusAction(JNIEnv *, jobject, jint objectId)
static jmethodID m_setVisibleToUserMethodID
static jint hitTest(JNIEnv *, jobject, jfloat x, jfloat y)
static bool focusAction_helper(int objectId)
static jboolean clickAction(JNIEnv *, jobject, jint objectId)
static jstring jvalueForAccessibleObject(int objectId)
static void setActive(JNIEnv *, jobject, jboolean active)
static void invokeActionOnInterfaceInMainThread(QAccessibleActionInterface *actionInterface, const QString &action)
static jmethodID m_setTextSelectionMethodID
bool registerNatives(QJniEnvironment &env)
QAccessibleInterface * interfaceFromId(jint objectId)
void notifyValueChanged(uint accessibilityObjectId)
void notifyAnnouncementEvent(uint accessibilityObjectId, const QString &message)
static int parentId_helper(int objectId)
static QRect screenRect_helper(int objectId, bool clip=true)
static QString descriptionForAccessibleObject_helper(int objectId)
static jint parentId(JNIEnv *, jobject, jint objectId)
static jobject screenRect(JNIEnv *env, jobject, jint objectId)
static jintArray childIdListForAccessibleObject(JNIEnv *env, jobject, jint objectId)
static jboolean populateNode(JNIEnv *env, jobject, jint objectId, jobject node)
void notifyScrolledEvent(uint accessiblityObjectId)
QBasicMutex * platformInterfaceMutex()
QAndroidPlatformIntegration * androidPlatformIntegration()
#define qApp