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
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(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 @@
 
 // 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 @@
   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 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 @@
     : 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 @@
       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 @@
   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 @@
   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 @@
   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 @@
   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 @@
 
   // 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 @@
   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 @@
 
   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 @@
     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 @@
   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::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 @@
     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 @@
     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 @@
 
   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 @@
     @GuardedBy("lock") private MediaPlayer.OnCompletionListener mCompletionListener;
 
     private VariableSpeed(Executor executor) throws UnsupportedOperationException {
+        Preconditions.checkNotNull(executor);
         mExecutor = executor;
         try {
             VariableSpeedNative.loadLibrary();
@@ -147,7 +150,7 @@
 
     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 @@
     @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);
+    }
+}