| // Copyright 2017 The Cobalt Authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package dev.cobalt.coat; |
| |
| import static dev.cobalt.util.Log.TAG; |
| |
| import android.accessibilityservice.AccessibilityServiceInfo; |
| import android.content.Context; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.speech.tts.TextToSpeech; |
| import android.view.accessibility.AccessibilityManager; |
| import dev.cobalt.util.Log; |
| import dev.cobalt.util.UsedByNative; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * Helper class to implement the SbSpeechSynthesis* Starboard API for Audio accessibility. |
| * |
| * <p>This class is intended to be a singleton in the system. It creates a single static Handler |
| * thread in lieu of other synchronization options. |
| */ |
| class CobaltTextToSpeechHelper |
| implements TextToSpeech.OnInitListener, |
| AccessibilityManager.AccessibilityStateChangeListener, |
| AccessibilityManager.TouchExplorationStateChangeListener { |
| private final Context context; |
| private final HandlerThread thread; |
| private final Handler handler; |
| |
| // The TTS engine should be used only on the background thread. |
| private TextToSpeech ttsEngine; |
| |
| private boolean wasScreenReaderEnabled; |
| |
| private enum State { |
| PENDING, |
| INITIALIZED, |
| FAILED |
| } |
| |
| // These are only accessed inside the Handler Thread |
| private State state = State.PENDING; |
| private long nextUtteranceId; |
| private final List<String> pendingUtterances = new ArrayList<>(); |
| |
| CobaltTextToSpeechHelper(Context context) { |
| this.context = context; |
| |
| thread = new HandlerThread("CobaltTextToSpeechHelper"); |
| thread.start(); |
| handler = new Handler(thread.getLooper()); |
| |
| AccessibilityManager accessibilityManager = |
| (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); |
| wasScreenReaderEnabled = isScreenReaderEnabled(); |
| accessibilityManager.addAccessibilityStateChangeListener(this); |
| accessibilityManager.addTouchExplorationStateChangeListener(this); |
| } |
| |
| public void shutdown() { |
| |
| handler.post( |
| new Runnable() { |
| @Override |
| public void run() { |
| if (ttsEngine != null) { |
| ttsEngine.shutdown(); |
| } |
| } |
| }); |
| thread.quitSafely(); |
| |
| AccessibilityManager accessibilityManager = |
| (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); |
| accessibilityManager.removeAccessibilityStateChangeListener(this); |
| accessibilityManager.removeTouchExplorationStateChangeListener(this); |
| } |
| |
| /** Returns whether a screen reader is currently enabled */ |
| @SuppressWarnings("unused") |
| @UsedByNative |
| public boolean isScreenReaderEnabled() { |
| AccessibilityManager am = |
| (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); |
| final List<AccessibilityServiceInfo> screenReaders = |
| am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_SPOKEN); |
| return !screenReaders.isEmpty(); |
| } |
| |
| /** Implementation of TextToSpeech.OnInitListener */ |
| @Override |
| public void onInit(final int status) { |
| handler.post( |
| new Runnable() { |
| @Override |
| public void run() { |
| if (status != TextToSpeech.SUCCESS) { |
| Log.e(TAG, "TextToSpeech.onInit failure: " + status); |
| state = State.FAILED; |
| return; |
| } |
| state = State.INITIALIZED; |
| for (String utterance : pendingUtterances) { |
| speak(utterance); |
| } |
| pendingUtterances.clear(); |
| } |
| }); |
| } |
| |
| /** |
| * Speaks the given text, enqueuing it if something is already speaking. Java-layer implementation |
| * of Starboard's SbSpeechSynthesisSpeak. |
| */ |
| @SuppressWarnings("unused") |
| @UsedByNative |
| void speak(final String text) { |
| |
| handler.post( |
| new Runnable() { |
| @Override |
| public void run() { |
| |
| if (ttsEngine == null) { |
| ttsEngine = new TextToSpeech(context, CobaltTextToSpeechHelper.this); |
| } |
| |
| switch (state) { |
| case PENDING: |
| pendingUtterances.add(text); |
| break; |
| case INITIALIZED: |
| int success = |
| ttsEngine.speak( |
| text, TextToSpeech.QUEUE_ADD, null, Long.toString(nextUtteranceId++)); |
| |
| if (success != TextToSpeech.SUCCESS) { |
| Log.e(TAG, "TextToSpeech.speak error: " + success); |
| return; |
| } |
| break; |
| case FAILED: |
| break; |
| } |
| } |
| }); |
| } |
| |
| /** Cancels all speaking. Java-layer implementation of Starboard's SbSpeechSynthesisCancel. */ |
| @SuppressWarnings("unused") |
| @UsedByNative |
| void cancel() { |
| handler.post( |
| new Runnable() { |
| @Override |
| public void run() { |
| if (ttsEngine != null) { |
| ttsEngine.stop(); |
| } |
| pendingUtterances.clear(); |
| } |
| }); |
| } |
| |
| @Override |
| public void onAccessibilityStateChanged(boolean enabled) { |
| // Note that this callback isn't perfect since it only tells us if accessibility was entirely |
| // enabled/disabled, but it's better than nothing. For example, it won't be called if the screen |
| // reader is enabled/disabled while text magnification remains enabled. |
| finishIfScreenReaderChanged(); |
| } |
| |
| @Override |
| public void onTouchExplorationStateChanged(boolean enabled) { |
| // We also listen for talkback changes because it's the standard (but not only) screen reader, |
| // and we can get a better signal than just listening for accessibility being enabled/disabled. |
| finishIfScreenReaderChanged(); |
| } |
| |
| /** |
| * Quit the app if screen reader settings changed so we respect the new setting the next time the |
| * app is run. This should only happen while stopped in the background since the user has to leave |
| * the app to change the setting. |
| */ |
| private void finishIfScreenReaderChanged() { |
| if (wasScreenReaderEnabled != isScreenReaderEnabled()) { |
| wasScreenReaderEnabled = isScreenReaderEnabled(); |
| nativeSendTTSChangedEvent(); |
| } |
| } |
| |
| private native void nativeSendTTSChangedEvent(); |
| } |