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:
parent
6d00aac109
commit
e0c61193ea
@ -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);
|
||||
|
||||
|
@ -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
|
||||
|
@ -69,4 +69,10 @@ void QAndroidPlatformAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *
|
||||
}
|
||||
}
|
||||
|
||||
void QAndroidPlatformAccessibility::setRootObject(QObject *obj)
|
||||
{
|
||||
QPlatformAccessibility::setRootObject(obj);
|
||||
QtAndroidAccessibility::createAccessibilityContextObject(obj);
|
||||
}
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
@ -52,6 +52,7 @@ public:
|
||||
~QAndroidPlatformAccessibility();
|
||||
|
||||
void notifyAccessibilityUpdate(QAccessibleEvent *event) override;
|
||||
void setRootObject(QObject *obj) override;
|
||||
};
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
Loading…
Reference in New Issue
Block a user