Skip to content
This repository has been archived by the owner on May 6, 2022. It is now read-only.

Commit

Permalink
breaking: TTS no longer responds to lifecycle
Browse files Browse the repository at this point in the history
This change stops the built-in TTS player from observing and
responding to Android lifecycle events. An app with multiple
activities will want to decide for itself which events trigger
changes in the TTS system. The new default setting will allow
playback to continue across activity transitions if Spokestack
is established as a long-lived resource not tied to a single
activity/fragment.
  • Loading branch information
space-pope committed Dec 14, 2020
1 parent 771d085 commit d258aaa
Show file tree
Hide file tree
Showing 10 changed files with 14 additions and 199 deletions.
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ dependencies {
implementation 'org.tensorflow:tensorflow-lite:2.3.0'
// for automatic playback of TTS audio
implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0'
implementation 'androidx.media:media:1.2.0'
implementation 'com.google.android.exoplayer:exoplayer-core:2.11.7'
}
Expand All @@ -85,9 +84,8 @@ spokestack = Spokestack.Builder()
.setProperty("wordpiece-vocab-path", "$cacheDir/vocab.txt")
.setProperty("spokestack-id", "your-client-id")
.setProperty("spokestack-secret", "your-secret-key")
// `applicationContext` and `lifecycle` are available inside all `Activity`s
// `applicationContext` is available inside all `Activity`s
.withAndroidContext(applicationContext)
.withLifecycle(lifecycle)
// see below; `listener` here inherits from `SpokestackAdapter`
.addListener(listener)
.build()
Expand Down
6 changes: 0 additions & 6 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,6 @@
<version>4.1.1.4</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>androidx.lifecycle</groupId>
<artifactId>lifecycle-common-java8</artifactId>
<version>2.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>androidx.media</groupId>
<artifactId>media</artifactId>
Expand Down
3 changes: 1 addition & 2 deletions src/main/java/io/spokestack/spokestack/SpeechOutput.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import android.content.Context;
import androidx.annotation.NonNull;
import androidx.lifecycle.LifecycleObserver;
import io.spokestack.spokestack.tts.AudioResponse;
import io.spokestack.spokestack.tts.TTSComponent;
import io.spokestack.spokestack.tts.TTSEvent;
Expand Down Expand Up @@ -36,7 +35,7 @@
* @see TTSComponent
*/
public abstract class SpeechOutput extends TTSComponent
implements AutoCloseable, LifecycleObserver, TTSListener {
implements AutoCloseable, TTSListener {

@Override
public void eventReceived(@NonNull TTSEvent event) {
Expand Down
35 changes: 0 additions & 35 deletions src/main/java/io/spokestack/spokestack/Spokestack.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.spokestack.spokestack;

import android.content.Context;
import androidx.lifecycle.Lifecycle;
import io.spokestack.spokestack.dialogue.DialogueManager;
import io.spokestack.spokestack.nlu.NLUManager;
import io.spokestack.spokestack.nlu.NLUResult;
Expand Down Expand Up @@ -162,17 +161,6 @@ public void setAndroidContext(Context context) {
}
}

/**
* Update the Android lifecycle used by the TTS manager.
*
* @param lifecycle The new Android lifecycle.
*/
public void setAndroidLifecycle(Lifecycle lifecycle) {
if (this.tts != null) {
this.tts.registerLifecycle(lifecycle);
}
}

// speech pipeline

/**
Expand Down Expand Up @@ -434,7 +422,6 @@ public static class Builder {
private SpeechConfig speechConfig;
private TranscriptEditor transcriptEditor;
private Context appContext;
private Lifecycle appLifecycle;

/**
* Create a Spokestack builder with a default configuration. The speech
Expand Down Expand Up @@ -514,11 +501,6 @@ public static class Builder {
* Android Application context used to manage the audio session
* for automatic playback.
* </li>
* <li>
* {@link #withLifecycle(androidx.lifecycle.Lifecycle)}:
* Android lifecycle context used to manage automatic pausing and
* resuming of audio on application lifecycle events.
* </li>
* </ul>
* </li>
* <li>
Expand Down Expand Up @@ -718,18 +700,6 @@ public Builder withAndroidContext(Context androidContext) {
return this;
}

/**
* Sets the Android Lifecycle used for management of TTS playback.
*
* @param lifecycle the Android Lifecycle.
* @return the updated builder
*/
public Builder withLifecycle(Lifecycle lifecycle) {
this.appLifecycle = lifecycle;
this.ttsBuilder.setLifecycle(lifecycle);
return this;
}

/**
* Signal that Spokestack's speech pipeline should not be used to
* recognize speech.
Expand Down Expand Up @@ -844,11 +814,6 @@ public Spokestack build() throws Exception {
+ "required for playback management; see"
+ "TTSManager.Builder.setAndroidContext()");
}
if (this.appLifecycle == null) {
throw new IllegalArgumentException("app lifecycle is "
+ "required for playback management; see"
+ "TTSManager.Builder.setLifecycle()");
}
}
if (!this.speechConfig.containsKey("dialogue-policy-file")
&& !this.speechConfig.containsKey("dialogue-policy-class")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.media.AudioAttributesCompat;
import androidx.media.AudioFocusRequestCompat;
import androidx.media.AudioManagerCompat;
Expand Down Expand Up @@ -38,15 +36,10 @@
* <p>
* The Spokestack audio player uses
* <a href="https://exoplayer.dev/">ExoPlayer</a>
* to handle automatic playback of TTS responses. It responds to events in the
* Android component lifecycle, pausing and resuming itself along with its host
* app's activities.
* </p>
*
* <p>
* Note that this audio player does not provide a UI. It is designed to be used
* within a TTS subsystem controlled by a {@link TTSManager} in an app that
* wants to delegate all media management to Spokestack; if fine control over
* to handle automatic playback of TTS responses. Note that it
* does not provide a UI. It is designed to be used within a TTS subsystem
* controlled by a {@link TTSManager} in an app that wants to delegate all
* media management to Spokestack; if fine control over
* playback is desired, consider adding a {@link TTSListener} to the {@code
* TTSManager} and managing audio via its methods.
* </p>
Expand All @@ -61,7 +54,7 @@
*/
public class SpokestackTTSOutput extends SpeechOutput
implements Player.EventListener,
AudioManager.OnAudioFocusChangeListener, DefaultLifecycleObserver {
AudioManager.OnAudioFocusChangeListener {

private final int contentType;
private final int usage;
Expand Down Expand Up @@ -250,23 +243,6 @@ public void onAudioFocusChange(int focusChange) {
}
}

@Override
public void onResume(@NonNull LifecycleOwner owner) {
this.taskHandler.run(() -> {
if (mediaPlayer != null) {
// restore player state
mediaPlayer.seekTo(playerState.window,
playerState.curPosition);
}
});
playContent();
}

@Override
public void onStop(@NonNull LifecycleOwner owner) {
pauseContent();
}

/**
* Start or resume playback of any TTS responses.
*/
Expand Down Expand Up @@ -377,18 +353,6 @@ ExoPlayer createPlayer(int usage, int contentType,
}
}

// these lifecycle methods are unused but must be implemented to maintain
// backwards compatibility with older Android APIs that don't allow the
// default implementations in DefaultLifecycleObserver

@Override public void onCreate(@NonNull LifecycleOwner owner) { }

@Override public void onStart(@NonNull LifecycleOwner owner) { }

@Override public void onPause(@NonNull LifecycleOwner owner) { }

@Override public void onDestroy(@NonNull LifecycleOwner owner) { }

// similarly, implementing these listener methods maintains backwards
// compatibility for ExoPlayer

Expand Down
42 changes: 0 additions & 42 deletions src/main/java/io/spokestack/spokestack/tts/TTSManager.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package io.spokestack.spokestack.tts;

import android.content.Context;
import androidx.annotation.NonNull;
import androidx.lifecycle.Lifecycle;
import io.spokestack.spokestack.SpeechConfig;
import io.spokestack.spokestack.SpeechOutput;

Expand Down Expand Up @@ -37,7 +35,6 @@
* "spokestack-secret",
* "5BD5483F573D691A15CFA493C1782F451D4BD666E39A9E7B2EBE287E6A72C6B6")
* .setAndroidContext(getApplicationContext())
* .setLifecycle(getLifecycle())
* .build();
* }
* </pre>
Expand All @@ -62,7 +59,6 @@ public final class TTSManager implements AutoCloseable {
private final List<TTSListener> listeners = new ArrayList<>();
private TTSService ttsService;
private SpeechOutput output;
private Lifecycle lifecycle;
private Context appContext;

/**
Expand Down Expand Up @@ -93,7 +89,6 @@ private TTSManager(Builder builder) throws Exception {
this.ttsServiceClass = builder.ttsServiceClass;
this.outputClass = builder.outputClass;
this.config = builder.config;
this.lifecycle = builder.lifecycle;
this.listeners.addAll(builder.listeners);
this.appContext = builder.appContext;
prepare();
Expand All @@ -118,24 +113,6 @@ public void stopPlayback() {
}
}

/**
* Registers the currently active lifecycle with the manager, allowing any
* output classes to handle media player components based on system
* lifecycle events.
*
* @param newLifecycle The current lifecycle.
*/
public void registerLifecycle(@NonNull Lifecycle newLifecycle) {
Lifecycle currentLifecycle = this.lifecycle;
this.lifecycle = newLifecycle;
if (output != null) {
if (currentLifecycle != null) {
currentLifecycle.removeObserver(output);
}
this.lifecycle.addObserver(output);
}
}

/**
* Sets the android context for the TTS Subsystem. Some components may
* require an application context instead of an activity context; see
Expand Down Expand Up @@ -204,9 +181,6 @@ public void prepare() throws Exception {
this.outputClass, SpeechOutput.class);
this.output.setAndroidContext(appContext);
this.ttsService.addListener(this.output);
if (this.lifecycle != null) {
this.registerLifecycle(this.lifecycle);
}
}
for (TTSListener listener : this.listeners) {
this.ttsService.addListener(listener);
Expand Down Expand Up @@ -239,9 +213,6 @@ private <T> T createComponent(String className, Class<T> clazz)
public void release() {
if (this.output != null) {
try {
if (this.lifecycle != null) {
this.lifecycle.removeObserver(this.output);
}
this.output.close();
} catch (Exception e) {
raiseError(e);
Expand Down Expand Up @@ -271,7 +242,6 @@ public static final class Builder {
private String ttsServiceClass;
private String outputClass;
private Context appContext;
private Lifecycle lifecycle;
private SpeechConfig config = new SpeechConfig();
private List<TTSListener> listeners = new ArrayList<>();

Expand Down Expand Up @@ -342,18 +312,6 @@ public Builder setAndroidContext(Context androidContext) {
return this;
}

/**
* Sets the manager's current lifecycle.
*
* @param curLifecycle The lifecycle dispatching events to this TTS
* manager.
* @return this
*/
public Builder setLifecycle(Lifecycle curLifecycle) {
this.lifecycle = curLifecycle;
return this;
}

/**
* Adds a TTS listener.
*
Expand Down
23 changes: 5 additions & 18 deletions src/test/java/io/spokestack/spokestack/SpokestackTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import android.content.Context;
import android.os.SystemClock;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LifecycleRegistry;
import io.spokestack.spokestack.nlu.NLUManager;
import io.spokestack.spokestack.nlu.NLUResult;
import io.spokestack.spokestack.nlu.tensorflow.NLUTestUtils;
Expand Down Expand Up @@ -38,13 +36,8 @@ public void testBuild() throws Exception {

Spokestack.Builder builder = new Spokestack.Builder();

// missing lifecycle
assertThrows(IllegalArgumentException.class,
() -> {
builder
.withAndroidContext(mock(Context.class))
.build();
});
// missing context
assertThrows(IllegalArgumentException.class, builder::build);

// TTS playback disabled
// avoid creating a real websocket by also faking the service class
Expand Down Expand Up @@ -211,14 +204,11 @@ public void testTts() throws Exception {

Spokestack spokestack = new Spokestack(builder, mockNlu());

// handle context/lifecycle separately to make sure the convenience
// handle context separately to make sure the convenience
// methods work
Context androidContext = mock(Context.class);
LifecycleRegistry lifecycleRegistry =
new LifecycleRegistry(mock(LifecycleOwner.class));

spokestack.setAndroidContext(androidContext);
spokestack.setAndroidLifecycle(lifecycleRegistry);

listener.setSpokestack(spokestack);
TTSManager tts = spokestack.getTts();
Expand Down Expand Up @@ -314,12 +304,9 @@ private TTSManager.Builder mockTts() {
private Spokestack.Builder mockAndroidComponents(
Spokestack.Builder builder) {
Context context = mock(Context.class);
LifecycleRegistry lifecycleRegistry =
new LifecycleRegistry(mock(LifecycleOwner.class));

return builder
.withAndroidContext(context)
.withLifecycle(lifecycleRegistry);
.withAndroidContext(context);
}

private SpeechConfig testConfig() {
Expand Down Expand Up @@ -387,4 +374,4 @@ public void error(@NotNull SpokestackModule module,
this.errors.add(error);
}
}
}
}

0 comments on commit d258aaa

Please sign in to comment.