|  | /*
  ==============================================================================
   This file is part of the JUCE library.
   Copyright (c) 2020 - Raw Material Software Limited
   JUCE is an open source library subject to commercial or open-source
   licensing.
   By using JUCE, you agree to the terms of both the JUCE 6 End-User License
   Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
   End User License Agreement: www.juce.com/juce-6-licence
   Privacy Policy: www.juce.com/juce-privacy-policy
   Or: You may also use this code under the terms of the GPL v3 (see
   www.gnu.org/licenses).
   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,              "<init>",                   "()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<bool> 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<jobject> 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<jobject> (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.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<Component> 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.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<int>
                    {
                        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<jobject> charSequence (env->CallObjectMethod (arguments,
                                                                                       AndroidBundle.getCharSequence,
                                                                                       charSequenceKey.get()));
                                LocalRef<jstring> 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<int, AccessibilityHandler*> virtualViewIdMap;
    static int getVirtualViewIdForHandler (const AccessibilityHandler& handler)
    {
        static int counter = 0;
        if (handler.getComponent().isOnDesktop())
            return HOST_VIEW_ID;
        return counter++;
    }
    LocalRef<jstring> 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<int>
            {
                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<int, AccessibilityHandler*> 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<jobject> 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<jobject> 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<jobject> activity (getMainActivity());
        if (activity != nullptr)
        {
            auto* env = getEnv();
            LocalRef<jobject> mainWindow (env->CallObjectMethod (activity.get(), AndroidActivity.getWindow));
            LocalRef<jobject> decorView (env->CallObjectMethod (mainWindow.get(), AndroidWindow.getDecorView));
            return LocalRef<jobject> (env->CallObjectMethod (decorView.get(), AndroidView.getRootView));
        }
        return LocalRef<jobject>();
    }();
    if (rootView != nullptr)
        getEnv()->CallVoidMethod (rootView.get(),
                                  AndroidView.announceForAccessibility,
                                  javaString (announcementString).get());
}
} // namespace juce
 |