diff options
author | Ivan Solovev <ivan.solovev@qt.io> | 2022-02-01 17:17:12 +0100 |
---|---|---|
committer | Ivan Solovev <ivan.solovev@qt.io> | 2022-02-09 16:48:26 +0100 |
commit | e0c61193ea8f6462192d2ef7f1d48d8fa3e38c99 (patch) | |
tree | 3b71787443839c847ddbcab966f57f2d04d23124 /src/plugins/platforms/android | |
parent | 6d00aac1092d813446a44fbb234995733233f003 (diff) |
Android A11Y: execute C++ code on main Qt thread
The C++ code, which is called from Java, was executed on Java thread.
However Qt has its own main GUI thread, where all GUI elements and their
accessibility instances are created.
As a result we have threading issues when accessing A11Y objects from
Java thread.
This patch uses QMetaObject::invokeMethod calls to dispatch all the
critical parts of the C++ code to the main thread.
It uses BlockingQueuedConnection, so that Java thread can still use
these methods synchronously.
The proper context is based on the m_accessibilityContext object, which
is created as a child of the base accessibility QObject of the
application (which is the QGuiApplication instance in most cases).
Task-number: QTBUG-95764
Pick-to: 6.3 6.2 5.15
Change-Id: Iff4f3f2645657f6aca426fa19ccc86a2cbe4d4d0
Reviewed-by: Assam Boudjelthia <assam.boudjelthia@qt.io>
Reviewed-by: Jarkko Koivikko <jarkko.koivikko@code-q.fi>
Diffstat (limited to 'src/plugins/platforms/android')
4 files changed, 191 insertions, 47 deletions
diff --git a/src/plugins/platforms/android/androidjniaccessibility.cpp b/src/plugins/platforms/android/androidjniaccessibility.cpp index 10a71bca4a..bf492a8bd2 100644 --- a/src/plugins/platforms/android/androidjniaccessibility.cpp +++ b/src/plugins/platforms/android/androidjniaccessibility.cpp @@ -50,6 +50,7 @@ #include <QtCore/private/qjnihelpers_p.h> #include <QtCore/QJniObject> #include <QtGui/private/qhighdpiscaling_p.h> +#include <QtCore/QObject> static const char m_qtTag[] = "Qt A11Y"; static const char m_classErrorMsg[] = "Can't find class \"%s\""; @@ -73,6 +74,28 @@ namespace QtAndroidAccessibility static bool m_accessibilityActivated = false; + // This object is needed to schedule the execution of the code that + // deals with accessibility instances to the Qt main thread. + // Because of that almost every method here is split into two parts. + // The _helper part is executed in the context of m_accessibilityContext + // on the main thread. The other part is executed in Java thread. + static QObject *m_accessibilityContext = nullptr; + + // This method is called from the Qt main thread, and normally a + // QGuiApplication instance will be used as a parent. + void createAccessibilityContextObject(QObject *parent) + { + if (m_accessibilityContext) + m_accessibilityContext->deleteLater(); + m_accessibilityContext = new QObject(parent); + } + + template <typename Func, typename Ret> + void runInObjectContext(QObject *context, Func &&func, Ret *retVal) + { + QMetaObject::invokeMethod(context, func, Qt::BlockingQueuedConnection, retVal); + } + void initialize() { QJniObject::callStaticMethod<void>(QtAndroid::applicationClass(), @@ -113,12 +136,11 @@ namespace QtAndroidAccessibility QtAndroid::notifyAccessibilityLocationChange(); } - static jint parentId(JNIEnv *, jobject, jint objectId); // forward declaration + static int parentId_helper(int objectId); // forward declaration void notifyObjectHide(uint accessibilityObjectId) { - jobject unused {}; - const auto parentObjectId = parentId(nullptr, unused, accessibilityObjectId); + const auto parentObjectId = parentId_helper(accessibilityObjectId); QtAndroid::notifyObjectHide(accessibilityObjectId, parentObjectId); } @@ -127,7 +149,7 @@ namespace QtAndroidAccessibility QtAndroid::notifyObjectFocus(accessibilityObjectId); } - static jintArray childIdListForAccessibleObject(JNIEnv *env, jobject /*thiz*/, jint objectId) + static QVarLengthArray<int, 8> childIdListForAccessibleObject_helper(int objectId) { QAccessibleInterface *iface = interfaceFromId(objectId); if (iface && iface->isValid()) { @@ -139,6 +161,18 @@ namespace QtAndroidAccessibility if (child && child->isValid()) ifaceIdArray.append(QAccessible::uniqueId(child)); } + return ifaceIdArray; + } + return {}; + } + + static jintArray childIdListForAccessibleObject(JNIEnv *env, jobject /*thiz*/, jint objectId) + { + if (m_accessibilityContext) { + QVarLengthArray<jint, 8> ifaceIdArray; + runInObjectContext(m_accessibilityContext, [objectId]() { + return childIdListForAccessibleObject_helper(objectId); + }, &ifaceIdArray); jintArray jArray = env->NewIntArray(jsize(ifaceIdArray.count())); env->SetIntArrayRegion(jArray, 0, ifaceIdArray.count(), ifaceIdArray.data()); return jArray; @@ -147,7 +181,7 @@ namespace QtAndroidAccessibility return env->NewIntArray(jsize(0)); } - static jint parentId(JNIEnv */*env*/, jobject /*thiz*/, jint objectId) + static int parentId_helper(int objectId) { QAccessibleInterface *iface = interfaceFromId(objectId); if (iface && iface->isValid()) { @@ -161,7 +195,18 @@ namespace QtAndroidAccessibility return -1; } - static jobject screenRect(JNIEnv *env, jobject /*thiz*/, jint objectId) + static jint parentId(JNIEnv */*env*/, jobject /*thiz*/, jint objectId) + { + jint result = -1; + if (m_accessibilityContext) { + runInObjectContext(m_accessibilityContext, [objectId]() { + return parentId_helper(objectId); + }, &result); + } + return result; + } + + static QRect screenRect_helper(int objectId) { QRect rect; QAccessibleInterface *iface = interfaceFromId(objectId); @@ -173,14 +218,24 @@ namespace QtAndroidAccessibility const auto parentRect = QHighDpi::toNativePixels(iface->parent()->rect(), iface->parent()->window()); rect = rect.intersected(parentRect); } + return rect; + } + static jobject screenRect(JNIEnv *env, jobject /*thiz*/, jint objectId) + { + QRect rect; + if (m_accessibilityContext) { + runInObjectContext(m_accessibilityContext, [objectId]() { + return screenRect_helper(objectId); + }, &rect); + } jclass rectClass = env->FindClass("android/graphics/Rect"); jmethodID ctor = env->GetMethodID(rectClass, "<init>", "(IIII)V"); jobject jrect = env->NewObject(rectClass, ctor, rect.left(), rect.top(), rect.right(), rect.bottom()); return jrect; } - static jint hitTest(JNIEnv */*env*/, jobject /*thiz*/, jfloat x, jfloat y) + static int hitTest_helper(float x, float y) { QAccessibleInterface *root = interfaceFromId(-1); if (root && root->isValid()) { @@ -198,17 +253,29 @@ namespace QtAndroidAccessibility return -1; } + static jint hitTest(JNIEnv */*env*/, jobject /*thiz*/, jfloat x, jfloat y) + { + jint result = -1; + if (m_accessibilityContext) { + runInObjectContext(m_accessibilityContext, [x, y]() { + return hitTest_helper(x, y); + }, &result); + } + return result; + } + static void invokeActionOnInterfaceInMainThread(QAccessibleActionInterface* actionInterface, const QString& action) { + // Queue the action and return back to Java thread, so that we do not + // block it for too long QMetaObject::invokeMethod(qApp, [actionInterface, action]() { actionInterface->doAction(action); - }); + }, Qt::QueuedConnection); } - static jboolean clickAction(JNIEnv */*env*/, jobject /*thiz*/, jint objectId) + static bool clickAction_helper(int objectId) { -// qDebug() << "A11Y: CLICK: " << objectId; QAccessibleInterface *iface = interfaceFromId(objectId); if (!iface || !iface->isValid() || !iface->actionInterface()) return false; @@ -227,20 +294,45 @@ namespace QtAndroidAccessibility return true; } - static jboolean scrollForward(JNIEnv */*env*/, jobject /*thiz*/, jint objectId) + static jboolean clickAction(JNIEnv */*env*/, jobject /*thiz*/, jint objectId) + { + bool result = false; + if (m_accessibilityContext) { + runInObjectContext(m_accessibilityContext, [objectId]() { + return clickAction_helper(objectId); + }, &result); + } + return result; + } + + static bool scroll_helper(int objectId, const QString &actionName) { QAccessibleInterface *iface = interfaceFromId(objectId); if (iface && iface->isValid()) - return QAccessibleBridgeUtils::performEffectiveAction(iface, QAccessibleActionInterface::increaseAction()); + return QAccessibleBridgeUtils::performEffectiveAction(iface, actionName); return false; } + static jboolean scrollForward(JNIEnv */*env*/, jobject /*thiz*/, jint objectId) + { + bool result = false; + if (m_accessibilityContext) { + runInObjectContext(m_accessibilityContext, [objectId]() { + return scroll_helper(objectId, QAccessibleActionInterface::increaseAction()); + }, &result); + } + return result; + } + static jboolean scrollBackward(JNIEnv */*env*/, jobject /*thiz*/, jint objectId) { - QAccessibleInterface *iface = interfaceFromId(objectId); - if (iface && iface->isValid()) - return QAccessibleBridgeUtils::performEffectiveAction(iface, QAccessibleActionInterface::decreaseAction()); - return false; + bool result = false; + if (m_accessibilityContext) { + runInObjectContext(m_accessibilityContext, [objectId]() { + return scroll_helper(objectId, QAccessibleActionInterface::decreaseAction()); + }, &result); + } + return result; } @@ -254,8 +346,7 @@ if (!clazz) { \ //__android_log_print(ANDROID_LOG_FATAL, m_qtTag, m_methodErrorMsg, METHOD_NAME, METHOD_SIGNATURE); - - static jstring descriptionForAccessibleObject_helper(JNIEnv *env, QAccessibleInterface *iface) + static QString descriptionForInterface(QAccessibleInterface *iface) { QString desc; if (iface && iface->isValid()) { @@ -271,48 +362,89 @@ if (!clazz) { \ } } } - return env->NewString((jchar*) desc.constData(), (jsize) desc.size()); + return desc; } - static jstring descriptionForAccessibleObject(JNIEnv *env, jobject /*thiz*/, jint objectId) + static QString descriptionForAccessibleObject_helper(int objectId) { QAccessibleInterface *iface = interfaceFromId(objectId); - return descriptionForAccessibleObject_helper(env, iface); + return descriptionForInterface(iface); + } + + static jstring descriptionForAccessibleObject(JNIEnv *env, jobject /*thiz*/, jint objectId) + { + QString desc; + if (m_accessibilityContext) { + runInObjectContext(m_accessibilityContext, [objectId]() { + return descriptionForAccessibleObject_helper(objectId); + }, &desc); + } + return env->NewString((jchar*) desc.constData(), (jsize) desc.size()); } - static bool populateNode(JNIEnv *env, jobject /*thiz*/, jint objectId, jobject node) + + struct NodeInfo { + bool valid = false; + QAccessible::State state; + QStringList actions; + QString description; + bool hasTextSelection = false; + int selectionStart = 0; + int selectionEnd = 0; + }; + + static NodeInfo populateNode_helper(int objectId) + { + NodeInfo info; QAccessibleInterface *iface = interfaceFromId(objectId); - if (!iface || !iface->isValid()) { + if (iface && iface->isValid()) { + info.valid = true; + info.state = iface->state(); + info.actions = QAccessibleBridgeUtils::effectiveActionNames(iface); + info.description = descriptionForInterface(iface); + QAccessibleTextInterface *textIface = iface->textInterface(); + if (textIface && (textIface->selectionCount() > 0)) { + info.hasTextSelection = true; + textIface->selection(0, &info.selectionStart, &info.selectionEnd); + } + } + return info; + } + + static jboolean populateNode(JNIEnv *env, jobject /*thiz*/, jint objectId, jobject node) + { + NodeInfo info; + if (m_accessibilityContext) { + runInObjectContext(m_accessibilityContext, [objectId]() { + return populateNode_helper(objectId); + }, &info); + } + if (!info.valid) { __android_log_print(ANDROID_LOG_WARN, m_qtTag, "Accessibility: populateNode for Invalid ID"); return false; } - QAccessible::State state = iface->state(); - const QStringList actions = QAccessibleBridgeUtils::effectiveActionNames(iface); - const bool hasClickableAction = actions.contains(QAccessibleActionInterface::pressAction()) - || actions.contains(QAccessibleActionInterface::toggleAction()); - const bool hasIncreaseAction = actions.contains(QAccessibleActionInterface::increaseAction()); - const bool hasDecreaseAction = actions.contains(QAccessibleActionInterface::decreaseAction()); - // try to fill in the text property, this is what the screen reader reads - jstring jdesc = descriptionForAccessibleObject_helper(env, iface); - - if (QAccessibleTextInterface *textIface = iface->textInterface()) { - if (m_setTextSelectionMethodID && textIface->selectionCount() > 0) { - int startSelection; - int endSelection; - textIface->selection(0, &startSelection, &endSelection); - env->CallVoidMethod(node, m_setTextSelectionMethodID, startSelection, endSelection); - } + const bool hasClickableAction = + info.actions.contains(QAccessibleActionInterface::pressAction()) || + info.actions.contains(QAccessibleActionInterface::toggleAction()); + const bool hasIncreaseAction = + info.actions.contains(QAccessibleActionInterface::increaseAction()); + const bool hasDecreaseAction = + info.actions.contains(QAccessibleActionInterface::decreaseAction()); + + if (info.hasTextSelection && m_setTextSelectionMethodID) { + env->CallVoidMethod(node, m_setTextSelectionMethodID, info.selectionStart, + info.selectionEnd); } - env->CallVoidMethod(node, m_setCheckableMethodID, (bool)state.checkable); - env->CallVoidMethod(node, m_setCheckedMethodID, (bool)state.checked); - env->CallVoidMethod(node, m_setEditableMethodID, state.editable); - env->CallVoidMethod(node, m_setEnabledMethodID, !state.disabled); - env->CallVoidMethod(node, m_setFocusableMethodID, (bool)state.focusable); - env->CallVoidMethod(node, m_setFocusedMethodID, (bool)state.focused); - env->CallVoidMethod(node, m_setVisibleToUserMethodID, !state.invisible); + env->CallVoidMethod(node, m_setCheckableMethodID, (bool)info.state.checkable); + env->CallVoidMethod(node, m_setCheckedMethodID, (bool)info.state.checked); + env->CallVoidMethod(node, m_setEditableMethodID, info.state.editable); + env->CallVoidMethod(node, m_setEnabledMethodID, !info.state.disabled); + env->CallVoidMethod(node, m_setFocusableMethodID, (bool)info.state.focusable); + env->CallVoidMethod(node, m_setFocusedMethodID, (bool)info.state.focused); + env->CallVoidMethod(node, m_setVisibleToUserMethodID, !info.state.invisible); env->CallVoidMethod(node, m_setScrollableMethodID, hasIncreaseAction || hasDecreaseAction); env->CallVoidMethod(node, m_setClickableMethodID, hasClickableAction); @@ -328,7 +460,9 @@ if (!clazz) { \ if (hasDecreaseAction) env->CallVoidMethod(node, m_addActionMethodID, (int)0x00002000); // ACTION_SCROLL_BACKWARD defined in AccessibilityNodeInfo - + // try to fill in the text property, this is what the screen reader reads + jstring jdesc = env->NewString((jchar*)info.description.constData(), + (jsize)info.description.size()); //CALL_METHOD(node, "setText", "(Ljava/lang/CharSequence;)V", jdesc) env->CallVoidMethod(node, m_setContentDescriptionMethodID, jdesc); diff --git a/src/plugins/platforms/android/androidjniaccessibility.h b/src/plugins/platforms/android/androidjniaccessibility.h index de9d32a099..a9044a32b1 100644 --- a/src/plugins/platforms/android/androidjniaccessibility.h +++ b/src/plugins/platforms/android/androidjniaccessibility.h @@ -44,6 +44,8 @@ QT_BEGIN_NAMESPACE +class QObject; + namespace QtAndroidAccessibility { void initialize(); @@ -52,6 +54,7 @@ namespace QtAndroidAccessibility void notifyLocationChange(); void notifyObjectHide(uint accessibilityObjectId); void notifyObjectFocus(uint accessibilityObjectId); + void createAccessibilityContextObject(QObject *parent); } QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformaccessibility.cpp b/src/plugins/platforms/android/qandroidplatformaccessibility.cpp index cc05dad749..75333d3f08 100644 --- a/src/plugins/platforms/android/qandroidplatformaccessibility.cpp +++ b/src/plugins/platforms/android/qandroidplatformaccessibility.cpp @@ -69,4 +69,10 @@ void QAndroidPlatformAccessibility::notifyAccessibilityUpdate(QAccessibleEvent * } } +void QAndroidPlatformAccessibility::setRootObject(QObject *obj) +{ + QPlatformAccessibility::setRootObject(obj); + QtAndroidAccessibility::createAccessibilityContextObject(obj); +} + QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformaccessibility.h b/src/plugins/platforms/android/qandroidplatformaccessibility.h index 8216c05fa6..df3fe43a04 100644 --- a/src/plugins/platforms/android/qandroidplatformaccessibility.h +++ b/src/plugins/platforms/android/qandroidplatformaccessibility.h @@ -52,6 +52,7 @@ public: ~QAndroidPlatformAccessibility(); void notifyAccessibilityUpdate(QAccessibleEvent *event) override; + void setRootObject(QObject *obj) override; }; QT_END_NAMESPACE |