From addff744edc1df08ff1eab9aefd7ae2488a03c7e Mon Sep 17 00:00:00 2001
From: Lais Andrade <lsandrade@google.com>
Date: Fri, 13 Oct 2023 11:36:08 +0100
Subject: [PATCH] Move RingtoneTest to media/tests/ringtone

Move ringtone tests to media/tests/ringtone module that maps to run on
presubmit on any code changes on *Ringtone*.java files.

Split the tests into Ringtone general ones, relying on
RingtoneManager.getRingtone for instantiation, and new API tests in
RingtoneBuilderTest, covering the new sound+vibration features.

Fix: 304497672
Test: atest com.android.media.RingtoneTest
      atest com.android.media.RingtoneBuilderTest
Change-Id: Idcaf8851252d1dba1bc89fa8639b7d9cde27d281
---
 media/java/android/media/RingtoneV1.java      |  43 +--
 media/tests/ringtone/Android.bp               |  15 +-
 media/tests/ringtone/OWNERS                   |   3 +
 .../android/media/RingtoneBuilderTest.java}   | 285 +++---------------
 .../media/testing/MediaPlayerTestHelper.java  |  75 +++++
 .../RingtoneInjectablesTrackingTestRule.java  | 225 ++++++++++++++
 6 files changed, 369 insertions(+), 277 deletions(-)
 create mode 100644 media/tests/ringtone/OWNERS
 rename media/tests/{MediaFrameworkTest/src/com/android/mediaframeworktest/unit/RingtoneTest.java => ringtone/src/com/android/media/RingtoneBuilderTest.java} (70%)
 create mode 100644 media/tests/ringtone/src/com/android/media/testing/MediaPlayerTestHelper.java
 create mode 100644 media/tests/ringtone/src/com/android/media/testing/RingtoneInjectablesTrackingTestRule.java

diff --git a/media/java/android/media/RingtoneV1.java b/media/java/android/media/RingtoneV1.java
index 3c54d4a0d166..b761afaeaa67 100644
--- a/media/java/android/media/RingtoneV1.java
+++ b/media/java/android/media/RingtoneV1.java
@@ -16,15 +16,14 @@
 
 package android.media;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.content.res.AssetFileDescriptor;
 import android.content.res.Resources.NotFoundException;
 import android.media.audiofx.HapticGenerator;
 import android.net.Uri;
 import android.os.Binder;
-import android.os.Build;
 import android.os.RemoteException;
 import android.os.Trace;
 import android.os.VibrationEffect;
