/* ============================================================================== This file is part of the JUCE 7 technical preview. Copyright (c) 2022 - Raw Material Software Limited You may use this code under the terms of the GPL v3 (see www.gnu.org/licenses). For the technical preview this file cannot be licensed commercially. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { #define JUCE_NATIVE_ACCESSIBILITY_INCLUDED 1 #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (constructor, "", "()V") \ METHOD (setSource, "setSource", "(Landroid/view/View;I)V") \ METHOD (addChild, "addChild", "(Landroid/view/View;I)V") \ METHOD (setParent, "setParent", "(Landroid/view/View;)V") \ METHOD (setVirtualParent, "setParent", "(Landroid/view/View;I)V") \ METHOD (setBoundsInScreen, "setBoundsInScreen", "(Landroid/graphics/Rect;)V") \ METHOD (setBoundsInParent, "setBoundsInParent", "(Landroid/graphics/Rect;)V") \ METHOD (setPackageName, "setPackageName", "(Ljava/lang/CharSequence;)V") \ METHOD (setClassName, "setClassName", "(Ljava/lang/CharSequence;)V") \ METHOD (setContentDescription, "setContentDescription", "(Ljava/lang/CharSequence;)V") \ METHOD (setCheckable, "setCheckable", "(Z)V") \ METHOD (setChecked, "setChecked", "(Z)V") \ METHOD (setClickable, "setClickable", "(Z)V") \ METHOD (setEnabled, "setEnabled", "(Z)V") \ METHOD (setFocusable, "setFocusable", "(Z)V") \ METHOD (setFocused, "setFocused", "(Z)V") \ METHOD (setPassword, "setPassword", "(Z)V") \ METHOD (setSelected, "setSelected", "(Z)V") \ METHOD (setVisibleToUser, "setVisibleToUser", "(Z)V") \ METHOD (setAccessibilityFocused, "setAccessibilityFocused", "(Z)V") \ METHOD (setText, "setText", "(Ljava/lang/CharSequence;)V") \ METHOD (setMovementGranularities, "setMovementGranularities", "(I)V") \ METHOD (addAction, "addAction", "(I)V") \ DECLARE_JNI_CLASS (AndroidAccessibilityNodeInfo, "android/view/accessibility/AccessibilityNodeInfo") #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ STATICMETHOD (obtain, "obtain", "(I)Landroid/view/accessibility/AccessibilityEvent;") \ METHOD (setPackageName, "setPackageName", "(Ljava/lang/CharSequence;)V") \ METHOD (setSource, "setSource", "(Landroid/view/View;I)V") \ DECLARE_JNI_CLASS (AndroidAccessibilityEvent, "android/view/accessibility/AccessibilityEvent") #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (isEnabled, "isEnabled", "()Z") \ DECLARE_JNI_CLASS (AndroidAccessibilityManager, "android/view/accessibility/AccessibilityManager") #undef JNI_CLASS_MEMBERS namespace { constexpr int HOST_VIEW_ID = -1; constexpr int TYPE_VIEW_CLICKED = 0x00000001, TYPE_VIEW_SELECTED = 0x00000004, TYPE_VIEW_ACCESSIBILITY_FOCUSED = 0x00008000, TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED = 0x00010000, TYPE_WINDOW_CONTENT_CHANGED = 0x00000800, TYPE_VIEW_TEXT_SELECTION_CHANGED = 0x00002000, TYPE_VIEW_TEXT_CHANGED = 0x00000010; constexpr int CONTENT_CHANGE_TYPE_SUBTREE = 0x00000001, CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION = 0x00000004; constexpr int ACTION_ACCESSIBILITY_FOCUS = 0x00000040, ACTION_CLEAR_ACCESSIBILITY_FOCUS = 0x00000080, ACTION_CLEAR_FOCUS = 0x00000002, ACTION_CLEAR_SELECTION = 0x00000008, ACTION_CLICK = 0x00000010, ACTION_COLLAPSE = 0x00080000, ACTION_EXPAND = 0x00040000, ACTION_FOCUS = 0x00000001, ACTION_NEXT_AT_MOVEMENT_GRANULARITY = 0x00000100, ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY = 0x00000200, ACTION_SCROLL_BACKWARD = 0x00002000, ACTION_SCROLL_FORWARD = 0x00001000, ACTION_SELECT = 0x00000004, ACTION_SET_SELECTION = 0x00020000, ACTION_SET_TEXT = 0x00200000; constexpr int MOVEMENT_GRANULARITY_CHARACTER = 0x00000001, MOVEMENT_GRANULARITY_LINE = 0x00000004, MOVEMENT_GRANULARITY_PAGE = 0x00000010, MOVEMENT_GRANULARITY_PARAGRAPH = 0x00000008, MOVEMENT_GRANULARITY_WORD = 0x00000002, ALL_GRANULARITIES = MOVEMENT_GRANULARITY_CHARACTER | MOVEMENT_GRANULARITY_LINE | MOVEMENT_GRANULARITY_PAGE | MOVEMENT_GRANULARITY_PARAGRAPH | MOVEMENT_GRANULARITY_WORD; constexpr int ACCESSIBILITY_LIVE_REGION_POLITE = 0x00000001; } static jmethodID nodeInfoSetEditable = nullptr; static jmethodID nodeInfoSetTextSelection = nullptr; static jmethodID nodeInfoSetLiveRegion = nullptr; static jmethodID accessibilityEventSetContentChangeTypes = nullptr; static void loadSDKDependentMethods() { static bool hasChecked = false; if (! hasChecked) { hasChecked = true; auto* env = getEnv(); const auto sdkVersion = getAndroidSDKVersion(); if (sdkVersion >= 18) { nodeInfoSetEditable = env->GetMethodID (AndroidAccessibilityNodeInfo, "setEditable", "(Z)V"); nodeInfoSetTextSelection = env->GetMethodID (AndroidAccessibilityNodeInfo, "setTextSelection", "(II)V"); } if (sdkVersion >= 19) { nodeInfoSetLiveRegion = env->GetMethodID (AndroidAccessibilityNodeInfo, "setLiveRegion", "(I)V"); accessibilityEventSetContentChangeTypes = env->GetMethodID (AndroidAccessibilityEvent, "setContentChangeTypes", "(I)V"); } } } static constexpr auto getClassName (AccessibilityRole role) { switch (role) { case AccessibilityRole::editableText: return "android.widget.EditText"; case AccessibilityRole::toggleButton: return "android.widget.CheckBox"; case AccessibilityRole::radioButton: return "android.widget.RadioButton"; case AccessibilityRole::image: return "android.widget.ImageView"; case AccessibilityRole::popupMenu: return "android.widget.PopupMenu"; case AccessibilityRole::comboBox: return "android.widget.Spinner"; case AccessibilityRole::tree: return "android.widget.ExpandableListView"; case AccessibilityRole::list: return "android.widget.ListView"; case AccessibilityRole::table: return "android.widget.TableLayout"; case AccessibilityRole::progressBar: return "android.widget.ProgressBar"; case AccessibilityRole::scrollBar: case AccessibilityRole::slider: return "android.widget.SeekBar"; case AccessibilityRole::hyperlink: case AccessibilityRole::button: return "android.widget.Button"; case AccessibilityRole::label: case AccessibilityRole::staticText: return "android.widget.TextView"; case AccessibilityRole::tooltip: case AccessibilityRole::splashScreen: case AccessibilityRole::dialogWindow: return "android.widget.PopupWindow"; case AccessibilityRole::column: case AccessibilityRole::row: case AccessibilityRole::cell: case AccessibilityRole::menuItem: case AccessibilityRole::menuBar: case AccessibilityRole::listItem: case AccessibilityRole::treeItem: case AccessibilityRole::window: case AccessibilityRole::tableHeader: case AccessibilityRole::unspecified: case AccessibilityRole::group: case AccessibilityRole::ignored: break; } return "android.view.View"; } static jobject getSourceView (const AccessibilityHandler& handler) { if (auto* peer = handler.getComponent().getPeer()) return (jobject) peer->getNativeHandle(); return nullptr; } void sendAccessibilityEventImpl (const AccessibilityHandler& handler, int eventType, int contentChangeTypes); //============================================================================== class AccessibilityNativeHandle { public: static AccessibilityHandler* getAccessibilityHandlerForVirtualViewId (int virtualViewId) { auto iter = virtualViewIdMap.find (virtualViewId); if (iter != virtualViewIdMap.end()) return iter->second; return nullptr; } explicit AccessibilityNativeHandle (AccessibilityHandler& h) : accessibilityHandler (h), virtualViewId (getVirtualViewIdForHandler (accessibilityHandler)) { loadSDKDependentMethods(); if (virtualViewId != HOST_VIEW_ID) virtualViewIdMap[virtualViewId] = &accessibilityHandler; } ~AccessibilityNativeHandle() { if (virtualViewId != HOST_VIEW_ID) virtualViewIdMap.erase (virtualViewId); } int getVirtualViewId() const noexcept { return virtualViewId; } void populateNodeInfo (jobject info) { const ScopedValueSetter svs (inPopulateNodeInfo, true); const auto sourceView = getSourceView (accessibilityHandler); if (sourceView == nullptr) return; auto* env = getEnv(); auto appContext = getAppContext(); if (appContext.get() == nullptr) return; { for (auto* child : accessibilityHandler.getChildren()) env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.addChild, sourceView, child->getNativeImplementation()->getVirtualViewId()); if (auto* parent = accessibilityHandler.getParent()) env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setVirtualParent, sourceView, parent->getNativeImplementation()->getVirtualViewId()); else env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setParent, sourceView); } { const auto scale = Desktop::getInstance().getDisplays().getPrimaryDisplay()->scale; const auto screenBounds = accessibilityHandler.getComponent().getScreenBounds() * scale; LocalRef rect (env->NewObject (AndroidRect, AndroidRect.constructor, screenBounds.getX(), screenBounds.getY(), screenBounds.getRight(), screenBounds.getBottom())); env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setBoundsInScreen, rect.get()); const auto boundsInParent = accessibilityHandler.getComponent().getBoundsInParent() * scale; rect = LocalRef (env->NewObject (AndroidRect, AndroidRect.constructor, boundsInParent.getX(), boundsInParent.getY(), boundsInParent.getRight(), boundsInParent.getBottom())); env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setBoundsInParent, rect.get()); } const auto state = accessibilityHandler.getCurrentState(); env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setEnabled, ! state.isIgnored()); env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setVisibleToUser, true); env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setPackageName, env->CallObjectMethod (appContext.get(), AndroidContext.getPackageName)); env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setSource, sourceView, virtualViewId); env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setClassName, javaString (getClassName (accessibilityHandler.getRole())).get()); env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setContentDescription, getDescriptionString().get()); if (state.isFocusable()) { env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setFocusable, true); const auto& component = accessibilityHandler.getComponent(); if (component.getWantsKeyboardFocus()) { const auto hasKeyboardFocus = component.hasKeyboardFocus (false); env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setFocused, hasKeyboardFocus); env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.addAction, hasKeyboardFocus ? ACTION_CLEAR_FOCUS : ACTION_FOCUS); } const auto isAccessibleFocused = accessibilityHandler.hasFocus (false); env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setAccessibilityFocused, isAccessibleFocused); env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.addAction, isAccessibleFocused ? ACTION_CLEAR_ACCESSIBILITY_FOCUS : ACTION_ACCESSIBILITY_FOCUS); } if (state.isCheckable()) { env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setCheckable, true); env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setChecked, state.isChecked()); } if (state.isSelectable() || state.isMultiSelectable()) { const auto isSelected = state.isSelected(); env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setSelected, isSelected); env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.addAction, isSelected ? ACTION_CLEAR_SELECTION : ACTION_SELECT); } if ((accessibilityHandler.getCurrentState().isCheckable() && accessibilityHandler.getActions().contains (AccessibilityActionType::toggle)) || accessibilityHandler.getActions().contains (AccessibilityActionType::press)) { env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setClickable, true); env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.addAction, ACTION_CLICK); } if (accessibilityHandler.getActions().contains (AccessibilityActionType::showMenu) && state.isExpandable()) { env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.addAction, state.isExpanded() ? ACTION_COLLAPSE : ACTION_EXPAND); } if (auto* textInterface = accessibilityHandler.getTextInterface()) { env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setText, javaString (textInterface->getText ({ 0, textInterface->getTotalNumCharacters() })).get()); const auto isReadOnly = textInterface->isReadOnly(); env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setPassword, textInterface->isDisplayingProtectedText()); if (nodeInfoSetEditable != nullptr) env->CallVoidMethod (info, nodeInfoSetEditable, ! isReadOnly); const auto selection = textInterface->getSelection(); if (nodeInfoSetTextSelection != nullptr && ! selection.isEmpty()) env->CallVoidMethod (info, nodeInfoSetTextSelection, selection.getStart(), selection.getEnd()); if (nodeInfoSetLiveRegion != nullptr && accessibilityHandler.hasFocus (false)) env->CallVoidMethod (info, nodeInfoSetLiveRegion, ACCESSIBILITY_LIVE_REGION_POLITE); env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setMovementGranularities, ALL_GRANULARITIES); env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.addAction, ACTION_NEXT_AT_MOVEMENT_GRANULARITY); env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.addAction, ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY); env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.addAction, ACTION_SET_SELECTION); if (! isReadOnly) env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.addAction, ACTION_SET_TEXT); } if (auto* valueInterface = accessibilityHandler.getValueInterface()) { if (! valueInterface->isReadOnly()) { const auto range = valueInterface->getRange(); if (range.isValid()) { env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.addAction, ACTION_SCROLL_FORWARD); env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.addAction, ACTION_SCROLL_BACKWARD); } } } } bool performAction (int action, jobject arguments) { switch (action) { case ACTION_ACCESSIBILITY_FOCUS: { const WeakReference safeComponent (&accessibilityHandler.getComponent()); accessibilityHandler.getActions().invoke (AccessibilityActionType::focus); if (safeComponent != nullptr) accessibilityHandler.grabFocus(); return true; } case ACTION_CLEAR_ACCESSIBILITY_FOCUS: { accessibilityHandler.giveAwayFocus(); return true; } case ACTION_FOCUS: case ACTION_CLEAR_FOCUS: { auto& component = accessibilityHandler.getComponent(); if (component.getWantsKeyboardFocus()) { const auto hasFocus = component.hasKeyboardFocus (false); if (hasFocus && action == ACTION_CLEAR_FOCUS) component.giveAwayKeyboardFocus(); else if (! hasFocus && action == ACTION_FOCUS) component.grabKeyboardFocus(); return true; } break; } case ACTION_CLICK: { if ((accessibilityHandler.getCurrentState().isCheckable() && accessibilityHandler.getActions().invoke (AccessibilityActionType::toggle)) || accessibilityHandler.getActions().invoke (AccessibilityActionType::press)) { sendAccessibilityEventImpl (accessibilityHandler, TYPE_VIEW_CLICKED, 0); return true; } break; } case ACTION_SELECT: case ACTION_CLEAR_SELECTION: { const auto state = accessibilityHandler.getCurrentState(); if (state.isSelectable() || state.isMultiSelectable()) { const auto isSelected = state.isSelected(); if ((isSelected && action == ACTION_CLEAR_SELECTION) || (! isSelected && action == ACTION_SELECT)) { return accessibilityHandler.getActions().invoke (AccessibilityActionType::toggle); } } break; } case ACTION_EXPAND: case ACTION_COLLAPSE: { const auto state = accessibilityHandler.getCurrentState(); if (state.isExpandable()) { const auto isExpanded = state.isExpanded(); if ((isExpanded && action == ACTION_COLLAPSE) || (! isExpanded && action == ACTION_EXPAND)) { return accessibilityHandler.getActions().invoke (AccessibilityActionType::showMenu); } } break; } case ACTION_NEXT_AT_MOVEMENT_GRANULARITY: return moveCursor (arguments, true); case ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: return moveCursor (arguments, false); case ACTION_SET_SELECTION: { if (auto* textInterface = accessibilityHandler.getTextInterface()) { auto* env = getEnv(); const auto selection = [&]() -> Range { const auto selectionStartKey = javaString ("ACTION_ARGUMENT_SELECTION_START_INT"); const auto selectionEndKey = javaString ("ACTION_ARGUMENT_SELECTION_END_INT"); const auto hasKey = [&env, &arguments] (const auto& key) { return env->CallBooleanMethod (arguments, AndroidBundle.containsKey, key.get()); }; if (hasKey (selectionStartKey) && hasKey (selectionEndKey)) { const auto getKey = [&env, &arguments] (const auto& key) { return env->CallIntMethod (arguments, AndroidBundle.getInt, key.get()); }; return { getKey (selectionStartKey), getKey (selectionEndKey) }; } return {}; }(); textInterface->setSelection (selection); return true; } break; } case ACTION_SET_TEXT: { if (auto* textInterface = accessibilityHandler.getTextInterface()) { if (! textInterface->isReadOnly()) { const auto charSequenceKey = javaString ("ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE"); auto* env = getEnv(); const auto text = [&]() -> String { if (env->CallBooleanMethod (arguments, AndroidBundle.containsKey, charSequenceKey.get())) { LocalRef charSequence (env->CallObjectMethod (arguments, AndroidBundle.getCharSequence, charSequenceKey.get())); LocalRef textStringRef ((jstring) env->CallObjectMethod (charSequence, JavaCharSequence.toString)); return juceString (textStringRef.get()); } return {}; }(); textInterface->setText (text); } } break; } case ACTION_SCROLL_BACKWARD: case ACTION_SCROLL_FORWARD: { if (auto* valueInterface = accessibilityHandler.getValueInterface()) { if (! valueInterface->isReadOnly()) { const auto range = valueInterface->getRange(); if (range.isValid()) { const auto interval = action == ACTION_SCROLL_BACKWARD ? -range.getInterval() : range.getInterval(); valueInterface->setValue (jlimit (range.getMinimumValue(), range.getMaximumValue(), valueInterface->getCurrentValue() + interval)); // required for Android to announce the new value sendAccessibilityEventImpl (accessibilityHandler, TYPE_VIEW_SELECTED, 0); return true; } } } break; } } return false; } bool isInPopulateNodeInfo() const noexcept { return inPopulateNodeInfo; } private: static std::unordered_map virtualViewIdMap; static int getVirtualViewIdForHandler (const AccessibilityHandler& handler) { static int counter = 0; if (handler.getComponent().isOnDesktop()) return HOST_VIEW_ID; return counter++; } LocalRef getDescriptionString() const { const auto valueString = [this]() -> String { if (auto* textInterface = accessibilityHandler.getTextInterface()) return textInterface->getText ({ 0, textInterface->getTotalNumCharacters() }); if (auto* valueInterface = accessibilityHandler.getValueInterface()) return valueInterface->getCurrentValueAsString(); return {}; }(); StringArray strings (accessibilityHandler.getTitle(), valueString, accessibilityHandler.getDescription(), accessibilityHandler.getHelp()); strings.removeEmptyStrings(); return javaString (strings.joinIntoString (",")); } bool moveCursor (jobject arguments, bool forwards) { if (auto* textInterface = accessibilityHandler.getTextInterface()) { const auto granularityKey = javaString ("ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT"); const auto extendSelectionKey = javaString ("ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN"); auto* env = getEnv(); const auto boundaryType = [&] { const auto granularity = env->CallIntMethod (arguments, AndroidBundle.getInt, granularityKey.get()); using BoundaryType = AccessibilityTextHelpers::BoundaryType; switch (granularity) { case MOVEMENT_GRANULARITY_CHARACTER: return BoundaryType::character; case MOVEMENT_GRANULARITY_WORD: return BoundaryType::word; case MOVEMENT_GRANULARITY_LINE: return BoundaryType::line; case MOVEMENT_GRANULARITY_PARAGRAPH: case MOVEMENT_GRANULARITY_PAGE: return BoundaryType::document; } jassertfalse; return BoundaryType::character; }(); using Direction = AccessibilityTextHelpers::Direction; const auto cursorPos = AccessibilityTextHelpers::findTextBoundary (*textInterface, textInterface->getTextInsertionOffset(), boundaryType, forwards ? Direction::forwards : Direction::backwards); const auto newSelection = [&]() -> Range { const auto currentSelection = textInterface->getSelection(); const auto extendSelection = env->CallBooleanMethod (arguments, AndroidBundle.getBoolean, extendSelectionKey.get()); if (! extendSelection) return { cursorPos, cursorPos }; const auto start = currentSelection.getStart(); const auto end = currentSelection.getEnd(); if (forwards) return { start, jmax (start, cursorPos) }; return { jmin (start, cursorPos), end }; }(); textInterface->setSelection (newSelection); return true; } return false; } AccessibilityHandler& accessibilityHandler; const int virtualViewId; bool inPopulateNodeInfo = false; //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibilityNativeHandle) }; std::unordered_map AccessibilityNativeHandle::virtualViewIdMap; class AccessibilityHandler::AccessibilityNativeImpl : public AccessibilityNativeHandle { public: using AccessibilityNativeHandle::AccessibilityNativeHandle; }; //============================================================================== AccessibilityNativeHandle* AccessibilityHandler::getNativeImplementation() const { return nativeImpl.get(); } static bool areAnyAccessibilityClientsActive() { auto* env = getEnv(); auto appContext = getAppContext(); if (appContext.get() != nullptr) { LocalRef accessibilityManager (env->CallObjectMethod (appContext.get(), AndroidContext.getSystemService, javaString ("accessibility").get())); if (accessibilityManager != nullptr) return env->CallBooleanMethod (accessibilityManager.get(), AndroidAccessibilityManager.isEnabled); } return false; } void sendAccessibilityEventImpl (const AccessibilityHandler& handler, int eventType, int contentChangeTypes) { if (! areAnyAccessibilityClientsActive()) return; if (const auto sourceView = getSourceView (handler)) { const auto* nativeImpl = handler.getNativeImplementation(); if (nativeImpl == nullptr || nativeImpl->isInPopulateNodeInfo()) return; auto* env = getEnv(); auto appContext = getAppContext(); if (appContext.get() == nullptr) return; LocalRef event (env->CallStaticObjectMethod (AndroidAccessibilityEvent, AndroidAccessibilityEvent.obtain, eventType)); env->CallVoidMethod (event, AndroidAccessibilityEvent.setPackageName, env->CallObjectMethod (appContext.get(), AndroidContext.getPackageName)); env->CallVoidMethod (event, AndroidAccessibilityEvent.setSource, sourceView, nativeImpl->getVirtualViewId()); if (contentChangeTypes != 0 && accessibilityEventSetContentChangeTypes != nullptr) env->CallVoidMethod (event, accessibilityEventSetContentChangeTypes, contentChangeTypes); env->CallBooleanMethod (sourceView, AndroidViewGroup.requestSendAccessibilityEvent, sourceView, event.get()); } } void notifyAccessibilityEventInternal (const AccessibilityHandler& handler, InternalAccessibilityEvent eventType) { if (eventType == InternalAccessibilityEvent::elementCreated || eventType == InternalAccessibilityEvent::elementDestroyed || eventType == InternalAccessibilityEvent::elementMovedOrResized) { if (auto* parent = handler.getParent()) sendAccessibilityEventImpl (*parent, TYPE_WINDOW_CONTENT_CHANGED, CONTENT_CHANGE_TYPE_SUBTREE); return; } auto notification = [&handler, eventType] { switch (eventType) { case InternalAccessibilityEvent::focusChanged: return handler.hasFocus (false) ? TYPE_VIEW_ACCESSIBILITY_FOCUSED : TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED; case InternalAccessibilityEvent::elementCreated: case InternalAccessibilityEvent::elementDestroyed: case InternalAccessibilityEvent::elementMovedOrResized: case InternalAccessibilityEvent::windowOpened: case InternalAccessibilityEvent::windowClosed: break; } return 0; }(); if (notification != 0) sendAccessibilityEventImpl (handler, notification, 0); } void AccessibilityHandler::notifyAccessibilityEvent (AccessibilityEvent eventType) const { auto notification = [eventType] { switch (eventType) { case AccessibilityEvent::textSelectionChanged: return TYPE_VIEW_TEXT_SELECTION_CHANGED; case AccessibilityEvent::textChanged: return TYPE_VIEW_TEXT_CHANGED; case AccessibilityEvent::titleChanged: case AccessibilityEvent::structureChanged: return TYPE_WINDOW_CONTENT_CHANGED; case AccessibilityEvent::rowSelectionChanged: case AccessibilityEvent::valueChanged: break; } return 0; }(); if (notification == 0) return; const auto contentChangeTypes = [eventType] { if (eventType == AccessibilityEvent::titleChanged) return CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION; if (eventType == AccessibilityEvent::structureChanged) return CONTENT_CHANGE_TYPE_SUBTREE; return 0; }(); sendAccessibilityEventImpl (*this, notification, contentChangeTypes); } void AccessibilityHandler::postAnnouncement (const String& announcementString, AnnouncementPriority) { if (! areAnyAccessibilityClientsActive()) return; const auto rootView = [] { LocalRef activity (getMainActivity()); if (activity != nullptr) { auto* env = getEnv(); LocalRef mainWindow (env->CallObjectMethod (activity.get(), AndroidActivity.getWindow)); LocalRef decorView (env->CallObjectMethod (mainWindow.get(), AndroidWindow.getDecorView)); return LocalRef (env->CallObjectMethod (decorView.get(), AndroidView.getRootView)); } return LocalRef(); }(); if (rootView != nullptr) getEnv()->CallVoidMethod (rootView.get(), AndroidView.announceForAccessibility, javaString (announcementString).get()); } } // namespace juce