From def16c1a7f12221463c8fc750f4102352abd913f Mon Sep 17 00:00:00 2001 From: Jesse Jurman Date: Thu, 23 Apr 2026 21:45:50 -0400 Subject: [PATCH] Android AccessibilityManager support --- CMakeLists.txt | 1 + Dep/AndroidAccessibilityManagerHelper.java | 38 ++++++++++++ Include/SRAL.h | 6 +- README.md | 2 +- SRC/AndroidAccessibilityManager.cpp | 70 ++++++++++++++++++++++ SRC/AndroidAccessibilityManager.h | 42 +++++++++++++ SRC/SRAL.cpp | 3 + 7 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 Dep/AndroidAccessibilityManagerHelper.java create mode 100644 SRC/AndroidAccessibilityManager.cpp create mode 100644 SRC/AndroidAccessibilityManager.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 01d0d13..37e4e81 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,6 +50,7 @@ elseif(APPLE) elseif(ANDROID) target_sources(${PROJECT_NAME}_obj PRIVATE "SRC/AndroidTextToSpeech.h" "SRC/AndroidTextToSpeech.cpp" + "SRC/AndroidAccessibilityManager.h" "SRC/AndroidAccessibilityManager.cpp" "Dep/AndroidContext.h" "Dep/AndroidContext.cpp") else() target_sources(${PROJECT_NAME}_obj PRIVATE diff --git a/Dep/AndroidAccessibilityManagerHelper.java b/Dep/AndroidAccessibilityManagerHelper.java new file mode 100644 index 0000000..ad0b0d6 --- /dev/null +++ b/Dep/AndroidAccessibilityManagerHelper.java @@ -0,0 +1,38 @@ +package org.sral; + +import android.content.Context; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; + +public class AndroidAccessibilityManagerHelper { + private final Context context; + private final AccessibilityManager am; + + public AndroidAccessibilityManagerHelper(Context context) { + this.context = context; + this.am = (AccessibilityManager) + context.getSystemService(Context.ACCESSIBILITY_SERVICE); + } + + public boolean isActive() { + return am != null && am.isEnabled() && am.isTouchExplorationEnabled(); + } + + public void announce(String text, boolean interrupt) { + if (am == null || !am.isEnabled() || text == null) return; + if (interrupt) am.interrupt(); + AccessibilityEvent event = AccessibilityEvent.obtain(); + event.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT); + event.setPackageName(context.getPackageName()); + event.setClassName(AndroidAccessibilityManagerHelper.class.getName()); + event.getText().add(text); + am.sendAccessibilityEvent(event); + } + + public void stop() { + if (am != null) am.interrupt(); + } + + public void shutdown() { + } +} diff --git a/Include/SRAL.h b/Include/SRAL.h index d6d828f..3cbafb3 100644 --- a/Include/SRAL.h +++ b/Include/SRAL.h @@ -88,7 +88,11 @@ SRAL_ENGINE_NS_SPEECH = 1 << 9, SRAL_ENGINE_AV_SPEECH = 1 << 10, // --- Android Speech Synthesis Engines --- -SRAL_ENGINE_ANDROID_TEXT_TO_SPEECH = 1 << 11 + +/** @brief Android AccessibilityManager, for driving the active screen reader (most commonly TalkBack) */ +SRAL_ENGINE_ANDROID_ACCESSIBILITY_MANAGER = 1 << 11, + +SRAL_ENGINE_ANDROID_TEXT_TO_SPEECH = 1 << 12 }; /** diff --git a/README.md b/README.md index 54678de..206ab7c 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ SRAL supports Windows, macOS, iOS, Android, and Linux. | **Windows Frameworks** | Microsoft UI Automation (UIA) | | **macOS** | VoiceOver, NSSpeech, AVFoundation (AVSpeech) | | **iOS** | VoiceOver, AVFoundation (AVSpeech) | -| **Android** | Android TextToSpeech | +| **Android** | Android TextToSpeech, Android AccessibilityManager (TalkBack etc.) | | **Linux** | Speech Dispatcher | | **General APIs** | Microsoft SAPI (Windows), BRLTTY (Braille) | diff --git a/SRC/AndroidAccessibilityManager.cpp b/SRC/AndroidAccessibilityManager.cpp new file mode 100644 index 0000000..4e4b55e --- /dev/null +++ b/SRC/AndroidAccessibilityManager.cpp @@ -0,0 +1,70 @@ +#include "AndroidAccessibilityManager.h" +#include "../Dep/AndroidContext.h" + +namespace Sral { + +bool AndroidAccessibilityManager::Initialize() { + env = GetAndroidJNIEnv(); + if (!env) return false; + + jobject activity = GetAndroidActivity(); + if (!activity) return false; + + announcerClass = env->FindClass("org/sral/AndroidAccessibilityManagerHelper"); + if (!announcerClass || env->ExceptionCheck()) { + env->ExceptionClear(); + return false; + } + announcerClass = (jclass)env->NewGlobalRef(announcerClass); + + constructor = env->GetMethodID(announcerClass, "", "(Landroid/content/Context;)V"); + midIsActive = env->GetMethodID(announcerClass, "isActive", "()Z"); + midAnnounce = env->GetMethodID(announcerClass, "announce", "(Ljava/lang/String;Z)V"); + midStop = env->GetMethodID(announcerClass, "stop", "()V"); + midShutdown = env->GetMethodID(announcerClass, "shutdown", "()V"); + + if (!constructor || !midIsActive || !midAnnounce || !midStop) return false; + + jobject localObj = env->NewObject(announcerClass, constructor, activity); + if (!localObj) return false; + announcerObj = env->NewGlobalRef(localObj); + env->DeleteLocalRef(localObj); + + return true; +} + +bool AndroidAccessibilityManager::Uninitialize() { + ReleaseAllStrings(); + if (env && announcerObj) { + if (midShutdown) env->CallVoidMethod(announcerObj, midShutdown); + env->DeleteGlobalRef(announcerObj); + announcerObj = nullptr; + } + if (env && announcerClass) { + env->DeleteGlobalRef(announcerClass); + announcerClass = nullptr; + } + return true; +} + +bool AndroidAccessibilityManager::GetActive() { + if (!env || !announcerObj || !midIsActive) return false; + return env->CallBooleanMethod(announcerObj, midIsActive); +} + +bool AndroidAccessibilityManager::Speak(const char* text, bool interrupt) { + if (!env || !announcerObj || !midAnnounce) return false; + jstring jtext = env->NewStringUTF(text); + if (!jtext) return false; + env->CallVoidMethod(announcerObj, midAnnounce, jtext, (jboolean)interrupt); + env->DeleteLocalRef(jtext); + return true; +} + +bool AndroidAccessibilityManager::StopSpeech() { + if (!env || !announcerObj || !midStop) return false; + env->CallVoidMethod(announcerObj, midStop); + return true; +} + +} // namespace Sral diff --git a/SRC/AndroidAccessibilityManager.h b/SRC/AndroidAccessibilityManager.h new file mode 100644 index 0000000..3507494 --- /dev/null +++ b/SRC/AndroidAccessibilityManager.h @@ -0,0 +1,42 @@ +#ifndef ANDROID_ACCESSIBILITY_MANAGER_H_ +#define ANDROID_ACCESSIBILITY_MANAGER_H_ +#pragma once +#include "../Include/SRAL.h" +#include "Engine.h" +#include + +namespace Sral { + class AndroidAccessibilityManager final : public Engine { + public: + bool Speak(const char* text, bool interrupt) override; + bool SpeakSsml(const char* ssml, bool interrupt) override { return false; } + void* SpeakToMemory(const char* text, uint64_t* buffer_size, int* channels, int* sample_rate, int* bits_per_sample) override { return nullptr; } + bool SetParameter(int param, const void* value) override { return false; } + bool GetParameter(int param, void* value) override { return false; } + + bool Braille(const char* text) override { return false; } + bool StopSpeech() override; + bool PauseSpeech() override { return false; } + bool ResumeSpeech() override { return false; } + bool IsSpeaking() override { return false; } + int GetNumber() override { + return SRAL_ENGINE_ANDROID_ACCESSIBILITY_MANAGER; + } + bool GetActive() override; + bool Initialize() override; + bool Uninitialize() override; + int GetFeatures() override { + return SRAL_SUPPORTS_SPEECH; + } + int GetKeyFlags() override { + return HANDLE_NONE; + } + + private: + jclass announcerClass; + jmethodID constructor, midIsActive, midAnnounce, midStop, midShutdown; + JNIEnv* env; + jobject announcerObj; + }; +} +#endif diff --git a/SRC/SRAL.cpp b/SRC/SRAL.cpp index 9732b0a..1b6cf0f 100644 --- a/SRC/SRAL.cpp +++ b/SRC/SRAL.cpp @@ -20,6 +20,7 @@ #include "VoiceOver.h" #elif defined(__ANDROID__) #include "AndroidTextToSpeech.h" +#include "AndroidAccessibilityManager.h" #include "../Dep/AndroidContext.h" #else #include "SpeechDispatcher.h" @@ -238,6 +239,7 @@ extern "C" SRAL_API bool SRAL_Initialize(int engines_exclude) { g_engines[SRAL_ENGINE_NS_SPEECH] = std::make_unique(); #endif #elif defined(__ANDROID__) + g_engines[SRAL_ENGINE_ANDROID_ACCESSIBILITY_MANAGER] = std::make_unique(); g_engines[SRAL_ENGINE_ANDROID_TEXT_TO_SPEECH] = std::make_unique(); #else g_engines[SRAL_ENGINE_SPEECH_DISPATCHER] = std::make_unique(); @@ -635,6 +637,7 @@ extern "C" SRAL_API const char* SRAL_GetEngineName(int engine) { case SRAL_ENGINE_VOICE_OVER: return "Voice Over"; case SRAL_ENGINE_ZDSR: return "ZDSR"; case SRAL_ENGINE_ANDROID_TEXT_TO_SPEECH: return "Android TTS"; + case SRAL_ENGINE_ANDROID_ACCESSIBILITY_MANAGER: return "Android AccessibilityManager"; default: return "Unknown"; } }