@@ -62,6 +61,7 @@ class RingtoneV1 implements Ringtone.ApiInterface {
 
     private final Context mContext;
     private final AudioManager mAudioManager;
+    private final Ringtone.Injectables mInjectables;
     private VolumeShaper.Configuration mVolumeShaperConfig;
     private VolumeShaper mVolumeShaper;
 
@@ -74,12 +74,10 @@ class RingtoneV1 implements Ringtone.ApiInterface {
     private final IRingtonePlayer mRemotePlayer;
     private final Binder mRemoteToken;
 
-    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     private MediaPlayer mLocalPlayer;
     private final MyOnCompletionListener mCompletionListener = new MyOnCompletionListener();
     private HapticGenerator mHapticGenerator;
 
-    @UnsupportedAppUsage
     private Uri mUri;
     private String mTitle;
 
@@ -94,10 +92,15 @@ class RingtoneV1 implements Ringtone.ApiInterface {
     private boolean mHapticGeneratorEnabled = false;
     private final Object mPlaybackSettingsLock = new Object();
 
-    /** {@hide} */
-    @UnsupportedAppUsage
+    /** @hide */
     public RingtoneV1(Context context, boolean allowRemote) {
+        this(context, new Ringtone.Injectables(), allowRemote);
+    }
+
+    /** @hide */
+    RingtoneV1(Context context, @NonNull Ringtone.Injectables injectables, boolean allowRemote) {
         mContext = context;
+        mInjectables = injectables;
         mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
         mAllowRemote = allowRemote;
         mRemotePlayer = allowRemote ? mAudioManager.getRingtonePlayer() : null;
@@ -200,7 +203,7 @@ class RingtoneV1 implements Ringtone.ApiInterface {
         }
         destroyLocalPlayer();
         // try opening uri locally before delegating to remote player
-        mLocalPlayer = new MediaPlayer();
+        mLocalPlayer = mInjectables.newMediaPlayer();
         try {
             mLocalPlayer.setDataSource(mContext, mUri);
             mLocalPlayer.setAudioAttributes(mAudioAttributes);
@@ -240,19 +243,7 @@ class RingtoneV1 implements Ringtone.ApiInterface {
      */
     public boolean hasHapticChannels() {
         // FIXME: support remote player, or internalize haptic channels support and remove entirely.
-        try {
-            android.os.Trace.beginSection("Ringtone.hasHapticChannels");
-            if (mLocalPlayer != null) {
-                for(MediaPlayer.TrackInfo trackInfo : mLocalPlayer.getTrackInfo()) {
-                    if (trackInfo.hasHapticChannels()) {
-                        return true;
-                    }
-                }
-            }
-        } finally {
-            android.os.Trace.endSection();
-        }
-        return false;
+        return mInjectables.hasHapticChannels(mLocalPlayer);
     }
 
     /**
@@ -334,7 +325,7 @@ class RingtoneV1 implements Ringtone.ApiInterface {
      * @see android.media.audiofx.HapticGenerator#isAvailable()
      */
     public boolean setHapticGeneratorEnabled(boolean enabled) {
-        if (!HapticGenerator.isAvailable()) {
+        if (!mInjectables.isHapticGeneratorAvailable()) {
             return false;
         }
         synchronized (mPlaybackSettingsLock) {
@@ -362,7 +353,7 @@ class RingtoneV1 implements Ringtone.ApiInterface {
             mLocalPlayer.setVolume(mVolume);
             mLocalPlayer.setLooping(mIsLooping);
             if (mHapticGenerator == null && mHapticGeneratorEnabled) {
-                mHapticGenerator = HapticGenerator.create(mLocalPlayer.getAudioSessionId());
+                mHapticGenerator = mInjectables.createHapticGenerator(mLocalPlayer);
             }
             if (mHapticGenerator != null) {
                 mHapticGenerator.setEnabled(mHapticGeneratorEnabled);
@@ -397,7 +388,6 @@ class RingtoneV1 implements Ringtone.ApiInterface {
      *
      * @hide
      */
-    @UnsupportedAppUsage
     public void setUri(Uri uri) {
         setUri(uri, null);
     }
@@ -425,7 +415,6 @@ class RingtoneV1 implements Ringtone.ApiInterface {
     }
 
     /** {@hide} */
-    @UnsupportedAppUsage
     public Uri getUri() {
         return mUri;
     }
@@ -556,7 +545,7 @@ class RingtoneV1 implements Ringtone.ApiInterface {
                 Log.e(TAG, "Could not load fallback ringtone");
                 return false;
             }
-            mLocalPlayer = new MediaPlayer();
+            mLocalPlayer = mInjectables.newMediaPlayer();
             if (afd.getDeclaredLength() < 0) {
                 mLocalPlayer.setDataSource(afd.getFileDescriptor());
             } else {
@@ -594,12 +583,12 @@ class RingtoneV1 implements Ringtone.ApiInterface {
     }
 
     public boolean isLocalOnly() {
-        return mAllowRemote;
+        return !mAllowRemote;
     }
 
     public boolean isUsingRemotePlayer() {
         // V2 testing api, but this is the v1 approximation.
-        return (mLocalPlayer == null) && mAllowRemote && (mRemotePlayer != null);
+        return (mLocalPlayer == null) && mAllowRemote && (mRemotePlayer != null) && (mUri != null);
     }
 
     class MyOnCompletionListener implements MediaPlayer.OnCompletionListener {
diff --git a/media/tests/ringtone/Android.bp b/media/tests/ringtone/Android.bp
index 55b98c4704b1..8d1e5e3a5bab 100644
--- a/media/tests/ringtone/Android.bp
+++ b/media/tests/ringtone/Android.bp
@@ -9,15 +9,24 @@ android_test {
     srcs: ["src/**/*.java"],
 
     libs: [
-        "android.test.runner",
         "android.test.base",
+        "android.test.mock",
+        "android.test.runner",
     ],
 
     static_libs: [
-        "androidx.test.rules",
-        "testng",
+        "androidx.test.ext.junit",
         "androidx.test.ext.truth",
+        "androidx.test.rules",
         "frameworks-base-testutils",
+        "mockito-target-inline-minus-junit4",
+        "testables",
+        "testng",
+    ],
+
+    jni_libs: [
+        "libdexmakerjvmtiagent",
+        "libstaticjvmtiagent",
     ],
 
     test_suites: [
diff --git a/media/tests/ringtone/OWNERS b/media/tests/ringtone/OWNERS
new file mode 100644
index 000000000000..93b44f4788c5
--- /dev/null
+++ b/media/tests/ringtone/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 345036
+
+include /services/core/java/com/android/server/vibrator/OWNERS
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/RingtoneTest.java b/media/tests/ringtone/src/com/android/media/RingtoneBuilderTest.java
similarity index 70%
rename from media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/RingtoneTest.java
rename to media/tests/ringtone/src/com/android/media/RingtoneBuilderTest.java
index 3c0c6847f557..2c8daba86d19 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/RingtoneTest.java
+++ b/media/tests/ringtone/src/com/android/media/RingtoneBuilderTest.java
@@ -14,20 +14,22 @@
  * limitations under the License.
  */
 
-package com.android.mediaframeworktest.unit;
+package com.android.media;
 
 import static android.media.Ringtone.MEDIA_SOUND;
 import static android.media.Ringtone.MEDIA_SOUND_AND_VIBRATION;
 import static android.media.Ringtone.MEDIA_VIBRATION;
 
+import static com.android.media.testing.MediaPlayerTestHelper.verifyPlayerFallbackSetup;
+import static com.android.media.testing.MediaPlayerTestHelper.verifyPlayerSetup;
+import static com.android.media.testing.MediaPlayerTestHelper.verifyPlayerStarted;
+import static com.android.media.testing.MediaPlayerTestHelper.verifyPlayerStopped;
+
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isNull;
-import static org.mockito.Mockito.doCallRealMethod;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.never;
@@ -53,34 +55,29 @@ import android.os.VibrationAttributes;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
 import android.testing.TestableContext;
-import android.util.ArrayMap;
-import android.util.ArraySet;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.mediaframeworktest.R;
+import com.android.framework.base.media.ringtone.tests.R;
+import com.android.media.testing.RingtoneInjectablesTrackingTestRule;
 
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.TestRule;
-import org.junit.runner.Description;
 import org.junit.runner.RunWith;
-import org.junit.runners.model.Statement;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
 import org.mockito.Mock;
-import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
 import java.io.FileNotFoundException;
-import java.util.ArrayDeque;
-import java.util.Map;
-import java.util.Queue;
 
+/**
+ * Test behavior of {@link Ringtone} when it's created via {@link Ringtone.Builder}.
+ */
 @RunWith(AndroidJUnit4.class)
-public class RingtoneTest {
+public class RingtoneBuilderTest {
 
     private static final Uri SOUND_URI = Uri.parse("content://fake-sound-uri");
 
@@ -93,11 +90,8 @@ public class RingtoneTest {
 
     private static final VibrationEffect VIBRATION_EFFECT =
             VibrationEffect.createWaveform(new long[] { 0, 100, 50, 100}, -1);
-    private static final VibrationEffect VIBRATION_EFFECT_REPEATING =
-            VibrationEffect.createWaveform(new long[] { 0, 100, 50, 100, 50}, 1);
 
-    @Rule
-    public final RingtoneInjectablesTrackingTestRule
+    @Rule public final RingtoneInjectablesTrackingTestRule
             mMediaPlayerRule = new RingtoneInjectablesTrackingTestRule();
 
     @Captor private ArgumentCaptor<IBinder> mIBinderCaptor;
@@ -122,6 +116,7 @@ public class RingtoneTest {
         mContext = spy(testContext);
     }
 
+
     @Test
     public void testRingtone_fullLifecycleUsingLocalMediaPlayer() throws Exception {
         MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer();
@@ -142,14 +137,14 @@ public class RingtoneTest {
         assertThat(ringtone.isLocalOnly()).isFalse();
 
         // Prepare
-        verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES);
+        verifyPlayerSetup(mContext, mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES);
         verify(mockMediaPlayer).setVolume(1.0f);
         verify(mockMediaPlayer).setLooping(false);
         verify(mockMediaPlayer).prepare();
 
         // Play
         ringtone.play();
-        verifyLocalPlay(mockMediaPlayer);
+        verifyPlayerStarted(mockMediaPlayer);
 
         // Verify dynamic controls.
         ringtone.setVolume(0.8f);
@@ -165,7 +160,7 @@ public class RingtoneTest {
 
         // Release
         ringtone.stop();
-        verifyLocalStop(mockMediaPlayer);
+        verifyPlayerStopped(mockMediaPlayer);
 
         // This test is intended to strictly verify all interactions with MediaPlayer in a local
         // playback case. This shouldn't be necessary in other tests that have the same basic
@@ -199,16 +194,16 @@ public class RingtoneTest {
         assertThat(ringtone.getAudioAttributes()).isEqualTo(audioAttributes);
 
         // Prepare
-        verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, audioAttributes);
+        verifyPlayerSetup(mContext, mockMediaPlayer, SOUND_URI, audioAttributes);
         verify(mockMediaPlayer).prepare();
 
         // Play
         ringtone.play();
-        verifyLocalPlay(mockMediaPlayer);
+        verifyPlayerStarted(mockMediaPlayer);
 
         // Release
         ringtone.stop();
-        verifyLocalStop(mockMediaPlayer);
+        verifyPlayerStopped(mockMediaPlayer);
 
         verifyZeroInteractions(mMockRemotePlayer);
         verifyZeroInteractions(mMockVibrator);
@@ -220,8 +215,8 @@ public class RingtoneTest {
         setupFileNotFound(mockMediaPlayer, SOUND_URI);
         Ringtone ringtone =
                 newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES)
-                .setUri(SOUND_URI)
-                .build();
+                        .setUri(SOUND_URI)
+                        .build();
         assertThat(ringtone).isNotNull();
         assertThat(ringtone.isUsingRemotePlayer()).isTrue();
 
@@ -284,7 +279,7 @@ public class RingtoneTest {
         // Prepare
         // Uses attributes with haptic channels enabled, but will use the effect when there aren't
         // any present.
-        verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC);
+        verifyPlayerSetup(mContext, mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC);
         verify(mockMediaPlayer).setVolume(1.0f);
         verify(mockMediaPlayer).setLooping(false);
         verify(mockMediaPlayer).prepare();
@@ -292,7 +287,7 @@ public class RingtoneTest {
         // Play
         ringtone.play();
 
-        verifyLocalPlay(mockMediaPlayer);
+        verifyPlayerStarted(mockMediaPlayer);
         verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES);
 
         // Verify dynamic controls.
@@ -310,7 +305,7 @@ public class RingtoneTest {
 
         // Release
         ringtone.stop();
-        verifyLocalStop(mockMediaPlayer);
+        verifyPlayerStopped(mockMediaPlayer);
         verify(mMockVibrator).cancel(VibrationAttributes.USAGE_RINGTONE);
 
         // This test is intended to strictly verify all interactions with MediaPlayer in a local
@@ -388,7 +383,7 @@ public class RingtoneTest {
         // Prepare
         // Uses attributes with haptic channels enabled, but will abandon the MediaPlayer when it
         // knows there aren't any.
-        verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC);
+        verifyPlayerSetup(mContext, mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC);
         verify(mockMediaPlayer).setVolume(0.0f);  // Vibration-only: sound muted.
         verify(mockMediaPlayer).setLooping(false);
         verify(mockMediaPlayer).prepare();
@@ -443,7 +438,7 @@ public class RingtoneTest {
         // Prepare
         // Uses attributes with haptic channels enabled, but will use the effect when there aren't
         // any present.
-        verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC);
+        verifyPlayerSetup(mContext, mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC);
         verify(mockMediaPlayer).setVolume(0.0f);  // Vibration-only: sound muted.
         verify(mockMediaPlayer).setLooping(false);
         verify(mockMediaPlayer).prepare();
@@ -451,7 +446,7 @@ public class RingtoneTest {
         // Play
         ringtone.play();
         // Vibrator.vibrate isn't called because the vibration comes from the sound.
-        verifyLocalPlay(mockMediaPlayer);
+        verifyPlayerStarted(mockMediaPlayer);
 
         // Verify dynamic controls (no-op without sound)
         ringtone.setVolume(0.8f);
@@ -466,7 +461,7 @@ public class RingtoneTest {
 
         // Release
         ringtone.stop();
-        verifyLocalStop(mockMediaPlayer);
+        verifyPlayerStopped(mockMediaPlayer);
 
         // This test is intended to strictly verify all interactions with MediaPlayer in a local
         // playback case. This shouldn't be necessary in other tests that have the same basic
@@ -496,17 +491,17 @@ public class RingtoneTest {
 
         // Prepare
         // The attributes here have haptic channels enabled (unlike above)
-        verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC);
+        verifyPlayerSetup(mContext, mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC);
         verify(mockMediaPlayer).prepare();
 
         // Play
         ringtone.play();
         when(mockMediaPlayer.isPlaying()).thenReturn(true);
-        verifyLocalPlay(mockMediaPlayer);
+        verifyPlayerStarted(mockMediaPlayer);
 
         // Release
         ringtone.stop();
-        verifyLocalStop(mockMediaPlayer);
+        verifyPlayerStopped(mockMediaPlayer);
 
         verifyZeroInteractions(mMockRemotePlayer);
         // Nothing after the initial hasVibrator - it uses audio-coupled.
@@ -536,7 +531,7 @@ public class RingtoneTest {
 
         // Prepare
         // The attributes here have haptic channels enabled (unlike above)
-        verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC);
+        verifyPlayerSetup(mContext, mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC);
         verify(mockMediaPlayer).prepare();
 
         // Play
@@ -559,7 +554,7 @@ public class RingtoneTest {
     @Test
     public void testRingtone_nullMediaOnBuilderUsesFallback() throws Exception {
         AssetFileDescriptor testResourceFd =
-                mContext.getResources().openRawResourceFd(R.raw.shortmp3);
+                mContext.getResources().openRawResourceFd(R.raw.test_sound_file);
         // Ensure it will flow as expected.
         assertThat(testResourceFd).isNotNull();
         assertThat(testResourceFd.getDeclaredLength()).isAtLeast(0);
@@ -575,18 +570,18 @@ public class RingtoneTest {
 
         // Delegates straight to fallback in local player.
         // Prepare
-        verifyLocalPlayerFallbackSetup(mockMediaPlayer, testResourceFd, RINGTONE_ATTRIBUTES);
+        verifyPlayerFallbackSetup(mockMediaPlayer, testResourceFd, RINGTONE_ATTRIBUTES);
         verify(mockMediaPlayer).setVolume(1.0f);
         verify(mockMediaPlayer).setLooping(false);
         verify(mockMediaPlayer).prepare();
 
         // Play
         ringtone.play();
-        verifyLocalPlay(mockMediaPlayer);
+        verifyPlayerStarted(mockMediaPlayer);
 
         // Release
         ringtone.stop();
-        verifyLocalStop(mockMediaPlayer);
+        verifyPlayerStopped(mockMediaPlayer);
 
         verifyNoMoreInteractions(mockMediaPlayer);
         verifyNoMoreInteractions(mMockRemotePlayer);
@@ -615,24 +610,10 @@ public class RingtoneTest {
         verifyNoMoreInteractions(mMockRemotePlayer);
     }
 
-    @Test
-    public void testRingtone_noMediaSetOnBuilderFallbackFailsAndNoRemote() throws Exception {
-        mContext.getOrCreateTestableResources()
-                .addOverride(com.android.internal.R.raw.fallbackring, null);
-        Ringtone ringtone = newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES)
-                .setUri(null)
-                .setLocalOnly()
-                .build();
-        // Local player fallback fails as the resource isn't found (no media player creation is
-        // attempted), and since there is no local player, the ringtone ends up having nothing to
-        // do.
-        assertThat(ringtone).isNull();
-    }
-
     private Ringtone.Builder newBuilder(@Ringtone.RingtoneMedia int ringtoneMedia,
             AudioAttributes audioAttributes) {
         return new Ringtone.Builder(mContext, ringtoneMedia, audioAttributes)
-                .setInjectables(mMediaPlayerRule.injectables);
+                .setInjectables(mMediaPlayerRule.getRingtoneTestInjectables());
     }
 
     private static AudioAttributes audioAttributes(int audioUsage) {
@@ -647,194 +628,4 @@ public class RingtoneTest {
         doThrow(new FileNotFoundException("Fake file not found"))
                 .when(mockMediaPlayer).setDataSource(any(Context.class), eq(uri));
     }
-
-    private void verifyLocalPlayerSetup(MediaPlayer mockPlayer, Uri expectedUri,
-            AudioAttributes expectedAudioAttributes) throws Exception {
-        verify(mockPlayer).setDataSource(mContext, expectedUri);
-        verify(mockPlayer).setAudioAttributes(expectedAudioAttributes);
-        verify(mockPlayer).setPreferredDevice(null);
-        verify(mockPlayer).prepare();
-    }
-
-    private void verifyLocalPlayerFallbackSetup(MediaPlayer mockPlayer, AssetFileDescriptor afd,
-            AudioAttributes expectedAudioAttributes) throws Exception {
-        // This is very specific but it's a simple way to test that the test resource matches.
-        if (afd.getDeclaredLength() < 0) {
-            verify(mockPlayer).setDataSource(afd.getFileDescriptor());
-        } else {
-            verify(mockPlayer).setDataSource(afd.getFileDescriptor(),
-                    afd.getStartOffset(),
-                    afd.getDeclaredLength());
-        }
-        verify(mockPlayer).setAudioAttributes(expectedAudioAttributes);
-        verify(mockPlayer).setPreferredDevice(null);
-        verify(mockPlayer).prepare();
-    }
-
-    private void verifyLocalPlay(MediaPlayer mockMediaPlayer) {
-        verify(mockMediaPlayer).setOnCompletionListener(any());
-        verify(mockMediaPlayer).start();
-    }
-
-    private void verifyLocalStop(MediaPlayer mockMediaPlayer) {
-        verify(mockMediaPlayer).stop();
-        verify(mockMediaPlayer).setOnCompletionListener(isNull());
-        verify(mockMediaPlayer).reset();
-        verify(mockMediaPlayer).release();
-    }
-
-    /**
-     * This rule ensures that all expected media player creations from the factory do actually
-     * occur. The reason for this level of control is that creating a media player is fairly
-     * expensive and blocking, so we do want unit tests of this class to "declare" interactions
-     * of all created media players.
-     *
-     * This needs to be a TestRule so that the teardown assertions can be skipped if the test has
-     * failed (and media player assertions may just be a distracting side effect). Otherwise, the
-     * teardown failures hide the real test ones.
-     */
-    public static class RingtoneInjectablesTrackingTestRule implements TestRule {
-        public Ringtone.Injectables injectables = new TestInjectables();
-        public boolean hapticGeneratorAvailable = true;
-
-        // Queue of (local) media players, in order of expected creation. Enqueue using
-        // expectNewMediaPlayer(), dequeued by the media player factory passed to Ringtone.
-        // This queue is asserted to be empty at the end of the test.
-        private Queue<MediaPlayer> mMockMediaPlayerQueue = new ArrayDeque<>();
-
-        // Similar to media players, but for haptic generator, which also needs releasing.
-        private Map<MediaPlayer, HapticGenerator> mMockHapticGeneratorMap = new ArrayMap<>();
-
-        // Media players with haptic channels.
-        private ArraySet<MediaPlayer> mHapticChannels = new ArraySet<>();
-
-        @Override
-        public Statement apply(Statement base, Description description) {
-            return new Statement() {
-                @Override
-                public void evaluate() throws Throwable {
-                    base.evaluate();
-                    // Only assert if the test didn't fail (base.evaluate() would throw).
-                    assertWithMessage("Test setup an expectLocalMediaPlayer but it wasn't consumed")
-                            .that(mMockMediaPlayerQueue).isEmpty();
-                    // Only assert if the test didn't fail (base.evaluate() would throw).
-                    assertWithMessage(
-                            "Test setup an expectLocalHapticGenerator but it wasn't consumed")
-                            .that(mMockHapticGeneratorMap).isEmpty();
-                }
-            };
-        }
-
-        private TestMediaPlayer expectLocalMediaPlayer() {
-            TestMediaPlayer mockMediaPlayer = Mockito.mock(TestMediaPlayer.class);
-            // Delegate to simulated methods. This means they can be verified but also reflect
-            // realistic transitions from the TestMediaPlayer.
-            doCallRealMethod().when(mockMediaPlayer).start();
-            doCallRealMethod().when(mockMediaPlayer).stop();
-            doCallRealMethod().when(mockMediaPlayer).setLooping(anyBoolean());
-            when(mockMediaPlayer.isLooping()).thenCallRealMethod();
-            when(mockMediaPlayer.isLooping()).thenCallRealMethod();
-            mMockMediaPlayerQueue.add(mockMediaPlayer);
-            return mockMediaPlayer;
-        }
-
-        private HapticGenerator expectHapticGenerator(MediaPlayer mockMediaPlayer) {
-            HapticGenerator mockHapticGenerator = Mockito.mock(HapticGenerator.class);
-            // A test should never want this.
-            assertWithMessage("Can't expect a second haptic generator created "
-                    + "for one media player")
-                    .that(mMockHapticGeneratorMap.put(mockMediaPlayer, mockHapticGenerator))
-                    .isNull();
-            return mockHapticGenerator;
-        }
-
-        private void setHasHapticChannels(MediaPlayer mp, boolean hasHapticChannels) {
-            if (hasHapticChannels) {
-                mHapticChannels.add(mp);
-            } else {
-                mHapticChannels.remove(mp);
-            }
-        }
-
-        private class TestInjectables extends Ringtone.Injectables {
-            @Override
-            public MediaPlayer newMediaPlayer() {
-                assertWithMessage(
-                        "Unexpected MediaPlayer creation. Bug or need expectNewMediaPlayer")
-                        .that(mMockMediaPlayerQueue)
-                        .isNotEmpty();
-                return mMockMediaPlayerQueue.remove();
-            }
-
-            @Override
-            public boolean isHapticGeneratorAvailable() {
-                return hapticGeneratorAvailable;
-            }
-
-            @Override
-            public HapticGenerator createHapticGenerator(MediaPlayer mediaPlayer) {
-                HapticGenerator mockHapticGenerator = mMockHapticGeneratorMap.remove(mediaPlayer);
-                assertWithMessage("Unexpected HapticGenerator creation. "
-                        + "Bug or need expectHapticGenerator")
-                        .that(mockHapticGenerator)
-                        .isNotNull();
-                return mockHapticGenerator;
-            }
-
-            @Override
-            public boolean isHapticPlaybackSupported() {
-                return true;
-            }
-
-            @Override
-            public boolean hasHapticChannels(MediaPlayer mp) {
-                return mHapticChannels.contains(mp);
-            }
-        }
-    }
-
-    /**
-     * MediaPlayer relies on a native backend and so its necessary to intercept calls from
-     * fake usage hitting them.
-     *
-     * Mocks don't work directly on native calls, but if they're overridden then it does work.
-     * Some basic state faking is also done to make the mocks more realistic.
-     */
-    private static class TestMediaPlayer extends MediaPlayer {
-        private boolean mIsPlaying = false;
-        private boolean mIsLooping = false;
-
-        @Override
-        public void start() {
-            mIsPlaying = true;
-        }
-
-        @Override
-        public void stop() {
-            mIsPlaying = false;
-        }
-
-        @Override
-        public void setLooping(boolean value) {
-            mIsLooping = value;
-        }
-
-        @Override
-        public boolean isLooping() {
-            return mIsLooping;
-        }
-
-        @Override
-        public boolean isPlaying() {
-            return mIsPlaying;
-        }
-
-        void simulatePlayingFinished() {
-            if (!mIsPlaying) {
-                throw new IllegalStateException(
-                        "Attempted to pretend playing finished when not playing");
-            }
-            mIsPlaying = false;
-        }
-    }
 }
diff --git a/media/tests/ringtone/src/com/android/media/testing/MediaPlayerTestHelper.java b/media/tests/ringtone/src/com/android/media/testing/MediaPlayerTestHelper.java
new file mode 100644
index 000000000000..e97e1173a1ea
--- /dev/null
+++ b/media/tests/ringtone/src/com/android/media/testing/MediaPlayerTestHelper.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * 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 com.android.media.testing;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.media.AudioAttributes;
+import android.media.MediaPlayer;
+import android.net.Uri;
+
+/**
+ * Helper class with assertion methods on mock {@link MediaPlayer} instances.
+ */
+public final class MediaPlayerTestHelper {
+
+    /** Verify this local media player mock instance was started. */
+    public static void verifyPlayerStarted(MediaPlayer mockMediaPlayer) {
+        verify(mockMediaPlayer).setOnCompletionListener(any());
+        verify(mockMediaPlayer).start();
+    }
+
+    /** Verify this local media player mock instance was stopped and released. */
+    public static void verifyPlayerStopped(MediaPlayer mockMediaPlayer) {
+        verify(mockMediaPlayer).stop();
+        verify(mockMediaPlayer).setOnCompletionListener(isNull());
+        verify(mockMediaPlayer).reset();
+        verify(mockMediaPlayer).release();
+    }
+
+    /** Verify this local media player mock instance was setup with given attributes. */
+    public static void verifyPlayerSetup(Context context, MediaPlayer mockPlayer,
+            Uri expectedUri, AudioAttributes expectedAudioAttributes) throws Exception {
+        verify(mockPlayer).setDataSource(context, expectedUri);
+        verify(mockPlayer).setAudioAttributes(expectedAudioAttributes);
+        verify(mockPlayer).setPreferredDevice(null);
+        verify(mockPlayer).prepare();
+    }
+
+    /** Verify this local media player mock instance was setup with given fallback attributes. */
+    public static void verifyPlayerFallbackSetup(MediaPlayer mockPlayer,
+            AssetFileDescriptor afd, AudioAttributes expectedAudioAttributes) throws Exception {
+        // This is very specific but it's a simple way to test that the test resource matches.
+        if (afd.getDeclaredLength() < 0) {
+            verify(mockPlayer).setDataSource(afd.getFileDescriptor());
+        } else {
+            verify(mockPlayer).setDataSource(afd.getFileDescriptor(),
+                    afd.getStartOffset(),
+                    afd.getDeclaredLength());
+        }
+        verify(mockPlayer).setAudioAttributes(expectedAudioAttributes);
+        verify(mockPlayer).setPreferredDevice(null);
+        verify(mockPlayer).prepare();
+    }
+
+    private MediaPlayerTestHelper() {
+    }
+}
diff --git a/media/tests/ringtone/src/com/android/media/testing/RingtoneInjectablesTrackingTestRule.java b/media/tests/ringtone/src/com/android/media/testing/RingtoneInjectablesTrackingTestRule.java
new file mode 100644
index 000000000000..25752ce83e5c
--- /dev/null
+++ b/media/tests/ringtone/src/com/android/media/testing/RingtoneInjectablesTrackingTestRule.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * 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 com.android.media.testing;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.doCallRealMethod;
+import static org.mockito.Mockito.when;
+
+import android.media.MediaPlayer;
+import android.media.Ringtone;
+import android.media.audiofx.HapticGenerator;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+import org.mockito.Mockito;
+
+import java.util.ArrayDeque;
+import java.util.Map;
+import java.util.Queue;
+
+/**
+ * This rule ensures that all expected media player creations from the factory do actually
+ * occur. The reason for this level of control is that creating a media player is fairly
+ * expensive and blocking, so we do want unit tests of this class to "declare" interactions
+ * of all created media players.
+ * <p>
+ * This needs to be a TestRule so that the teardown assertions can be skipped if the test has
+ * failed (and media player assertions may just be a distracting side effect). Otherwise, the
+ * teardown failures hide the real test ones.
+ */
+public class RingtoneInjectablesTrackingTestRule implements TestRule {
+
+    private final Ringtone.Injectables mRingtoneTestInjectables = new TestInjectables();
+
+    // Queue of (local) media players, in order of expected creation. Enqueue using
+    // expectNewMediaPlayer(), dequeued by the media player factory passed to Ringtone.
+    // This queue is asserted to be empty at the end of the test.
+    private final Queue<MediaPlayer> mMockMediaPlayerQueue = new ArrayDeque<>();
+
+    // Similar to media players, but for haptic generator, which also needs releasing.
+    private final Map<MediaPlayer, HapticGenerator> mMockHapticGeneratorMap = new ArrayMap<>();
+
+    // Media players with haptic channels.
+    private final ArraySet<MediaPlayer> mHapticChannels = new ArraySet<>();
+
+    private boolean mHapticGeneratorAvailable = true;
+
+    @Override
+    public Statement apply(Statement base, Description description) {
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                base.evaluate();
+                // Only assert if the test didn't fail (base.evaluate() would throw).
+                assertWithMessage("Test setup an expectLocalMediaPlayer but it wasn't consumed")
+                        .that(mMockMediaPlayerQueue).isEmpty();
+                // Only assert if the test didn't fail (base.evaluate() would throw).
+                assertWithMessage(
+                        "Test setup an expectLocalHapticGenerator but it wasn't consumed")
+                        .that(mMockHapticGeneratorMap).isEmpty();
+            }
+        };
+    }
+
+    /** The {@link Ringtone.Injectables} to be used for creating a testable {@link Ringtone}. */
+    public Ringtone.Injectables getRingtoneTestInjectables() {
+        return mRingtoneTestInjectables;
+    }
+
+    /**
+     * Create a test {@link MediaPlayer} that will be provided to the {@link Ringtone} instance
+     * created with {@link #getRingtoneTestInjectables()}.
+     *
+     * <p>If a media player is not created during the test execution after this method is called
+     * then the test will fail. It will also fail if the ringtone attempts to create one without
+     * this method being called first.
+     */
+    public TestMediaPlayer expectLocalMediaPlayer() {
+        TestMediaPlayer mockMediaPlayer = Mockito.mock(TestMediaPlayer.class);
+        // Delegate to simulated methods. This means they can be verified but also reflect
+        // realistic transitions from the TestMediaPlayer.
+        doCallRealMethod().when(mockMediaPlayer).start();
+        doCallRealMethod().when(mockMediaPlayer).stop();
+        doCallRealMethod().when(mockMediaPlayer).setLooping(anyBoolean());
+        when(mockMediaPlayer.isLooping()).thenCallRealMethod();
+        mMockMediaPlayerQueue.add(mockMediaPlayer);
+        return mockMediaPlayer;
+    }
+
+    /**
+     * Create a test {@link HapticGenerator} that will be provided to the {@link Ringtone} instance
+     * created with {@link #getRingtoneTestInjectables()}.
+     *
+     * <p>If a haptic generator is not created during the test execution after this method is called
+     * then the test will fail. It will also fail if the ringtone attempts to create one without
+     * this method being called first.
+     */
+    public HapticGenerator expectHapticGenerator(MediaPlayer mediaPlayer) {
+        HapticGenerator mockHapticGenerator = Mockito.mock(HapticGenerator.class);
+        // A test should never want this.
+        assertWithMessage("Can't expect a second haptic generator created "
+                + "for one media player")
+                .that(mMockHapticGeneratorMap.put(mediaPlayer, mockHapticGenerator))
+                .isNull();
+        return mockHapticGenerator;
+    }
+
+    /**
+     * Configures the {@link MediaPlayer} to always return given flag when
+     * {@link Ringtone.Injectables#hasHapticChannels(MediaPlayer)} is called.
+     */
+    public void setHasHapticChannels(MediaPlayer mp, boolean hasHapticChannels) {
+        if (hasHapticChannels) {
+            mHapticChannels.add(mp);
+        } else {
+            mHapticChannels.remove(mp);
+        }
+    }
+
+    /** Test implementation of {@link Ringtone.Injectables} that uses the test rule setup. */
+    private class TestInjectables extends Ringtone.Injectables {
+        @Override
+        public MediaPlayer newMediaPlayer() {
+            assertWithMessage(
+                    "Unexpected MediaPlayer creation. Bug or need expectNewMediaPlayer")
+                    .that(mMockMediaPlayerQueue)
+                    .isNotEmpty();
+            return mMockMediaPlayerQueue.remove();
+        }
+
+        @Override
+        public boolean isHapticGeneratorAvailable() {
+            return mHapticGeneratorAvailable;
+        }
+
+        @Override
+        public HapticGenerator createHapticGenerator(MediaPlayer mediaPlayer) {
+            HapticGenerator mockHapticGenerator = mMockHapticGeneratorMap.remove(mediaPlayer);
+            assertWithMessage("Unexpected HapticGenerator creation. "
+                    + "Bug or need expectHapticGenerator")
+                    .that(mockHapticGenerator)
+                    .isNotNull();
+            return mockHapticGenerator;
+        }
+
+        @Override
+        public boolean isHapticPlaybackSupported() {
+            return true;
+        }
+
+        @Override
+        public boolean hasHapticChannels(MediaPlayer mp) {
+            return mHapticChannels.contains(mp);
+        }
+    }
+
+    /**
+     * MediaPlayer relies on a native backend and so its necessary to intercept calls from
+     * fake usage hitting them.
+     * <p>
+     * Mocks don't work directly on native calls, but if they're overridden then it does work.
+     * Some basic state faking is also done to make the mocks more realistic.
+     */
+    public static class TestMediaPlayer extends MediaPlayer {
+        private boolean mIsPlaying = false;
+        private boolean mIsLooping = false;
+
+        @Override
+        public void start() {
+            mIsPlaying = true;
+        }
+
+        @Override
+        public void stop() {
+            mIsPlaying = false;
+        }
+
+        @Override
+        public void setLooping(boolean value) {
+            mIsLooping = value;
+        }
+
+        @Override
+        public boolean isLooping() {
+            return mIsLooping;
+        }
+
+        @Override
+        public boolean isPlaying() {
+            return mIsPlaying;
+        }
+
+        /**
+         * Updates {@link #isPlaying()} result to false, if it's set to true.
+         *
+         * @throws IllegalStateException is {@link #isPlaying()} is already false
+         */
+        public void simulatePlayingFinished() {
+            if (!mIsPlaying) {
+                throw new IllegalStateException(
+                        "Attempted to pretend playing finished when not playing");
+            }
+            mIsPlaying = false;
+        }
+    }
+}
-- 
GitLab