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