The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1060 lines
45KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2022 - Raw Material Software Limited
  5. JUCE is an open source library subject to commercial or open-source
  6. licensing.
  7. By using JUCE, you agree to the terms of both the JUCE 7 End-User License
  8. Agreement and JUCE Privacy Policy.
  9. End User License Agreement: www.juce.com/juce-7-licence
  10. Privacy Policy: www.juce.com/juce-privacy-policy
  11. Or: You may also use this code under the terms of the GPL v3 (see
  12. www.gnu.org/licenses).
  13. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  14. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  15. DISCLAIMED.
  16. ==============================================================================
  17. */
  18. namespace juce
  19. {
  20. #define JUCE_NATIVE_ACCESSIBILITY_INCLUDED 1
  21. #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
  22. METHOD (setSource, "setSource", "(Landroid/view/View;I)V") \
  23. METHOD (addChild, "addChild", "(Landroid/view/View;I)V") \
  24. METHOD (setParent, "setParent", "(Landroid/view/View;)V") \
  25. METHOD (setVirtualParent, "setParent", "(Landroid/view/View;I)V") \
  26. METHOD (setBoundsInScreen, "setBoundsInScreen", "(Landroid/graphics/Rect;)V") \
  27. METHOD (setBoundsInParent, "setBoundsInParent", "(Landroid/graphics/Rect;)V") \
  28. METHOD (setPackageName, "setPackageName", "(Ljava/lang/CharSequence;)V") \
  29. METHOD (setClassName, "setClassName", "(Ljava/lang/CharSequence;)V") \
  30. METHOD (setContentDescription, "setContentDescription", "(Ljava/lang/CharSequence;)V") \
  31. METHOD (setCheckable, "setCheckable", "(Z)V") \
  32. METHOD (setChecked, "setChecked", "(Z)V") \
  33. METHOD (setClickable, "setClickable", "(Z)V") \
  34. METHOD (setEnabled, "setEnabled", "(Z)V") \
  35. METHOD (setFocusable, "setFocusable", "(Z)V") \
  36. METHOD (setFocused, "setFocused", "(Z)V") \
  37. METHOD (setPassword, "setPassword", "(Z)V") \
  38. METHOD (setSelected, "setSelected", "(Z)V") \
  39. METHOD (setVisibleToUser, "setVisibleToUser", "(Z)V") \
  40. METHOD (setAccessibilityFocused, "setAccessibilityFocused", "(Z)V") \
  41. METHOD (setText, "setText", "(Ljava/lang/CharSequence;)V") \
  42. METHOD (setMovementGranularities, "setMovementGranularities", "(I)V") \
  43. METHOD (addAction, "addAction", "(I)V") \
  44. DECLARE_JNI_CLASS (AndroidAccessibilityNodeInfo, "android/view/accessibility/AccessibilityNodeInfo")
  45. #undef JNI_CLASS_MEMBERS
  46. #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
  47. METHOD (setCollectionInfo, "setCollectionInfo", "(Landroid/view/accessibility/AccessibilityNodeInfo$CollectionInfo;)V") \
  48. METHOD (setCollectionItemInfo, "setCollectionItemInfo", "(Landroid/view/accessibility/AccessibilityNodeInfo$CollectionItemInfo;)V")
  49. DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidAccessibilityNodeInfo19, "android/view/accessibility/AccessibilityNodeInfo", 19)
  50. #undef JNI_CLASS_MEMBERS
  51. #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
  52. STATICMETHOD (obtain, "obtain", "(IIZ)Landroid/view/accessibility/AccessibilityNodeInfo$CollectionInfo;")
  53. DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidAccessibilityNodeInfoCollectionInfo, "android/view/accessibility/AccessibilityNodeInfo$CollectionInfo", 19)
  54. #undef JNI_CLASS_MEMBERS
  55. #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
  56. STATICMETHOD (obtain, "obtain", "(IIIIZ)Landroid/view/accessibility/AccessibilityNodeInfo$CollectionItemInfo;")
  57. DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidAccessibilityNodeInfoCollectionItemInfo, "android/view/accessibility/AccessibilityNodeInfo$CollectionItemInfo", 19)
  58. #undef JNI_CLASS_MEMBERS
  59. #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
  60. STATICMETHOD (obtain, "obtain", "(I)Landroid/view/accessibility/AccessibilityEvent;") \
  61. METHOD (setPackageName, "setPackageName", "(Ljava/lang/CharSequence;)V") \
  62. METHOD (setSource, "setSource","(Landroid/view/View;I)V") \
  63. METHOD (setAction, "setAction", "(I)V") \
  64. METHOD (setFromIndex, "setFromIndex", "(I)V") \
  65. METHOD (setToIndex, "setToIndex", "(I)V") \
  66. DECLARE_JNI_CLASS (AndroidAccessibilityEvent, "android/view/accessibility/AccessibilityEvent")
  67. #undef JNI_CLASS_MEMBERS
  68. #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
  69. METHOD (isEnabled, "isEnabled", "()Z") \
  70. DECLARE_JNI_CLASS (AndroidAccessibilityManager, "android/view/accessibility/AccessibilityManager")
  71. #undef JNI_CLASS_MEMBERS
  72. namespace
  73. {
  74. constexpr int HOST_VIEW_ID = -1;
  75. constexpr int TYPE_VIEW_CLICKED = 0x00000001,
  76. TYPE_VIEW_SELECTED = 0x00000004,
  77. TYPE_VIEW_ACCESSIBILITY_FOCUSED = 0x00008000,
  78. TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED = 0x00010000,
  79. TYPE_WINDOW_CONTENT_CHANGED = 0x00000800,
  80. TYPE_VIEW_TEXT_SELECTION_CHANGED = 0x00002000,
  81. TYPE_VIEW_TEXT_CHANGED = 0x00000010,
  82. TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY = 0x00020000;
  83. constexpr int CONTENT_CHANGE_TYPE_SUBTREE = 0x00000001,
  84. CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION = 0x00000004;
  85. constexpr int ACTION_ACCESSIBILITY_FOCUS = 0x00000040,
  86. ACTION_CLEAR_ACCESSIBILITY_FOCUS = 0x00000080,
  87. ACTION_CLEAR_FOCUS = 0x00000002,
  88. ACTION_CLEAR_SELECTION = 0x00000008,
  89. ACTION_CLICK = 0x00000010,
  90. ACTION_COLLAPSE = 0x00080000,
  91. ACTION_EXPAND = 0x00040000,
  92. ACTION_FOCUS = 0x00000001,
  93. ACTION_NEXT_AT_MOVEMENT_GRANULARITY = 0x00000100,
  94. ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY = 0x00000200,
  95. ACTION_SCROLL_BACKWARD = 0x00002000,
  96. ACTION_SCROLL_FORWARD = 0x00001000,
  97. ACTION_SELECT = 0x00000004,
  98. ACTION_SET_SELECTION = 0x00020000,
  99. ACTION_SET_TEXT = 0x00200000;
  100. constexpr int MOVEMENT_GRANULARITY_CHARACTER = 0x00000001,
  101. MOVEMENT_GRANULARITY_LINE = 0x00000004,
  102. MOVEMENT_GRANULARITY_PAGE = 0x00000010,
  103. MOVEMENT_GRANULARITY_PARAGRAPH = 0x00000008,
  104. MOVEMENT_GRANULARITY_WORD = 0x00000002,
  105. ALL_GRANULARITIES = MOVEMENT_GRANULARITY_CHARACTER
  106. | MOVEMENT_GRANULARITY_LINE
  107. | MOVEMENT_GRANULARITY_PAGE
  108. | MOVEMENT_GRANULARITY_PARAGRAPH
  109. | MOVEMENT_GRANULARITY_WORD;
  110. constexpr int ACCESSIBILITY_LIVE_REGION_POLITE = 0x00000001;
  111. }
  112. static jmethodID nodeInfoSetEditable = nullptr;
  113. static jmethodID nodeInfoSetTextSelection = nullptr;
  114. static jmethodID nodeInfoSetLiveRegion = nullptr;
  115. static jmethodID accessibilityEventSetContentChangeTypes = nullptr;
  116. static void loadSDKDependentMethods()
  117. {
  118. static bool hasChecked = false;
  119. if (! hasChecked)
  120. {
  121. hasChecked = true;
  122. auto* env = getEnv();
  123. const auto sdkVersion = getAndroidSDKVersion();
  124. if (sdkVersion >= 18)
  125. {
  126. nodeInfoSetEditable = env->GetMethodID (AndroidAccessibilityNodeInfo, "setEditable", "(Z)V");
  127. nodeInfoSetTextSelection = env->GetMethodID (AndroidAccessibilityNodeInfo, "setTextSelection", "(II)V");
  128. }
  129. if (sdkVersion >= 19)
  130. {
  131. nodeInfoSetLiveRegion = env->GetMethodID (AndroidAccessibilityNodeInfo, "setLiveRegion", "(I)V");
  132. accessibilityEventSetContentChangeTypes = env->GetMethodID (AndroidAccessibilityEvent, "setContentChangeTypes", "(I)V");
  133. }
  134. }
  135. }
  136. static constexpr auto getClassName (AccessibilityRole role)
  137. {
  138. switch (role)
  139. {
  140. case AccessibilityRole::editableText: return "android.widget.EditText";
  141. case AccessibilityRole::toggleButton: return "android.widget.CheckBox";
  142. case AccessibilityRole::radioButton: return "android.widget.RadioButton";
  143. case AccessibilityRole::image: return "android.widget.ImageView";
  144. case AccessibilityRole::popupMenu: return "android.widget.PopupMenu";
  145. case AccessibilityRole::comboBox: return "android.widget.Spinner";
  146. case AccessibilityRole::tree: return "android.widget.ExpandableListView";
  147. case AccessibilityRole::progressBar: return "android.widget.ProgressBar";
  148. case AccessibilityRole::scrollBar:
  149. case AccessibilityRole::slider: return "android.widget.SeekBar";
  150. case AccessibilityRole::hyperlink:
  151. case AccessibilityRole::button: return "android.widget.Button";
  152. case AccessibilityRole::label:
  153. case AccessibilityRole::staticText: return "android.widget.TextView";
  154. case AccessibilityRole::tooltip:
  155. case AccessibilityRole::splashScreen:
  156. case AccessibilityRole::dialogWindow: return "android.widget.PopupWindow";
  157. // If we don't supply a custom class type, then TalkBack will use the node's CollectionInfo
  158. // to make a sensible decision about how to describe the container
  159. case AccessibilityRole::list:
  160. case AccessibilityRole::table:
  161. case AccessibilityRole::column:
  162. case AccessibilityRole::row:
  163. case AccessibilityRole::cell:
  164. case AccessibilityRole::menuItem:
  165. case AccessibilityRole::menuBar:
  166. case AccessibilityRole::listItem:
  167. case AccessibilityRole::treeItem:
  168. case AccessibilityRole::window:
  169. case AccessibilityRole::tableHeader:
  170. case AccessibilityRole::unspecified:
  171. case AccessibilityRole::group:
  172. case AccessibilityRole::ignored: break;
  173. }
  174. return "android.view.View";
  175. }
  176. static jobject getSourceView (const AccessibilityHandler& handler)
  177. {
  178. if (auto* peer = handler.getComponent().getPeer())
  179. return (jobject) peer->getNativeHandle();
  180. return nullptr;
  181. }
  182. static jobject makeAndroidRect (Rectangle<int> r)
  183. {
  184. return getEnv()->NewObject (AndroidRect,
  185. AndroidRect.constructor,
  186. r.getX(),
  187. r.getY(),
  188. r.getRight(),
  189. r.getBottom());
  190. }
  191. static jobject makeAndroidPoint (Point<int> p)
  192. {
  193. return getEnv()->NewObject (AndroidPoint,
  194. AndroidPoint.create,
  195. p.getX(),
  196. p.getY());
  197. }
  198. //==============================================================================
  199. class AccessibilityNativeHandle
  200. {
  201. public:
  202. static AccessibilityHandler* getAccessibilityHandlerForVirtualViewId (int virtualViewId)
  203. {
  204. auto iter = virtualViewIdMap.find (virtualViewId);
  205. if (iter != virtualViewIdMap.end())
  206. return iter->second;
  207. return nullptr;
  208. }
  209. explicit AccessibilityNativeHandle (AccessibilityHandler& h)
  210. : accessibilityHandler (h),
  211. virtualViewId (getVirtualViewIdForHandler (accessibilityHandler))
  212. {
  213. loadSDKDependentMethods();
  214. if (virtualViewId != HOST_VIEW_ID)
  215. virtualViewIdMap[virtualViewId] = &accessibilityHandler;
  216. }
  217. ~AccessibilityNativeHandle()
  218. {
  219. if (virtualViewId != HOST_VIEW_ID)
  220. virtualViewIdMap.erase (virtualViewId);
  221. if (nativeChildViewId.has_value())
  222. virtualViewIdMap.erase (*nativeChildViewId);
  223. }
  224. int getVirtualViewId() const noexcept { return virtualViewId; }
  225. jobject getNativeView (int viewId) const
  226. {
  227. if (nativeChildViewId == viewId)
  228. return static_cast<jobject> (AccessibilityHandler::getNativeChildForComponent (accessibilityHandler.getComponent()));
  229. return nullptr;
  230. }
  231. void populateNodeInfo (jobject info, int infoVirtualViewId)
  232. {
  233. if ( AccessibilityHandler::getNativeChildForComponent (accessibilityHandler.getComponent()) != nullptr
  234. && ! nativeChildViewId.has_value())
  235. {
  236. nativeChildViewId.emplace (getNextVirtualViewId());
  237. virtualViewIdMap[*nativeChildViewId] = &accessibilityHandler;
  238. }
  239. const ScopedValueSetter<bool> svs (inPopulateNodeInfo, true);
  240. const auto sourceView = getSourceView (accessibilityHandler);
  241. if (sourceView == nullptr)
  242. return;
  243. auto* env = getEnv();
  244. auto appContext = getAppContext();
  245. if (appContext.get() == nullptr)
  246. return;
  247. {
  248. for (auto* child : accessibilityHandler.getChildren())
  249. env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.addChild,
  250. sourceView, child->getNativeImplementation()->getVirtualViewId());
  251. if (nativeChildViewId.has_value() && nativeChildViewId != infoVirtualViewId)
  252. env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.addChild,
  253. sourceView, *nativeChildViewId);
  254. if (auto* parent = accessibilityHandler.getParent())
  255. env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setVirtualParent,
  256. sourceView, parent->getNativeImplementation()->getVirtualViewId());
  257. }
  258. {
  259. const auto scale = Desktop::getInstance().getDisplays().getPrimaryDisplay()->scale;
  260. LocalRef<jobject> screenBounds (makeAndroidRect (accessibilityHandler.getComponent().getScreenBounds() * scale));
  261. env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setBoundsInScreen, screenBounds.get());
  262. LocalRef<jobject> boundsInParent (makeAndroidRect (accessibilityHandler.getComponent().getBoundsInParent() * scale));
  263. env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setBoundsInParent, boundsInParent.get());
  264. }
  265. const auto state = accessibilityHandler.getCurrentState();
  266. env->CallVoidMethod (info,
  267. AndroidAccessibilityNodeInfo.setEnabled,
  268. ! state.isIgnored());
  269. env->CallVoidMethod (info,
  270. AndroidAccessibilityNodeInfo.setVisibleToUser,
  271. true);
  272. env->CallVoidMethod (info,
  273. AndroidAccessibilityNodeInfo.setPackageName,
  274. env->CallObjectMethod (appContext.get(),
  275. AndroidContext.getPackageName));
  276. env->CallVoidMethod (info,
  277. AndroidAccessibilityNodeInfo.setSource,
  278. sourceView,
  279. virtualViewId);
  280. env->CallVoidMethod (info,
  281. AndroidAccessibilityNodeInfo.setClassName,
  282. javaString (getClassName (accessibilityHandler.getRole())).get());
  283. env->CallVoidMethod (info,
  284. AndroidAccessibilityNodeInfo.setContentDescription,
  285. getDescriptionString().get());
  286. if (state.isFocusable())
  287. {
  288. env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setFocusable, true);
  289. const auto& component = accessibilityHandler.getComponent();
  290. if (component.getWantsKeyboardFocus())
  291. {
  292. const auto hasKeyboardFocus = component.hasKeyboardFocus (false);
  293. env->CallVoidMethod (info,
  294. AndroidAccessibilityNodeInfo.setFocused,
  295. hasKeyboardFocus);
  296. env->CallVoidMethod (info,
  297. AndroidAccessibilityNodeInfo.addAction,
  298. hasKeyboardFocus ? ACTION_CLEAR_FOCUS : ACTION_FOCUS);
  299. }
  300. const auto isAccessibleFocused = accessibilityHandler.hasFocus (false);
  301. env->CallVoidMethod (info,
  302. AndroidAccessibilityNodeInfo.setAccessibilityFocused,
  303. isAccessibleFocused);
  304. env->CallVoidMethod (info,
  305. AndroidAccessibilityNodeInfo.addAction,
  306. isAccessibleFocused ? ACTION_CLEAR_ACCESSIBILITY_FOCUS
  307. : ACTION_ACCESSIBILITY_FOCUS);
  308. }
  309. if (state.isCheckable())
  310. {
  311. env->CallVoidMethod (info,
  312. AndroidAccessibilityNodeInfo.setCheckable,
  313. true);
  314. env->CallVoidMethod (info,
  315. AndroidAccessibilityNodeInfo.setChecked,
  316. state.isChecked());
  317. }
  318. if (state.isSelectable() || state.isMultiSelectable())
  319. {
  320. const auto isSelected = state.isSelected();
  321. env->CallVoidMethod (info,
  322. AndroidAccessibilityNodeInfo.setSelected,
  323. isSelected);
  324. env->CallVoidMethod (info,
  325. AndroidAccessibilityNodeInfo.addAction,
  326. isSelected ? ACTION_CLEAR_SELECTION : ACTION_SELECT);
  327. }
  328. if ((accessibilityHandler.getCurrentState().isCheckable() && accessibilityHandler.getActions().contains (AccessibilityActionType::toggle))
  329. || accessibilityHandler.getActions().contains (AccessibilityActionType::press))
  330. {
  331. env->CallVoidMethod (info,
  332. AndroidAccessibilityNodeInfo.setClickable,
  333. true);
  334. env->CallVoidMethod (info,
  335. AndroidAccessibilityNodeInfo.addAction,
  336. ACTION_CLICK);
  337. }
  338. if (accessibilityHandler.getActions().contains (AccessibilityActionType::showMenu)
  339. && state.isExpandable())
  340. {
  341. env->CallVoidMethod (info,
  342. AndroidAccessibilityNodeInfo.addAction,
  343. state.isExpanded() ? ACTION_COLLAPSE : ACTION_EXPAND);
  344. }
  345. if (auto* textInterface = accessibilityHandler.getTextInterface())
  346. {
  347. env->CallVoidMethod (info,
  348. AndroidAccessibilityNodeInfo.setText,
  349. javaString (textInterface->getAllText()).get());
  350. const auto isReadOnly = textInterface->isReadOnly();
  351. env->CallVoidMethod (info,
  352. AndroidAccessibilityNodeInfo.setPassword,
  353. textInterface->isDisplayingProtectedText());
  354. if (nodeInfoSetEditable != nullptr)
  355. env->CallVoidMethod (info, nodeInfoSetEditable, ! isReadOnly);
  356. const auto selection = textInterface->getSelection();
  357. if (nodeInfoSetTextSelection != nullptr && ! selection.isEmpty())
  358. env->CallVoidMethod (info,
  359. nodeInfoSetTextSelection,
  360. selection.getStart(), selection.getEnd());
  361. if (nodeInfoSetLiveRegion != nullptr && accessibilityHandler.hasFocus (false))
  362. env->CallVoidMethod (info,
  363. nodeInfoSetLiveRegion,
  364. ACCESSIBILITY_LIVE_REGION_POLITE);
  365. env->CallVoidMethod (info,
  366. AndroidAccessibilityNodeInfo.setMovementGranularities,
  367. ALL_GRANULARITIES);
  368. env->CallVoidMethod (info,
  369. AndroidAccessibilityNodeInfo.addAction,
  370. ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
  371. env->CallVoidMethod (info,
  372. AndroidAccessibilityNodeInfo.addAction,
  373. ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
  374. env->CallVoidMethod (info,
  375. AndroidAccessibilityNodeInfo.addAction,
  376. ACTION_SET_SELECTION);
  377. if (! isReadOnly)
  378. env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.addAction, ACTION_SET_TEXT);
  379. }
  380. if (auto* valueInterface = accessibilityHandler.getValueInterface())
  381. {
  382. if (! valueInterface->isReadOnly())
  383. {
  384. const auto range = valueInterface->getRange();
  385. if (range.isValid())
  386. {
  387. env->CallVoidMethod (info,
  388. AndroidAccessibilityNodeInfo.addAction,
  389. ACTION_SCROLL_FORWARD);
  390. env->CallVoidMethod (info,
  391. AndroidAccessibilityNodeInfo.addAction,
  392. ACTION_SCROLL_BACKWARD);
  393. }
  394. }
  395. }
  396. if (getAndroidSDKVersion() >= 19)
  397. {
  398. if (auto* tableInterface = accessibilityHandler.getTableInterface())
  399. {
  400. const auto rows = tableInterface->getNumRows();
  401. const auto columns = tableInterface->getNumColumns();
  402. const LocalRef<jobject> collectionInfo { env->CallStaticObjectMethod (AndroidAccessibilityNodeInfoCollectionInfo,
  403. AndroidAccessibilityNodeInfoCollectionInfo.obtain,
  404. (jint) rows,
  405. (jint) columns,
  406. (jboolean) false) };
  407. env->CallVoidMethod (info, AndroidAccessibilityNodeInfo19.setCollectionInfo, collectionInfo.get());
  408. }
  409. if (auto* enclosingTableHandler = detail::AccessibilityHelpers::getEnclosingHandlerWithInterface (&accessibilityHandler, &AccessibilityHandler::getTableInterface))
  410. {
  411. auto* interface = enclosingTableHandler->getTableInterface();
  412. jassert (interface != nullptr);
  413. const auto rowSpan = interface->getRowSpan (accessibilityHandler);
  414. const auto columnSpan = interface->getColumnSpan (accessibilityHandler);
  415. enum class IsHeader { no, yes };
  416. const auto addCellInfo = [env, &info] (AccessibilityTableInterface::Span rows, AccessibilityTableInterface::Span columns, IsHeader header)
  417. {
  418. const LocalRef<jobject> collectionItemInfo { env->CallStaticObjectMethod (AndroidAccessibilityNodeInfoCollectionItemInfo,
  419. AndroidAccessibilityNodeInfoCollectionItemInfo.obtain,
  420. (jint) rows.begin,
  421. (jint) rows.num,
  422. (jint) columns.begin,
  423. (jint) columns.num,
  424. (jboolean) (header == IsHeader::yes)) };
  425. env->CallVoidMethod (info, AndroidAccessibilityNodeInfo19.setCollectionItemInfo, collectionItemInfo.get());
  426. };
  427. if (rowSpan.hasValue() && columnSpan.hasValue())
  428. {
  429. addCellInfo (*rowSpan, *columnSpan, IsHeader::no);
  430. }
  431. else
  432. {
  433. if (auto* tableHeader = interface->getHeaderHandler())
  434. {
  435. if (accessibilityHandler.getParent() == tableHeader)
  436. {
  437. const auto children = tableHeader->getChildren();
  438. const auto column = std::distance (children.cbegin(), std::find (children.cbegin(), children.cend(), &accessibilityHandler));
  439. // Talkback will only treat a row as a column header if its row index is zero
  440. // https://github.com/google/talkback/blob/acd0bc7631a3dfbcf183789c7557596a45319e1f/utils/src/main/java/CollectionState.java#L853
  441. addCellInfo ({ 0, 1 }, { (int) column, 1 }, IsHeader::yes);
  442. }
  443. }
  444. }
  445. }
  446. }
  447. }
  448. bool performAction (int action, jobject arguments)
  449. {
  450. switch (action)
  451. {
  452. case ACTION_ACCESSIBILITY_FOCUS:
  453. {
  454. const WeakReference<Component> safeComponent (&accessibilityHandler.getComponent());
  455. accessibilityHandler.getActions().invoke (AccessibilityActionType::focus);
  456. if (safeComponent != nullptr)
  457. accessibilityHandler.grabFocus();
  458. return true;
  459. }
  460. case ACTION_CLEAR_ACCESSIBILITY_FOCUS:
  461. {
  462. accessibilityHandler.giveAwayFocus();
  463. return true;
  464. }
  465. case ACTION_FOCUS:
  466. case ACTION_CLEAR_FOCUS:
  467. {
  468. auto& component = accessibilityHandler.getComponent();
  469. if (component.getWantsKeyboardFocus())
  470. {
  471. const auto hasFocus = component.hasKeyboardFocus (false);
  472. if (hasFocus && action == ACTION_CLEAR_FOCUS)
  473. component.giveAwayKeyboardFocus();
  474. else if (! hasFocus && action == ACTION_FOCUS)
  475. component.grabKeyboardFocus();
  476. return true;
  477. }
  478. break;
  479. }
  480. case ACTION_CLICK:
  481. {
  482. // Invoking the action may delete this handler
  483. const WeakReference<AccessibilityNativeHandle> savedHandle { this };
  484. if ((accessibilityHandler.getCurrentState().isCheckable() && accessibilityHandler.getActions().invoke (AccessibilityActionType::toggle))
  485. || accessibilityHandler.getActions().invoke (AccessibilityActionType::press))
  486. {
  487. if (savedHandle != nullptr)
  488. sendAccessibilityEventImpl (accessibilityHandler, TYPE_VIEW_CLICKED, 0);
  489. return true;
  490. }
  491. break;
  492. }
  493. case ACTION_SELECT:
  494. case ACTION_CLEAR_SELECTION:
  495. {
  496. const auto state = accessibilityHandler.getCurrentState();
  497. if (state.isSelectable() || state.isMultiSelectable())
  498. {
  499. const auto isSelected = state.isSelected();
  500. if ((isSelected && action == ACTION_CLEAR_SELECTION)
  501. || (! isSelected && action == ACTION_SELECT))
  502. {
  503. return accessibilityHandler.getActions().invoke (AccessibilityActionType::toggle);
  504. }
  505. }
  506. break;
  507. }
  508. case ACTION_EXPAND:
  509. case ACTION_COLLAPSE:
  510. {
  511. const auto state = accessibilityHandler.getCurrentState();
  512. if (state.isExpandable())
  513. {
  514. const auto isExpanded = state.isExpanded();
  515. if ((isExpanded && action == ACTION_COLLAPSE)
  516. || (! isExpanded && action == ACTION_EXPAND))
  517. {
  518. return accessibilityHandler.getActions().invoke (AccessibilityActionType::showMenu);
  519. }
  520. }
  521. break;
  522. }
  523. case ACTION_NEXT_AT_MOVEMENT_GRANULARITY: return moveCursor (arguments, true);
  524. case ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: return moveCursor (arguments, false);
  525. case ACTION_SET_SELECTION:
  526. {
  527. if (auto* textInterface = accessibilityHandler.getTextInterface())
  528. {
  529. auto* env = getEnv();
  530. const auto selection = [&]() -> Range<int>
  531. {
  532. const auto selectionStartKey = javaString ("ACTION_ARGUMENT_SELECTION_START_INT");
  533. const auto selectionEndKey = javaString ("ACTION_ARGUMENT_SELECTION_END_INT");
  534. const auto hasKey = [&env, &arguments] (const auto& key)
  535. {
  536. return env->CallBooleanMethod (arguments, AndroidBundle.containsKey, key.get());
  537. };
  538. if (hasKey (selectionStartKey) && hasKey (selectionEndKey))
  539. {
  540. const auto getKey = [&env, &arguments] (const auto& key)
  541. {
  542. return env->CallIntMethod (arguments, AndroidBundle.getInt, key.get());
  543. };
  544. const auto start = getKey (selectionStartKey);
  545. const auto end = getKey (selectionEndKey);
  546. return Range<int>::between (start, end);
  547. }
  548. return {};
  549. }();
  550. textInterface->setSelection (selection);
  551. return true;
  552. }
  553. break;
  554. }
  555. case ACTION_SET_TEXT:
  556. {
  557. if (auto* textInterface = accessibilityHandler.getTextInterface())
  558. {
  559. if (! textInterface->isReadOnly())
  560. {
  561. const auto charSequenceKey = javaString ("ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE");
  562. auto* env = getEnv();
  563. const auto text = [&]() -> String
  564. {
  565. if (env->CallBooleanMethod (arguments, AndroidBundle.containsKey, charSequenceKey.get()))
  566. {
  567. LocalRef<jobject> charSequence (env->CallObjectMethod (arguments,
  568. AndroidBundle.getCharSequence,
  569. charSequenceKey.get()));
  570. LocalRef<jstring> textStringRef ((jstring) env->CallObjectMethod (charSequence,
  571. JavaCharSequence.toString));
  572. return juceString (textStringRef.get());
  573. }
  574. return {};
  575. }();
  576. textInterface->setText (text);
  577. }
  578. }
  579. break;
  580. }
  581. case ACTION_SCROLL_BACKWARD:
  582. case ACTION_SCROLL_FORWARD:
  583. {
  584. if (auto* valueInterface = accessibilityHandler.getValueInterface())
  585. {
  586. if (! valueInterface->isReadOnly())
  587. {
  588. const auto range = valueInterface->getRange();
  589. if (range.isValid())
  590. {
  591. const auto interval = action == ACTION_SCROLL_BACKWARD ? -range.getInterval()
  592. : range.getInterval();
  593. valueInterface->setValue (jlimit (range.getMinimumValue(),
  594. range.getMaximumValue(),
  595. valueInterface->getCurrentValue() + interval));
  596. // required for Android to announce the new value
  597. sendAccessibilityEventImpl (accessibilityHandler, TYPE_VIEW_SELECTED, 0);
  598. return true;
  599. }
  600. }
  601. }
  602. break;
  603. }
  604. }
  605. return false;
  606. }
  607. bool isInPopulateNodeInfo() const noexcept { return inPopulateNodeInfo; }
  608. static bool areAnyAccessibilityClientsActive()
  609. {
  610. auto* env = getEnv();
  611. auto appContext = getAppContext();
  612. if (appContext.get() != nullptr)
  613. {
  614. LocalRef<jobject> accessibilityManager (env->CallObjectMethod (appContext.get(), AndroidContext.getSystemService,
  615. javaString ("accessibility").get()));
  616. if (accessibilityManager != nullptr)
  617. return env->CallBooleanMethod (accessibilityManager.get(), AndroidAccessibilityManager.isEnabled);
  618. }
  619. return false;
  620. }
  621. template <typename ModificationCallback>
  622. static void sendAccessibilityEventExtendedImpl (const AccessibilityHandler& handler,
  623. int eventType,
  624. ModificationCallback&& modificationCallback)
  625. {
  626. if (! areAnyAccessibilityClientsActive())
  627. return;
  628. if (const auto sourceView = getSourceView (handler))
  629. {
  630. const auto* nativeImpl = handler.getNativeImplementation();
  631. if (nativeImpl == nullptr || nativeImpl->isInPopulateNodeInfo())
  632. return;
  633. auto* env = getEnv();
  634. auto appContext = getAppContext();
  635. if (appContext.get() == nullptr)
  636. return;
  637. LocalRef<jobject> event (env->CallStaticObjectMethod (AndroidAccessibilityEvent,
  638. AndroidAccessibilityEvent.obtain,
  639. eventType));
  640. env->CallVoidMethod (event,
  641. AndroidAccessibilityEvent.setPackageName,
  642. env->CallObjectMethod (appContext.get(),
  643. AndroidContext.getPackageName));
  644. env->CallVoidMethod (event,
  645. AndroidAccessibilityEvent.setSource,
  646. sourceView,
  647. nativeImpl->getVirtualViewId());
  648. modificationCallback (event);
  649. env->CallBooleanMethod (sourceView,
  650. AndroidViewGroup.requestSendAccessibilityEvent,
  651. sourceView,
  652. event.get());
  653. }
  654. }
  655. static void sendAccessibilityEventImpl (const AccessibilityHandler& handler, int eventType, int contentChangeTypes)
  656. {
  657. sendAccessibilityEventExtendedImpl (handler, eventType, [contentChangeTypes] (auto event)
  658. {
  659. if (contentChangeTypes != 0 && accessibilityEventSetContentChangeTypes != nullptr)
  660. getEnv()->CallVoidMethod (event,
  661. accessibilityEventSetContentChangeTypes,
  662. contentChangeTypes);
  663. });
  664. }
  665. private:
  666. static std::unordered_map<int, AccessibilityHandler*> virtualViewIdMap;
  667. static int getNextVirtualViewId()
  668. {
  669. static int counter = 0;
  670. return counter++;
  671. }
  672. static int getVirtualViewIdForHandler (const AccessibilityHandler& handler)
  673. {
  674. if (handler.getComponent().isOnDesktop())
  675. return HOST_VIEW_ID;
  676. return getNextVirtualViewId();
  677. }
  678. LocalRef<jstring> getDescriptionString() const
  679. {
  680. const auto valueString = [this]() -> String
  681. {
  682. if (auto* textInterface = accessibilityHandler.getTextInterface())
  683. return textInterface->getAllText();
  684. if (auto* valueInterface = accessibilityHandler.getValueInterface())
  685. return valueInterface->getCurrentValueAsString();
  686. return {};
  687. }();
  688. StringArray strings (accessibilityHandler.getTitle(),
  689. valueString,
  690. accessibilityHandler.getDescription(),
  691. accessibilityHandler.getHelp());
  692. strings.removeEmptyStrings();
  693. return javaString (strings.joinIntoString (","));
  694. }
  695. bool moveCursor (jobject arguments, bool forwards)
  696. {
  697. using ATH = AccessibilityTextHelpers;
  698. auto* textInterface = accessibilityHandler.getTextInterface();
  699. if (textInterface == nullptr)
  700. return false;
  701. const auto granularityKey = javaString ("ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT");
  702. const auto extendSelectionKey = javaString ("ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN");
  703. auto* env = getEnv();
  704. const auto boundaryType = [&]
  705. {
  706. const auto granularity = env->CallIntMethod (arguments, AndroidBundle.getInt, granularityKey.get());
  707. using BoundaryType = ATH::BoundaryType;
  708. switch (granularity)
  709. {
  710. case MOVEMENT_GRANULARITY_CHARACTER: return BoundaryType::character;
  711. case MOVEMENT_GRANULARITY_WORD: return BoundaryType::word;
  712. case MOVEMENT_GRANULARITY_LINE: return BoundaryType::line;
  713. case MOVEMENT_GRANULARITY_PARAGRAPH:
  714. case MOVEMENT_GRANULARITY_PAGE: return BoundaryType::document;
  715. }
  716. jassertfalse;
  717. return BoundaryType::character;
  718. }();
  719. const auto direction = forwards
  720. ? ATH::Direction::forwards
  721. : ATH::Direction::backwards;
  722. const auto extend = env->CallBooleanMethod (arguments, AndroidBundle.getBoolean, extendSelectionKey.get())
  723. ? ATH::ExtendSelection::yes
  724. : ATH::ExtendSelection::no;
  725. const auto oldSelection = textInterface->getSelection();
  726. const auto newSelection = ATH::findNewSelectionRangeAndroid (*textInterface, boundaryType, extend, direction);
  727. textInterface->setSelection (newSelection);
  728. // Required for Android to read back the text that the cursor moved over
  729. sendAccessibilityEventExtendedImpl (accessibilityHandler, TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY, [&] (auto event)
  730. {
  731. env->CallVoidMethod (event,
  732. AndroidAccessibilityEvent.setAction,
  733. forwards ? ACTION_NEXT_AT_MOVEMENT_GRANULARITY : ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
  734. env->CallVoidMethod (event,
  735. AndroidAccessibilityEvent.setFromIndex,
  736. oldSelection.getStart() != newSelection.getStart() ? oldSelection.getStart()
  737. : oldSelection.getEnd());
  738. env->CallVoidMethod (event,
  739. AndroidAccessibilityEvent.setToIndex,
  740. oldSelection.getStart() != newSelection.getStart() ? newSelection.getStart()
  741. : newSelection.getEnd());
  742. });
  743. return true;
  744. }
  745. AccessibilityHandler& accessibilityHandler;
  746. const int virtualViewId;
  747. std::optional<int> nativeChildViewId;
  748. bool inPopulateNodeInfo = false;
  749. //==============================================================================
  750. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibilityNativeHandle)
  751. JUCE_DECLARE_WEAK_REFERENCEABLE (AccessibilityNativeHandle)
  752. };
  753. std::unordered_map<int, AccessibilityHandler*> AccessibilityNativeHandle::virtualViewIdMap;
  754. class AccessibilityHandler::AccessibilityNativeImpl : public AccessibilityNativeHandle
  755. {
  756. public:
  757. using AccessibilityNativeHandle::AccessibilityNativeHandle;
  758. };
  759. //==============================================================================
  760. AccessibilityNativeHandle* AccessibilityHandler::getNativeImplementation() const
  761. {
  762. return nativeImpl.get();
  763. }
  764. void detail::AccessibilityHelpers::notifyAccessibilityEvent (const AccessibilityHandler& handler,
  765. Event eventType)
  766. {
  767. if (eventType == Event::elementCreated
  768. || eventType == Event::elementDestroyed
  769. || eventType == Event::elementMovedOrResized)
  770. {
  771. if (auto* parent = handler.getParent())
  772. AccessibilityNativeHandle::sendAccessibilityEventImpl (*parent, TYPE_WINDOW_CONTENT_CHANGED, CONTENT_CHANGE_TYPE_SUBTREE);
  773. return;
  774. }
  775. auto notification = [&handler, eventType]
  776. {
  777. switch (eventType)
  778. {
  779. case Event::focusChanged:
  780. return handler.hasFocus (false) ? TYPE_VIEW_ACCESSIBILITY_FOCUSED
  781. : TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED;
  782. case Event::elementCreated:
  783. case Event::elementDestroyed:
  784. case Event::elementMovedOrResized:
  785. case Event::windowOpened:
  786. case Event::windowClosed:
  787. break;
  788. }
  789. return 0;
  790. }();
  791. if (notification != 0)
  792. AccessibilityNativeHandle::sendAccessibilityEventImpl (handler, notification, 0);
  793. }
  794. void AccessibilityHandler::notifyAccessibilityEvent (AccessibilityEvent eventType) const
  795. {
  796. auto notification = [eventType]
  797. {
  798. switch (eventType)
  799. {
  800. case AccessibilityEvent::textSelectionChanged: return TYPE_VIEW_TEXT_SELECTION_CHANGED;
  801. case AccessibilityEvent::textChanged: return TYPE_VIEW_TEXT_CHANGED;
  802. case AccessibilityEvent::titleChanged:
  803. case AccessibilityEvent::structureChanged: return TYPE_WINDOW_CONTENT_CHANGED;
  804. case AccessibilityEvent::rowSelectionChanged:
  805. case AccessibilityEvent::valueChanged: break;
  806. }
  807. return 0;
  808. }();
  809. if (notification == 0)
  810. return;
  811. const auto contentChangeTypes = [eventType]
  812. {
  813. if (eventType == AccessibilityEvent::titleChanged) return CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION;
  814. if (eventType == AccessibilityEvent::structureChanged) return CONTENT_CHANGE_TYPE_SUBTREE;
  815. return 0;
  816. }();
  817. AccessibilityNativeHandle::sendAccessibilityEventImpl (*this, notification, contentChangeTypes);
  818. }
  819. void AccessibilityHandler::postAnnouncement (const String& announcementString,
  820. AnnouncementPriority)
  821. {
  822. if (! AccessibilityNativeHandle::areAnyAccessibilityClientsActive())
  823. return;
  824. const auto rootView = []
  825. {
  826. LocalRef<jobject> activity (getMainActivity());
  827. if (activity != nullptr)
  828. {
  829. auto* env = getEnv();
  830. LocalRef<jobject> mainWindow (env->CallObjectMethod (activity.get(), AndroidActivity.getWindow));
  831. LocalRef<jobject> decorView (env->CallObjectMethod (mainWindow.get(), AndroidWindow.getDecorView));
  832. return LocalRef<jobject> (env->CallObjectMethod (decorView.get(), AndroidView.getRootView));
  833. }
  834. return LocalRef<jobject>();
  835. }();
  836. if (rootView != nullptr)
  837. getEnv()->CallVoidMethod (rootView.get(),
  838. AndroidView.announceForAccessibility,
  839. javaString (announcementString).get());
  840. }
  841. } // namespace juce