summaryrefslogtreecommitdiffstats
path: root/variablespeed
diff options
context:
space:
mode:
authorHugo Hudson <hugohudson@google.com>2011-07-25 17:04:42 +0100
committerHugo Hudson <hugohudson@google.com>2011-07-25 20:45:39 +0100
commit9730f15ebbf4b64cd48e0777850e56cb516a9ed4 (patch)
tree796a76a6b0f18c1d4f5799eb1d359b0961b083f3 /variablespeed
parent376a291b7a3016cc85501ee1c044629cce60e75c (diff)
downloadandroid_frameworks_ex-9730f15ebbf4b64cd48e0777850e56cb516a9ed4.tar.gz
android_frameworks_ex-9730f15ebbf4b64cd48e0777850e56cb516a9ed4.tar.bz2
android_frameworks_ex-9730f15ebbf4b64cd48e0777850e56cb516a9ed4.zip
Adds tests for Variable Speed code.
The test changes: - Adds many, many test cases against a MediaPlayerProxy, checking that it behaves to the contract of a MediaPlayer. - Adds the RealMediaPlayer class to check a real MediaPlayer. - Adds the VariableSpeed class, to check a VariableSpeed instance against the same contract as the MediaPlayer. - Adds an Android.mk for the unit tests. - Adds also an AndroidManifest.xml for the unit tests. - Adds some test asset media files (3gpp file and mp3 file). Required for the test changes: - Adds a DynamicProxy class to adapt a MediaPlayer as a MediaPlayerProxy class, i.e. to test the implementation of MediaPlayerProxy, required to avoid writing an adapter. - Adds a couple of listeners, OnErrorListener and OnCompletionListener, that can be waited for synchronously in unit tests. Improvements as a result of the tests: - During the testing, fixes the case where we weren't throwing IllegalStateException if asked for the duration on released player. - Refactored the create engine, create and realize output mix, create and realize audio player, get play interfaces and callbacks, all separated into their own static methods. - This allows me to create the audio player during the main while loop actually after the decoding has begun rather than before starting. This work is a precursor to using the decoder's report on sample rate and channels as the input to these methods. - slSampleRate and slOutputChannels no longer computed in the constructor, but computed when needed in the construction and realization of the audio player. Other changes: - Remove some overly verbose logs on getDuration() and getCurrentPosition(). - Adding the decoder interface to the callback. - Extract metadata from decoder method now takes the metadata interface, so this will be usable from the decoder callack in a follow up. - Temporarily stop getting the metadata out of the decoder, I'm going to be doing it on the decoding callback instead. - Renames the comment in AndroidManifest.xml to describe the correct invocation to run the common tests. Bug: 5048252 Bug: 5048257 Change-Id: Icdc18b19ef89c9924f73128b70aa4696b4e727c5
Diffstat (limited to 'variablespeed')
-rw-r--r--variablespeed/jni/jni_entry.cc2
-rw-r--r--variablespeed/jni/variablespeed.cc201
-rw-r--r--variablespeed/jni/variablespeed.h2
-rw-r--r--variablespeed/src/com/android/ex/variablespeed/VariableSpeed.java9
-rw-r--r--variablespeed/tests/Android.mk28
-rw-r--r--variablespeed/tests/AndroidManifest.xml33
-rw-r--r--variablespeed/tests/assets/README.txt4
-rw-r--r--variablespeed/tests/assets/count_and_test.3gppbin0 -> 7405 bytes
-rw-r--r--variablespeed/tests/assets/quick_test_recording.mp3bin0 -> 30591 bytes
-rw-r--r--variablespeed/tests/src/com/android/ex/variablespeed/AwaitableCompletionListener.java59
-rw-r--r--variablespeed/tests/src/com/android/ex/variablespeed/AwaitableErrorListener.java67
-rw-r--r--variablespeed/tests/src/com/android/ex/variablespeed/DynamicProxy.java62
-rw-r--r--variablespeed/tests/src/com/android/ex/variablespeed/MediaPlayerProxyTestCase.java522
-rw-r--r--variablespeed/tests/src/com/android/ex/variablespeed/RealMediaPlayerTest.java44
-rw-r--r--variablespeed/tests/src/com/android/ex/variablespeed/VariableSpeedTest.java53
15 files changed, 999 insertions, 87 deletions
diff --git a/variablespeed/jni/jni_entry.cc b/variablespeed/jni/jni_entry.cc
index d751b09..f7b1f2e 100644
--- a/variablespeed/jni/jni_entry.cc
+++ b/variablespeed/jni/jni_entry.cc
@@ -68,12 +68,10 @@ JNI_METHOD(stopPlayback, void) (JNIEnv*, jclass) {
}
JNI_METHOD(getCurrentPosition, int) (JNIEnv*, jclass) {
- MethodLog _("getCurrentPosition");
return AudioEngine::GetEngine()->GetCurrentPosition();
}
JNI_METHOD(getTotalDuration, int) (JNIEnv*, jclass) {
- MethodLog _("getTotalDuration");
return AudioEngine::GetEngine()->GetTotalDuration();
}
diff --git a/variablespeed/jni/variablespeed.cc b/variablespeed/jni/variablespeed.cc
index 49eddbe..bf99c4d 100644
--- a/variablespeed/jni/variablespeed.cc
+++ b/variablespeed/jni/variablespeed.cc
@@ -58,7 +58,7 @@ const SLuint32 kPrefetchErrorCandidate =
// Structure used when we perform a decoding callback.
typedef struct CallbackContext_ {
- SLPlayItf decoderPlay;
+ SLMetadataExtractionItf decoderMetadata;
// Pointer to local storage buffers for decoded audio data.
int8_t* pDataBase;
// Pointer to the current buffer within local storage.
@@ -161,13 +161,11 @@ static void StopPlaying(SLPlayItf playItf) {
CheckSLResult("stop playing", result);
}
-static void ExtractMetadataFromDecoder(SLObjectItf decoder) {
- SLMetadataExtractionItf decoderMetadata;
- SLresult result = (*decoder)->GetInterface(decoder,
- SL_IID_METADATAEXTRACTION, &decoderMetadata);
- CheckSLResult("getting metadata interface", result);
+static void ExtractMetadataFromDecoder(
+ SLMetadataExtractionItf decoderMetadata) {
SLuint32 itemCount;
- result = (*decoderMetadata)->GetItemCount(decoderMetadata, &itemCount);
+ SLresult result = (*decoderMetadata)->GetItemCount(
+ decoderMetadata, &itemCount);
CheckSLResult("getting item count", result);
SLuint32 i, keySize, valueSize;
SLMetadataInfo *keyInfo, *value;
@@ -213,32 +211,31 @@ static void SeekToPosition(SLSeekItf seekItf, size_t startPositionMillis) {
}
static void RegisterCallbackContextAndAddEnqueueBuffersToDecoder(
- SLAndroidSimpleBufferQueueItf decoderQueue, SLPlayItf player,
- android::Mutex &callbackLock) {
+ SLAndroidSimpleBufferQueueItf decoderQueue,
+ SLMetadataExtractionItf decoderMetadata, android::Mutex &callbackLock,
+ CallbackContext* context) {
android::Mutex::Autolock autoLock(callbackLock);
// Initialize the callback structure, used during the decoding.
// Then register a callback on the decoder queue, so that we will be called
// throughout the decoding process (and can then extract the decoded audio
// for the next bit of the pipeline).
- CallbackContext cntxt;
- cntxt.decoderPlay = player;
- cntxt.pDataBase = pcmData;
- cntxt.pData = pcmData;
- {
- SLresult result = (*decoderQueue)->RegisterCallback(
- decoderQueue, DecodingBufferQueueCb, &cntxt);
- CheckSLResult("decode callback", result);
- }
+ context->decoderMetadata = decoderMetadata;
+ context->pDataBase = pcmData;
+ context->pData = pcmData;
+
+ SLresult result = (*decoderQueue)->RegisterCallback(
+ decoderQueue, DecodingBufferQueueCb, context);
+ CheckSLResult("decode callback", result);
// Enqueue buffers to map the region of memory allocated to store the
// decoded data.
for (size_t i = 0; i < kNumberOfBuffersInQueue; i++) {
SLresult result = (*decoderQueue)->Enqueue(
- decoderQueue, cntxt.pData, kBufferSizeInBytes);
+ decoderQueue, context->pData, kBufferSizeInBytes);
CheckSLResult("enqueue something", result);
- cntxt.pData += kBufferSizeInBytes;
+ context->pData += kBufferSizeInBytes;
}
- cntxt.pData = cntxt.pDataBase;
+ context->pData = context->pDataBase;
}
// ****************************************************************************
@@ -251,8 +248,7 @@ AudioEngine::AudioEngine(size_t channels, size_t sampleRate,
: decodeBuffer_(decodeInitialSize, decodeMaxSize),
playingBuffers_(), freeBuffers_(), timeScaler_(NULL),
floatBuffer_(NULL), injectBuffer_(NULL),
- channels_(channels), sampleRate_(sampleRate), slSampleRate_(0),
- slOutputChannels_(0),
+ channels_(channels), sampleRate_(sampleRate),
targetFrames_(targetFrames),
windowDuration_(windowDuration),
windowOverlapDuration_(windowOverlapDuration),
@@ -260,23 +256,8 @@ AudioEngine::AudioEngine(size_t channels, size_t sampleRate,
startPositionMillis_(startPositionMillis),
totalDurationMs_(0), startRequested_(false),
stopRequested_(false), finishedDecoding_(false) {
- if (sampleRate_ == 44100) {
- slSampleRate_ = SL_SAMPLINGRATE_44_1;
- } else if (sampleRate_ == 8000) {
- slSampleRate_ = SL_SAMPLINGRATE_8;
- } else if (sampleRate_ == 11025) {
- slSampleRate_ = SL_SAMPLINGRATE_11_025;
- } else {
- LOGE("unknown sample rate, not changing");
- CHECK(false);
- }
- if (channels_ == 2) {
- slOutputChannels_ = SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT;
- } else if (channels_ == 1) {
- slOutputChannels_ = SL_SPEAKER_FRONT_LEFT;
- } else {
- LOGE("unknown channels, not changing");
- }
+ floatBuffer_ = new float[targetFrames_ * channels_];
+ injectBuffer_ = new float[targetFrames_ * channels_];
}
AudioEngine::~AudioEngine() {
@@ -357,7 +338,6 @@ void AudioEngine::PrefetchDurationSampleRateAndChannels(
PausePlaying(playItf);
// Wait until the data has been prefetched.
- // TODO(hugohudson): 0. Not dealing with error just yet.
{
SLuint32 prefetchStatus = SL_PREFETCHSTATUS_UNDERFLOW;
android::Mutex::Autolock autoLock(prefetchLock_);
@@ -501,38 +481,70 @@ void AudioEngine::ClearDecodeBuffer() {
decodeBuffer_.Clear();
}
-bool AudioEngine::PlayFromThisSource(const SLDataSource& audioSrc) {
- ClearDecodeBuffer();
- floatBuffer_ = new float[targetFrames_ * channels_];
- injectBuffer_ = new float[targetFrames_ * channels_];
-
- // Create the engine.
+static void CreateAndRealizeEngine(SLObjectItf &engine,
+ SLEngineItf &engineInterface) {
SLEngineOption EngineOption[] = { {
SL_ENGINEOPTION_THREADSAFE, SL_BOOLEAN_TRUE } };
- SLObjectItf engine;
SLresult result = slCreateEngine(&engine, 1, EngineOption, 0, NULL, NULL);
CheckSLResult("create engine", result);
result = (*engine)->Realize(engine, SL_BOOLEAN_FALSE);
CheckSLResult("realise engine", result);
- SLEngineItf engineInterface;
result = (*engine)->GetInterface(engine, SL_IID_ENGINE, &engineInterface);
CheckSLResult("get interface", result);
+}
+static void CreateAndRealizeOutputMix(SLEngineItf &engineInterface,
+ SLObjectItf &outputMix) {
+ SLresult result;
// Create the output mix for playing.
- SLObjectItf outputMix;
result = (*engineInterface)->CreateOutputMix(
engineInterface, &outputMix, 0, NULL, NULL);
CheckSLResult("create output mix", result);
result = (*outputMix)->Realize(outputMix, SL_BOOLEAN_FALSE);
CheckSLResult("realize", result);
+}
+
+static void CreateAndRealizeAudioPlayer(size_t sampleRate, size_t channels,
+ SLObjectItf &outputMix, SLObjectItf &audioPlayer,
+ SLEngineItf &engineInterface) {
+ SLresult result;
+ SLuint32 slSampleRate;
+ SLuint32 slOutputChannels;
+ switch (sampleRate) {
+ case 44100:
+ slSampleRate = SL_SAMPLINGRATE_44_1;
+ break;
+ case 8000:
+ slSampleRate = SL_SAMPLINGRATE_8;
+ break;
+ case 11025:
+ slSampleRate = SL_SAMPLINGRATE_11_025;
+ break;
+ default:
+ LOGE("unknown sample rate, using SL_SAMPLINGRATE_44_1");
+ slSampleRate = SL_SAMPLINGRATE_44_1;
+ break;
+ }
+ switch (channels) {
+ case 2:
+ slOutputChannels = SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT;
+ break;
+ case 1:
+ slOutputChannels = SL_SPEAKER_FRONT_LEFT;
+ break;
+ default:
+ LOGE("unknown channels, using 2");
+ slOutputChannels = SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT;
+ break;
+ }
// Define the source and sink for the audio player: comes from a buffer queue
// and goes to the output mix.
SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {
SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2 };
- SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM, channels_, slSampleRate_,
+ SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM, channels, slSampleRate,
SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16,
- slOutputChannels_, SL_BYTEORDER_LITTLEENDIAN};
+ slOutputChannels, SL_BYTEORDER_LITTLEENDIAN};
SLDataSource playingSrc = {&loc_bufq, &format_pcm};
SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, outputMix};
SLDataSink audioSnk = {&loc_outmix, NULL};
@@ -543,27 +555,39 @@ bool AudioEngine::PlayFromThisSource(const SLDataSource& audioSrc) {
const SLInterfaceID iids[playerInterfaceCount] = {
SL_IID_ANDROIDSIMPLEBUFFERQUEUE };
const SLboolean reqs[playerInterfaceCount] = { SL_BOOLEAN_TRUE };
- SLObjectItf audioPlayer;
result = (*engineInterface)->CreateAudioPlayer(engineInterface, &audioPlayer,
&playingSrc, &audioSnk, playerInterfaceCount, iids, reqs);
CheckSLResult("create audio player", result);
result = (*audioPlayer)->Realize(audioPlayer, SL_BOOLEAN_FALSE);
CheckSLResult("realize buffer queue", result);
+}
+static void GetAudioPlayInterfacesAndRegisterCallback(SLObjectItf &audioPlayer,
+ SLPlayItf &audioPlayerPlay,
+ SLAndroidSimpleBufferQueueItf &audioPlayerQueue) {
+ SLresult result;
// Get the play interface from the player, as well as the buffer queue
// interface from its source.
// Register for callbacks during play.
- SLPlayItf audioPlayerPlay;
result = (*audioPlayer)->GetInterface(
audioPlayer, SL_IID_PLAY, &audioPlayerPlay);
CheckSLResult("get interface", result);
- SLAndroidSimpleBufferQueueItf audioPlayerQueue;
result = (*audioPlayer)->GetInterface(audioPlayer,
SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &audioPlayerQueue);
CheckSLResult("get interface again", result);
result = (*audioPlayerQueue)->RegisterCallback(
audioPlayerQueue, PlayingBufferQueueCb, NULL);
CheckSLResult("register callback", result);
+}
+
+bool AudioEngine::PlayFromThisSource(const SLDataSource& audioSrc) {
+ ClearDecodeBuffer();
+
+ SLresult result;
+
+ SLObjectItf engine;
+ SLEngineItf engineInterface;
+ CreateAndRealizeEngine(engine, engineInterface);
// Define the source and sink for the decoding player: comes from the source
// this method was called with, is sent to another buffer queue.
@@ -571,7 +595,7 @@ bool AudioEngine::PlayFromThisSource(const SLDataSource& audioSrc) {
decBuffQueue.locatorType = SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE;
decBuffQueue.numBuffers = kNumberOfBuffersInQueue;
// A valid value seems required here but is currently ignored.
- SLDataFormat_PCM pcm = {SL_DATAFORMAT_PCM, 1, slSampleRate_,
+ SLDataFormat_PCM pcm = {SL_DATAFORMAT_PCM, 1, SL_SAMPLINGRATE_44_1,
SL_PCMSAMPLEFORMAT_FIXED_16, 16,
SL_SPEAKER_FRONT_LEFT, SL_BYTEORDER_LITTLEENDIAN};
SLDataSink decDest = { &decBuffQueue, &pcm };
@@ -594,7 +618,7 @@ bool AudioEngine::PlayFromThisSource(const SLDataSource& audioSrc) {
// Get the play interface from the decoder, and register event callbacks.
// Get the buffer queue, prefetch and seek interfaces.
- SLPlayItf decoderPlay;
+ SLPlayItf decoderPlay = NULL;
result = (*decoder)->GetInterface(decoder, SL_IID_PLAY, &decoderPlay);
CheckSLResult("get play interface, implicit", result);
result = (*decoderPlay)->SetCallbackEventsMask(
@@ -615,8 +639,15 @@ bool AudioEngine::PlayFromThisSource(const SLDataSource& audioSrc) {
result = (*decoder)->GetInterface(decoder, SL_IID_SEEK, &decoderSeek);
CheckSLResult("get seek interface", result);
+ // Get the metadata interface from the decoder.
+ SLMetadataExtractionItf decoderMetadata;
+ result = (*decoder)->GetInterface(decoder,
+ SL_IID_METADATAEXTRACTION, &decoderMetadata);
+ CheckSLResult("getting metadata interface", result);
+
+ CallbackContext callbackContext;
RegisterCallbackContextAndAddEnqueueBuffersToDecoder(
- decoderQueue, decoderPlay, callbackLock_);
+ decoderQueue, decoderMetadata, callbackLock_, &callbackContext);
// Initialize the callback for prefetch errors, if we can't open the
// resource to decode.
@@ -631,15 +662,23 @@ bool AudioEngine::PlayFromThisSource(const SLDataSource& audioSrc) {
PrefetchDurationSampleRateAndChannels(decoderPlay, decoderPrefetch);
- ExtractMetadataFromDecoder(decoder);
-
StartPlaying(decoderPlay);
+ SLObjectItf outputMix = NULL;
+ SLObjectItf audioPlayer = NULL;
+ SLPlayItf audioPlayerPlay = NULL;
+ SLAndroidSimpleBufferQueueItf audioPlayerQueue = NULL;
+
// The main loop - until we're told to stop: if there is audio data coming
// out of the decoder, feed it through the time scaler.
// As it comes out of the time scaler, feed it into the audio player.
while (!Finished()) {
if (GetWasStartRequested()) {
+ CreateAndRealizeOutputMix(engineInterface, outputMix);
+ CreateAndRealizeAudioPlayer(sampleRate_, channels_, outputMix,
+ audioPlayer, engineInterface);
+ GetAudioPlayInterfacesAndRegisterCallback(audioPlayer, audioPlayerPlay,
+ audioPlayerQueue);
ClearRequestStart();
StartPlaying(audioPlayerPlay);
}
@@ -647,23 +686,27 @@ bool AudioEngine::PlayFromThisSource(const SLDataSource& audioSrc) {
usleep(kSleepTimeMicros);
}
- StopPlaying(audioPlayerPlay);
- StopPlaying(decoderPlay);
-
- // Delete the audio player.
- result = (*audioPlayerQueue)->Clear(audioPlayerQueue);
- CheckSLResult("clear audio player queue", result);
- result = (*audioPlayerQueue)->RegisterCallback(audioPlayerQueue, NULL, NULL);
- CheckSLResult("clear callback", result);
- (*audioPlayer)->AbortAsyncOperation(audioPlayer);
- audioPlayerPlay = NULL;
- audioPlayerQueue = NULL;
- (*audioPlayer)->Destroy(audioPlayer);
+ // Delete the audio player and output mix, iff they have been created.
+ if (audioPlayer != NULL) {
+ StopPlaying(audioPlayerPlay);
+ result = (*audioPlayerQueue)->Clear(audioPlayerQueue);
+ CheckSLResult("clear audio player queue", result);
+ result = (*audioPlayerQueue)->RegisterCallback(audioPlayerQueue, NULL, NULL);
+ CheckSLResult("clear callback", result);
+ (*audioPlayer)->AbortAsyncOperation(audioPlayer);
+ (*audioPlayer)->Destroy(audioPlayer);
+ (*outputMix)->Destroy(outputMix);
+ audioPlayer = NULL;
+ audioPlayerPlay = NULL;
+ audioPlayerQueue = NULL;
+ outputMix = NULL;
+ }
// Delete the decoder.
+ StopPlaying(decoderPlay);
result = (*decoderPrefetch)->RegisterCallback(decoderPrefetch, NULL, NULL);
CheckSLResult("clearing prefetch error callback", result);
- // TODO(hugohudson): 0. This is returning slresult 13 if I do no playback.
+ // This is returning slresult 13 if I do no playback.
// Repro is to comment out all before this line, and all after enqueueing
// my buffers.
// result = (*decoderQueue)->Clear(decoderQueue);
@@ -679,10 +722,9 @@ bool AudioEngine::PlayFromThisSource(const SLDataSource& audioSrc) {
decoderPlay = NULL;
(*decoder)->Destroy(decoder);
- // Delete the output mix, then the engine.
- (*outputMix)->Destroy(outputMix);
- engineInterface = NULL;
+ // Delete the engine.
(*engine)->Destroy(engine);
+ engineInterface = NULL;
return true;
}
@@ -738,7 +780,6 @@ void AudioEngine::PlayingBufferQueueCallback() {
void AudioEngine::PrefetchEventCallback(
SLPrefetchStatusItf caller, SLuint32 event) {
// If there was a problem during decoding, then signal the end.
- LOGI("in the prefetch callback");
SLpermille level = 0;
SLresult result = (*caller)->GetFillLevel(caller, &level);
CheckSLResult("get fill level", result);
@@ -752,10 +793,8 @@ void AudioEngine::PrefetchEventCallback(
SetEndOfDecoderReached();
}
if (SL_PREFETCHSTATUS_SUFFICIENTDATA == event) {
- LOGI("looks like our event...");
// android::Mutex::Autolock autoLock(prefetchLock_);
// prefetchCondition_.broadcast();
- LOGI("just sent a broadcast");
}
}
@@ -775,6 +814,10 @@ void AudioEngine::DecodingBufferQueueCallback(
decodeBuffer_.AddData(pCntxt->pDataBase, kBufferSizeInBytes);
}
+ // TODO: This call must be added back in to fix the bug relating to using
+ // the correct sample rate and channels. I will do this in the follow-up.
+ // ExtractMetadataFromDecoder(pCntxt->decoderMetadata);
+
// Increase data pointer by buffer size
pCntxt->pData += kBufferSizeInBytes;
if (pCntxt->pData >= pCntxt->pDataBase +
diff --git a/variablespeed/jni/variablespeed.h b/variablespeed/jni/variablespeed.h
index 5c6b45d..9da7df1 100644
--- a/variablespeed/jni/variablespeed.h
+++ b/variablespeed/jni/variablespeed.h
@@ -109,8 +109,6 @@ class AudioEngine {
size_t channels_;
size_t sampleRate_;
- SLuint32 slSampleRate_;
- SLuint32 slOutputChannels_;
size_t targetFrames_;
float windowDuration_;
float windowOverlapDuration_;
diff --git a/variablespeed/src/com/android/ex/variablespeed/VariableSpeed.java b/variablespeed/src/com/android/ex/variablespeed/VariableSpeed.java
index b3b3efb..1d4e973 100644
--- a/variablespeed/src/com/android/ex/variablespeed/VariableSpeed.java
+++ b/variablespeed/src/com/android/ex/variablespeed/VariableSpeed.java
@@ -16,6 +16,8 @@
package com.android.ex.variablespeed;
+import com.google.common.base.Preconditions;
+
import android.content.Context;
import android.media.MediaPlayer;
import android.net.Uri;
@@ -62,6 +64,7 @@ public class VariableSpeed implements MediaPlayerProxy {
@GuardedBy("lock") private MediaPlayer.OnCompletionListener mCompletionListener;
private VariableSpeed(Executor executor) throws UnsupportedOperationException {
+ Preconditions.checkNotNull(executor);
mExecutor = executor;
try {
VariableSpeedNative.loadLibrary();
@@ -147,7 +150,7 @@ public class VariableSpeed implements MediaPlayerProxy {
private void waitForLatch(CountDownLatch latch) {
try {
- boolean success = latch.await(10, TimeUnit.SECONDS);
+ boolean success = latch.await(1, TimeUnit.SECONDS);
if (!success) {
reportException(new TimeoutException("waited too long"));
}
@@ -344,9 +347,7 @@ public class VariableSpeed implements MediaPlayerProxy {
@Override
public int getCurrentPosition() {
synchronized (lock) {
- if (mHasBeenReleased) {
- return 0;
- }
+ check(!mHasBeenReleased, "has been released, reset before use");
if (!mHasStartedPlayback) {
return 0;
}
diff --git a/variablespeed/tests/Android.mk b/variablespeed/tests/Android.mk
new file mode 100644
index 0000000..fe386a2
--- /dev/null
+++ b/variablespeed/tests/Android.mk
@@ -0,0 +1,28 @@
+# 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.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_CERTIFICATE := shared
+LOCAL_JAVA_LIBRARIES := android.test.runner
+LOCAL_MODULE_TAGS := tests
+LOCAL_PACKAGE_NAME := AndroidExVariablespeedTests
+LOCAL_SDK_VERSION := current
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_STATIC_JAVA_LIBRARIES := android-ex-variablespeed
+LOCAL_REQUIRED_MODULES := libvariablespeed
+LOCAL_PROGUARD_ENABLED := disabled
+
+include $(BUILD_PACKAGE)
diff --git a/variablespeed/tests/AndroidManifest.xml b/variablespeed/tests/AndroidManifest.xml
new file mode 100644
index 0000000..abbe65c
--- /dev/null
+++ b/variablespeed/tests/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.ex.variablespeed.tests"
+>
+ <application>
+ <uses-library
+ android:name="android.test.runner"
+ />
+ </application>
+ <instrumentation
+ android:name="android.test.InstrumentationTestRunner"
+ android:targetPackage="com.android.ex.variablespeed.tests"
+ android:label="Android Variablespeed Library Tests"
+ />
+ <!-- The tests need these permissions to add test voicemail entries. -->
+ <uses-permission android:name="com.android.voicemail.permission.READ_WRITE_OWN_VOICEMAIL" />
+</manifest>
diff --git a/variablespeed/tests/assets/README.txt b/variablespeed/tests/assets/README.txt
new file mode 100644
index 0000000..3e69968
--- /dev/null
+++ b/variablespeed/tests/assets/README.txt
@@ -0,0 +1,4 @@
+Files quick_test_recording.mp3 and count_and_test.3gpp are copyright 2011 by
+Hugo Hudson and are licensed under a
+Creative Commons Attribution 3.0 Unported License:
+ http://creativecommons.org/licenses/by/3.0/
diff --git a/variablespeed/tests/assets/count_and_test.3gpp b/variablespeed/tests/assets/count_and_test.3gpp
new file mode 100644
index 0000000..c71a423
--- /dev/null
+++ b/variablespeed/tests/assets/count_and_test.3gpp
Binary files differ
diff --git a/variablespeed/tests/assets/quick_test_recording.mp3 b/variablespeed/tests/assets/quick_test_recording.mp3
new file mode 100644
index 0000000..ad7cb9c
--- /dev/null
+++ b/variablespeed/tests/assets/quick_test_recording.mp3
Binary files differ
diff --git a/variablespeed/tests/src/com/android/ex/variablespeed/AwaitableCompletionListener.java b/variablespeed/tests/src/com/android/ex/variablespeed/AwaitableCompletionListener.java
new file mode 100644
index 0000000..ef2648c
--- /dev/null
+++ b/variablespeed/tests/src/com/android/ex/variablespeed/AwaitableCompletionListener.java
@@ -0,0 +1,59 @@
+/*
+ * 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 android.media.MediaPlayer;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import javax.annotation.concurrent.ThreadSafe;
+
+// TODO: There is sufficent similarity between this and the awaitable error listener that I should
+// extract a common base class.
+/** Implementation of {@link MediaPlayer.OnErrorListener} that we can wait for in tests. */
+@ThreadSafe
+public class AwaitableCompletionListener implements MediaPlayer.OnCompletionListener {
+ private final BlockingQueue<Object> mQueue = new LinkedBlockingQueue<Object>();
+
+ @Override
+ public void onCompletion(MediaPlayer mp) {
+ try {
+ mQueue.put(new Object());
+ } catch (InterruptedException e) {
+ // This should not happen in practice, the queue is unbounded so this method will not
+ // block.
+ // If this thread is using interrupt to shut down, preserve interrupt status and return.
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ public void awaitOneCallback(long timeout, TimeUnit unit) throws InterruptedException,
+ TimeoutException {
+ if (mQueue.poll(timeout, unit) == null) {
+ throw new TimeoutException();
+ }
+ }
+
+ public void assertNoMoreCallbacks() {
+ if (mQueue.peek() != null) {
+ throw new IllegalStateException("there was an unexpected callback on the queue");
+ }
+ }
+}
diff --git a/variablespeed/tests/src/com/android/ex/variablespeed/AwaitableErrorListener.java b/variablespeed/tests/src/com/android/ex/variablespeed/AwaitableErrorListener.java
new file mode 100644
index 0000000..bf5fb42
--- /dev/null
+++ b/variablespeed/tests/src/com/android/ex/variablespeed/AwaitableErrorListener.java
@@ -0,0 +1,67 @@
+/*
+ * 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 android.media.MediaPlayer;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import javax.annotation.concurrent.ThreadSafe;
+
+/** Implementation of {@link MediaPlayer.OnCompletionListener} that we can wait for in tests. */
+@ThreadSafe
+public class AwaitableErrorListener implements MediaPlayer.OnErrorListener {
+ private final BlockingQueue<Object> mQueue = new LinkedBlockingQueue<Object>();
+ private volatile boolean mOnErrorReturnValue = true;
+
+ @Override
+ public boolean onError(MediaPlayer mp, int what, int extra) {
+ addAnObjectToTheQueue();
+ return mOnErrorReturnValue;
+ }
+
+ public void setOnErrorReturnValue(boolean value) {
+ mOnErrorReturnValue = value;
+ }
+
+ private void addAnObjectToTheQueue() {
+ try {
+ mQueue.put(new Object());
+ } catch (InterruptedException e) {
+ // This should not happen in practice, the queue is unbounded so this method will not
+ // block.
+ // If this thread is using interrupt to shut down, preserve interrupt status and return.
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ public void awaitOneCallback(long timeout, TimeUnit unit) throws InterruptedException,
+ TimeoutException {
+ if (mQueue.poll(timeout, unit) == null) {
+ throw new TimeoutException();
+ }
+ }
+
+ public void assertNoMoreCallbacks() {
+ if (mQueue.peek() != null) {
+ throw new IllegalStateException("there was an unexpected callback on the queue");
+ }
+ }
+}
diff --git a/variablespeed/tests/src/com/android/ex/variablespeed/DynamicProxy.java b/variablespeed/tests/src/com/android/ex/variablespeed/DynamicProxy.java
new file mode 100644
index 0000000..429f2cc
--- /dev/null
+++ b/variablespeed/tests/src/com/android/ex/variablespeed/DynamicProxy.java
@@ -0,0 +1,62 @@
+/*
+ * 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 java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+/**
+ * Contains a utility method for adapting a given interface against a real implementation.
+ * <p>
+ * This class is thead-safe.
+ */
+public class DynamicProxy {
+ /**
+ * Dynamically adapts a given interface against a delegate object.
+ * <p>
+ * For the given {@code clazz} object, which should be an interface, we return a new dynamic
+ * proxy object implementing that interface, which will forward all method calls made on the
+ * interface onto the delegate object.
+ * <p>
+ * In practice this means that you can make it appear as though {@code delegate} implements the
+ * {@code clazz} interface, without this in practice being the case. As an example, if you
+ * create an interface representing the {@link android.media.MediaPlayer}, you could pass this
+ * interface in as the first argument, and a real {@link android.media.MediaPlayer} in as the
+ * second argument, and now calls to the interface will be automatically sent on to the real
+ * media player. The reason you may be interested in doing this in the first place is that this
+ * allows you to test classes that have dependencies that are final or cannot be easily mocked.
+ */
+ // This is safe, because we know that proxy instance implements the interface.
+ @SuppressWarnings("unchecked")
+ public static <T> T dynamicProxy(Class<T> clazz, final Object delegate) {
+ InvocationHandler invoke = new InvocationHandler() {
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ try {
+ return delegate.getClass()
+ .getMethod(method.getName(), method.getParameterTypes())
+ .invoke(delegate, args);
+ } catch (InvocationTargetException e) {
+ throw e.getCause();
+ }
+ }
+ };
+ return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class<?>[] { clazz }, invoke);
+ }
+}
diff --git a/variablespeed/tests/src/com/android/ex/variablespeed/MediaPlayerProxyTestCase.java b/variablespeed/tests/src/com/android/ex/variablespeed/MediaPlayerProxyTestCase.java
new file mode 100644
index 0000000..59bdfcf
--- /dev/null
+++ b/variablespeed/tests/src/com/android/ex/variablespeed/MediaPlayerProxyTestCase.java
@@ -0,0 +1,522 @@
+/*
+ * 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}.
+ * <p>
+ * The purpose behind this class is to collect tests that implementations of
+ * MediaPlayerProxy should support.
+ * <p>
+ * 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.
+ * <p>
+ * It allows us to test the current {@link VariableSpeed} implementation, and make sure that this
+ * too corresponds with the MediaPlayer implementation.
+ * <p>
+ * 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.
+ * <p>
+ * This is lazily filled in by the {@link #getTestContentUri(String, String)} method.
+ * <p>
+ * 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<String, Uri> mContentUriMap = new HashMap<String, Uri>();
+
+ /** 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 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);
+ }
+
+ /**
+ * Gets the {@link Uri} for the test audio content we should play.
+ * <p>
+ * 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.
+ * <p>
+ * 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();
+ }
+}
diff --git a/variablespeed/tests/src/com/android/ex/variablespeed/RealMediaPlayerTest.java b/variablespeed/tests/src/com/android/ex/variablespeed/RealMediaPlayerTest.java
new file mode 100644
index 0000000..7f12671
--- /dev/null
+++ b/variablespeed/tests/src/com/android/ex/variablespeed/RealMediaPlayerTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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 android.media.MediaPlayer;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests that MediaPlayerProxyTestCase contains reasonable tests with a real {@link MediaPlayer}.
+ */
+public class RealMediaPlayerTest extends MediaPlayerProxyTestCase {
+ @Override
+ public MediaPlayerProxy createTestMediaPlayer() throws Exception {
+ // We have to construct the MediaPlayer on the main thread (or at least on a thread with an
+ // associated looper) otherwise we don't get sent the messages when callbacks should be
+ // invoked. I've raised a bug for this: http://b/4602011.
+ Callable<MediaPlayer> callable = new Callable<MediaPlayer>() {
+ @Override
+ public MediaPlayer call() throws Exception {
+ return new MediaPlayer();
+ }
+ };
+ FutureTask<MediaPlayer> future = new FutureTask<MediaPlayer>(callable);
+ getInstrumentation().runOnMainSync(future);
+ return DynamicProxy.dynamicProxy(MediaPlayerProxy.class, future.get(1, TimeUnit.SECONDS));
+ }
+}
diff --git a/variablespeed/tests/src/com/android/ex/variablespeed/VariableSpeedTest.java b/variablespeed/tests/src/com/android/ex/variablespeed/VariableSpeedTest.java
new file mode 100644
index 0000000..433b7a5
--- /dev/null
+++ b/variablespeed/tests/src/com/android/ex/variablespeed/VariableSpeedTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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 android.util.Log;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/** Tests for the {@link VariableSpeed} class. */
+public class VariableSpeedTest extends MediaPlayerProxyTestCase {
+ private static final String TAG = "VariableSpeedTest";
+
+ private ScheduledExecutorService mExecutor;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mExecutor = Executors.newScheduledThreadPool(2);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ // I explicitly want to do super's tear-down first, because I need to get it to reset
+ // the media player before I can be confident that I can shut down the executor service.
+ super.tearDown();
+ mExecutor.shutdown();
+ if (mExecutor.awaitTermination(10, TimeUnit.SECONDS)) {
+ Log.e(TAG, "Couldn't shut down Executor during test, check your cleanup code!");
+ }
+ mExecutor = null;
+ }
+
+ @Override
+ public MediaPlayerProxy createTestMediaPlayer() throws Exception {
+ return VariableSpeed.createVariableSpeed(mExecutor);
+ }
+}