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