From a2afc2dcbaf5198f67776f8c9ed3889882aaaebf Mon Sep 17 00:00:00 2001 From: Googler Date: Tue, 9 Feb 2021 10:45:11 -0800 Subject: [PATCH] Shadow tone generator that implements a static state tracking mechanism for all played tones PiperOrigin-RevId: 356538655 --- .../shadows/ShadowToneGeneratorTest.java | 42 +++++++++ .../shadows/ShadowToneGenerator.java | 89 +++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 robolectric/src/test/java/org/robolectric/shadows/ShadowToneGeneratorTest.java create mode 100644 shadows/framework/src/main/java/org/robolectric/shadows/ShadowToneGenerator.java diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowToneGeneratorTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowToneGeneratorTest.java new file mode 100644 index 00000000000..1247898bde3 --- /dev/null +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowToneGeneratorTest.java @@ -0,0 +1,42 @@ +package org.robolectric.shadows; + +import static com.google.common.truth.Truth.assertThat; +import static org.robolectric.shadows.ShadowToneGenerator.MAXIMUM_STORED_TONES; + +import android.media.AudioManager; +import android.media.ToneGenerator; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.time.Duration; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.shadows.ShadowToneGenerator.Tone; + +/** Test class for ShadowToneGenerator */ +@RunWith(AndroidJUnit4.class) +public class ShadowToneGeneratorTest { + private static final int TONE_RELATIVE_VOLUME = 80; + private ToneGenerator toneGenerator; + + @Before + public void setUp() { + toneGenerator = new ToneGenerator(AudioManager.STREAM_DTMF, TONE_RELATIVE_VOLUME); + } + + @Test + public void testProvideToneAndDuration() { + assertThat(toneGenerator.startTone(ToneGenerator.TONE_CDMA_ALERT_NETWORK_LITE)).isTrue(); + Tone initialTone = + Tone.create(ToneGenerator.TONE_CDMA_ALERT_NETWORK_LITE, Duration.ofMillis(-1)); + + assertThat(ShadowToneGenerator.getPlayedTones()).containsExactly(initialTone); + + for (int i = 0; i < MAXIMUM_STORED_TONES; i++) { + assertThat(toneGenerator.startTone(ToneGenerator.TONE_CDMA_ABBR_ALERT, 1000)).isTrue(); + } + + assertThat(ShadowToneGenerator.getPlayedTones()).hasSize(MAXIMUM_STORED_TONES); + + assertThat(ShadowToneGenerator.getPlayedTones()).doesNotContain(initialTone); + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowToneGenerator.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowToneGenerator.java new file mode 100644 index 00000000000..f244976476e --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowToneGenerator.java @@ -0,0 +1,89 @@ +package org.robolectric.shadows; + +import android.media.ToneGenerator; +import androidx.annotation.VisibleForTesting; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import java.time.Duration; +import java.util.ArrayDeque; +import java.util.Deque; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.Resetter; + +/** + * Shadow of ToneGenerator. + * + *

Records all tones that were passed to the class. + * + *

This class uses _static_ state to store the tones that were passed to it. This is because + * users of the original class are expected to instantiate new instances of ToneGenerator on demand + * and clean up the instance after use. This makes it messy to grab the correct instance of + * ToneGenerator to properly shadow. + * + *

Additionally, there is a maximum number of tones that this class can support. Tones are stored + * in a first-in-first-out basis. + */ +@Implements(value = ToneGenerator.class) +public class ShadowToneGenerator { + // A maximum value is required to avoid OOM errors + // The number chosen here is arbitrary but should be reasonable for any use case of this class + @VisibleForTesting static final int MAXIMUM_STORED_TONES = 2000; + + // This is static because using ToneGenerator has the object created or destroyed on demand. + // This makes it very difficult for a test to get access the appropriate instance of + // toneGenerator. + // The list offers a record of all tones that have been started. + // The list has a maximum size of MAXIMUM_STORED_TONES to avoid OOM errors + private static final Deque playedTones = new ArrayDeque<>(); + + /** + * This method will intercept calls to startTone and record the played tone into a static list. + * + *

Note in the original {@link ToneGenerator}, this function will start a tone. Subsequent + * calls to this function will cancel the currently playing tone and play a new tone instead. + * Since no tone is actually played and no process is started, this tone cannot be interrupted. + */ + @Implementation + protected boolean startTone(int toneType, int durationMs) { + playedTones.add(Tone.create(toneType, Duration.ofMillis(durationMs))); + if (playedTones.size() > MAXIMUM_STORED_TONES) { + playedTones.removeFirst(); + } + + return true; + } + + /** + * This function returns the list of tones that the application requested to be played. Note that + * this will return all tones requested by all ToneGenerators. + * + * @return A defensive copy of the list of tones played by all tone generators. + */ + public static ImmutableList getPlayedTones() { + return ImmutableList.copyOf(playedTones); + } + + @Resetter + public static void reset() { + playedTones.clear(); + } + + /** Stores data about a tone played by the ToneGenerator */ + @AutoValue + public abstract static class Tone { + + /** + * The type of the tone. + * + * @see ToneGenerator for a list of possible tones + */ + public abstract int type(); + + public abstract Duration duration(); + + static Tone create(int type, Duration duration) { + return new AutoValue_ShadowToneGenerator_Tone(type, duration); + } + } +}