Skip to content
Snippets Groups Projects
Commit addff744 authored by Lais Andrade's avatar Lais Andrade
Browse files

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
parent c64b23bd
No related branches found
No related tags found
No related merge requests found
......@@ -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 {
......
......@@ -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: [
......
# Bug component: 345036
include /services/core/java/com/android/server/vibrator/OWNERS
/*
* 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() {
}
}
/*
* 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;
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment