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 QString textFromValue(QAccessibleInterface *iface)
417 {
418 QString valueStr;
419 QAccessibleValueInterface *valueIface = iface->valueInterface();
420 if (valueIface) {
421 const QVariant valueVar = valueIface->currentValue();
422 const auto type = valueVar.typeId();
423 if (type == QMetaType::Double || type == QMetaType::Float) {
424 // QVariant's toString() formats floating-point values with
425 // FloatingPointShortest, which is not an accessible
426 // representation; nor, in many cases, is it suitable to the UI
427 // element whose value we're looking at. So roll our own
428 // A11Y-friendly conversion to string.
429 const double val = valueVar.toDouble();
430 // Try to use minimumStepSize() to determine precision
431 bool stepIsValid = false;
432 const double step = qAbs(valueIface->minimumStepSize().toDouble(&stepIsValid));
433 if (!stepIsValid || qFuzzyIsNull(step)) {
434 // Ignore step, use default precision
435 valueStr = qFuzzyIsNull(val) ? u"0"_s : QString::number(val, 'f');
436 } else {
437 const int precision = [](double s) {
438 int count = 0;
439 while (s < 1. && !qFuzzyCompare(s, 1.)) {
440 ++count;
441 s *= 10;
442 }
443 // If s is now 1.25, we want to show some more digits,
444 // but don't want to get silly with a step like 1./7;
445 // so only include a few extra digits.
446 const int stop = count + 3;
447 const auto fractional = [](double v) {
448 double whole = 0.0;
449 std::modf(v + 0.5, &whole);
450 return qAbs(v - whole);
451 };
452 s = fractional(s);
453 while (count < stop && !qFuzzyIsNull(s)) {
454 ++count;
455 s = fractional(s * 10);
456 }
457 return count;
458 }(step);
459 valueStr = qFuzzyIsNull(val / step) ? u"0"_s
460 : QString::number(val, 'f', precision);
461 }
462 } else {
463 valueStr = valueVar.toString();
464 }
465 }
466 return valueStr;
467 }
468
470 {
471 QAccessibleInterface *iface = interfaceFromId(objectId);
472 const QString value = textFromValue(iface);
473 QJniEnvironment env;
474 jstring jstr = env->NewString((jchar*)value.constData(), (jsize)value.size());
475 if (env.checkAndClearExceptions())
476 __android_log_print(ANDROID_LOG_WARN, m_qtTag, "Failed to create jstring");
477 return jstr;
478 }
479
480 static QString classNameForRole(QAccessible::Role role, QAccessible::State state) {
481 switch (role) {
482 case QAccessible::Role::Button:
483 case QAccessible::Role::Link:
484 {
485 if (state.checkable)
486 return QStringLiteral("android.widget.ToggleButton");
487 return QStringLiteral("android.widget.Button");
488 }
489 case QAccessible::Role::CheckBox:
490 // As of android/accessibility/utils/Role.java::getRole a CheckBox
491 // is NOT android.widget.CheckBox
492 return QStringLiteral("android.widget.CompoundButton");
493 case QAccessible::Role::Switch:
494 return QStringLiteral("android.widget.Switch");
495 case QAccessible::Role::Clock:
496 return QStringLiteral("android.widget.TextClock");
497 case QAccessible::Role::ComboBox:
498 return QStringLiteral("android.widget.Spinner");
499 case QAccessible::Role::Graphic:
500 // QQuickImage does not provide this role it inherits Client from QQuickItem
501 return QStringLiteral("android.widget.ImageView");
502 case QAccessible::Role::Grouping:
503 return QStringLiteral("android.view.ViewGroup");
504 case QAccessible::Role::List:
505 // As of android/accessibility/utils/Role.java::getRole a List
506 // is NOT android.widget.ListView
507 return QStringLiteral("android.widget.AbsListView");
508 case QAccessible::Role::MenuItem:
509 return QStringLiteral("android.view.MenuItem");
510 case QAccessible::Role::PopupMenu:
511 return QStringLiteral("android.widget.PopupMenu");
512 case QAccessible::Role::Separator:
513 return QStringLiteral("android.widget.Space");
514 case QAccessible::Role::ToolBar:
515 return QStringLiteral("android.view.Toolbar");
516 case QAccessible::Role::Heading: [[fallthrough]];
517 case QAccessible::Role::StaticText:
518 // Heading vs. regular Text is finally determined by AccessibilityNodeInfo.isHeading()
519 return QStringLiteral("android.widget.TextView");
520 case QAccessible::Role::EditableText:
521 return QStringLiteral("android.widget.EditText");
522 case QAccessible::Role::RadioButton:
523 return QStringLiteral("android.widget.RadioButton");
524 case QAccessible::Role::ProgressBar:
525 return QStringLiteral("android.widget.ProgressBar");
526 case QAccessible::Role::SpinBox:
527 return QStringLiteral("android.widget.NumberPicker");
528 case QAccessible::Role::WebDocument:
529 return QStringLiteral("android.webkit.WebView");
530 case QAccessible::Role::Dialog:
531 return QStringLiteral("android.app.AlertDialog");
532 case QAccessible::Role::PageTab:
533 return QStringLiteral("android.app.ActionBar.Tab");
534 case QAccessible::Role::PageTabList:
535 return QStringLiteral("android.widget.TabWidget");
536 case QAccessible::Role::ScrollBar:
537 return QStringLiteral("android.widget.Scroller");
538 case QAccessible::Role::Slider:
539 return QStringLiteral("com.google.android.material.slider.Slider");
540 case QAccessible::Role::Table:
541 // #TODO Evaluate the usage of AccessibleNodeInfo.setCollectionItemInfo() to provide
542 // infos about colums, rows und items.
543 return QStringLiteral("android.widget.GridView");
544 case QAccessible::Role::Pane:
545 // #TODO QQuickScrollView, QQuickListView (see QTBUG-137806)
546 return QStringLiteral("android.view.ViewGroup");
547 case QAccessible::Role::AlertMessage:
548 case QAccessible::Role::Animation:
549 case QAccessible::Role::Application:
550 case QAccessible::Role::Assistant:
551 case QAccessible::Role::BlockQuote:
552 case QAccessible::Role::Border:
553 case QAccessible::Role::ButtonDropGrid:
554 case QAccessible::Role::ButtonDropDown:
555 case QAccessible::Role::ButtonMenu:
556 case QAccessible::Role::Canvas:
557 case QAccessible::Role::Caret:
558 case QAccessible::Role::Cell:
559 case QAccessible::Role::Chart:
560 case QAccessible::Role::Client:
561 case QAccessible::Role::ColorChooser:
562 case QAccessible::Role::Column:
563 case QAccessible::Role::ColumnHeader:
564 case QAccessible::Role::ComplementaryContent:
565 case QAccessible::Role::Cursor:
566 case QAccessible::Role::Desktop:
567 case QAccessible::Role::Dial:
568 case QAccessible::Role::Document:
569 case QAccessible::Role::Equation:
570 case QAccessible::Role::Footer:
571 case QAccessible::Role::Form:
572 case QAccessible::Role::Grip:
573 case QAccessible::Role::HelpBalloon:
574 case QAccessible::Role::HotkeyField:
575 case QAccessible::Role::Indicator:
576 case QAccessible::Role::LayeredPane:
577 case QAccessible::Role::ListItem:
578 case QAccessible::Role::MenuBar:
579 case QAccessible::Role::NoRole:
580 case QAccessible::Role::Note:
581 case QAccessible::Role::Notification:
582 case QAccessible::Role::Paragraph:
583 case QAccessible::Role::PropertyPage:
584 case QAccessible::Role::Row:
585 case QAccessible::Role::RowHeader:
586 case QAccessible::Role::Section:
587 case QAccessible::Role::Sound:
588 case QAccessible::Role::Splitter:
589 case QAccessible::Role::StatusBar:
590 case QAccessible::Role::Terminal:
591 case QAccessible::Role::TitleBar:
592 case QAccessible::Role::ToolTip:
593 case QAccessible::Role::Tree:
594 case QAccessible::Role::TreeItem:
595 case QAccessible::Role::UserRole:
596 case QAccessible::Role::Whitespace:
597 case QAccessible::Role::Window:
598 // If unsure, every visible or interactive element in Android
599 // inherits android.view.View and by many extends also TextView.
600 // Android itself does a similar thing e.g. in its Settings-App.
601 return QStringLiteral("android.view.TextView");
602 }
603 }
604
605 static QString descriptionForInterface(QAccessibleInterface *iface)
606 {
607 QString desc;
608 if (iface && iface->isValid()) {
609 bool hasValue = false;
610 desc = iface->text(QAccessible::Name);
611 const QString descStr = iface->text(QAccessible::Description);
612 if (!descStr.isEmpty()) {
613 if (!desc.isEmpty())
614 desc.append(QStringLiteral(", "));
615 desc.append(descStr);
616 }
617 if (desc.isEmpty()) {
618 desc = iface->text(QAccessible::Value);
619 hasValue = !desc.isEmpty();
620 }
621 if (!hasValue && iface->valueInterface()) {
622 const QString valueStr = textFromValue(iface);
623 if (!valueStr.isEmpty()) {
624 if (!desc.isEmpty())
625 desc.append(QChar(QChar::Space));
626 desc.append(valueStr);
627 }
628 }
629 }
630 return desc;
631 }
632
633 static QString descriptionForAccessibleObject_helper(int objectId)
634 {
635 QAccessibleInterface *iface = interfaceFromId(objectId);
636 return descriptionForInterface(iface);
637 }
638
639 static jstring descriptionForAccessibleObject(JNIEnv *env, jobject /*thiz*/, jint objectId)
640 {
641 QString desc;
642 if (m_accessibilityContext) {
643 runInObjectContext(m_accessibilityContext, [objectId]() {
644 return descriptionForAccessibleObject_helper(objectId);
645 }, &desc);
646 }
647 return env->NewString((jchar*) desc.constData(), (jsize) desc.size());
648 }
649
650
651 struct NodeInfo
652 {
653 bool valid = false;
657 QString description;
658 QString identifier;
659 bool hasTextSelection = false;
662 bool hasValue = false;
663 QVariant minValue = 0;
664 QVariant maxValue = 0;
665 QVariant currentValue = 0;
666 QVariant valueStepSize = 0;
667 };
668
669 static NodeInfo populateNode_helper(int objectId)
670 {
671 NodeInfo info;
672 QAccessibleInterface *iface = interfaceFromId(objectId);
673 if (iface && iface->isValid()) {
674 info.valid = true;
675 info.state = iface->state();
676 info.role = iface->role();
677 info.actions = QAccessibleBridgeUtils::effectiveActionNames(iface);
678 info.description = descriptionForInterface(iface);
679 info.identifier = QAccessibleBridgeUtils::accessibleId(iface);
680 QAccessibleTextInterface *textIface = iface->textInterface();
681 if (textIface && (textIface->selectionCount() > 0)) {
682 info.hasTextSelection = true;
683 textIface->selection(0, &info.selectionStart, &info.selectionEnd);
684 }
685 QAccessibleValueInterface *valueInterface = iface->valueInterface();
686 if (valueInterface) {
687 info.hasValue = true;
688 info.minValue = valueInterface->minimumValue();
689 info.maxValue = valueInterface->maximumValue();
690 info.currentValue = valueInterface->currentValue();
691 info.valueStepSize = valueInterface->minimumStepSize();
692 }
693 }
694 return info;
695 }
696
697 static jboolean populateNode(JNIEnv *env, jobject /*thiz*/, jint objectId, jobject node)
698 {
699 NodeInfo info;
700 if (m_accessibilityContext) {
701 runInObjectContext(m_accessibilityContext, [objectId]() {
702 return populateNode_helper(objectId);
703 }, &info);
704 }
705 if (!info.valid) {
706 __android_log_print(ANDROID_LOG_WARN, m_qtTag, "Accessibility: populateNode for Invalid ID");
707 return false;
708 }
709
710 const QString role = classNameForRole(info.role, info.state);
711 jstring jrole = env->NewString((jchar*)role.constData(), (jsize)role.size());
712 env->CallVoidMethod(node, m_setClassNameMethodID, jrole);
713
714 const bool hasClickableAction =
715 (info.actions.contains(QAccessibleActionInterface::pressAction())
716 || info.actions.contains(QAccessibleActionInterface::toggleAction()))
717 && !(info.role == QAccessible::StaticText || info.role == QAccessible::Heading);
718 const bool hasIncreaseAction =
719 info.actions.contains(QAccessibleActionInterface::increaseAction());
720 const bool hasDecreaseAction =
721 info.actions.contains(QAccessibleActionInterface::decreaseAction());
722
723 if (info.hasTextSelection && m_setTextSelectionMethodID) {
724 env->CallVoidMethod(node, m_setTextSelectionMethodID, info.selectionStart,
725 info.selectionEnd);
726 }
727
728 if (info.hasValue && m_setRangeInfoMethodID) {
729 int valueType = info.currentValue.typeId();
730 jint rangeType = RANGE_TYPE_INDETERMINATE;
731 switch (valueType) {
732 case QMetaType::Float:
733 case QMetaType::Double:
734 rangeType = RANGE_TYPE_FLOAT;
735 break;
736 case QMetaType::Int:
737 rangeType = RANGE_TYPE_INT;
738 break;
739 }
740
741 float min = info.minValue.toFloat();
742 float max = info.maxValue.toFloat();
743 float current = info.currentValue.toFloat();
744 if (info.role == QAccessible::ProgressBar) {
745 rangeType = RANGE_TYPE_PERCENT;
746 current = 100 * (current - min) / (max - min);
747 min = 0.0f;
748 max = 100.0f;
749 }
750
751 QJniObject rangeInfo("android/view/accessibility/AccessibilityNodeInfo$RangeInfo",
752 "(IFFF)V", rangeType, min, max, current);
753
754 if (rangeInfo.isValid()) {
755 env->CallVoidMethod(node, m_setRangeInfoMethodID, rangeInfo.object());
756 }
757 }
758
759 env->CallVoidMethod(node, m_setCheckableMethodID, (bool)info.state.checkable);
760 env->CallVoidMethod(node, m_setCheckedMethodID, (bool)info.state.checked);
761 env->CallVoidMethod(node, m_setEditableMethodID, info.state.editable);
762 env->CallVoidMethod(node, m_setEnabledMethodID, !info.state.disabled);
763 env->CallVoidMethod(node, m_setFocusableMethodID, (bool)info.state.focusable);
764 env->CallVoidMethod(node, m_setFocusedMethodID, (bool)info.state.focused);
766 env->CallVoidMethod(node, m_setHeadingMethodID, info.role == QAccessible::Heading);
767 env->CallVoidMethod(node, m_setVisibleToUserMethodID, !info.state.invisible);
768 env->CallVoidMethod(node, m_setScrollableMethodID, hasIncreaseAction || hasDecreaseAction);
769 env->CallVoidMethod(node, m_setClickableMethodID, hasClickableAction || info.role == QAccessible::Link);
770
771 // Add ACTION_CLICK
772 if (hasClickableAction)
773 env->CallVoidMethod(node, m_addActionMethodID, (int)0x00000010); // ACTION_CLICK defined in AccessibilityNodeInfo
774
775 // Add ACTION_SCROLL_FORWARD
776 if (hasIncreaseAction)
777 env->CallVoidMethod(node, m_addActionMethodID, (int)0x00001000); // ACTION_SCROLL_FORWARD defined in AccessibilityNodeInfo
778
779 // Add ACTION_SCROLL_BACKWARD
780 if (hasDecreaseAction)
781 env->CallVoidMethod(node, m_addActionMethodID, (int)0x00002000); // ACTION_SCROLL_BACKWARD defined in AccessibilityNodeInfo
782
783 // try to fill in the text property, this is what the screen reader reads
784 jstring jdesc = env->NewString((jchar*)info.description.constData(),
785 (jsize)info.description.size());
786 //CALL_METHOD(node, "setText", "(Ljava/lang/CharSequence;)V", jdesc)
787 env->CallVoidMethod(node, m_setContentDescriptionMethodID, jdesc);
788
789 QJniObject(node).callMethod<void>("setViewIdResourceName", info.identifier);
790
791 return true;
792 }
793
794 static const JNINativeMethod methods[] = {
795 {"setActive","(Z)V",(void*)setActive},
796 {"childIdListForAccessibleObject", "(I)[I", (jintArray)childIdListForAccessibleObject},
797 {"parentId", "(I)I", (void*)parentId},
798 {"descriptionForAccessibleObject", "(I)Ljava/lang/String;", (jstring)descriptionForAccessibleObject},
799 {"screenRect", "(I)Landroid/graphics/Rect;", (jobject)screenRect},
800 {"hitTest", "(FF)I", (void*)hitTest},
801 {"populateNode", "(ILandroid/view/accessibility/AccessibilityNodeInfo;)Z", (void*)populateNode},
802 {"clickAction", "(I)Z", (void*)clickAction},
803 {"focusAction", "(I)Z", (void*)focusAction},
804 {"scrollForward", "(I)Z", (void*)scrollForward},
805 {"scrollBackward", "(I)Z", (void*)scrollBackward},
806 };
807
808#define GET_AND_CHECK_STATIC_METHOD(VAR, CLASS, METHOD_NAME, METHOD_SIGNATURE)
809 VAR = env->GetMethodID(CLASS, METHOD_NAME, METHOD_SIGNATURE);
810 if (!VAR) {
811 __android_log_print(ANDROID_LOG_FATAL, QtAndroid::qtTagText(), QtAndroid::methodErrorMsgFmt(), METHOD_NAME, METHOD_SIGNATURE);
812 return false;
813 }
814
815#define CHECK_AND_INIT_STATIC_FIELD(TYPE, VAR, CLASS, FIELD_NAME)
816 if (env.findStaticField<TYPE>(CLASS, FIELD_NAME) == nullptr) {
817 __android_log_print(ANDROID_LOG_FATAL, QtAndroid::qtTagText(),
818 QtAndroid::staticFieldErrorMsgFmt(), FIELD_NAME);
819 return false;
820 }
821 VAR = QJniObject::getStaticField<TYPE>(CLASS, FIELD_NAME);
822
823 bool registerNatives(QJniEnvironment &env)
824 {
825 if (!env.registerNativeMethods("org/qtproject/qt/android/QtNativeAccessibility",
826 methods, sizeof(methods) / sizeof(methods[0]))) {
827 __android_log_print(ANDROID_LOG_FATAL,"Qt A11y", "RegisterNatives failed");
828 return false;
829 }
830
831 jclass nodeInfoClass = env->FindClass("android/view/accessibility/AccessibilityNodeInfo");
832 GET_AND_CHECK_STATIC_METHOD(m_setClassNameMethodID, nodeInfoClass, "setClassName", "(Ljava/lang/CharSequence;)V");
833 GET_AND_CHECK_STATIC_METHOD(m_addActionMethodID, nodeInfoClass, "addAction", "(I)V");
834 GET_AND_CHECK_STATIC_METHOD(m_setCheckableMethodID, nodeInfoClass, "setCheckable", "(Z)V");
835 GET_AND_CHECK_STATIC_METHOD(m_setCheckedMethodID, nodeInfoClass, "setChecked", "(Z)V");
836 GET_AND_CHECK_STATIC_METHOD(m_setClickableMethodID, nodeInfoClass, "setClickable", "(Z)V");
837 GET_AND_CHECK_STATIC_METHOD(m_setContentDescriptionMethodID, nodeInfoClass, "setContentDescription", "(Ljava/lang/CharSequence;)V");
838 GET_AND_CHECK_STATIC_METHOD(m_setEditableMethodID, nodeInfoClass, "setEditable", "(Z)V");
839 GET_AND_CHECK_STATIC_METHOD(m_setEnabledMethodID, nodeInfoClass, "setEnabled", "(Z)V");
840 GET_AND_CHECK_STATIC_METHOD(m_setFocusableMethodID, nodeInfoClass, "setFocusable", "(Z)V");
841 GET_AND_CHECK_STATIC_METHOD(m_setFocusedMethodID, nodeInfoClass, "setFocused", "(Z)V");
842 if (QtAndroidPrivate::androidSdkVersion() >= 28) {
843 GET_AND_CHECK_STATIC_METHOD(m_setHeadingMethodID, nodeInfoClass, "setHeading", "(Z)V");
844 }
845 GET_AND_CHECK_STATIC_METHOD(m_setScrollableMethodID, nodeInfoClass, "setScrollable", "(Z)V");
846 GET_AND_CHECK_STATIC_METHOD(m_setVisibleToUserMethodID, nodeInfoClass, "setVisibleToUser", "(Z)V");
847 GET_AND_CHECK_STATIC_METHOD(m_setTextSelectionMethodID, nodeInfoClass, "setTextSelection", "(II)V");
849 m_setRangeInfoMethodID, nodeInfoClass, "setRangeInfo",
850 "(Landroid/view/accessibility/AccessibilityNodeInfo$RangeInfo;)V");
851
852 jclass rangeInfoClass =
853 env->FindClass("android/view/accessibility/AccessibilityNodeInfo$RangeInfo");
854 CHECK_AND_INIT_STATIC_FIELD(int, RANGE_TYPE_INT, rangeInfoClass, "RANGE_TYPE_INT");
855 CHECK_AND_INIT_STATIC_FIELD(int, RANGE_TYPE_FLOAT, rangeInfoClass, "RANGE_TYPE_FLOAT");
856 CHECK_AND_INIT_STATIC_FIELD(int, RANGE_TYPE_PERCENT, rangeInfoClass, "RANGE_TYPE_PERCENT");
857 if (QtAndroidPrivate::androidSdkVersion() >= 36) {
858 CHECK_AND_INIT_STATIC_FIELD(int, RANGE_TYPE_INDETERMINATE, rangeInfoClass,
859 "RANGE_TYPE_INDETERMINATE");
860 } else {
862 }
863
864 return true;
865 }
866}
867
868QT_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
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