1 /**************************************************************************** 2 ** 3 ** Copyright (C) 2016 The Qt Company Ltd. 4 ** Contact: https://www.qt.io/licensing/ 5 ** 6 ** This file is part of the plugins of the Qt Toolkit. 7 ** 8 ** $QT_BEGIN_LICENSE:LGPL$ 9 ** Commercial License Usage 10 ** Licensees holding valid commercial Qt licenses may use this file in 11 ** accordance with the commercial license agreement provided with the 12 ** Software or, alternatively, in accordance with the terms contained in 13 ** a written agreement between you and The Qt Company. For licensing terms 14 ** and conditions see https://www.qt.io/terms-conditions. For further 15 ** information use the contact form at https://www.qt.io/contact-us. 16 ** 17 ** GNU Lesser General Public License Usage 18 ** Alternatively, this file may be used under the terms of the GNU Lesser 19 ** General Public License version 3 as published by the Free Software 20 ** Foundation and appearing in the file LICENSE.LGPL3 included in the 21 ** packaging of this file. Please review the following information to 22 ** ensure the GNU Lesser General Public License version 3 requirements 23 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. 24 ** 25 ** GNU General Public License Usage 26 ** Alternatively, this file may be used under the terms of the GNU 27 ** General Public License version 2.0 or (at your option) the GNU General 28 ** Public license version 3 or any later version approved by the KDE Free 29 ** Qt Foundation. The licenses are as published by the Free Software 30 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 31 ** included in the packaging of this file. Please review the following 32 ** information to ensure the GNU General Public License requirements will 33 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and 34 ** https://www.gnu.org/licenses/gpl-3.0.html. 35 ** 36 ** $QT_END_LICENSE$ 37 ** 38 ****************************************************************************/ 39 40 #include "androidjniaccessibility.h" 41 #include "androidjnimain.h" 42 #include "qandroidplatformintegration.h" 43 #include "qpa/qplatformaccessibility.h" 44 #include <QtAccessibilitySupport/private/qaccessiblebridgeutils_p.h> 45 #include "qguiapplication.h" 46 #include "qwindow.h" 47 #include "qrect.h" 48 #include "QtGui/qaccessible.h" 49 #include <QtCore/qmath.h> 50 #include <QtCore/private/qjnihelpers_p.h> 51 #include <QtCore/private/qjni_p.h> 52 #include <QtGui/private/qhighdpiscaling_p.h> 53 54 #include "qdebug.h" 55 56 static const char m_qtTag[] = "Qt A11Y"; 57 static const char m_classErrorMsg[] = "Can't find class \"%s\""; 58 59 QT_BEGIN_NAMESPACE 60 61 namespace QtAndroidAccessibility 62 { 63 static jmethodID m_addActionMethodID = 0; 64 static jmethodID m_setCheckableMethodID = 0; 65 static jmethodID m_setCheckedMethodID = 0; 66 static jmethodID m_setClickableMethodID = 0; 67 static jmethodID m_setContentDescriptionMethodID = 0; 68 static jmethodID m_setEditableMethodID = 0; 69 static jmethodID m_setEnabledMethodID = 0; 70 static jmethodID m_setFocusableMethodID = 0; 71 static jmethodID m_setFocusedMethodID = 0; 72 static jmethodID m_setScrollableMethodID = 0; 73 static jmethodID m_setTextSelectionMethodID = 0; 74 static jmethodID m_setVisibleToUserMethodID = 0; 75 76 static bool m_accessibilityActivated = false; 77 initialize()78 void initialize() 79 { 80 QJNIObjectPrivate::callStaticMethod<void>(QtAndroid::applicationClass(), 81 "initializeAccessibility"); 82 } 83 isActive()84 bool isActive() 85 { 86 return m_accessibilityActivated; 87 } 88 setActive(JNIEnv *,jobject,jboolean active)89 static void setActive(JNIEnv */*env*/, jobject /*thiz*/, jboolean active) 90 { 91 QMutexLocker lock(QtAndroid::platformInterfaceMutex()); 92 QAndroidPlatformIntegration *platformIntegration = QtAndroid::androidPlatformIntegration(); 93 m_accessibilityActivated = active; 94 if (platformIntegration) 95 platformIntegration->accessibility()->setActive(active); 96 else 97 __android_log_print(ANDROID_LOG_WARN, m_qtTag, "Could not (yet) activate platform accessibility."); 98 } 99 interfaceFromId(jint objectId)100 QAccessibleInterface *interfaceFromId(jint objectId) 101 { 102 QAccessibleInterface *iface = 0; 103 if (objectId == -1) { 104 QWindow *win = qApp->focusWindow(); 105 if (win) 106 iface = win->accessibleRoot(); 107 } else { 108 iface = QAccessible::accessibleInterface(objectId); 109 } 110 return iface; 111 } 112 notifyLocationChange()113 void notifyLocationChange() 114 { 115 QtAndroid::notifyAccessibilityLocationChange(); 116 } 117 notifyObjectHide(uint accessibilityObjectId)118 void notifyObjectHide(uint accessibilityObjectId) 119 { 120 QtAndroid::notifyObjectHide(accessibilityObjectId); 121 } 122 notifyObjectFocus(uint accessibilityObjectId)123 void notifyObjectFocus(uint accessibilityObjectId) 124 { 125 QtAndroid::notifyObjectFocus(accessibilityObjectId); 126 } 127 childIdListForAccessibleObject(JNIEnv * env,jobject,jint objectId)128 static jintArray childIdListForAccessibleObject(JNIEnv *env, jobject /*thiz*/, jint objectId) 129 { 130 QAccessibleInterface *iface = interfaceFromId(objectId); 131 if (iface && iface->isValid()) { 132 const int childCount = iface->childCount(); 133 QVarLengthArray<jint, 8> ifaceIdArray; 134 ifaceIdArray.reserve(childCount); 135 for (int i = 0; i < childCount; ++i) { 136 QAccessibleInterface *child = iface->child(i); 137 if (child && child->isValid()) 138 ifaceIdArray.append(QAccessible::uniqueId(child)); 139 } 140 jintArray jArray = env->NewIntArray(jsize(ifaceIdArray.count())); 141 env->SetIntArrayRegion(jArray, 0, ifaceIdArray.count(), ifaceIdArray.data()); 142 return jArray; 143 } 144 145 return env->NewIntArray(jsize(0)); 146 } 147 parentId(JNIEnv *,jobject,jint objectId)148 static jint parentId(JNIEnv */*env*/, jobject /*thiz*/, jint objectId) 149 { 150 QAccessibleInterface *iface = interfaceFromId(objectId); 151 if (iface && iface->isValid()) { 152 QAccessibleInterface *parent = iface->parent(); 153 if (parent && parent->isValid()) { 154 if (parent->role() == QAccessible::Application) 155 return -1; 156 return QAccessible::uniqueId(parent); 157 } 158 } 159 return -1; 160 } 161 screenRect(JNIEnv * env,jobject,jint objectId)162 static jobject screenRect(JNIEnv *env, jobject /*thiz*/, jint objectId) 163 { 164 QRect rect; 165 QAccessibleInterface *iface = interfaceFromId(objectId); 166 if (iface && iface->isValid()) { 167 rect = QHighDpi::toNativePixels(iface->rect(), iface->window()); 168 } 169 // If the widget is not fully in-bound in its parent then we have to clip the rectangle to draw 170 if (iface && iface->parent() && iface->parent()->isValid()) { 171 const auto parentRect = QHighDpi::toNativePixels(iface->parent()->rect(), iface->parent()->window()); 172 rect = rect.intersected(parentRect); 173 } 174 175 jclass rectClass = env->FindClass("android/graphics/Rect"); 176 jmethodID ctor = env->GetMethodID(rectClass, "<init>", "(IIII)V"); 177 jobject jrect = env->NewObject(rectClass, ctor, rect.left(), rect.top(), rect.right(), rect.bottom()); 178 return jrect; 179 } 180 hitTest(JNIEnv *,jobject,jfloat x,jfloat y)181 static jint hitTest(JNIEnv */*env*/, jobject /*thiz*/, jfloat x, jfloat y) 182 { 183 QAccessibleInterface *root = interfaceFromId(-1); 184 if (root && root->isValid()) { 185 QPoint pos = QHighDpi::fromNativePixels(QPoint(int(x), int(y)), root->window()); 186 187 QAccessibleInterface *child = root->childAt(pos.x(), pos.y()); 188 QAccessibleInterface *lastChild = 0; 189 while (child && (child != lastChild)) { 190 lastChild = child; 191 child = child->childAt(pos.x(), pos.y()); 192 } 193 if (lastChild) 194 return QAccessible::uniqueId(lastChild); 195 } 196 return -1; 197 } 198 invokeActionOnInterfaceInMainThread(QAccessibleActionInterface * actionInterface,const QString & action)199 static void invokeActionOnInterfaceInMainThread(QAccessibleActionInterface* actionInterface, 200 const QString& action) 201 { 202 QMetaObject::invokeMethod(qApp, [actionInterface, action]() { 203 actionInterface->doAction(action); 204 }); 205 } 206 clickAction(JNIEnv *,jobject,jint objectId)207 static jboolean clickAction(JNIEnv */*env*/, jobject /*thiz*/, jint objectId) 208 { 209 // qDebug() << "A11Y: CLICK: " << objectId; 210 QAccessibleInterface *iface = interfaceFromId(objectId); 211 if (!iface || !iface->isValid() || !iface->actionInterface()) 212 return false; 213 214 const auto& actionNames = iface->actionInterface()->actionNames(); 215 216 if (actionNames.contains(QAccessibleActionInterface::pressAction())) { 217 invokeActionOnInterfaceInMainThread(iface->actionInterface(), 218 QAccessibleActionInterface::pressAction()); 219 } else if (actionNames.contains(QAccessibleActionInterface::toggleAction())) { 220 invokeActionOnInterfaceInMainThread(iface->actionInterface(), 221 QAccessibleActionInterface::toggleAction()); 222 } else { 223 return false; 224 } 225 return true; 226 } 227 scrollForward(JNIEnv *,jobject,jint objectId)228 static jboolean scrollForward(JNIEnv */*env*/, jobject /*thiz*/, jint objectId) 229 { 230 QAccessibleInterface *iface = interfaceFromId(objectId); 231 if (iface && iface->isValid()) 232 return QAccessibleBridgeUtils::performEffectiveAction(iface, QAccessibleActionInterface::increaseAction()); 233 return false; 234 } 235 scrollBackward(JNIEnv *,jobject,jint objectId)236 static jboolean scrollBackward(JNIEnv */*env*/, jobject /*thiz*/, jint objectId) 237 { 238 QAccessibleInterface *iface = interfaceFromId(objectId); 239 if (iface && iface->isValid()) 240 return QAccessibleBridgeUtils::performEffectiveAction(iface, QAccessibleActionInterface::decreaseAction()); 241 return false; 242 } 243 244 245 #define FIND_AND_CHECK_CLASS(CLASS_NAME) \ 246 clazz = env->FindClass(CLASS_NAME); \ 247 if (!clazz) { \ 248 __android_log_print(ANDROID_LOG_FATAL, m_qtTag, m_classErrorMsg, CLASS_NAME); \ 249 return JNI_FALSE; \ 250 } 251 252 //__android_log_print(ANDROID_LOG_FATAL, m_qtTag, m_methodErrorMsg, METHOD_NAME, METHOD_SIGNATURE); 253 254 255 descriptionForAccessibleObject_helper(JNIEnv * env,QAccessibleInterface * iface)256 static jstring descriptionForAccessibleObject_helper(JNIEnv *env, QAccessibleInterface *iface) 257 { 258 QString desc; 259 if (iface && iface->isValid()) { 260 desc = iface->text(QAccessible::Name); 261 if (desc.isEmpty()) 262 desc = iface->text(QAccessible::Description); 263 if (desc.isEmpty()) { 264 desc = iface->text(QAccessible::Value); 265 if (desc.isEmpty()) { 266 if (QAccessibleValueInterface *valueIface = iface->valueInterface()) { 267 desc= valueIface->currentValue().toString(); 268 } 269 } 270 } 271 } 272 return env->NewString((jchar*) desc.constData(), (jsize) desc.size()); 273 } 274 descriptionForAccessibleObject(JNIEnv * env,jobject,jint objectId)275 static jstring descriptionForAccessibleObject(JNIEnv *env, jobject /*thiz*/, jint objectId) 276 { 277 QAccessibleInterface *iface = interfaceFromId(objectId); 278 return descriptionForAccessibleObject_helper(env, iface); 279 } 280 populateNode(JNIEnv * env,jobject,jint objectId,jobject node)281 static bool populateNode(JNIEnv *env, jobject /*thiz*/, jint objectId, jobject node) 282 { 283 QAccessibleInterface *iface = interfaceFromId(objectId); 284 if (!iface || !iface->isValid()) { 285 __android_log_print(ANDROID_LOG_WARN, m_qtTag, "Accessibility: populateNode for Invalid ID"); 286 return false; 287 } 288 QAccessible::State state = iface->state(); 289 const QStringList actions = QAccessibleBridgeUtils::effectiveActionNames(iface); 290 const bool hasClickableAction = actions.contains(QAccessibleActionInterface::pressAction()) 291 || actions.contains(QAccessibleActionInterface::toggleAction()); 292 const bool hasIncreaseAction = actions.contains(QAccessibleActionInterface::increaseAction()); 293 const bool hasDecreaseAction = actions.contains(QAccessibleActionInterface::decreaseAction()); 294 295 // try to fill in the text property, this is what the screen reader reads 296 jstring jdesc = descriptionForAccessibleObject_helper(env, iface); 297 298 if (QAccessibleTextInterface *textIface = iface->textInterface()) { 299 if (m_setTextSelectionMethodID && textIface->selectionCount() > 0) { 300 int startSelection; 301 int endSelection; 302 textIface->selection(0, &startSelection, &endSelection); 303 env->CallVoidMethod(node, m_setTextSelectionMethodID, startSelection, endSelection); 304 } 305 } 306 307 env->CallVoidMethod(node, m_setCheckableMethodID, (bool)state.checkable); 308 env->CallVoidMethod(node, m_setCheckedMethodID, (bool)state.checked); 309 env->CallVoidMethod(node, m_setEditableMethodID, state.editable); 310 env->CallVoidMethod(node, m_setEnabledMethodID, !state.disabled); 311 env->CallVoidMethod(node, m_setFocusableMethodID, (bool)state.focusable); 312 env->CallVoidMethod(node, m_setFocusedMethodID, (bool)state.focused); 313 env->CallVoidMethod(node, m_setVisibleToUserMethodID, !state.invisible); 314 env->CallVoidMethod(node, m_setScrollableMethodID, hasIncreaseAction || hasDecreaseAction); 315 env->CallVoidMethod(node, m_setClickableMethodID, hasClickableAction); 316 317 // Add ACTION_CLICK 318 if (hasClickableAction) 319 env->CallVoidMethod(node, m_addActionMethodID, (int)0x00000010); // ACTION_CLICK defined in AccessibilityNodeInfo 320 321 // Add ACTION_SCROLL_FORWARD 322 if (hasIncreaseAction) 323 env->CallVoidMethod(node, m_addActionMethodID, (int)0x00001000); // ACTION_SCROLL_FORWARD defined in AccessibilityNodeInfo 324 325 // Add ACTION_SCROLL_BACKWARD 326 if (hasDecreaseAction) 327 env->CallVoidMethod(node, m_addActionMethodID, (int)0x00002000); // ACTION_SCROLL_BACKWARD defined in AccessibilityNodeInfo 328 329 330 //CALL_METHOD(node, "setText", "(Ljava/lang/CharSequence;)V", jdesc) 331 env->CallVoidMethod(node, m_setContentDescriptionMethodID, jdesc); 332 333 return true; 334 } 335 336 static JNINativeMethod methods[] = { 337 {"setActive","(Z)V",(void*)setActive}, 338 {"childIdListForAccessibleObject", "(I)[I", (jintArray)childIdListForAccessibleObject}, 339 {"parentId", "(I)I", (void*)parentId}, 340 {"descriptionForAccessibleObject", "(I)Ljava/lang/String;", (jstring)descriptionForAccessibleObject}, 341 {"screenRect", "(I)Landroid/graphics/Rect;", (jobject)screenRect}, 342 {"hitTest", "(FF)I", (void*)hitTest}, 343 {"populateNode", "(ILandroid/view/accessibility/AccessibilityNodeInfo;)Z", (void*)populateNode}, 344 {"clickAction", "(I)Z", (void*)clickAction}, 345 {"scrollForward", "(I)Z", (void*)scrollForward}, 346 {"scrollBackward", "(I)Z", (void*)scrollBackward}, 347 }; 348 349 #define GET_AND_CHECK_STATIC_METHOD(VAR, CLASS, METHOD_NAME, METHOD_SIGNATURE) \ 350 VAR = env->GetMethodID(CLASS, METHOD_NAME, METHOD_SIGNATURE); \ 351 if (!VAR) { \ 352 __android_log_print(ANDROID_LOG_FATAL, QtAndroid::qtTagText(), QtAndroid::methodErrorMsgFmt(), METHOD_NAME, METHOD_SIGNATURE); \ 353 return false; \ 354 } 355 registerNatives(JNIEnv * env)356 bool registerNatives(JNIEnv *env) 357 { 358 jclass clazz; 359 FIND_AND_CHECK_CLASS("org/qtproject/qt5/android/accessibility/QtNativeAccessibility"); 360 jclass appClass = static_cast<jclass>(env->NewGlobalRef(clazz)); 361 362 if (env->RegisterNatives(appClass, methods, sizeof(methods) / sizeof(methods[0])) < 0) { 363 __android_log_print(ANDROID_LOG_FATAL,"Qt A11y", "RegisterNatives failed"); 364 return false; 365 } 366 367 jclass nodeInfoClass = env->FindClass("android/view/accessibility/AccessibilityNodeInfo"); 368 GET_AND_CHECK_STATIC_METHOD(m_addActionMethodID, nodeInfoClass, "addAction", "(I)V"); 369 GET_AND_CHECK_STATIC_METHOD(m_setCheckableMethodID, nodeInfoClass, "setCheckable", "(Z)V"); 370 GET_AND_CHECK_STATIC_METHOD(m_setCheckedMethodID, nodeInfoClass, "setChecked", "(Z)V"); 371 GET_AND_CHECK_STATIC_METHOD(m_setClickableMethodID, nodeInfoClass, "setClickable", "(Z)V"); 372 GET_AND_CHECK_STATIC_METHOD(m_setContentDescriptionMethodID, nodeInfoClass, "setContentDescription", "(Ljava/lang/CharSequence;)V"); 373 GET_AND_CHECK_STATIC_METHOD(m_setEditableMethodID, nodeInfoClass, "setEditable", "(Z)V"); 374 GET_AND_CHECK_STATIC_METHOD(m_setEnabledMethodID, nodeInfoClass, "setEnabled", "(Z)V"); 375 GET_AND_CHECK_STATIC_METHOD(m_setFocusableMethodID, nodeInfoClass, "setFocusable", "(Z)V"); 376 GET_AND_CHECK_STATIC_METHOD(m_setFocusedMethodID, nodeInfoClass, "setFocused", "(Z)V"); 377 GET_AND_CHECK_STATIC_METHOD(m_setScrollableMethodID, nodeInfoClass, "setScrollable", "(Z)V"); 378 GET_AND_CHECK_STATIC_METHOD(m_setVisibleToUserMethodID, nodeInfoClass, "setVisibleToUser", "(Z)V"); 379 GET_AND_CHECK_STATIC_METHOD(m_setTextSelectionMethodID, nodeInfoClass, "setTextSelection", "(II)V"); 380 381 return true; 382 } 383 } 384 385 QT_END_NAMESPACE 386