From 9b6017ac4e0da4b530245616b120a188fc544ed2 Mon Sep 17 00:00:00 2001 From: Josh Ziegler Date: Fri, 10 Jan 2020 23:08:01 -0500 Subject: [PATCH] Store Android Context directly in TTSManager --- .../spokestack/spokestack/SpeechOutput.java | 6 +- .../spokestack/tts/SpokestackTTSOutput.java | 35 ++++++------ .../spokestack/spokestack/tts/TTSManager.java | 55 ++++++++++++++----- .../tts/SpokestackTTSOutputTest.java | 3 +- .../spokestack/tts/TTSManagerTest.java | 14 ++--- 5 files changed, 71 insertions(+), 42 deletions(-) diff --git a/src/main/java/io/spokestack/spokestack/SpeechOutput.java b/src/main/java/io/spokestack/spokestack/SpeechOutput.java index abbdb62..438de9d 100644 --- a/src/main/java/io/spokestack/spokestack/SpeechOutput.java +++ b/src/main/java/io/spokestack/spokestack/SpeechOutput.java @@ -52,9 +52,9 @@ public void eventReceived(TTSEvent event) { public abstract void audioReceived(AudioResponse response); /** - * Sets the output's application context. + * Sets the output's Android context. * - * @param appContext The application context. + * @param androidContext The Android context. */ - public abstract void setAppContext(Context appContext); + public abstract void setAndroidContext(Context androidContext); } diff --git a/src/main/java/io/spokestack/spokestack/tts/SpokestackTTSOutput.java b/src/main/java/io/spokestack/spokestack/tts/SpokestackTTSOutput.java index c624220..b7360d1 100644 --- a/src/main/java/io/spokestack/spokestack/tts/SpokestackTTSOutput.java +++ b/src/main/java/io/spokestack/spokestack/tts/SpokestackTTSOutput.java @@ -4,8 +4,6 @@ import android.media.AudioManager; import android.net.Uri; import android.os.Build; -import android.os.Handler; -import android.os.Looper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.DefaultLifecycleObserver; @@ -29,9 +27,9 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.util.Util; -import io.spokestack.spokestack.util.TaskHandler; import io.spokestack.spokestack.SpeechConfig; import io.spokestack.spokestack.SpeechOutput; +import io.spokestack.spokestack.util.TaskHandler; import org.jetbrains.annotations.NotNull; /** @@ -52,6 +50,14 @@ * playback is desired, consider adding a {@link TTSListener} to the {@code * TTSManager} and managing audio via its methods. *

+ * + *

+ * Additionally, this component requires an Android {@code Context} to be + * attached to the manager that has created it. If the manager is meant to + * persist across different {@code Activity}s, the {@code Context} used must + * either be the application context, or it must be re-set on the + * manager when the Activity context changes. + *

*/ public class SpokestackTTSOutput extends SpeechOutput implements Player.EventListener, @@ -60,7 +66,6 @@ public class SpokestackTTSOutput extends SpeechOutput private final int contentType; private final int usage; private TaskHandler taskHandler; - private Handler playerHandler; private PlayerFactory playerFactory; private ExoPlayer mediaPlayer; private ConcatenatingMediaSource mediaSource; @@ -74,9 +79,11 @@ public class SpokestackTTSOutput extends SpeechOutput * configuration properties, but this constructor is required * for participation in the TTS subsystem. */ - public SpokestackTTSOutput(@Nullable SpeechConfig config) { - // the constant value here is the same in ExoPlayer as in - // AudioAttributesCompat in versions 2.11.0 and v1.1.0, respectively + @SuppressWarnings("unused") + public SpokestackTTSOutput(SpeechConfig config) { + // this constant's value (1) is the same in ExoPlayer as in + // AudioAttributesCompat in versions 2.11.0 and v1.1.0, respectively, + // which is why it's used as a stand-in for both in this class this.contentType = C.CONTENT_TYPE_SPEECH; this.playerState = new PlayerState(); @@ -93,13 +100,6 @@ public SpokestackTTSOutput(@Nullable SpeechConfig config) { this.mediaSource = new ConcatenatingMediaSource(); } - private void runOnPlayerThread(Runnable task) { - if (playerHandler == null) { - playerHandler = new Handler(Looper.getMainLooper()); - } - playerHandler.post(task); - } - /** * Creates a new instance of the audio player with a custom player factory * and task handler. Used for testing. @@ -115,8 +115,8 @@ private void runOnPlayerThread(Runnable task) { } @Override - public void setAppContext(@NonNull Context context) { - this.appContext = context; + public void setAndroidContext(@NonNull Context androidContext) { + this.appContext = androidContext; } /** @@ -379,6 +379,9 @@ ExoPlayer createPlayer(int usage, int contentType, @Override public void onTimelineChanged(Timeline timeline, int reason) { } @Override + // it's deprecated, but it's still a default method, so we have to + // implement it for older versions of Android + @SuppressWarnings("deprecation") public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) { } diff --git a/src/main/java/io/spokestack/spokestack/tts/TTSManager.java b/src/main/java/io/spokestack/spokestack/tts/TTSManager.java index 4badb0c..da19209 100644 --- a/src/main/java/io/spokestack/spokestack/tts/TTSManager.java +++ b/src/main/java/io/spokestack/spokestack/tts/TTSManager.java @@ -32,7 +32,11 @@ * TTSManager ttsManager = new TTSManager.Builder() * .setTTSServiceClass("io.spokestack.spokestack.tts.SpokestackTTSService") * .setOutputClass("io.spokestack.spokestack.tts.SpokestackTTSOutput") - * .setProperty("spokestack-key", "f854fbf30a5f40c189ecb1b38bc78059") + * .setProperty("spokestack-key", "f0bc990c-e9db-4a0c-a2b1-6a6395a3d97e") + * .setProperty( + * "spokestack-secret", + * "5BD5483F573D691A15CFA493C1782F451D4BD666E39A9E7B2EBE287E6A72C6B6") + * .setAndroidContext(getApplicationContext()) * .setLifecycle(getLifecycle()) * .build(); * } @@ -58,8 +62,8 @@ public final class TTSManager implements AutoCloseable { private final List listeners = new ArrayList<>(); private TTSService ttsService; private SpeechOutput output; - private Context context; private Lifecycle lifecycle; + private Context appContext; /** * Get the current TTS service. @@ -89,9 +93,9 @@ private TTSManager(Builder builder) throws Exception { this.ttsServiceClass = builder.ttsServiceClass; this.outputClass = builder.outputClass; this.config = builder.config; - this.context = builder.context.getApplicationContext(); this.lifecycle = builder.lifecycle; this.listeners.addAll(builder.listeners); + this.appContext = builder.appContext; prepare(); } @@ -123,6 +127,22 @@ public void registerLifecycle(@NonNull Lifecycle newLifecycle) { } } + /** + * Sets the android context for the TTS Subsystem. Some components may + * require an application context instead of an activity context; + * see individual component documentation for details. + * + * @param androidContext the android context for the TTS subsystem. + * + * @see io.spokestack.spokestack.tts.SpokestackTTSOutput + */ + public void setAndroidContext(Context androidContext) { + this.appContext = androidContext; + if (this.output != null) { + this.output.setAndroidContext(androidContext); + } + } + @Override public void close() { release(); @@ -149,8 +169,8 @@ public void prepare() throws Exception { } if (this.outputClass != null && this.output == null) { this.output = createComponent(this.outputClass, SpeechOutput.class); + this.output.setAndroidContext(appContext); this.ttsService.addListener(this.output); - this.output.setAppContext(this.context); if (this.lifecycle != null) { this.registerLifecycle(this.lifecycle); } @@ -205,22 +225,15 @@ private void raiseError(Throwable e) { public static final class Builder { private String ttsServiceClass; private String outputClass; - private Context context; + private Context appContext; private Lifecycle lifecycle; private SpeechConfig config = new SpeechConfig(); private List listeners = new ArrayList<>(); /** * Initializes a new builder with no default configuration. - * - * @param appContext The context for the TTS manager. Since the manager - * is meant to cross activity boundaries, this should - * be the application context rather than an activity - * context. */ - public Builder(Context appContext) { - this.context = appContext; - } + public Builder() { } /** * Sets the class name of the external TTS service component. @@ -267,11 +280,25 @@ public Builder setProperty(String key, Object value) { return this; } + /** + * Sets the Android context for the pipeline. Some components may + * require an Application context instead of an Activity context; + * see individual component documentation for details. + * + * @param androidContext The Android context for the pipeline. + * @return this + * @see io.spokestack.spokestack.tts.SpokestackTTSOutput + */ + public Builder setAndroidContext(Context androidContext) { + this.appContext = androidContext; + return this; + } + /** * Sets the manager's current lifecycle. * * @param curLifecycle The lifecycle dispatching events to this TTS - * manager. + * manager. * @return this */ public Builder setLifecycle(Lifecycle curLifecycle) { diff --git a/src/test/java/io/spokestack/spokestack/tts/SpokestackTTSOutputTest.java b/src/test/java/io/spokestack/spokestack/tts/SpokestackTTSOutputTest.java index 02c3195..3a17376 100644 --- a/src/test/java/io/spokestack/spokestack/tts/SpokestackTTSOutputTest.java +++ b/src/test/java/io/spokestack/spokestack/tts/SpokestackTTSOutputTest.java @@ -74,7 +74,7 @@ public void testConstruction() { public void testResourceManagement() { SpokestackTTSOutput ttsOutput = new SpokestackTTSOutput(null, factory); - ttsOutput.setAppContext(mockContext); + ttsOutput.setAndroidContext(mockContext); lifecycleRegistry.addObserver(ttsOutput); ttsOutput.prepare(); lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME); @@ -197,7 +197,6 @@ private SpokestackTTSOutput spiedOutput() { .when(ttsOutput).createMediaSource(any()); doReturn(AudioManager.AUDIOFOCUS_REQUEST_GRANTED) .when(ttsOutput).requestFocus(); - ttsOutput.setAppContext(mockContext); return ttsOutput; } diff --git a/src/test/java/io/spokestack/spokestack/tts/TTSManagerTest.java b/src/test/java/io/spokestack/spokestack/tts/TTSManagerTest.java index c002a4b..859edc2 100644 --- a/src/test/java/io/spokestack/spokestack/tts/TTSManagerTest.java +++ b/src/test/java/io/spokestack/spokestack/tts/TTSManagerTest.java @@ -42,13 +42,13 @@ public void before() { public void testBuilder() throws Exception { // invalid TTS service assertThrows(ClassNotFoundException.class, - () -> new TTSManager.Builder(context) + () -> new TTSManager.Builder() .setTTSServiceClass("invalid") .build()); // invalid output component assertThrows(ClassNotFoundException.class, - () -> new TTSManager.Builder(context) + () -> new TTSManager.Builder() .setTTSServiceClass("io.spokestack.spokestack.tts.SpokestackTTSService") .setProperty("spokestack-key", "test") .setProperty("spokestack-secret", "test") @@ -60,11 +60,12 @@ public void testBuilder() throws Exception { LifecycleRegistry lifecycleRegistry = new LifecycleRegistry(mock(LifecycleOwner.class)); - TTSManager manager = new TTSManager.Builder(context) + TTSManager manager = new TTSManager.Builder() .setTTSServiceClass("io.spokestack.spokestack.tts.TTSManagerTest$Input") .setOutputClass("io.spokestack.spokestack.tts.TTSManagerTest$Output") .setProperty("spokestack-key", "test") .setConfig(new SpeechConfig()) + .setAndroidContext(context) .setLifecycle(lifecycleRegistry) .addTTSListener(this) .build(); @@ -138,8 +139,8 @@ public void close() { public static class Output extends SpeechOutput implements DefaultLifecycleObserver { - public Output(SpeechConfig config) { - } + @SuppressWarnings("unused") + public Output(SpeechConfig config) { } @Override public void onResume(@NotNull LifecycleOwner owner) { @@ -155,8 +156,7 @@ public void audioReceived(AudioResponse response) { } @Override - public void setAppContext(Context appContext) { - } + public void setAndroidContext(Context appContext) { } @Override public void close() {