Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions Dep/AndroidAccessibilityManagerHelper.java
Original file line number Diff line number Diff line change
@@ -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() {
}
}
6 changes: 5 additions & 1 deletion Include/SRAL.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
};

/**
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |

Expand Down
70 changes: 70 additions & 0 deletions SRC/AndroidAccessibilityManager.cpp
Original file line number Diff line number Diff line change
@@ -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, "<init>", "(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
42 changes: 42 additions & 0 deletions SRC/AndroidAccessibilityManager.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#ifndef ANDROID_ACCESSIBILITY_MANAGER_H_
#define ANDROID_ACCESSIBILITY_MANAGER_H_
#pragma once
#include "../Include/SRAL.h"
#include "Engine.h"
#include <jni.h>

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
3 changes: 3 additions & 0 deletions SRC/SRAL.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "VoiceOver.h"
#elif defined(__ANDROID__)
#include "AndroidTextToSpeech.h"
#include "AndroidAccessibilityManager.h"
#include "../Dep/AndroidContext.h"
#else
#include "SpeechDispatcher.h"
Expand Down Expand Up @@ -238,6 +239,7 @@ extern "C" SRAL_API bool SRAL_Initialize(int engines_exclude) {
g_engines[SRAL_ENGINE_NS_SPEECH] = std::make_unique<Sral::NsSpeech>();
#endif
#elif defined(__ANDROID__)
g_engines[SRAL_ENGINE_ANDROID_ACCESSIBILITY_MANAGER] = std::make_unique<Sral::AndroidAccessibilityManager>();
g_engines[SRAL_ENGINE_ANDROID_TEXT_TO_SPEECH] = std::make_unique<Sral::AndroidTextToSpeech>();
#else
g_engines[SRAL_ENGINE_SPEECH_DISPATCHER] = std::make_unique<Sral::SpeechDispatcher>();
Expand Down Expand Up @@ -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";
}
}
Expand Down
Loading