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>
This commit is contained in:
Ivan Solovev 2022-02-01 17:17:12 +01:00
parent 6d00aac109
commit e0c61193ea
4 changed files with 190 additions and 46 deletions

View File

@ -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 QString descriptionForAccessibleObject_helper(int objectId)
{
QAccessibleInterface *iface = interfaceFromId(objectId);
return descriptionForInterface(iface);
}
static jstring descriptionForAccessibleObject(JNIEnv *env, jobject /*thiz*/, jint objectId)
{
QAccessibleInterface *iface = interfaceFromId(objectId);
return descriptionForAccessibleObject_helper(env, iface);
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);
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 (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);
}
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);

View File

@ -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

View File

@ -69,4 +69,10 @@ void QAndroidPlatformAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *
}
}
void QAndroidPlatformAccessibility::setRootObject(QObject *obj)
{
QPlatformAccessibility::setRootObject(obj);
QtAndroidAccessibility::createAccessibilityContextObject(obj);
}
QT_END_NAMESPACE

View File

@ -52,6 +52,7 @@ public:
~QAndroidPlatformAccessibility();
void notifyAccessibilityUpdate(QAccessibleEvent *event) override;
void setRootObject(QObject *obj) override;
};
QT_END_NAMESPACE