diff options
author | Hugo Hudson <hugohudson@google.com> | 2011-07-25 17:04:42 +0100 |
---|---|---|
committer | Hugo Hudson <hugohudson@google.com> | 2011-07-25 20:45:39 +0100 |
commit | 9730f15ebbf4b64cd48e0777850e56cb516a9ed4 (patch) | |
tree | 796a76a6b0f18c1d4f5799eb1d359b0961b083f3 /variablespeed | |
parent | 376a291b7a3016cc85501ee1c044629cce60e75c (diff) | |
download | android_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')
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 Binary files differnew file mode 100644 index 0000000..c71a423 --- /dev/null +++ b/variablespeed/tests/assets/count_and_test.3gpp diff --git a/variablespeed/tests/assets/quick_test_recording.mp3 b/variablespeed/tests/assets/quick_test_recording.mp3 Binary files differnew file mode 100644 index 0000000..ad7cb9c --- /dev/null +++ b/variablespeed/tests/assets/quick_test_recording.mp3 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); + } +} |