From addff744edc1df08ff1eab9aefd7ae2488a03c7e Mon Sep 17 00:00:00 2001 From: Lais Andrade Date: Fri, 13 Oct 2023 11:36:08 +0100 Subject: 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 +- .../mediaframeworktest/unit/RingtoneTest.java | 840 --------------------- media/tests/ringtone/Android.bp | 15 +- media/tests/ringtone/OWNERS | 3 + .../src/com/android/media/RingtoneBuilderTest.java | 631 ++++++++++++++++ .../media/testing/MediaPlayerTestHelper.java | 75 ++ .../RingtoneInjectablesTrackingTestRule.java | 225 ++++++ 7 files changed, 962 insertions(+), 870 deletions(-) delete mode 100644 media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/RingtoneTest.java create mode 100644 media/tests/ringtone/OWNERS create mode 100644 media/tests/ringtone/src/com/android/media/RingtoneBuilderTest.java 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/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/RingtoneTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/RingtoneTest.java deleted file mode 100644 index 3c0c6847f557..000000000000 --- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/RingtoneTest.java +++ /dev/null @@ -1,840 +0,0 @@ -/* - * Copyright (C) 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.mediaframeworktest.unit; - -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.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; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.verifyZeroInteractions; -import static org.mockito.Mockito.when; - -import android.Manifest; -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.res.AssetFileDescriptor; -import android.media.AudioAttributes; -import android.media.AudioManager; -import android.media.IRingtonePlayer; -import android.media.MediaPlayer; -import android.media.Ringtone; -import android.media.audiofx.HapticGenerator; -import android.net.Uri; -import android.os.IBinder; -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 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; - -@RunWith(AndroidJUnit4.class) -public class RingtoneTest { - - private static final Uri SOUND_URI = Uri.parse("content://fake-sound-uri"); - - private static final AudioAttributes RINGTONE_ATTRIBUTES = - audioAttributes(AudioAttributes.USAGE_NOTIFICATION_RINGTONE); - private static final AudioAttributes RINGTONE_ATTRIBUTES_WITH_HC = - new AudioAttributes.Builder(RINGTONE_ATTRIBUTES).setHapticChannelsMuted(false).build(); - private static final VibrationAttributes RINGTONE_VIB_ATTRIBUTES = - new VibrationAttributes.Builder(RINGTONE_ATTRIBUTES).build(); - - 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 - mMediaPlayerRule = new RingtoneInjectablesTrackingTestRule(); - - @Captor private ArgumentCaptor mIBinderCaptor; - @Mock private IRingtonePlayer mMockRemotePlayer; - @Mock private Vibrator mMockVibrator; - private AudioManager mSpyAudioManager; - private TestableContext mContext; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - TestableContext testContext = - new TestableContext(InstrumentationRegistry.getTargetContext(), null); - testContext.getTestablePermissions().setPermission(Manifest.permission.VIBRATE, - PackageManager.PERMISSION_GRANTED); - AudioManager realAudioManager = testContext.getSystemService(AudioManager.class); - mSpyAudioManager = spy(realAudioManager); - when(mSpyAudioManager.getRingtonePlayer()).thenReturn(mMockRemotePlayer); - testContext.addMockSystemService(AudioManager.class, mSpyAudioManager); - testContext.addMockSystemService(Vibrator.class, mMockVibrator); - - mContext = spy(testContext); - } - - @Test - public void testRingtone_fullLifecycleUsingLocalMediaPlayer() throws Exception { - MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); - Ringtone ringtone = - newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES).setUri(SOUND_URI).build(); - assertThat(ringtone).isNotNull(); - assertThat(ringtone.isUsingRemotePlayer()).isFalse(); - - // Verify all the properties. - assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND); - assertThat(ringtone.getUri()).isEqualTo(SOUND_URI); - assertThat(ringtone.getAudioAttributes()).isEqualTo(RINGTONE_ATTRIBUTES); - assertThat(ringtone.getVolume()).isEqualTo(1.0f); - assertThat(ringtone.isLooping()).isEqualTo(false); - assertThat(ringtone.isHapticGeneratorEnabled()).isEqualTo(false); - assertThat(ringtone.getPreferBuiltinDevice()).isFalse(); - assertThat(ringtone.getVolumeShaperConfig()).isNull(); - assertThat(ringtone.isLocalOnly()).isFalse(); - - // Prepare - verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES); - verify(mockMediaPlayer).setVolume(1.0f); - verify(mockMediaPlayer).setLooping(false); - verify(mockMediaPlayer).prepare(); - - // Play - ringtone.play(); - verifyLocalPlay(mockMediaPlayer); - - // Verify dynamic controls. - ringtone.setVolume(0.8f); - verify(mockMediaPlayer).setVolume(0.8f); - when(mockMediaPlayer.isLooping()).thenReturn(false); - ringtone.setLooping(true); - verify(mockMediaPlayer).isLooping(); - verify(mockMediaPlayer).setLooping(true); - HapticGenerator mockHapticGenerator = - mMediaPlayerRule.expectHapticGenerator(mockMediaPlayer); - ringtone.setHapticGeneratorEnabled(true); - verify(mockHapticGenerator).setEnabled(true); - - // Release - ringtone.stop(); - verifyLocalStop(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 - // setup. - verifyNoMoreInteractions(mockMediaPlayer); - verify(mockHapticGenerator).release(); - verifyNoMoreInteractions(mockHapticGenerator); - verifyZeroInteractions(mMockRemotePlayer); - verifyZeroInteractions(mMockVibrator); - } - - @Test - public void testRingtone_localMediaPlayerWithAudioCoupledOverride() throws Exception { - // Audio coupled playback is enabled in the incoming attributes, plus an instruction - // to leave the attributes alone. This test verifies that the attributes reach the - // media player without changing. - final AudioAttributes audioAttributes = RINGTONE_ATTRIBUTES_WITH_HC; - MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); - mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, true); - Ringtone ringtone = - newBuilder(MEDIA_SOUND, audioAttributes) - .setUri(SOUND_URI) - .setUseExactAudioAttributes(true) - .build(); - assertThat(ringtone).isNotNull(); - assertThat(ringtone.isUsingRemotePlayer()).isFalse(); - - // Verify all the properties. - assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND); - assertThat(ringtone.getUri()).isEqualTo(SOUND_URI); - assertThat(ringtone.getAudioAttributes()).isEqualTo(audioAttributes); - - // Prepare - verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, audioAttributes); - verify(mockMediaPlayer).prepare(); - - // Play - ringtone.play(); - verifyLocalPlay(mockMediaPlayer); - - // Release - ringtone.stop(); - verifyLocalStop(mockMediaPlayer); - - verifyZeroInteractions(mMockRemotePlayer); - verifyZeroInteractions(mMockVibrator); - } - - @Test - public void testRingtone_fullLifecycleUsingRemoteMediaPlayer() throws Exception { - MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); - setupFileNotFound(mockMediaPlayer, SOUND_URI); - Ringtone ringtone = - newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES) - .setUri(SOUND_URI) - .build(); - assertThat(ringtone).isNotNull(); - assertThat(ringtone.isUsingRemotePlayer()).isTrue(); - - // Verify all the properties. - assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND); - assertThat(ringtone.getUri()).isEqualTo(SOUND_URI); - assertThat(ringtone.getAudioAttributes()).isEqualTo(RINGTONE_ATTRIBUTES); - assertThat(ringtone.getVolume()).isEqualTo(1.0f); - assertThat(ringtone.isLooping()).isEqualTo(false); - assertThat(ringtone.isHapticGeneratorEnabled()).isEqualTo(false); - assertThat(ringtone.getPreferBuiltinDevice()).isFalse(); - assertThat(ringtone.getVolumeShaperConfig()).isNull(); - assertThat(ringtone.isLocalOnly()).isFalse(); - - // Initialization did try to create a local media player. - verify(mockMediaPlayer).setDataSource(mContext, SOUND_URI); - // setDataSource throws file not found, so nothing else will happen on the local player. - verify(mockMediaPlayer).release(); - - // Delegates to remote media player. - ringtone.play(); - verify(mMockRemotePlayer).playRemoteRingtone(mIBinderCaptor.capture(), eq(SOUND_URI), - eq(RINGTONE_ATTRIBUTES), eq(false), eq(MEDIA_SOUND), isNull(), - eq(1.0f), eq(false), eq(false), isNull()); - IBinder remoteToken = mIBinderCaptor.getValue(); - - // Verify dynamic controls. - ringtone.setVolume(0.8f); - verify(mMockRemotePlayer).setVolume(remoteToken, 0.8f); - ringtone.setLooping(true); - verify(mMockRemotePlayer).setLooping(remoteToken, true); - ringtone.setHapticGeneratorEnabled(true); - verify(mMockRemotePlayer).setHapticGeneratorEnabled(remoteToken, true); - - ringtone.stop(); - verify(mMockRemotePlayer).stop(remoteToken); - verifyNoMoreInteractions(mMockRemotePlayer); - verifyNoMoreInteractions(mockMediaPlayer); - verifyZeroInteractions(mMockVibrator); - } - - @Test - public void testRingtone_localMediaWithVibration() throws Exception { - MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); - when(mMockVibrator.hasVibrator()).thenReturn(true); - Ringtone ringtone = - newBuilder(MEDIA_SOUND_AND_VIBRATION, RINGTONE_ATTRIBUTES) - .setUri(SOUND_URI) - .setVibrationEffect(VIBRATION_EFFECT) - .build(); - assertThat(ringtone).isNotNull(); - assertThat(ringtone.isUsingRemotePlayer()).isFalse(); - verify(mMockVibrator).hasVibrator(); - - // Verify all the properties. - assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND_AND_VIBRATION); - assertThat(ringtone.getUri()).isEqualTo(SOUND_URI); - assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT); - - // 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); - verify(mockMediaPlayer).setVolume(1.0f); - verify(mockMediaPlayer).setLooping(false); - verify(mockMediaPlayer).prepare(); - - // Play - ringtone.play(); - - verifyLocalPlay(mockMediaPlayer); - verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES); - - // Verify dynamic controls. - ringtone.setVolume(0.8f); - verify(mockMediaPlayer).setVolume(0.8f); - - // Set looping doesn't affect an already-started vibration. - when(mockMediaPlayer.isLooping()).thenReturn(false); // Checks original - ringtone.setLooping(true); - verify(mockMediaPlayer).isLooping(); - verify(mockMediaPlayer).setLooping(true); - - // This is ignored because there's a vibration effect being used. - ringtone.setHapticGeneratorEnabled(true); - - // Release - ringtone.stop(); - verifyLocalStop(mockMediaPlayer); - verify(mMockVibrator).cancel(VibrationAttributes.USAGE_RINGTONE); - - // 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 - // setup. - verifyNoMoreInteractions(mockMediaPlayer); - verifyZeroInteractions(mMockRemotePlayer); - verifyNoMoreInteractions(mMockVibrator); - } - - @Test - public void testRingtone_localMediaWithVibrationOnly() throws Exception { - when(mMockVibrator.hasVibrator()).thenReturn(true); - Ringtone ringtone = - newBuilder(MEDIA_VIBRATION, RINGTONE_ATTRIBUTES) - // TODO: set sound uri too in diff test - .setVibrationEffect(VIBRATION_EFFECT) - .build(); - assertThat(ringtone).isNotNull(); - assertThat(ringtone.isUsingRemotePlayer()).isFalse(); - verify(mMockVibrator).hasVibrator(); - - // Verify all the properties. - assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_VIBRATION); - assertThat(ringtone.getUri()).isNull(); - assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT); - - // Play - ringtone.play(); - - verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES); - - // Verify dynamic controls (no-op without sound) - ringtone.setVolume(0.8f); - - // Set looping doesn't affect an already-started vibration. - ringtone.setLooping(true); - - // This is ignored because there's a vibration effect being used and no sound. - ringtone.setHapticGeneratorEnabled(true); - - // Release - ringtone.stop(); - verify(mMockVibrator).cancel(VibrationAttributes.USAGE_RINGTONE); - - // 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 - // setup. - verifyZeroInteractions(mMockRemotePlayer); - verifyNoMoreInteractions(mMockVibrator); - } - - @Test - public void testRingtone_localMediaWithVibrationOnlyAndSoundUriNoHapticChannels() - throws Exception { - // A media player will still be created for vibration-only because the vibration can come - // from haptic channels on the sound file (although in this case it doesn't). - MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); - mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, false); - when(mMockVibrator.hasVibrator()).thenReturn(true); - Ringtone ringtone = - newBuilder(MEDIA_VIBRATION, RINGTONE_ATTRIBUTES) - .setUri(SOUND_URI) - .setVibrationEffect(VIBRATION_EFFECT) - .build(); - assertThat(ringtone).isNotNull(); - assertThat(ringtone.isUsingRemotePlayer()).isFalse(); - verify(mMockVibrator).hasVibrator(); - - // Verify all the properties. - assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_VIBRATION); - assertThat(ringtone.getUri()).isEqualTo(SOUND_URI); - assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT); - - // 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); - verify(mockMediaPlayer).setVolume(0.0f); // Vibration-only: sound muted. - verify(mockMediaPlayer).setLooping(false); - verify(mockMediaPlayer).prepare(); - verify(mockMediaPlayer).release(); // abandoned: no haptic channels. - - // Play - ringtone.play(); - - verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES); - - // Verify dynamic controls (no-op without sound) - ringtone.setVolume(0.8f); - - // Set looping doesn't affect an already-started vibration. - ringtone.setLooping(true); - - // This is ignored because there's a vibration effect being used and no sound. - ringtone.setHapticGeneratorEnabled(true); - - // Release - ringtone.stop(); - verify(mMockVibrator).cancel(VibrationAttributes.USAGE_RINGTONE); - - // 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 - // setup. - verifyZeroInteractions(mMockRemotePlayer); - verifyNoMoreInteractions(mMockVibrator); - verifyNoMoreInteractions(mockMediaPlayer); - } - - @Test - public void testRingtone_localMediaWithVibrationOnlyAndSoundUriWithHapticChannels() - throws Exception { - MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); - when(mMockVibrator.hasVibrator()).thenReturn(true); - mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, true); - Ringtone ringtone = - newBuilder(MEDIA_VIBRATION, RINGTONE_ATTRIBUTES) - .setUri(SOUND_URI) - .setVibrationEffect(VIBRATION_EFFECT) - .build(); - assertThat(ringtone).isNotNull(); - assertThat(ringtone.isUsingRemotePlayer()).isFalse(); - verify(mMockVibrator).hasVibrator(); - - // Verify all the properties. - assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_VIBRATION); - assertThat(ringtone.getUri()).isEqualTo(SOUND_URI); - assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT); - - // 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); - verify(mockMediaPlayer).setVolume(0.0f); // Vibration-only: sound muted. - verify(mockMediaPlayer).setLooping(false); - verify(mockMediaPlayer).prepare(); - - // Play - ringtone.play(); - // Vibrator.vibrate isn't called because the vibration comes from the sound. - verifyLocalPlay(mockMediaPlayer); - - // Verify dynamic controls (no-op without sound) - ringtone.setVolume(0.8f); - - when(mockMediaPlayer.isLooping()).thenReturn(false); // Checks original - ringtone.setLooping(true); - verify(mockMediaPlayer).isLooping(); - verify(mockMediaPlayer).setLooping(true); - - // This is ignored because it's using haptic channels. - ringtone.setHapticGeneratorEnabled(true); - - // Release - ringtone.stop(); - verifyLocalStop(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 - // setup. - verifyZeroInteractions(mMockRemotePlayer); - verifyZeroInteractions(mMockVibrator); - } - - @Test - public void testRingtone_localMediaWithVibrationPrefersHapticChannels() throws Exception { - MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); - mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, true); - when(mMockVibrator.hasVibrator()).thenReturn(true); - Ringtone ringtone = - newBuilder(MEDIA_SOUND_AND_VIBRATION, RINGTONE_ATTRIBUTES) - .setUri(SOUND_URI) - .setVibrationEffect(VIBRATION_EFFECT) - .build(); - assertThat(ringtone).isNotNull(); - assertThat(ringtone.isUsingRemotePlayer()).isFalse(); - verify(mMockVibrator).hasVibrator(); - - // Verify all the properties. - assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND_AND_VIBRATION); - assertThat(ringtone.getUri()).isEqualTo(SOUND_URI); - assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT); - - // Prepare - // The attributes here have haptic channels enabled (unlike above) - verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC); - verify(mockMediaPlayer).prepare(); - - // Play - ringtone.play(); - when(mockMediaPlayer.isPlaying()).thenReturn(true); - verifyLocalPlay(mockMediaPlayer); - - // Release - ringtone.stop(); - verifyLocalStop(mockMediaPlayer); - - verifyZeroInteractions(mMockRemotePlayer); - // Nothing after the initial hasVibrator - it uses audio-coupled. - verifyNoMoreInteractions(mMockVibrator); - } - - @Test - public void testRingtone_localMediaWithVibrationButSoundMuted() throws Exception { - MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); - mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, false); - doReturn(0).when(mSpyAudioManager) - .getStreamVolume(AudioAttributes.toLegacyStreamType(RINGTONE_ATTRIBUTES)); - when(mMockVibrator.hasVibrator()).thenReturn(true); - Ringtone ringtone = - newBuilder(MEDIA_SOUND_AND_VIBRATION, RINGTONE_ATTRIBUTES) - .setUri(SOUND_URI) - .setVibrationEffect(VIBRATION_EFFECT) - .build(); - assertThat(ringtone).isNotNull(); - assertThat(ringtone.isUsingRemotePlayer()).isFalse(); - verify(mMockVibrator).hasVibrator(); - - // Verify all the properties. - assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND_AND_VIBRATION); - assertThat(ringtone.getUri()).isEqualTo(SOUND_URI); - assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT); - - // Prepare - // The attributes here have haptic channels enabled (unlike above) - verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC); - verify(mockMediaPlayer).prepare(); - - // Play - ringtone.play(); - // The media player is never played, because sound is muted. - verify(mockMediaPlayer, never()).start(); - when(mockMediaPlayer.isPlaying()).thenReturn(true); - verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES); - - // Release - ringtone.stop(); - verify(mockMediaPlayer).release(); - verify(mMockVibrator).cancel(VibrationAttributes.USAGE_RINGTONE); - - verifyZeroInteractions(mMockRemotePlayer); - // Nothing after the initial hasVibrator - it uses audio-coupled. - verifyNoMoreInteractions(mMockVibrator); - } - - @Test - public void testRingtone_nullMediaOnBuilderUsesFallback() throws Exception { - AssetFileDescriptor testResourceFd = - mContext.getResources().openRawResourceFd(R.raw.shortmp3); - // Ensure it will flow as expected. - assertThat(testResourceFd).isNotNull(); - assertThat(testResourceFd.getDeclaredLength()).isAtLeast(0); - mContext.getOrCreateTestableResources() - .addOverride(com.android.internal.R.raw.fallbackring, testResourceFd); - - MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); - Ringtone ringtone = newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES) - .setUri(null) - .build(); - assertThat(ringtone).isNotNull(); - assertThat(ringtone.isUsingRemotePlayer()).isFalse(); - - // Delegates straight to fallback in local player. - // Prepare - verifyLocalPlayerFallbackSetup(mockMediaPlayer, testResourceFd, RINGTONE_ATTRIBUTES); - verify(mockMediaPlayer).setVolume(1.0f); - verify(mockMediaPlayer).setLooping(false); - verify(mockMediaPlayer).prepare(); - - // Play - ringtone.play(); - verifyLocalPlay(mockMediaPlayer); - - // Release - ringtone.stop(); - verifyLocalStop(mockMediaPlayer); - - verifyNoMoreInteractions(mockMediaPlayer); - verifyNoMoreInteractions(mMockRemotePlayer); - } - - @Test - public void testRingtone_nullMediaOnBuilderUsesFallbackViaRemote() throws Exception { - mContext.getOrCreateTestableResources() - .addOverride(com.android.internal.R.raw.fallbackring, null); - Ringtone ringtone = newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES) - .setUri(null) - .setLooping(true) // distinct from haptic generator, to match plumbing - .build(); - assertThat(ringtone).isNotNull(); - // Local player fallback fails as the resource isn't found (no media player creation is - // attempted), and then goes on to create the remote player. - assertThat(ringtone.isUsingRemotePlayer()).isTrue(); - - ringtone.play(); - verify(mMockRemotePlayer).playRemoteRingtone(mIBinderCaptor.capture(), isNull(), - eq(RINGTONE_ATTRIBUTES), eq(false), - eq(MEDIA_SOUND), isNull(), - eq(1.0f), eq(true), eq(false), isNull()); - ringtone.stop(); - verify(mMockRemotePlayer).stop(mIBinderCaptor.getValue()); - 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); - } - - private static AudioAttributes audioAttributes(int audioUsage) { - return new AudioAttributes.Builder() - .setUsage(audioUsage) - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .build(); - } - - /** Makes the mock get some sort of file access problem. */ - private void setupFileNotFound(MediaPlayer mockMediaPlayer, Uri uri) throws Exception { - 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 mMockMediaPlayerQueue = new ArrayDeque<>(); - - // Similar to media players, but for haptic generator, which also needs releasing. - private Map mMockHapticGeneratorMap = new ArrayMap<>(); - - // Media players with haptic channels. - private ArraySet 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/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/ringtone/src/com/android/media/RingtoneBuilderTest.java b/media/tests/ringtone/src/com/android/media/RingtoneBuilderTest.java new file mode 100644 index 000000000000..2c8daba86d19 --- /dev/null +++ b/media/tests/ringtone/src/com/android/media/RingtoneBuilderTest.java @@ -0,0 +1,631 @@ +/* + * Copyright (C) 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; + +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 org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import android.Manifest; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.AssetFileDescriptor; +import android.media.AudioAttributes; +import android.media.AudioManager; +import android.media.IRingtonePlayer; +import android.media.MediaPlayer; +import android.media.Ringtone; +import android.media.audiofx.HapticGenerator; +import android.net.Uri; +import android.os.IBinder; +import android.os.VibrationAttributes; +import android.os.VibrationEffect; +import android.os.Vibrator; +import android.testing.TestableContext; + +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +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.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.FileNotFoundException; + +/** + * Test behavior of {@link Ringtone} when it's created via {@link Ringtone.Builder}. + */ +@RunWith(AndroidJUnit4.class) +public class RingtoneBuilderTest { + + private static final Uri SOUND_URI = Uri.parse("content://fake-sound-uri"); + + private static final AudioAttributes RINGTONE_ATTRIBUTES = + audioAttributes(AudioAttributes.USAGE_NOTIFICATION_RINGTONE); + private static final AudioAttributes RINGTONE_ATTRIBUTES_WITH_HC = + new AudioAttributes.Builder(RINGTONE_ATTRIBUTES).setHapticChannelsMuted(false).build(); + private static final VibrationAttributes RINGTONE_VIB_ATTRIBUTES = + new VibrationAttributes.Builder(RINGTONE_ATTRIBUTES).build(); + + private static final VibrationEffect VIBRATION_EFFECT = + VibrationEffect.createWaveform(new long[] { 0, 100, 50, 100}, -1); + + @Rule public final RingtoneInjectablesTrackingTestRule + mMediaPlayerRule = new RingtoneInjectablesTrackingTestRule(); + + @Captor private ArgumentCaptor mIBinderCaptor; + @Mock private IRingtonePlayer mMockRemotePlayer; + @Mock private Vibrator mMockVibrator; + private AudioManager mSpyAudioManager; + private TestableContext mContext; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + TestableContext testContext = + new TestableContext(InstrumentationRegistry.getTargetContext(), null); + testContext.getTestablePermissions().setPermission(Manifest.permission.VIBRATE, + PackageManager.PERMISSION_GRANTED); + AudioManager realAudioManager = testContext.getSystemService(AudioManager.class); + mSpyAudioManager = spy(realAudioManager); + when(mSpyAudioManager.getRingtonePlayer()).thenReturn(mMockRemotePlayer); + testContext.addMockSystemService(AudioManager.class, mSpyAudioManager); + testContext.addMockSystemService(Vibrator.class, mMockVibrator); + + mContext = spy(testContext); + } + + + @Test + public void testRingtone_fullLifecycleUsingLocalMediaPlayer() throws Exception { + MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); + Ringtone ringtone = + newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES).setUri(SOUND_URI).build(); + assertThat(ringtone).isNotNull(); + assertThat(ringtone.isUsingRemotePlayer()).isFalse(); + + // Verify all the properties. + assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND); + assertThat(ringtone.getUri()).isEqualTo(SOUND_URI); + assertThat(ringtone.getAudioAttributes()).isEqualTo(RINGTONE_ATTRIBUTES); + assertThat(ringtone.getVolume()).isEqualTo(1.0f); + assertThat(ringtone.isLooping()).isEqualTo(false); + assertThat(ringtone.isHapticGeneratorEnabled()).isEqualTo(false); + assertThat(ringtone.getPreferBuiltinDevice()).isFalse(); + assertThat(ringtone.getVolumeShaperConfig()).isNull(); + assertThat(ringtone.isLocalOnly()).isFalse(); + + // Prepare + verifyPlayerSetup(mContext, mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES); + verify(mockMediaPlayer).setVolume(1.0f); + verify(mockMediaPlayer).setLooping(false); + verify(mockMediaPlayer).prepare(); + + // Play + ringtone.play(); + verifyPlayerStarted(mockMediaPlayer); + + // Verify dynamic controls. + ringtone.setVolume(0.8f); + verify(mockMediaPlayer).setVolume(0.8f); + when(mockMediaPlayer.isLooping()).thenReturn(false); + ringtone.setLooping(true); + verify(mockMediaPlayer).isLooping(); + verify(mockMediaPlayer).setLooping(true); + HapticGenerator mockHapticGenerator = + mMediaPlayerRule.expectHapticGenerator(mockMediaPlayer); + ringtone.setHapticGeneratorEnabled(true); + verify(mockHapticGenerator).setEnabled(true); + + // Release + ringtone.stop(); + 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 + // setup. + verifyNoMoreInteractions(mockMediaPlayer); + verify(mockHapticGenerator).release(); + verifyNoMoreInteractions(mockHapticGenerator); + verifyZeroInteractions(mMockRemotePlayer); + verifyZeroInteractions(mMockVibrator); + } + + @Test + public void testRingtone_localMediaPlayerWithAudioCoupledOverride() throws Exception { + // Audio coupled playback is enabled in the incoming attributes, plus an instruction + // to leave the attributes alone. This test verifies that the attributes reach the + // media player without changing. + final AudioAttributes audioAttributes = RINGTONE_ATTRIBUTES_WITH_HC; + MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); + mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, true); + Ringtone ringtone = + newBuilder(MEDIA_SOUND, audioAttributes) + .setUri(SOUND_URI) + .setUseExactAudioAttributes(true) + .build(); + assertThat(ringtone).isNotNull(); + assertThat(ringtone.isUsingRemotePlayer()).isFalse(); + + // Verify all the properties. + assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND); + assertThat(ringtone.getUri()).isEqualTo(SOUND_URI); + assertThat(ringtone.getAudioAttributes()).isEqualTo(audioAttributes); + + // Prepare + verifyPlayerSetup(mContext, mockMediaPlayer, SOUND_URI, audioAttributes); + verify(mockMediaPlayer).prepare(); + + // Play + ringtone.play(); + verifyPlayerStarted(mockMediaPlayer); + + // Release + ringtone.stop(); + verifyPlayerStopped(mockMediaPlayer); + + verifyZeroInteractions(mMockRemotePlayer); + verifyZeroInteractions(mMockVibrator); + } + + @Test + public void testRingtone_fullLifecycleUsingRemoteMediaPlayer() throws Exception { + MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); + setupFileNotFound(mockMediaPlayer, SOUND_URI); + Ringtone ringtone = + newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES) + .setUri(SOUND_URI) + .build(); + assertThat(ringtone).isNotNull(); + assertThat(ringtone.isUsingRemotePlayer()).isTrue(); + + // Verify all the properties. + assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND); + assertThat(ringtone.getUri()).isEqualTo(SOUND_URI); + assertThat(ringtone.getAudioAttributes()).isEqualTo(RINGTONE_ATTRIBUTES); + assertThat(ringtone.getVolume()).isEqualTo(1.0f); + assertThat(ringtone.isLooping()).isEqualTo(false); + assertThat(ringtone.isHapticGeneratorEnabled()).isEqualTo(false); + assertThat(ringtone.getPreferBuiltinDevice()).isFalse(); + assertThat(ringtone.getVolumeShaperConfig()).isNull(); + assertThat(ringtone.isLocalOnly()).isFalse(); + + // Initialization did try to create a local media player. + verify(mockMediaPlayer).setDataSource(mContext, SOUND_URI); + // setDataSource throws file not found, so nothing else will happen on the local player. + verify(mockMediaPlayer).release(); + + // Delegates to remote media player. + ringtone.play(); + verify(mMockRemotePlayer).playRemoteRingtone(mIBinderCaptor.capture(), eq(SOUND_URI), + eq(RINGTONE_ATTRIBUTES), eq(false), eq(MEDIA_SOUND), isNull(), + eq(1.0f), eq(false), eq(false), isNull()); + IBinder remoteToken = mIBinderCaptor.getValue(); + + // Verify dynamic controls. + ringtone.setVolume(0.8f); + verify(mMockRemotePlayer).setVolume(remoteToken, 0.8f); + ringtone.setLooping(true); + verify(mMockRemotePlayer).setLooping(remoteToken, true); + ringtone.setHapticGeneratorEnabled(true); + verify(mMockRemotePlayer).setHapticGeneratorEnabled(remoteToken, true); + + ringtone.stop(); + verify(mMockRemotePlayer).stop(remoteToken); + verifyNoMoreInteractions(mMockRemotePlayer); + verifyNoMoreInteractions(mockMediaPlayer); + verifyZeroInteractions(mMockVibrator); + } + + @Test + public void testRingtone_localMediaWithVibration() throws Exception { + MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); + when(mMockVibrator.hasVibrator()).thenReturn(true); + Ringtone ringtone = + newBuilder(MEDIA_SOUND_AND_VIBRATION, RINGTONE_ATTRIBUTES) + .setUri(SOUND_URI) + .setVibrationEffect(VIBRATION_EFFECT) + .build(); + assertThat(ringtone).isNotNull(); + assertThat(ringtone.isUsingRemotePlayer()).isFalse(); + verify(mMockVibrator).hasVibrator(); + + // Verify all the properties. + assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND_AND_VIBRATION); + assertThat(ringtone.getUri()).isEqualTo(SOUND_URI); + assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT); + + // Prepare + // Uses attributes with haptic channels enabled, but will use the effect when there aren't + // any present. + verifyPlayerSetup(mContext, mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC); + verify(mockMediaPlayer).setVolume(1.0f); + verify(mockMediaPlayer).setLooping(false); + verify(mockMediaPlayer).prepare(); + + // Play + ringtone.play(); + + verifyPlayerStarted(mockMediaPlayer); + verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES); + + // Verify dynamic controls. + ringtone.setVolume(0.8f); + verify(mockMediaPlayer).setVolume(0.8f); + + // Set looping doesn't affect an already-started vibration. + when(mockMediaPlayer.isLooping()).thenReturn(false); // Checks original + ringtone.setLooping(true); + verify(mockMediaPlayer).isLooping(); + verify(mockMediaPlayer).setLooping(true); + + // This is ignored because there's a vibration effect being used. + ringtone.setHapticGeneratorEnabled(true); + + // Release + ringtone.stop(); + verifyPlayerStopped(mockMediaPlayer); + verify(mMockVibrator).cancel(VibrationAttributes.USAGE_RINGTONE); + + // 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 + // setup. + verifyNoMoreInteractions(mockMediaPlayer); + verifyZeroInteractions(mMockRemotePlayer); + verifyNoMoreInteractions(mMockVibrator); + } + + @Test + public void testRingtone_localMediaWithVibrationOnly() throws Exception { + when(mMockVibrator.hasVibrator()).thenReturn(true); + Ringtone ringtone = + newBuilder(MEDIA_VIBRATION, RINGTONE_ATTRIBUTES) + // TODO: set sound uri too in diff test + .setVibrationEffect(VIBRATION_EFFECT) + .build(); + assertThat(ringtone).isNotNull(); + assertThat(ringtone.isUsingRemotePlayer()).isFalse(); + verify(mMockVibrator).hasVibrator(); + + // Verify all the properties. + assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_VIBRATION); + assertThat(ringtone.getUri()).isNull(); + assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT); + + // Play + ringtone.play(); + + verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES); + + // Verify dynamic controls (no-op without sound) + ringtone.setVolume(0.8f); + + // Set looping doesn't affect an already-started vibration. + ringtone.setLooping(true); + + // This is ignored because there's a vibration effect being used and no sound. + ringtone.setHapticGeneratorEnabled(true); + + // Release + ringtone.stop(); + verify(mMockVibrator).cancel(VibrationAttributes.USAGE_RINGTONE); + + // 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 + // setup. + verifyZeroInteractions(mMockRemotePlayer); + verifyNoMoreInteractions(mMockVibrator); + } + + @Test + public void testRingtone_localMediaWithVibrationOnlyAndSoundUriNoHapticChannels() + throws Exception { + // A media player will still be created for vibration-only because the vibration can come + // from haptic channels on the sound file (although in this case it doesn't). + MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); + mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, false); + when(mMockVibrator.hasVibrator()).thenReturn(true); + Ringtone ringtone = + newBuilder(MEDIA_VIBRATION, RINGTONE_ATTRIBUTES) + .setUri(SOUND_URI) + .setVibrationEffect(VIBRATION_EFFECT) + .build(); + assertThat(ringtone).isNotNull(); + assertThat(ringtone.isUsingRemotePlayer()).isFalse(); + verify(mMockVibrator).hasVibrator(); + + // Verify all the properties. + assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_VIBRATION); + assertThat(ringtone.getUri()).isEqualTo(SOUND_URI); + assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT); + + // Prepare + // Uses attributes with haptic channels enabled, but will abandon the MediaPlayer when it + // knows there aren't any. + 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(); + verify(mockMediaPlayer).release(); // abandoned: no haptic channels. + + // Play + ringtone.play(); + + verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES); + + // Verify dynamic controls (no-op without sound) + ringtone.setVolume(0.8f); + + // Set looping doesn't affect an already-started vibration. + ringtone.setLooping(true); + + // This is ignored because there's a vibration effect being used and no sound. + ringtone.setHapticGeneratorEnabled(true); + + // Release + ringtone.stop(); + verify(mMockVibrator).cancel(VibrationAttributes.USAGE_RINGTONE); + + // 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 + // setup. + verifyZeroInteractions(mMockRemotePlayer); + verifyNoMoreInteractions(mMockVibrator); + verifyNoMoreInteractions(mockMediaPlayer); + } + + @Test + public void testRingtone_localMediaWithVibrationOnlyAndSoundUriWithHapticChannels() + throws Exception { + MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); + when(mMockVibrator.hasVibrator()).thenReturn(true); + mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, true); + Ringtone ringtone = + newBuilder(MEDIA_VIBRATION, RINGTONE_ATTRIBUTES) + .setUri(SOUND_URI) + .setVibrationEffect(VIBRATION_EFFECT) + .build(); + assertThat(ringtone).isNotNull(); + assertThat(ringtone.isUsingRemotePlayer()).isFalse(); + verify(mMockVibrator).hasVibrator(); + + // Verify all the properties. + assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_VIBRATION); + assertThat(ringtone.getUri()).isEqualTo(SOUND_URI); + assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT); + + // Prepare + // Uses attributes with haptic channels enabled, but will use the effect when there aren't + // any present. + 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(); + + // Play + ringtone.play(); + // Vibrator.vibrate isn't called because the vibration comes from the sound. + verifyPlayerStarted(mockMediaPlayer); + + // Verify dynamic controls (no-op without sound) + ringtone.setVolume(0.8f); + + when(mockMediaPlayer.isLooping()).thenReturn(false); // Checks original + ringtone.setLooping(true); + verify(mockMediaPlayer).isLooping(); + verify(mockMediaPlayer).setLooping(true); + + // This is ignored because it's using haptic channels. + ringtone.setHapticGeneratorEnabled(true); + + // Release + ringtone.stop(); + 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 + // setup. + verifyZeroInteractions(mMockRemotePlayer); + verifyZeroInteractions(mMockVibrator); + } + + @Test + public void testRingtone_localMediaWithVibrationPrefersHapticChannels() throws Exception { + MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); + mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, true); + when(mMockVibrator.hasVibrator()).thenReturn(true); + Ringtone ringtone = + newBuilder(MEDIA_SOUND_AND_VIBRATION, RINGTONE_ATTRIBUTES) + .setUri(SOUND_URI) + .setVibrationEffect(VIBRATION_EFFECT) + .build(); + assertThat(ringtone).isNotNull(); + assertThat(ringtone.isUsingRemotePlayer()).isFalse(); + verify(mMockVibrator).hasVibrator(); + + // Verify all the properties. + assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND_AND_VIBRATION); + assertThat(ringtone.getUri()).isEqualTo(SOUND_URI); + assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT); + + // Prepare + // The attributes here have haptic channels enabled (unlike above) + verifyPlayerSetup(mContext, mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC); + verify(mockMediaPlayer).prepare(); + + // Play + ringtone.play(); + when(mockMediaPlayer.isPlaying()).thenReturn(true); + verifyPlayerStarted(mockMediaPlayer); + + // Release + ringtone.stop(); + verifyPlayerStopped(mockMediaPlayer); + + verifyZeroInteractions(mMockRemotePlayer); + // Nothing after the initial hasVibrator - it uses audio-coupled. + verifyNoMoreInteractions(mMockVibrator); + } + + @Test + public void testRingtone_localMediaWithVibrationButSoundMuted() throws Exception { + MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); + mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, false); + doReturn(0).when(mSpyAudioManager) + .getStreamVolume(AudioAttributes.toLegacyStreamType(RINGTONE_ATTRIBUTES)); + when(mMockVibrator.hasVibrator()).thenReturn(true); + Ringtone ringtone = + newBuilder(MEDIA_SOUND_AND_VIBRATION, RINGTONE_ATTRIBUTES) + .setUri(SOUND_URI) + .setVibrationEffect(VIBRATION_EFFECT) + .build(); + assertThat(ringtone).isNotNull(); + assertThat(ringtone.isUsingRemotePlayer()).isFalse(); + verify(mMockVibrator).hasVibrator(); + + // Verify all the properties. + assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND_AND_VIBRATION); + assertThat(ringtone.getUri()).isEqualTo(SOUND_URI); + assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT); + + // Prepare + // The attributes here have haptic channels enabled (unlike above) + verifyPlayerSetup(mContext, mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC); + verify(mockMediaPlayer).prepare(); + + // Play + ringtone.play(); + // The media player is never played, because sound is muted. + verify(mockMediaPlayer, never()).start(); + when(mockMediaPlayer.isPlaying()).thenReturn(true); + verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES); + + // Release + ringtone.stop(); + verify(mockMediaPlayer).release(); + verify(mMockVibrator).cancel(VibrationAttributes.USAGE_RINGTONE); + + verifyZeroInteractions(mMockRemotePlayer); + // Nothing after the initial hasVibrator - it uses audio-coupled. + verifyNoMoreInteractions(mMockVibrator); + } + + @Test + public void testRingtone_nullMediaOnBuilderUsesFallback() throws Exception { + AssetFileDescriptor testResourceFd = + mContext.getResources().openRawResourceFd(R.raw.test_sound_file); + // Ensure it will flow as expected. + assertThat(testResourceFd).isNotNull(); + assertThat(testResourceFd.getDeclaredLength()).isAtLeast(0); + mContext.getOrCreateTestableResources() + .addOverride(com.android.internal.R.raw.fallbackring, testResourceFd); + + MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); + Ringtone ringtone = newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES) + .setUri(null) + .build(); + assertThat(ringtone).isNotNull(); + assertThat(ringtone.isUsingRemotePlayer()).isFalse(); + + // Delegates straight to fallback in local player. + // Prepare + verifyPlayerFallbackSetup(mockMediaPlayer, testResourceFd, RINGTONE_ATTRIBUTES); + verify(mockMediaPlayer).setVolume(1.0f); + verify(mockMediaPlayer).setLooping(false); + verify(mockMediaPlayer).prepare(); + + // Play + ringtone.play(); + verifyPlayerStarted(mockMediaPlayer); + + // Release + ringtone.stop(); + verifyPlayerStopped(mockMediaPlayer); + + verifyNoMoreInteractions(mockMediaPlayer); + verifyNoMoreInteractions(mMockRemotePlayer); + } + + @Test + public void testRingtone_nullMediaOnBuilderUsesFallbackViaRemote() throws Exception { + mContext.getOrCreateTestableResources() + .addOverride(com.android.internal.R.raw.fallbackring, null); + Ringtone ringtone = newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES) + .setUri(null) + .setLooping(true) // distinct from haptic generator, to match plumbing + .build(); + assertThat(ringtone).isNotNull(); + // Local player fallback fails as the resource isn't found (no media player creation is + // attempted), and then goes on to create the remote player. + assertThat(ringtone.isUsingRemotePlayer()).isTrue(); + + ringtone.play(); + verify(mMockRemotePlayer).playRemoteRingtone(mIBinderCaptor.capture(), isNull(), + eq(RINGTONE_ATTRIBUTES), eq(false), + eq(MEDIA_SOUND), isNull(), + eq(1.0f), eq(true), eq(false), isNull()); + ringtone.stop(); + verify(mMockRemotePlayer).stop(mIBinderCaptor.getValue()); + verifyNoMoreInteractions(mMockRemotePlayer); + } + + private Ringtone.Builder newBuilder(@Ringtone.RingtoneMedia int ringtoneMedia, + AudioAttributes audioAttributes) { + return new Ringtone.Builder(mContext, ringtoneMedia, audioAttributes) + .setInjectables(mMediaPlayerRule.getRingtoneTestInjectables()); + } + + private static AudioAttributes audioAttributes(int audioUsage) { + return new AudioAttributes.Builder() + .setUsage(audioUsage) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build(); + } + + /** Makes the mock get some sort of file access problem. */ + private void setupFileNotFound(MediaPlayer mockMediaPlayer, Uri uri) throws Exception { + doThrow(new FileNotFoundException("Fake file not found")) + .when(mockMediaPlayer).setDataSource(any(Context.class), eq(uri)); + } +} 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. + *

+ * 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 mMockMediaPlayerQueue = new ArrayDeque<>(); + + // Similar to media players, but for haptic generator, which also needs releasing. + private final Map mMockHapticGeneratorMap = new ArrayMap<>(); + + // Media players with haptic channels. + private final ArraySet 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()}. + * + *

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()}. + * + *

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. + *

+ * 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; + } + } +} -- cgit v1.2.3-59-g8ed1b