/*
* Copyright (C) 2011 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.ex.variablespeed;
import com.google.common.io.Closeables;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.res.AssetManager;
import android.net.Uri;
import android.provider.VoicemailContract;
import android.test.InstrumentationTestCase;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* Base test for checking implementations of {@link MediaPlayerProxy}.
*
* The purpose behind this class is to collect tests that implementations of
* MediaPlayerProxy should support.
*
* This allows tests to show that the built-in {@link android.media.MediaPlayer} is performing
* correctly with respect to the contract it provides, i.e. test my understanding of that contract.
*
* It allows us to test the current {@link VariableSpeed} implementation, and make sure that this
* too corresponds with the MediaPlayer implementation.
*
* These tests cannot be run on their own - you must provide a concrete subclass of this test case -
* and in that subclass you will provide an implementation of the abstract
* {@link #createTestMediaPlayer()} method to construct the player you would like to test. Every
* test will construct the player in {@link #setUp()} and release it in {@link #tearDown()}.
*/
public abstract class MediaPlayerProxyTestCase extends InstrumentationTestCase {
private static final float ERROR_TOLERANCE_MILLIS = 1000f;
/** The phone number to use when inserting test data into the content provider. */
private static final String CONTACT_NUMBER = "01234567890";
/**
* A map from filename + mime type to the uri we can use to play from the content provider.
*
* This is lazily filled in by the {@link #getTestContentUri(String, String)} method.
*
* This map is keyed from the concatenation of filename and mime type with a "+" separator, it's
* not perfect but it doesn't matter in this test code.
*/
private final Map mContentUriMap = new HashMap();
/** The system under test. */
private MediaPlayerProxy mPlayer;
private AwaitableCompletionListener mCompletionListener;
private AwaitableErrorListener mErrorListener;
@Override
protected void setUp() throws Exception {
super.setUp();
mPlayer = createTestMediaPlayer();
mCompletionListener = new AwaitableCompletionListener();
mErrorListener = new AwaitableErrorListener();
}
@Override
protected void tearDown() throws Exception {
mCompletionListener = null;
mErrorListener = null;
mPlayer.release();
mPlayer = null;
cleanupContentUriIfNecessary();
super.tearDown();
}
public abstract MediaPlayerProxy createTestMediaPlayer() throws Exception;
/** Annotation to indicate that test should throw an {@link IllegalStateException}. */
@Retention(RetentionPolicy.RUNTIME)
public @interface ShouldThrowIllegalStateException {
}
@Override
protected void runTest() throws Throwable {
// Tests annotated with ShouldThrowIllegalStateException will fail if they don't.
// Tests not annotated this way are run as normal.
if (getClass().getMethod(getName()).isAnnotationPresent(
ShouldThrowIllegalStateException.class)) {
try {
super.runTest();
fail("Expected this method to throw an IllegalStateException, but it didn't");
} catch (IllegalStateException e) {
// Expected.
}
} else {
super.runTest();
}
}
public void testReleaseMultipleTimesHasNoEffect() throws Exception {
mPlayer.release();
mPlayer.release();
}
public void testResetOnNewlyCreatedObject() throws Exception {
mPlayer.reset();
}
public void testSetDataSource() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
}
@ShouldThrowIllegalStateException
public void testSetDataSourceTwice_ShouldFailWithIllegalState() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
}
@ShouldThrowIllegalStateException
public void testSetDataSourceAfterRelease_ShouldFailWithIllegalState() throws Exception {
mPlayer.release();
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
}
public void testPrepare() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.prepare();
}
@ShouldThrowIllegalStateException
public void testPrepareBeforeSetDataSource_ShouldFail() throws Exception {
mPlayer.prepare();
}
@ShouldThrowIllegalStateException
public void testPrepareTwice_ShouldFailWithIllegalState() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.prepare();
mPlayer.prepare();
}
public void testStartThenImmediatelyRelease() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.prepare();
mPlayer.start();
}
public void testPlayABitThenRelease() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.prepare();
mPlayer.start();
Thread.sleep(2000);
}
public void testPlayFully() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.prepare();
mPlayer.setOnCompletionListener(mCompletionListener);
mPlayer.start();
mCompletionListener.awaitOneCallback(10, TimeUnit.SECONDS);
}
public void testGetDuration() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.prepare();
int duration = mPlayer.getDuration();
assertTrue("duration was " + duration, duration > 0);
mPlayer.setOnCompletionListener(mCompletionListener);
mPlayer.start();
assertEquals(duration, mPlayer.getDuration());
mCompletionListener.awaitOneCallback(10, TimeUnit.SECONDS);
assertEquals(duration, mPlayer.getDuration());
}
@ShouldThrowIllegalStateException
public void testGetDurationAfterRelease_ShouldFail() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.release();
mPlayer.getDuration();
}
@ShouldThrowIllegalStateException
public void testGetPositionAfterRelease_ShouldFail() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.release();
mPlayer.getCurrentPosition();
}
public void testGetCurrentPosition_ZeroBeforePlaybackBegins() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
assertEquals(0, mPlayer.getCurrentPosition());
mPlayer.prepare();
assertEquals(0, mPlayer.getCurrentPosition());
}
public void testGetCurrentPosition_DuringPlayback() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.prepare();
mPlayer.start();
Thread.sleep(2000);
assertEquals(2000, mPlayer.getCurrentPosition(), ERROR_TOLERANCE_MILLIS);
}
public void testGetCurrentPosition_FinishedPlaying() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.prepare();
mPlayer.setOnCompletionListener(mCompletionListener);
mPlayer.start();
mCompletionListener.awaitOneCallback(10, TimeUnit.SECONDS);
assertEquals(mPlayer.getDuration(), mPlayer.getCurrentPosition(), ERROR_TOLERANCE_MILLIS);
}
public void testGetCurrentPosition_DuringPlaybackWithSeek() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.prepare();
mPlayer.seekTo(1500);
mPlayer.start();
Thread.sleep(1500);
assertEquals(3000, mPlayer.getCurrentPosition(), ERROR_TOLERANCE_MILLIS);
}
public void testSeekHalfWayBeforePlaying() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.prepare();
assertTrue(mPlayer.getDuration() > 0);
mPlayer.seekTo(mPlayer.getDuration() / 2);
mPlayer.start();
mPlayer.setOnCompletionListener(mCompletionListener);
mCompletionListener.awaitOneCallback(10, TimeUnit.SECONDS);
}
public void testHalfWaySeekWithStutteringAudio() throws Exception {
// The audio contained in this file has a stutter if we seek to half way and play.
// It shouldn't have.
setDataSourceFromContentProvider(mPlayer, "fake_voicemail2.mp3", "audio/mp3");
mPlayer.prepare();
assertTrue(mPlayer.getDuration() > 0);
mPlayer.seekTo(mPlayer.getDuration() / 2);
mPlayer.start();
mPlayer.setOnCompletionListener(mCompletionListener);
mCompletionListener.awaitOneCallback(10, TimeUnit.SECONDS);
}
public void testResetWithoutReleaseAndThenReUse() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.reset();
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.prepare();
mPlayer.seekTo(mPlayer.getDuration() / 2);
mPlayer.start();
Thread.sleep(1000);
}
public void testResetAfterPlaybackThenReUse() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.setOnCompletionListener(mCompletionListener);
mPlayer.prepare();
mPlayer.start();
mCompletionListener.awaitOneCallback(10, TimeUnit.SECONDS);
mPlayer.reset();
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.prepare();
mPlayer.start();
Thread.sleep(2000);
}
public void testResetDuringPlaybackThenReUse() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.prepare();
mPlayer.start();
Thread.sleep(2000);
mPlayer.reset();
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.prepare();
mPlayer.start();
Thread.sleep(2000);
}
public void testFinishPlayingThenSeekToHalfWayThenPlayAgain() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.prepare();
mPlayer.setOnCompletionListener(mCompletionListener);
mPlayer.start();
mCompletionListener.awaitOneCallback(10, TimeUnit.SECONDS);
mPlayer.seekTo(mPlayer.getDuration() / 2);
mPlayer.start();
mCompletionListener.awaitOneCallback(10, TimeUnit.SECONDS);
}
public void testPause_DuringPlayback() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.prepare();
mPlayer.start();
assertTrue(mPlayer.isPlaying());
Thread.sleep(2000);
assertTrue(mPlayer.isPlaying());
mPlayer.pause();
assertFalse(mPlayer.isPlaying());
}
public void testPause_DoesNotInvokeCallback() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.prepare();
mPlayer.setOnCompletionListener(mCompletionListener);
mPlayer.start();
mPlayer.pause();
Thread.sleep(200);
mCompletionListener.assertNoMoreCallbacks();
}
public void testReset_DoesNotInvokeCallback() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.prepare();
mPlayer.setOnCompletionListener(mCompletionListener);
mPlayer.start();
mPlayer.reset();
Thread.sleep(200);
mCompletionListener.assertNoMoreCallbacks();
}
public void testPause_MultipleTimes() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.prepare();
mPlayer.start();
Thread.sleep(2000);
mPlayer.pause();
mPlayer.pause();
}
public void testDoubleStartWaitingForFinish() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.prepare();
mPlayer.setOnCompletionListener(mCompletionListener);
mPlayer.start();
mCompletionListener.awaitOneCallback(10, TimeUnit.SECONDS);
mPlayer.start();
mCompletionListener.awaitOneCallback(10, TimeUnit.SECONDS);
}
public void testTwoFastConsecutiveStarts() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.prepare();
mPlayer.setOnCompletionListener(mCompletionListener);
mPlayer.start();
mPlayer.start();
mCompletionListener.awaitOneCallback(10, TimeUnit.SECONDS);
Thread.sleep(200);
mCompletionListener.assertNoMoreCallbacks();
}
public void testThreeFastConsecutiveStarts() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.prepare();
mPlayer.setOnCompletionListener(mCompletionListener);
mPlayer.start();
mPlayer.start();
mPlayer.start();
mCompletionListener.awaitOneCallback(10, TimeUnit.SECONDS);
Thread.sleep(4000);
mCompletionListener.assertNoMoreCallbacks();
}
public void testSeekDuringPlayback() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.prepare();
mPlayer.setOnCompletionListener(mCompletionListener);
mPlayer.start();
Thread.sleep(2000);
mPlayer.seekTo(0);
mCompletionListener.awaitOneCallback(10, TimeUnit.SECONDS);
Thread.sleep(200);
mCompletionListener.assertNoMoreCallbacks();
}
public void testPlaySingleChannelLowSampleRate3gppFile() throws Exception {
setDataSourceFromContentProvider(mPlayer, "count_and_test.3gpp", "audio/3gpp");
mPlayer.prepare();
mPlayer.setOnCompletionListener(mCompletionListener);
mPlayer.start();
mCompletionListener.awaitOneCallback(10, TimeUnit.SECONDS);
}
public void testPlayTwoDifferentTypesWithSameMediaPlayer() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.prepare();
mPlayer.setOnCompletionListener(mCompletionListener);
mPlayer.start();
mCompletionListener.awaitOneCallback(10, TimeUnit.SECONDS);
mPlayer.reset();
setDataSourceFromContentProvider(mPlayer, "count_and_test.3gpp", "audio/3gpp");
mPlayer.prepare();
mPlayer.start();
mCompletionListener.awaitOneCallback(10, TimeUnit.SECONDS);
}
public void testIllegalPreparingDoesntFireErrorListener() throws Exception {
mPlayer.setOnErrorListener(mErrorListener);
try {
mPlayer.prepare();
fail("This should have thrown an IllegalStateException");
} catch (IllegalStateException e) {
// Good, expected.
}
mErrorListener.assertNoMoreCallbacks();
}
public void testSetDataSourceForMissingFile_ThrowsIOExceptionInPrepare() throws Exception {
mPlayer.setOnErrorListener(mErrorListener);
mPlayer.setDataSource("/this/file/does/not/exist/");
try {
mPlayer.prepare();
fail("Should have thrown IOException");
} catch (IOException e) {
// Good, expected.
}
// Synchronous prepare does not report errors to the error listener.
mErrorListener.assertNoMoreCallbacks();
}
public void testRepeatedlySeekingDuringPlayback() throws Exception {
// Start playback then seek repeatedly during playback to the same point.
// The real media player should play a stuttering audio, hopefully my player does too.
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.prepare();
mPlayer.setOnCompletionListener(mCompletionListener);
mPlayer.start();
Thread.sleep(500);
for (int i = 0; i < 40; ++i) {
Thread.sleep(200);
mPlayer.seekTo(2000);
}
mCompletionListener.awaitOneCallback(10, TimeUnit.SECONDS);
}
public void testRepeatedlySeekingDuringPlaybackRandomAndVeryFast() throws Exception {
setDataSourceFromContentProvider(mPlayer, "quick_test_recording.mp3", "audio/mp3");
mPlayer.prepare();
mPlayer.setOnCompletionListener(mCompletionListener);
mPlayer.start();
Thread.sleep(500);
for (int i = 0; i < 40; ++i) {
Thread.sleep(250);
mPlayer.seekTo(1500 + (int) (Math.random() * 1000));
}
mCompletionListener.awaitOneCallback(10, TimeUnit.SECONDS);
}
public void testSeekToEndThenPlayThenRateChangeCrash() throws Exception {
// Unit test for this bug: http://b/5140693
// This test proves that the bug is fixed.
setDataSourceFromContentProvider(mPlayer, "fake_voicemail.mp3", "audio/mp3");
mPlayer.prepare();
mPlayer.seekTo(mPlayer.getDuration() - 1);
mPlayer.setOnCompletionListener(mCompletionListener);
mPlayer.start();
mCompletionListener.awaitOneCallback(10, TimeUnit.SECONDS);
// Prior to the fix, this next line was causing a crash.
// The reason behind this was due to our having seeked so close to the end of the file
// that insufficient data was being read, and thus we weren't able to yet determine the
// sample rate and number of channels, which was causing an assertion failure when trying
// to create the time scaler.
setVariableSpeedRateIfSupported(1.0f);
}
public void testVariableSpeedRateChangeAtDifferentTimes() throws Exception {
// Just check that we can set the rate at any point during playback.
setVariableSpeedRateIfSupported(1.05f);
setDataSourceFromContentProvider(mPlayer, "fake_voicemail.mp3", "audio/mp3");
setVariableSpeedRateIfSupported(1.10f);
mPlayer.prepare();
setVariableSpeedRateIfSupported(1.15f);
mPlayer.seekTo(mPlayer.getDuration() / 2);
setVariableSpeedRateIfSupported(1.20f);
mPlayer.setOnCompletionListener(mCompletionListener);
setVariableSpeedRateIfSupported(1.25f);
mPlayer.start();
setVariableSpeedRateIfSupported(1.30f);
mCompletionListener.awaitOneCallback(10, TimeUnit.SECONDS);
setVariableSpeedRateIfSupported(1.35f);
}
/**
* If we have a variable speed media player proxy, set the variable speed rate.
*
* If we don't have a variable speed media player proxy, this method will be a no-op.
*/
private void setVariableSpeedRateIfSupported(float rate) {
if (mPlayer instanceof SingleThreadedMediaPlayerProxy) {
((SingleThreadedMediaPlayerProxy) mPlayer).setVariableSpeed(rate);
} else if (mPlayer instanceof VariableSpeed) {
((VariableSpeed) mPlayer).setVariableSpeed(rate);
}
}
/**
* Gets the {@link Uri} for the test audio content we should play.
*
* If this is the first time we've called this method, for a given file type and mime type, then
* we'll have to insert some data into the content provider so that we can play it.
*
* This is not thread safe, but doesn't need to be because all unit tests are executed from a
* single thread, sequentially.
*/
private Uri getTestContentUri(String assetFilename, String assetMimeType) throws IOException {
String key = keyFor(assetFilename, assetMimeType);
if (mContentUriMap.containsKey(key)) {
return mContentUriMap.get(key);
}
ContentValues values = new ContentValues();
values.put(VoicemailContract.Voicemails.DATE, String.valueOf(System.currentTimeMillis()));
values.put(VoicemailContract.Voicemails.NUMBER, CONTACT_NUMBER);
values.put(VoicemailContract.Voicemails.MIME_TYPE, assetMimeType);
String packageName = getInstrumentation().getTargetContext().getPackageName();
Uri uri = getContentResolver().insert(
VoicemailContract.Voicemails.buildSourceUri(packageName), values);
AssetManager assets = getAssets();
OutputStream outputStream = null;
InputStream inputStream = null;
try {
inputStream = assets.open(assetFilename);
outputStream = getContentResolver().openOutputStream(uri);
copyBetweenStreams(inputStream, outputStream);
mContentUriMap.put(key, uri);
return uri;
} finally {
Closeables.closeQuietly(outputStream);
Closeables.closeQuietly(inputStream);
}
}
private String keyFor(String assetFilename, String assetMimeType) {
return assetFilename + "+" + assetMimeType;
}
public void copyBetweenStreams(InputStream in, OutputStream out) throws IOException {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
}
private void cleanupContentUriIfNecessary() {
for (Uri uri : mContentUriMap.values()) {
getContentResolver().delete(uri, null, null);
}
mContentUriMap.clear();
}
private void setDataSourceFromContentProvider(MediaPlayerProxy player, String assetFilename,
String assetMimeType) throws IOException {
player.setDataSource(getInstrumentation().getTargetContext(),
getTestContentUri(assetFilename, assetMimeType));
}
private ContentResolver getContentResolver() {
return getInstrumentation().getContext().getContentResolver();
}
private AssetManager getAssets() {
return getInstrumentation().getContext().getAssets();
}
}