Refactor Voicemail Playback into standalone view.

+ Substitutes the existing playback widget in CallDetailActivity,
although the plan is to move this to the call log shortly.
+ Convert the widget from a fragment into a layout. This allows us
to more easily create multiple instances of the voicemail widget in
the same view, as we intend to do in the call log.
+ Shift UI-related logic from Presenter to the Layout.
+ Fix janky seeking, so that it now works correctly consistently
rather than sporadically, and doesn't need to buffer again.
- Remove the VariableSpeed player formerly used in the Presenter. We
don't use this functionality anymore, and this allows us to directly
used the framework MediaPlayer (instead of a custom legacy proxy).

Bug: 21170557
Bug: 20693172
Change-Id: Ia34f459df10e43763b32fdb0954f83e882664231
diff --git a/Android.mk b/Android.mk
index 0a93c32..1440fcc 100644
--- a/Android.mk
+++ b/Android.mk
@@ -32,7 +32,6 @@
 LOCAL_JAVA_LIBRARIES := telephony-common
 LOCAL_STATIC_JAVA_LIBRARIES := \
     android-common \
-    android-ex-variablespeed \
     android-support-v13 \
     android-support-v4 \
     android-support-v7-cardview \
@@ -42,8 +41,6 @@
     guava \
     libphonenumber
 
-LOCAL_REQUIRED_MODULES := libvariablespeed
-
 LOCAL_PACKAGE_NAME := Dialer
 LOCAL_CERTIFICATE := shared
 LOCAL_PRIVILEGED_MODULE := true
diff --git a/res/layout/call_detail.xml b/res/layout/call_detail.xml
index c077851..5d1607e 100644
--- a/res/layout/call_detail.xml
+++ b/res/layout/call_detail.xml
@@ -87,6 +87,12 @@
             </LinearLayout>
         </LinearLayout>
 
+        <com.android.dialer.voicemail.VoicemailPlaybackLayout
+            android:id="@+id/voicemail_playback_layout"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:visibility="gone" />
+
         <!--
           The list view is under everything.
           It contains a first header element which is hidden under the controls UI.
diff --git a/src/com/android/dialer/CallDetailActivity.java b/src/com/android/dialer/CallDetailActivity.java
index 1c684a5..7fa3750 100644
--- a/src/com/android/dialer/CallDetailActivity.java
+++ b/src/com/android/dialer/CallDetailActivity.java
@@ -61,7 +61,8 @@
 import com.android.dialer.util.IntentUtil;
 import com.android.dialer.util.DialerUtils;
 import com.android.dialer.util.TelecomUtil;
-import com.android.dialer.voicemail.VoicemailPlaybackFragment;
+import com.android.dialer.voicemail.VoicemailPlaybackLayout;
+import com.android.dialer.voicemail.VoicemailPlaybackPresenter;
 
 import java.util.List;
 
@@ -217,7 +218,7 @@
     /** Helper to load contact photos. */
     private ContactPhotoManager mContactPhotoManager;
 
-    private LinearLayout mVoicemailHeader;
+    private VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
 
     private Uri mVoicemailUri;
     private BidiFormatter mBidiFormatter = BidiFormatter.getInstance();
@@ -255,6 +256,7 @@
         getActionBar().setDisplayHomeAsUpEnabled(true);
 
         optionallyHandleVoicemail();
+
         if (getIntent().getBooleanExtra(EXTRA_FROM_NOTIFICATION, false)) {
             closeSystemDialogs();
         }
@@ -267,6 +269,38 @@
         CallLogAsyncTaskUtil.getCallDetails(this, getCallLogEntryUris(), mCallLogAsyncTaskListener);
     }
 
+    @Override
+    public void onPause() {
+        if (mVoicemailPlaybackPresenter != null) {
+            mVoicemailPlaybackPresenter.onPause();
+        }
+        super.onPause();
+    }
+
+    @Override
+    public void onDestroy() {
+        if (mVoicemailPlaybackPresenter != null) {
+            mVoicemailPlaybackPresenter.onDestroy();
+        }
+        super.onDestroy();
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        if (mVoicemailPlaybackPresenter != null) {
+            mVoicemailPlaybackPresenter.onSaveInstanceState(outState);
+        }
+    }
+
+    @Override
+    public void onRestoreInstanceState(Bundle savedInstanceState) {
+        if (mVoicemailPlaybackPresenter != null) {
+            mVoicemailPlaybackPresenter.onRestoreInstanceState(savedInstanceState);
+        }
+        super.onRestoreInstanceState(savedInstanceState);
+    }
+
     /**
      * Handle voicemail playback or hide voicemail ui.
      * <p>
@@ -274,37 +308,15 @@
      * playback.  If it doesn't, then don't inflate the voicemail ui.
      */
     private void optionallyHandleVoicemail() {
-
         if (hasVoicemail()) {
-            LayoutInflater inflater =
-                    (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-            mVoicemailHeader =
-                    (LinearLayout) inflater.inflate(R.layout.call_details_voicemail_header, null);
-            View voicemailContainer = mVoicemailHeader.findViewById(R.id.voicemail_container);
-            ListView historyList = (ListView) findViewById(R.id.history);
-            historyList.addHeaderView(mVoicemailHeader);
-            // Has voicemail: add the voicemail fragment.  Add suitable arguments to set the uri
-            // to play and optionally start the playback.
-            // Do a query to fetch the voicemail status messages.
-            VoicemailPlaybackFragment playbackFragment;
+            VoicemailPlaybackLayout voicemailPlaybackLayout =
+                (VoicemailPlaybackLayout) findViewById(R.id.voicemail_playback_layout);
 
-            playbackFragment = (VoicemailPlaybackFragment) getFragmentManager().findFragmentByTag(
-                    VOICEMAIL_FRAGMENT_TAG);
+            mVoicemailPlaybackPresenter = new VoicemailPlaybackPresenter(this);
+            mVoicemailPlaybackPresenter.setPlaybackView(
+                    voicemailPlaybackLayout, mVoicemailUri, false /* startPlayingImmediately */);
 
-            if (playbackFragment == null) {
-                playbackFragment = new VoicemailPlaybackFragment();
-                Bundle fragmentArguments = new Bundle();
-                fragmentArguments.putParcelable(EXTRA_VOICEMAIL_URI, mVoicemailUri);
-                if (getIntent().getBooleanExtra(EXTRA_VOICEMAIL_START_PLAYBACK, false)) {
-                    fragmentArguments.putBoolean(EXTRA_VOICEMAIL_START_PLAYBACK, true);
-                }
-                playbackFragment.setArguments(fragmentArguments);
-                getFragmentManager().beginTransaction()
-                        .add(R.id.voicemail_container, playbackFragment, VOICEMAIL_FRAGMENT_TAG)
-                                .commitAllowingStateLoss();
-            }
-
-            voicemailContainer.setVisibility(View.VISIBLE);
+            voicemailPlaybackLayout.setVisibility(View.VISIBLE);
             CallLogAsyncTaskUtil.markVoicemailAsRead(this, mVoicemailUri);
         }
     }
diff --git a/src/com/android/dialer/calllog/CallLogFragment.java b/src/com/android/dialer/calllog/CallLogFragment.java
index f98fc21..36d9bb6 100644
--- a/src/com/android/dialer/calllog/CallLogFragment.java
+++ b/src/com/android/dialer/calllog/CallLogFragment.java
@@ -92,7 +92,6 @@
     /** Whether there is at least one voicemail source installed. */
     private boolean mVoicemailSourcesAvailable = false;
 
-    private VoicemailStatusHelper mVoicemailStatusHelper;
     private View mEmptyListView;
     private KeyguardManager mKeyguardManager;
 
@@ -277,7 +276,6 @@
                 this);
         mRecyclerView.setAdapter(mAdapter);
 
-        mVoicemailStatusHelper = new VoicemailStatusHelperImpl();
         fetchCalls();
         return view;
     }
diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java b/src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java
deleted file mode 100644
index ed70551..0000000
--- a/src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java
+++ /dev/null
@@ -1,378 +0,0 @@
-/*
- * 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.dialer.voicemail;
-
-import static com.android.dialer.CallDetailActivity.EXTRA_VOICEMAIL_START_PLAYBACK;
-import static com.android.dialer.CallDetailActivity.EXTRA_VOICEMAIL_URI;
-
-import android.app.Activity;
-import android.app.Fragment;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import android.media.AudioManager;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.PowerManager;
-import android.provider.VoicemailContract;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageButton;
-import android.widget.SeekBar;
-import android.widget.TextView;
-
-import com.android.common.io.MoreCloseables;
-import com.android.contacts.commonbind.analytics.AnalyticsUtil;
-import com.android.dialer.R;
-
-import com.google.common.base.Preconditions;
-
-import java.util.concurrent.TimeUnit;
-
-import javax.annotation.concurrent.GuardedBy;
-import javax.annotation.concurrent.NotThreadSafe;
-
-/**
- * Displays and plays back a single voicemail.
- * <p>
- * When the Activity containing this Fragment is created, voicemail playback
- * will begin immediately. The Activity is expected to be started via an intent
- * containing a suitable voicemail uri to playback.
- * <p>
- * This class is not thread-safe, it is thread-confined. All calls to all public
- * methods on this class are expected to come from the main ui thread.
- */
-@NotThreadSafe
-public class VoicemailPlaybackFragment extends Fragment {
-    private static final String TAG = VoicemailPlaybackFragment.class.getSimpleName();
-    private static final int NUMBER_OF_THREADS_IN_POOL = 2;
-    private static final String[] HAS_CONTENT_PROJECTION = new String[] {
-        VoicemailContract.Voicemails.HAS_CONTENT,
-    };
-
-    private VoicemailPlaybackPresenter mPresenter;
-    private View mPlaybackLayout;
-
-    private PowerManager.WakeLock mProximityWakeLock;
-
-    @Override
-    public View onCreateView(LayoutInflater inflater, ViewGroup container,
-            Bundle savedInstanceState) {
-        mPlaybackLayout = inflater.inflate(R.layout.playback_layout, null);
-        return mPlaybackLayout;
-    }
-
-    @Override
-    public void onActivityCreated(Bundle savedInstanceState) {
-        super.onActivityCreated(savedInstanceState);
-        Bundle arguments = getArguments();
-        Preconditions.checkNotNull(arguments, "fragment must be started with arguments");
-        Uri voicemailUri = arguments.getParcelable(EXTRA_VOICEMAIL_URI);
-        Preconditions.checkNotNull(voicemailUri, "fragment must contain EXTRA_VOICEMAIL_URI");
-        boolean startPlayback = arguments.getBoolean(EXTRA_VOICEMAIL_START_PLAYBACK, false);
-
-        PowerManager powerManager =
-                (PowerManager) getActivity().getSystemService(Context.POWER_SERVICE);
-        if (powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
-            mProximityWakeLock = powerManager.newWakeLock(
-                    PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);
-        } else {
-            mProximityWakeLock = null;
-        }
-
-        mPresenter = new VoicemailPlaybackPresenter(
-                createPlaybackViewImpl(),
-                voicemailUri,
-                startPlayback,
-                mProximityWakeLock);
-        mPresenter.onCreate(savedInstanceState);
-    }
-
-    @Override
-    public void onSaveInstanceState(Bundle outState) {
-        mPresenter.onSaveInstanceState(outState);
-        super.onSaveInstanceState(outState);
-    }
-
-    @Override
-    public void onStart() {
-        super.onStart();
-        AnalyticsUtil.sendScreenView(this);
-    }
-
-    @Override
-    public void onViewStateRestored(Bundle savedInstanceState) {
-        mPresenter.onRestoreInstanceState(savedInstanceState);
-        super.onViewStateRestored(savedInstanceState);
-    }
-
-    @Override
-    public void onPause() {
-        mPresenter.onPause();
-        super.onPause();
-    }
-
-    @Override
-    public void onDestroy() {
-        mPresenter.onDestroy();
-        super.onDestroy();
-    }
-
-    private PlaybackViewImpl createPlaybackViewImpl() {
-        return new PlaybackViewImpl(new ActivityReference(), getActivity().getApplicationContext(),
-                mPlaybackLayout);
-    }
-
-     /**
-     * Formats a number of milliseconds as something that looks like {@code 00:05}.
-     * <p>
-     * We always use four digits, two for minutes two for seconds.  In the very unlikely event
-     * that the voicemail duration exceeds 99 minutes, the display is capped at 99 minutes.
-     */
-    private static String formatAsMinutesAndSeconds(int millis) {
-        int seconds = millis / 1000;
-        int minutes = seconds / 60;
-        seconds -= minutes * 60;
-        if (minutes > 99) {
-            minutes = 99;
-        }
-        return String.format("%02d:%02d", minutes, seconds);
-    }
-
-    /**
-     * An object that can provide us with an Activity.
-     * <p>
-     * Fragments suffer the drawback that the Activity they belong to may sometimes be null. This
-     * can happen if the Fragment is detached, for example. In that situation a call to
-     * {@link Fragment#getString(int)} will throw and {@link IllegalStateException}. Also, calling
-     * {@link Fragment#getActivity()} is dangerous - it may sometimes return null. And thus blindly
-     * calling a method on the result of getActivity() is dangerous too.
-     * <p>
-     * To work around this, I have made the {@link PlaybackViewImpl} class static, so that it does
-     * not have access to any Fragment methods directly. Instead it uses an application Context for
-     * things like accessing strings, accessing system services. It only uses the Activity when it
-     * absolutely needs it - and does so through this class. This makes it easy to see where we have
-     * to check for null properly.
-     */
-    private final class ActivityReference {
-        /** Gets this Fragment's Activity: <b>may be null</b>. */
-        public final Activity get() {
-            return getActivity();
-        }
-    }
-
-    /**  Methods required by the PlaybackView for the VoicemailPlaybackPresenter. */
-    private final class PlaybackViewImpl implements VoicemailPlaybackPresenter.PlaybackView {
-        private final ActivityReference mActivityReference;
-        private final Context mApplicationContext;
-        private final SeekBar mPlaybackSeek;
-        private final ImageButton mStartStopButton;
-        private final ImageButton mPlaybackSpeakerphone;
-        private final TextView mPlaybackPosition;
-
-        public PlaybackViewImpl(ActivityReference activityReference, Context applicationContext,
-                View playbackLayout) {
-            Preconditions.checkNotNull(activityReference);
-            Preconditions.checkNotNull(applicationContext);
-            Preconditions.checkNotNull(playbackLayout);
-            mActivityReference = activityReference;
-            mApplicationContext = applicationContext;
-            mPlaybackSeek = (SeekBar) playbackLayout.findViewById(R.id.playback_seek);
-            mStartStopButton = (ImageButton) playbackLayout.findViewById(
-                    R.id.playback_start_stop);
-            mPlaybackSpeakerphone = (ImageButton) playbackLayout.findViewById(
-                    R.id.playback_speakerphone);
-            mPlaybackPosition =
-                    (TextView) playbackLayout.findViewById(R.id.playback_position_text);
-        }
-
-        @Override
-        public void finish() {
-            Activity activity = mActivityReference.get();
-            if (activity != null) {
-                activity.finish();
-            }
-        }
-
-        @Override
-        public void runOnUiThread(Runnable runnable) {
-            Activity activity = mActivityReference.get();
-            if (activity != null) {
-                activity.runOnUiThread(runnable);
-            }
-        }
-
-        @Override
-        public Context getDataSourceContext() {
-            return mApplicationContext;
-        }
-
-        @Override
-        public void setStartStopListener(View.OnClickListener listener) {
-            mStartStopButton.setOnClickListener(listener);
-        }
-
-        @Override
-        public void setSpeakerphoneListener(View.OnClickListener listener) {
-            mPlaybackSpeakerphone.setOnClickListener(listener);
-        }
-
-        @Override
-        public void setPositionSeekListener(SeekBar.OnSeekBarChangeListener listener) {
-            mPlaybackSeek.setOnSeekBarChangeListener(listener);
-        }
-
-        @Override
-        public void playbackStarted() {
-            mStartStopButton.setImageResource(R.drawable.ic_hold_pause);
-        }
-
-        @Override
-        public void playbackStopped() {
-            mStartStopButton.setImageResource(R.drawable.ic_play);
-        }
-
-        @Override
-        public void registerContentObserver(Uri uri, ContentObserver observer) {
-            mApplicationContext.getContentResolver().registerContentObserver(uri, false, observer);
-        }
-
-        @Override
-        public void unregisterContentObserver(ContentObserver observer) {
-            mApplicationContext.getContentResolver().unregisterContentObserver(observer);
-        }
-
-        @Override
-        public void setClipPosition(int clipPositionInMillis, int clipLengthInMillis) {
-            int seekBarPosition = Math.max(0, clipPositionInMillis);
-            int seekBarMax = Math.max(seekBarPosition, clipLengthInMillis);
-            if (mPlaybackSeek.getMax() != seekBarMax) {
-                mPlaybackSeek.setMax(seekBarMax);
-            }
-            mPlaybackSeek.setProgress(seekBarPosition);
-            mPlaybackPosition.setText(formatAsMinutesAndSeconds(seekBarMax - seekBarPosition));
-        }
-
-        private String getString(int resId) {
-            return mApplicationContext.getString(resId);
-        }
-
-        @Override
-        public void setIsBuffering() {
-            disableUiElements();
-            mPlaybackPosition.setText(getString(R.string.voicemail_buffering));
-        }
-
-        @Override
-        public void setIsFetchingContent() {
-            disableUiElements();
-            mPlaybackPosition.setText(getString(R.string.voicemail_fetching_content));
-        }
-
-        @Override
-        public void setFetchContentTimeout() {
-            disableUiElements();
-            mPlaybackPosition.setText(getString(R.string.voicemail_fetching_timout));
-        }
-
-        @Override
-        public int getDesiredClipPosition() {
-            return mPlaybackSeek.getProgress();
-        }
-
-        @Override
-        public void disableUiElements() {
-            mStartStopButton.setEnabled(false);
-            mPlaybackSpeakerphone.setEnabled(false);
-            mPlaybackSeek.setProgress(0);
-            mPlaybackSeek.setEnabled(false);
-        }
-
-        @Override
-        public void playbackError(Exception e) {
-            disableUiElements();
-            mPlaybackPosition.setText(getString(R.string.voicemail_playback_error));
-            Log.e(TAG, "Could not play voicemail", e);
-        }
-
-        @Override
-        public void enableUiElements() {
-            mStartStopButton.setEnabled(true);
-            mPlaybackSpeakerphone.setEnabled(true);
-            mPlaybackSeek.setEnabled(true);
-        }
-
-        @Override
-        public void sendFetchVoicemailRequest(Uri voicemailUri) {
-            Intent intent = new Intent(VoicemailContract.ACTION_FETCH_VOICEMAIL, voicemailUri);
-            mApplicationContext.sendBroadcast(intent);
-        }
-
-        @Override
-        public boolean queryHasContent(Uri voicemailUri) {
-            ContentResolver contentResolver = mApplicationContext.getContentResolver();
-            Cursor cursor = contentResolver.query(
-                    voicemailUri, HAS_CONTENT_PROJECTION, null, null, null);
-            try {
-                if (cursor != null && cursor.moveToNext()) {
-                    return cursor.getInt(cursor.getColumnIndexOrThrow(
-                            VoicemailContract.Voicemails.HAS_CONTENT)) == 1;
-                }
-            } finally {
-                MoreCloseables.closeQuietly(cursor);
-            }
-            return false;
-        }
-
-        private AudioManager getAudioManager() {
-            return (AudioManager) mApplicationContext.getSystemService(Context.AUDIO_SERVICE);
-        }
-
-        @Override
-        public boolean isSpeakerPhoneOn() {
-            return getAudioManager().isSpeakerphoneOn();
-        }
-
-        @Override
-        public void setSpeakerPhoneOn(boolean on) {
-            getAudioManager().setSpeakerphoneOn(on);
-            if (on) {
-                mPlaybackSpeakerphone.setImageResource(R.drawable.ic_speakerphone_on);
-                // Speaker is now on, tapping button will turn it off.
-                mPlaybackSpeakerphone.setContentDescription(
-                        mApplicationContext.getString(R.string.voicemail_speaker_off));
-            } else {
-                mPlaybackSpeakerphone.setImageResource(R.drawable.ic_speakerphone_off);
-                // Speaker is now off, tapping button will turn it on.
-                mPlaybackSpeakerphone.setContentDescription(
-                        mApplicationContext.getString(R.string.voicemail_speaker_on));
-            }
-        }
-
-        @Override
-        public void setVolumeControlStream(int streamType) {
-            Activity activity = mActivityReference.get();
-            if (activity != null) {
-                activity.setVolumeControlStream(streamType);
-            }
-        }
-    }
-}
diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java b/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java
new file mode 100644
index 0000000..0e9ff3b
--- /dev/null
+++ b/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java
@@ -0,0 +1,350 @@
+/*
+ * 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.dialer.voicemail;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.Context;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.PowerManager;
+import android.provider.VoicemailContract;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.TextView;
+
+import com.android.common.io.MoreCloseables;
+import com.android.dialer.R;
+
+import com.google.common.base.Preconditions;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledExecutorService;
+
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.NotThreadSafe;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * Displays and plays a single voicemail.
+ * <p>
+ * This class is not thread-safe, it is thread-confined. All calls to all public
+ * methods on this class are expected to come from the main ui thread.
+ */
+@NotThreadSafe
+public class VoicemailPlaybackLayout extends LinearLayout
+        implements VoicemailPlaybackPresenter.PlaybackView {
+    private static final String TAG = VoicemailPlaybackLayout.class.getSimpleName();
+
+    /**
+     * Controls the animation of the playback slider.
+     */
+    @ThreadSafe
+    private final class PositionUpdater implements Runnable {
+
+        /** Update rate for the slider, 30fps. */
+        private static final int SLIDER_UPDATE_PERIOD_MILLIS = 1000 / 30;
+
+        private final MediaPlayer mMediaPlayer;
+        private final int mDuration;
+        private final ScheduledExecutorService mExecutorService;
+        private final Object mLock = new Object();
+        @GuardedBy("mLock") private ScheduledFuture<?> mScheduledFuture;
+
+        public PositionUpdater(
+                MediaPlayer mediaPlayer,
+                int duration,
+                ScheduledExecutorService executorService) {
+            mMediaPlayer = mediaPlayer;
+            mDuration = duration;
+            mExecutorService = executorService;
+        }
+
+        @Override
+        public void run() {
+            post(new Runnable() {
+                @Override
+                public void run() {
+                    int currentPosition = 0;
+                    synchronized (mLock) {
+                        if (mScheduledFuture == null) {
+                            // This task has been canceled. Just stop now.
+                            return;
+                        }
+                        currentPosition = mMediaPlayer.getCurrentPosition();
+                    }
+                    setClipPosition(currentPosition, mDuration);
+                }
+            });
+        }
+
+        public void startUpdating() {
+            synchronized (mLock) {
+                if (mScheduledFuture != null) {
+                    mScheduledFuture.cancel(false);
+                    mScheduledFuture = null;
+                }
+                mScheduledFuture = mExecutorService.scheduleAtFixedRate(
+                        this, 0, SLIDER_UPDATE_PERIOD_MILLIS, TimeUnit.MILLISECONDS);
+            }
+        }
+
+        public void stopUpdating() {
+            synchronized (mLock) {
+                if (mScheduledFuture != null) {
+                    mScheduledFuture.cancel(false);
+                    mScheduledFuture = null;
+                }
+            }
+        }
+    }
+
+    /**
+     * Handle state changes when the user manipulates the seek bar.
+     */
+    private final OnSeekBarChangeListener seekBarChangeListener = new OnSeekBarChangeListener() {
+        @Override
+        public void onStartTrackingTouch(SeekBar seekBar) {
+            if (mPresenter != null) {
+                mPresenter.pausePlaybackForSeeking();
+            }
+        }
+
+        @Override
+        public void onStopTrackingTouch(SeekBar seekBar) {
+            if (mPresenter != null) {
+                mPresenter.resumePlaybackAfterSeeking(seekBar.getProgress());
+            }
+        }
+
+        @Override
+        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+            setClipPosition(seekBar.getProgress(), seekBar.getMax());
+        }
+    };
+
+    /**
+     * Click listener to toggle speakerphone.
+     */
+    private final View.OnClickListener speakerphoneListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            if (mPresenter != null) {
+                onSpeakerphoneOn(!mPresenter.isSpeakerphoneOn());
+            }
+        }
+    };
+
+    /**
+     * Click listener to play or pause voicemail playback.
+     */
+    private final View.OnClickListener startStopButtonListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View view) {
+            if (mPresenter == null) {
+                return;
+            }
+            if (mIsPlaying) {
+                mPresenter.pausePlayback();
+            } else {
+                mPresenter.resumePlayback();
+            }
+        }
+    };
+
+    private Context mContext;
+    private VoicemailPlaybackPresenter mPresenter;
+
+    private boolean mIsPlaying = false;
+
+    private SeekBar mPlaybackSeek;
+    private ImageButton mStartStopButton;
+    private ImageButton mPlaybackSpeakerphone;
+    private TextView mPlaybackPosition;
+
+    private PositionUpdater mPositionUpdater;
+
+    public VoicemailPlaybackLayout(Context context) {
+        this(context, null);
+    }
+
+    public VoicemailPlaybackLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        mContext = context;
+        LayoutInflater inflater =
+                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        inflater.inflate(R.layout.playback_layout, this);
+    }
+
+    @Override
+    public void setPresenter(VoicemailPlaybackPresenter presenter) {
+        mPresenter = presenter;
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        mPlaybackSeek = (SeekBar) findViewById(R.id.playback_seek);
+        mStartStopButton = (ImageButton) findViewById(R.id.playback_start_stop);
+        mPlaybackSpeakerphone = (ImageButton) findViewById(R.id.playback_speakerphone);
+        mPlaybackPosition = (TextView) findViewById(R.id.playback_position_text);
+
+        mPlaybackSeek.setOnSeekBarChangeListener(seekBarChangeListener);
+        mStartStopButton.setOnClickListener(startStopButtonListener);
+        mPlaybackSpeakerphone.setOnClickListener(speakerphoneListener);
+    }
+
+    @Override
+    public void onPlaybackStarted(
+            MediaPlayer mediaPlayer,
+            int duration,
+            ScheduledExecutorService executorService) {
+        mIsPlaying = true;
+
+        mStartStopButton.setImageResource(R.drawable.ic_hold_pause);
+
+        if (mPresenter != null) {
+            onSpeakerphoneOn(mPresenter.isSpeakerphoneOn());
+        }
+
+        mPositionUpdater = new PositionUpdater(mediaPlayer, duration, executorService);
+        mPositionUpdater.startUpdating();
+    }
+
+    @Override
+    public void onPlaybackStopped() {
+        mIsPlaying = false;
+
+        mStartStopButton.setImageResource(R.drawable.ic_play);
+
+        if (mPositionUpdater != null) {
+            mPositionUpdater.stopUpdating();
+            mPositionUpdater = null;
+        }
+    }
+
+    @Override
+    public void onPlaybackError(Exception e) {
+        if (mPositionUpdater != null) {
+            mPositionUpdater.stopUpdating();
+        }
+
+        disableUiElements();
+        mPlaybackPosition.setText(getString(R.string.voicemail_playback_error));
+
+        Log.e(TAG, "Could not play voicemail", e);
+    }
+
+
+    public void onSpeakerphoneOn(boolean on) {
+        if (mPresenter != null) {
+            mPresenter.setSpeakerphoneOn(on);
+        }
+
+        if (on) {
+            mPlaybackSpeakerphone.setImageResource(R.drawable.ic_speakerphone_on);
+            // Speaker is now on, tapping button will turn it off.
+            mPlaybackSpeakerphone.setContentDescription(
+                    mContext.getString(R.string.voicemail_speaker_off));
+        } else {
+            mPlaybackSpeakerphone.setImageResource(R.drawable.ic_speakerphone_off);
+            // Speaker is now off, tapping button will turn it on.
+            mPlaybackSpeakerphone.setContentDescription(
+                    mContext.getString(R.string.voicemail_speaker_on));
+        }
+    }
+
+    @Override
+    public void setClipPosition(int clipPositionInMillis, int clipLengthInMillis) {
+        int seekBarPosition = Math.max(0, clipPositionInMillis);
+        int seekBarMax = Math.max(seekBarPosition, clipLengthInMillis);
+        if (mPlaybackSeek.getMax() != seekBarMax) {
+            mPlaybackSeek.setMax(seekBarMax);
+        }
+        mPlaybackSeek.setProgress(seekBarPosition);
+        mPlaybackPosition.setText(formatAsMinutesAndSeconds(seekBarMax - seekBarPosition));
+    }
+
+    @Override
+    public void setIsBuffering() {
+        disableUiElements();
+        mPlaybackPosition.setText(getString(R.string.voicemail_buffering));
+    }
+
+    @Override
+    public void setIsFetchingContent() {
+        disableUiElements();
+        mPlaybackPosition.setText(getString(R.string.voicemail_fetching_content));
+    }
+
+    @Override
+    public void setFetchContentTimeout() {
+        disableUiElements();
+        mPlaybackPosition.setText(getString(R.string.voicemail_fetching_timout));
+    }
+
+    @Override
+    public int getDesiredClipPosition() {
+        return mPlaybackSeek.getProgress();
+    }
+
+    @Override
+    public void disableUiElements() {
+        mStartStopButton.setEnabled(false);
+        mPlaybackSpeakerphone.setEnabled(false);
+        mPlaybackSeek.setProgress(0);
+        mPlaybackSeek.setEnabled(false);
+    }
+
+    @Override
+    public void enableUiElements() {
+        mStartStopButton.setEnabled(true);
+        mPlaybackSpeakerphone.setEnabled(true);
+        mPlaybackSeek.setEnabled(true);
+    }
+
+    private String getString(int resId) {
+        return mContext.getString(resId);
+    }
+
+    /**
+     * Formats a number of milliseconds as something that looks like {@code 00:05}.
+     * <p>
+     * We always use four digits, two for minutes two for seconds.  In the very unlikely event
+     * that the voicemail duration exceeds 99 minutes, the display is capped at 99 minutes.
+     */
+    private String formatAsMinutesAndSeconds(int millis) {
+        int seconds = millis / 1000;
+        int minutes = seconds / 60;
+        seconds -= minutes * 60;
+        if (minutes > 99) {
+            minutes = 99;
+        }
+        return String.format("%02d:%02d", minutes, seconds);
+    }
+}
diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
index 1f63f5d..1ab87fd 100644
--- a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
+++ b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
@@ -16,8 +16,12 @@
 
 package com.android.dialer.voicemail;
 
+import android.app.Activity;
 import android.content.Context;
+import android.content.ContentResolver;
+import android.content.Intent;
 import android.database.ContentObserver;
+import android.database.Cursor;
 import android.media.AudioManager;
 import android.media.AudioManager.OnAudioFocusChangeListener;
 import android.media.MediaPlayer;
@@ -26,6 +30,7 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.PowerManager;
+import android.provider.VoicemailContract;
 import android.util.Log;
 import android.view.View;
 import android.widget.SeekBar;
@@ -33,10 +38,8 @@
 import com.android.dialer.R;
 import com.android.dialer.util.AsyncTaskExecutor;
 import com.android.dialer.util.AsyncTaskExecutors;
-import com.android.ex.variablespeed.MediaPlayerProxy;
-import com.android.ex.variablespeed.SingleThreadedMediaPlayerProxy;
-import com.android.ex.variablespeed.VariableSpeed;
 
+import com.android.common.io.MoreCloseables;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 
@@ -45,83 +48,66 @@
 import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 
-import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.NotThreadSafe;
 import javax.annotation.concurrent.ThreadSafe;
 
 /**
- * Contains the controlling logic for a voicemail playback ui.
+ * Contains the controlling logic for a voicemail playback UI.
  * <p>
- * Specifically right now this class is used to control the
- * {@link com.android.dialer.voicemail.VoicemailPlaybackFragment}.
+ * This controls a single {@link com.android.dialer.voicemail.VoicemailPlaybackLayout}. A single
+ * instance can be reused for different such layouts, using {@link #setVoicemailPlaybackView}.
  * <p>
- * This class is not thread safe. The thread policy for this class is
- * thread-confinement, all calls into this class from outside must be done from
- * the main ui thread.
+ * This class is not thread safe. The thread policy for this class is thread-confinement, all calls
+ * into this class from outside must be done from the main UI thread.
  */
 @NotThreadSafe
 @VisibleForTesting
-public class VoicemailPlaybackPresenter implements OnAudioFocusChangeListener {
+public class VoicemailPlaybackPresenter
+        implements OnAudioFocusChangeListener, MediaPlayer.OnPreparedListener,
+                MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener {
+
     private static final String TAG = VoicemailPlaybackPresenter.class.getSimpleName();
-    /** The stream used to playback voicemail. */
-    private static final int PLAYBACK_STREAM = AudioManager.STREAM_VOICE_CALL;
 
     /** Contract describing the behaviour we need from the ui we are controlling. */
     public interface PlaybackView {
-        Context getDataSourceContext();
-        void runOnUiThread(Runnable runnable);
-        void setStartStopListener(View.OnClickListener listener);
-        void setPositionSeekListener(SeekBar.OnSeekBarChangeListener listener);
-        void setSpeakerphoneListener(View.OnClickListener listener);
-        void setIsBuffering();
-        void setClipPosition(int clipPositionInMillis, int clipLengthInMillis);
         int getDesiredClipPosition();
-        void playbackStarted();
-        void playbackStopped();
-        void playbackError(Exception e);
-        boolean isSpeakerPhoneOn();
-        void setSpeakerPhoneOn(boolean on);
-        void finish();
-        void setIsFetchingContent();
         void disableUiElements();
         void enableUiElements();
-        void sendFetchVoicemailRequest(Uri voicemailUri);
-        boolean queryHasContent(Uri voicemailUri);
+        void onPlaybackError(Exception e);
+        void onPlaybackStarted(MediaPlayer mediaPlayer, int duration,
+                ScheduledExecutorService executorService);
+        void onPlaybackStopped();
+        void setClipPosition(int clipPositionInMillis, int clipLengthInMillis);
         void setFetchContentTimeout();
-        void registerContentObserver(Uri uri, ContentObserver observer);
-        void unregisterContentObserver(ContentObserver observer);
-        void setVolumeControlStream(int streamType);
+        void setIsBuffering();
+        void setIsFetchingContent();
+        void setPresenter(VoicemailPlaybackPresenter presenter);
     }
 
     /** The enumeration of {@link AsyncTask} objects we use in this class. */
     public enum Tasks {
         CHECK_FOR_CONTENT,
         CHECK_CONTENT_AFTER_CHANGE,
-        PREPARE_MEDIA_PLAYER,
-        RESET_PREPARE_START_MEDIA_PLAYER,
     }
 
+    private static final String[] HAS_CONTENT_PROJECTION = new String[] {
+        VoicemailContract.Voicemails.HAS_CONTENT,
+    };
+
+    private static final int PLAYBACK_STREAM = AudioManager.STREAM_VOICE_CALL;
     private static final int NUMBER_OF_THREADS_IN_POOL = 2;
-    /** Update rate for the slider, 30fps. */
-    private static final int SLIDER_UPDATE_PERIOD_MILLIS = 1000 / 30;
-    /** Time our ui will wait for content to be fetched before reporting not available. */
+    // Time to wait for content to be fetched before timing out.
     private static final long FETCH_CONTENT_TIMEOUT_MS = 20000;
-    /**
-     * If present in the saved instance bundle, we should not resume playback on
-     * create.
-     */
-    private static final String IS_PLAYING_STATE_KEY = VoicemailPlaybackPresenter.class.getName()
-            + ".IS_PLAYING_STATE_KEY";
-    /**
-     * If present in the saved instance bundle, indicates where to set the
-     * playback slider.
-     */
-    private static final String CLIP_POSITION_KEY = VoicemailPlaybackPresenter.class.getName()
-            + ".CLIP_POSITION_KEY";
+
+    // If present in the saved instance bundle, we should not resume playback on create.
+    private static final String IS_PLAYING_STATE_KEY =
+            VoicemailPlaybackPresenter.class.getName() + ".IS_PLAYING_STATE_KEY";
+    // If present in the saved instance bundle, indicates where to set the playback slider.
+    private static final String CLIP_POSITION_KEY =
+            VoicemailPlaybackPresenter.class.getName() + ".CLIP_POSITION_KEY";
 
     /**
      * The most recently calculated duration.
@@ -132,20 +118,18 @@
      */
     private final AtomicInteger mDuration = new AtomicInteger(0);
 
-    private MediaPlayerProxy mPlayer;
-    private static int mMediaPlayerRefCount = 0;
-    private static MediaPlayerProxy mMediaPlayerInstance;
+    private Context mContext;
+    private MediaPlayer mMediaPlayer;
+    private PlaybackView mView;
 
-    private final PlaybackView mView;
-    private final PositionUpdater mPositionUpdater;
-    /** Voicemail uri to play. */
-    private final Uri mVoicemailUri;
-    /** Start playing in onCreate iff this is true. */
-    private final boolean mStartPlayingImmediately;
-    /** Used to run async tasks that need to interact with the ui. */
+    private Uri mVoicemailUri;
+    private int mPosition;
+    private boolean mIsPlaying;
+    private boolean mShouldResumePlaybackAfterSeeking;
+
+    // Used to run async tasks that need to interact with the UI.
     private final AsyncTaskExecutor mAsyncTaskExecutor;
     private static ScheduledExecutorService mScheduledExecutorService;
-
     /**
      * Used to handle the result of a successful or time-out fetch result.
      * <p>
@@ -153,37 +137,78 @@
      */
     private FetchResultHandler mFetchResultHandler;
     private PowerManager.WakeLock mProximityWakeLock;
-    private AsyncTask<Void, ?, ?> mPrepareTask;
-    private int mPosition;
-    private boolean mPlaying;
     private AudioManager mAudioManager;
 
-    public VoicemailPlaybackPresenter(
-            PlaybackView view,
-            Uri voicemailUri,
-            boolean startPlayingImmediately,
-            PowerManager.WakeLock wakeLock) {
-        mView = view;
-        mVoicemailUri = voicemailUri;
-        mStartPlayingImmediately = startPlayingImmediately;
-        mPositionUpdater = new PositionUpdater(
-                getScheduledExecutorServiceInstance(), SLIDER_UPDATE_PERIOD_MILLIS);
-        mProximityWakeLock = wakeLock;
-
+    public VoicemailPlaybackPresenter(Activity activity) {
+        mContext = activity;
         mAsyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor();
-        mPlayer = VariableSpeed.createVariableSpeed(getScheduledExecutorServiceInstance());
+        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
 
-        ++mMediaPlayerRefCount;
-        if (mMediaPlayerInstance == null) {
-            mMediaPlayerInstance = VariableSpeed.createVariableSpeed(
-                    getScheduledExecutorServiceInstance());
+        PowerManager powerManager =
+                (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+        if (powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
+            mProximityWakeLock = powerManager.newWakeLock(
+                    PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);
         }
-        mPlayer = mMediaPlayerInstance;
+
+        if (mMediaPlayer == null) {
+            mMediaPlayer = new MediaPlayer();
+            mMediaPlayer.setOnPreparedListener(this);
+            mMediaPlayer.setOnErrorListener(this);
+            mMediaPlayer.setOnCompletionListener(this);
+        }
+
+        activity.setVolumeControlStream(PLAYBACK_STREAM);
     }
 
-    public void onCreate(Bundle bundle) {
-        mView.setVolumeControlStream(PLAYBACK_STREAM);
-        checkThatWeHaveContent();
+    /**
+     * Specify the view which this presenter controls and the voicemail for playback.
+     */
+    public void setPlaybackView(
+            PlaybackView view, Uri voicemailUri, boolean startPlayingImmediately) {
+        mView = view;
+        mVoicemailUri = voicemailUri;
+        setPosition(0, startPlayingImmediately);
+
+        mView.setPresenter(this);
+
+        checkForContent();
+    }
+
+    public void onPause() {
+        if (mMediaPlayer.isPlaying()) {
+            pausePlayback(mMediaPlayer.getCurrentPosition(), mIsPlaying);
+        }
+
+        disableProximitySensor(false /* waitForFarState */);
+    }
+
+    public void onDestroy() {
+        if (mScheduledExecutorService != null) {
+            mScheduledExecutorService.shutdown();
+            mScheduledExecutorService = null;
+        }
+
+        if (mFetchResultHandler != null) {
+            mFetchResultHandler.destroy();
+            mFetchResultHandler = null;
+        }
+
+        disableProximitySensor(false /* waitForFarState */);
+    }
+
+    public void onSaveInstanceState(Bundle outState) {
+        outState.putInt(CLIP_POSITION_KEY, mView.getDesiredClipPosition());
+        outState.putBoolean(IS_PLAYING_STATE_KEY, mIsPlaying);
+    }
+
+    public void onRestoreInstanceState(Bundle inState) {
+        if (inState != null) {
+            int position = inState.getInt(CLIP_POSITION_KEY, 0);
+            boolean isPlaying = inState.getBoolean(IS_PLAYING_STATE_KEY, false);
+            // Playback will be automatically resumed, if appropriate, in onPrepared().
+            setPosition(position, isPlaying);
+        }
     }
 
     /**
@@ -192,30 +217,44 @@
      * This method will be called once, after the fragment has been created, before we know if the
      * voicemail we've been asked to play has any content available.
      * <p>
-     * This method will notify the user through the ui that we are fetching the content, then check
-     * to see if the content field in the db is set. If set, we proceed to
-     * {@link #postSuccessfullyFetchedContent()} method. If not set, we will make a request to fetch
-     * the content asynchronously via {@link #makeRequestForContent()}.
+     * Notify the user that we are fetching the content, then check to see if the content field in
+     * the DB is set. If set, we proceed to {@link #prepareToPlayContent()} method. If not set, make
+     * a request to fetch the content asynchronously via {@link #requestContent()}.
      */
-    private void checkThatWeHaveContent() {
+    private void checkForContent() {
         mView.setIsFetchingContent();
         mAsyncTaskExecutor.submit(Tasks.CHECK_FOR_CONTENT, new AsyncTask<Void, Void, Boolean>() {
             @Override
             public Boolean doInBackground(Void... params) {
-                return mView.queryHasContent(mVoicemailUri);
+                return queryHasContent(mVoicemailUri);
             }
 
             @Override
             public void onPostExecute(Boolean hasContent) {
                 if (hasContent) {
-                    postSuccessfullyFetchedContent();
+                    prepareToPlayContent();
                 } else {
-                    makeRequestForContent();
+                    requestContent();
                 }
             }
         });
     }
 
+    private boolean queryHasContent(Uri voicemailUri) {
+        ContentResolver contentResolver = mContext.getContentResolver();
+        Cursor cursor = contentResolver.query(
+                voicemailUri, HAS_CONTENT_PROJECTION, null, null, null);
+        try {
+            if (cursor != null && cursor.moveToNext()) {
+                return cursor.getInt(cursor.getColumnIndexOrThrow(
+                        VoicemailContract.Voicemails.HAS_CONTENT)) == 1;
+            }
+        } finally {
+            MoreCloseables.closeQuietly(cursor);
+        }
+        return false;
+    }
+
     /**
      * Makes a broadcast request to ask that a voicemail source fetch this content.
      * <p>
@@ -225,17 +264,22 @@
      * will trigger a broadcast to request that the content be downloaded. It will add a listener to
      * the content resolver so that it will be notified when the has_content field changes. It will
      * also set a timer. If the has_content field changes to true within the allowed time, we will
-     * proceed to {@link #postSuccessfullyFetchedContent()}. If the has_content field does not
+     * proceed to {@link #prepareToPlayContent()}. If the has_content field does not
      * become true within the allowed time, we will update the ui to reflect the fact that content
      * was not available.
      */
-    private void makeRequestForContent() {
-        Handler handler = new Handler();
+    private void requestContent() {
         Preconditions.checkState(mFetchResultHandler == null, "mFetchResultHandler should be null");
+
+        Handler handler = new Handler();
         mFetchResultHandler = new FetchResultHandler(handler);
-        mView.registerContentObserver(mVoicemailUri, mFetchResultHandler);
+        mContext.getContentResolver().registerContentObserver(
+                mVoicemailUri, false, mFetchResultHandler);
         handler.postDelayed(mFetchResultHandler.getTimeoutRunnable(), FETCH_CONTENT_TIMEOUT_MS);
-        mView.sendFetchVoicemailRequest(mVoicemailUri);
+
+        // Send voicemail fetch request.
+        Intent intent = new Intent(VoicemailContract.ACTION_FETCH_VOICEMAIL, mVoicemailUri);
+        mContext.sendBroadcast(intent);
     }
 
     @ThreadSafe
@@ -255,14 +299,14 @@
         @Override
         public void run() {
             if (mResultStillPending.getAndSet(false)) {
-                mView.unregisterContentObserver(FetchResultHandler.this);
+                mContext.getContentResolver().unregisterContentObserver(FetchResultHandler.this);
                 mView.setFetchContentTimeout();
             }
         }
 
         public void destroy() {
             if (mResultStillPending.getAndSet(false)) {
-                mView.unregisterContentObserver(FetchResultHandler.this);
+                mContext.getContentResolver().unregisterContentObserver(FetchResultHandler.this);
                 mHandler.removeCallbacks(this);
             }
         }
@@ -273,15 +317,16 @@
                     new AsyncTask<Void, Void, Boolean>() {
                 @Override
                 public Boolean doInBackground(Void... params) {
-                    return mView.queryHasContent(mVoicemailUri);
+                    return queryHasContent(mVoicemailUri);
                 }
 
                 @Override
                 public void onPostExecute(Boolean hasContent) {
                     if (hasContent) {
                         if (mResultStillPending.getAndSet(false)) {
-                            mView.unregisterContentObserver(FetchResultHandler.this);
-                            postSuccessfullyFetchedContent();
+                            mContext.getContentResolver().unregisterContentObserver(
+                                    FetchResultHandler.this);
+                            prepareToPlayContent();
                         }
                     }
                 }
@@ -293,124 +338,160 @@
      * Prepares the voicemail content for playback.
      * <p>
      * This method will be called once we know that our voicemail has content (according to the
-     * content provider). This method will try to prepare the data source through the media player.
-     * If preparing the media player works, we will call through to
-     * {@link #postSuccessfulPrepareActions()}. If preparing the media player fails (perhaps the
-     * file the content provider points to is actually missing, perhaps it is of an unknown file
-     * format that we can't play, who knows) then we will show an error on the ui.
+     * content provider). this method asynchronously tries to prepare the data source through the
+     * media player. If preparation is successful, the media player will {@link #onPrepared()},
+     * and it will call {@link #onError()} otherwise.
      */
-    private void postSuccessfullyFetchedContent() {
+    private void prepareToPlayContent() {
         mView.setIsBuffering();
-        mAsyncTaskExecutor.submit(Tasks.PREPARE_MEDIA_PLAYER,
-                new AsyncTask<Void, Void, Exception>() {
-                    @Override
-                    public Exception doInBackground(Void... params) {
-                        try {
-                            mPlayer.reset();
-                            mPlayer.setDataSource(mView.getDataSourceContext(), mVoicemailUri);
-                            mPlayer.setAudioStreamType(PLAYBACK_STREAM);
-                            mPlayer.prepare();
-                            mDuration.set(mPlayer.getDuration());
-                            return null;
-                        } catch (Exception e) {
-                            return e;
-                        }
-                    }
 
-                    @Override
-                    public void onPostExecute(Exception exception) {
-                        if (exception == null) {
-                            postSuccessfulPrepareActions();
-                        } else {
-                            mView.playbackError(exception);
-                        }
-                    }
-                });
+        try {
+            mMediaPlayer.reset();
+            mMediaPlayer.setDataSource(mContext, mVoicemailUri);
+            mMediaPlayer.setAudioStreamType(PLAYBACK_STREAM);
+            mMediaPlayer.prepareAsync();
+        } catch (Exception e) {
+            handleError(e);
+        }
     }
 
     /**
-     * Enables the ui, and optionally starts playback immediately.
-     * <p>
-     * This will be called once we have successfully prepared the media player, and will optionally
-     * playback immediately.
+     * Once the media player is prepared, enables the UI and adopts the appropriate playback state.
      */
-    private void postSuccessfulPrepareActions() {
+    @Override
+    public void onPrepared(MediaPlayer mp) {
         mView.enableUiElements();
-        mView.setPositionSeekListener(new PlaybackPositionListener());
-        mView.setStartStopListener(new StartStopButtonListener());
-        mView.setSpeakerphoneListener(new SpeakerphoneListener());
-        mPlayer.setOnErrorListener(new MediaPlayerErrorListener());
-        mPlayer.setOnCompletionListener(new MediaPlayerCompletionListener());
-        mView.setSpeakerPhoneOn(mView.isSpeakerPhoneOn());
-        if (mPlaying) {
-           resetPrepareStartPlaying(mPosition);
+
+        if (mIsPlaying) {
+            resumePlayback();
         } else {
-           stopPlaybackAtPosition(mPosition, mDuration.get());
-           if ((mPosition == 0) && (mStartPlayingImmediately)) {
-               resetPrepareStartPlaying(0);
-           }
+            pausePlayback();
         }
     }
 
-    public void onSaveInstanceState(Bundle outState) {
-        outState.putInt(CLIP_POSITION_KEY, mView.getDesiredClipPosition());
-        outState.putBoolean(IS_PLAYING_STATE_KEY, mPlaying);
+    /**
+     * Invoked if preparing the media player fails, for example, if file is missing or the voicemail
+     * is an unknown file format that can't be played.
+     */
+    @Override
+    public boolean onError(MediaPlayer mp, int what, int extra) {
+        handleError(new IllegalStateException("MediaPlayer error listener invoked"));
+        return true;
     }
 
-    public void onRestoreInstanceState(Bundle inState) {
-        int position = 0;
-        boolean isPlaying = false;
-        if (inState != null) {
-            position = inState.getInt(CLIP_POSITION_KEY, 0);
-            isPlaying = inState.getBoolean(IS_PLAYING_STATE_KEY, false);
+    private void handleError(Exception e) {
+        mMediaPlayer.release();
+        mView.onPlaybackError(e);
+        setPosition(0, false);
+    }
+
+    /**
+     * After done playing the voicemail clip, reset the clip position to the start.
+     */
+    @Override
+    public void onCompletion(MediaPlayer mediaPlayer) {
+        pausePlayback(0, false);
+    }
+
+    @Override
+    public void onAudioFocusChange(int focusChange) {
+        boolean lostFocus = focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT ||
+                focusChange == AudioManager.AUDIOFOCUS_LOSS;
+        if (mMediaPlayer.isPlaying() && lostFocus) {
+            pausePlayback();
+        } else if (!mMediaPlayer.isPlaying() && focusChange == AudioManager.AUDIOFOCUS_GAIN) {
+            resumePlayback();
         }
-        setPositionAndPlayingStatus(position, isPlaying) ;
     }
 
-    private void setPositionAndPlayingStatus(int position, boolean isPlaying) {
-       mPosition = position;
-       mPlaying = isPlaying;
+    /**
+     * Sets the position and playing state for when playback is resumed.
+     */
+    private void setPosition(int position, boolean isPlaying) {
+        mPosition = position;
+        mIsPlaying = isPlaying;
     }
 
-    public void onDestroy() {
-        --mMediaPlayerRefCount;
-        if (mMediaPlayerRefCount == 0) {
-            if (mScheduledExecutorService != null) {
-                mScheduledExecutorService.shutdown();
-                mScheduledExecutorService = null;
+    /**
+     * Resumes voicemail playback at the clip position stored by the presenter.
+     */
+    public void resumePlayback() {
+        final int duration = mMediaPlayer.getDuration();
+        mDuration.set(duration);
+
+        // Clamp the start position between 0 and the duration.
+        int startPosition = Math.max(0, Math.min(mPosition, duration));
+        mMediaPlayer.seekTo(startPosition);
+        setPosition(startPosition, true);
+
+        try {
+            // Grab audio focus here
+            int result = mAudioManager.requestAudioFocus(
+                    VoicemailPlaybackPresenter.this,
+                    PLAYBACK_STREAM,
+                    AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+
+            if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+                throw new RejectedExecutionException("Could not capture audio focus.");
             }
-            if (mPlayer != null) {
-                mPlayer.release();
-                mPlayer = null;
-            }
-        }
 
-        if (mPrepareTask != null) {
-            mPrepareTask.cancel(false);
-            mPrepareTask = null;
-        }
-        if (mFetchResultHandler != null) {
-            mFetchResultHandler.destroy();
-            mFetchResultHandler = null;
-        }
-        mPositionUpdater.stopUpdating();
-        if (mProximityWakeLock.isHeld()) {
-            mProximityWakeLock.release();
+            // Can throw RejectedExecutionException
+            mMediaPlayer.start();
+
+            mView.onPlaybackStarted(mMediaPlayer, duration, getScheduledExecutorServiceInstance());
+            enableProximitySensor();
+        } catch (RejectedExecutionException e) {
+            handleError(e);
         }
     }
 
-    private static synchronized ScheduledExecutorService getScheduledExecutorServiceInstance() {
-        if (mScheduledExecutorService == null) {
-            mScheduledExecutorService = Executors.newScheduledThreadPool(
-                    NUMBER_OF_THREADS_IN_POOL);
+    public void pausePlayback() {
+        pausePlayback(mMediaPlayer.getCurrentPosition(), false);
+    }
+
+    /**
+     * {@link isPlaying} may be set to {@code true} so voicemail playback can be resumed after a
+     * rotation.
+     */
+    private void pausePlayback(int position, boolean isPlaying) {
+        setPosition(position, isPlaying);
+
+        if (mMediaPlayer.isPlaying()) {
+            mMediaPlayer.pause();
         }
-        return mScheduledExecutorService;
+
+        mAudioManager.abandonAudioFocus(this);
+        mView.onPlaybackStopped();
+
+        // Always disable the proximity sensor on stop.
+        disableProximitySensor(true /* waitForFarState */);
+
+        int duration = mDuration.get();
+        mView.setClipPosition(position, duration);
+    }
+
+    /**
+     * Pauses playback when the user starts seeking the position, and notes whether the voicemail is
+     * playing to know whether to resume playback once the user selects a new position.
+     */
+    public void pausePlaybackForSeeking() {
+        mShouldResumePlaybackAfterSeeking = mMediaPlayer.isPlaying();
+        pausePlayback();
+    }
+
+    public void resumePlaybackAfterSeeking(int desiredPosition) {
+        setPosition(desiredPosition, mShouldResumePlaybackAfterSeeking);
+        if (mShouldResumePlaybackAfterSeeking) {
+            resumePlayback();
+        }
+        mShouldResumePlaybackAfterSeeking = false;
     }
 
     private void enableProximitySensor() {
-        if (mProximityWakeLock == null) {
+        if (mProximityWakeLock == null || isSpeakerphoneOn() || !mMediaPlayer.isPlaying()) {
             return;
         }
+
         if (!mProximityWakeLock.isHeld()) {
             Log.i(TAG, "Acquiring proximity wake lock");
             mProximityWakeLock.acquire();
@@ -432,282 +513,24 @@
         }
     }
 
-    private class MediaPlayerErrorListener implements MediaPlayer.OnErrorListener {
-        @Override
-        public boolean onError(MediaPlayer mp, int what, int extra) {
-            mView.runOnUiThread(new Runnable() {
-                @Override
-                public void run() {
-                    handleError(new IllegalStateException("MediaPlayer error listener invoked"));
-                }
-            });
-            return true;
+    public void setSpeakerphoneOn(boolean on) {
+        mAudioManager.setSpeakerphoneOn(on);
+        if (on) {
+            disableProximitySensor(false /* waitForFarState */);
+        } else {
+            enableProximitySensor();
         }
     }
 
-    private class MediaPlayerCompletionListener implements MediaPlayer.OnCompletionListener {
-        @Override
-        public void onCompletion(final MediaPlayer mp) {
-            mView.runOnUiThread(new Runnable() {
-                @Override
-                public void run() {
-                    handleCompletion(mp);
-                }
-            });
-        }
+    public boolean isSpeakerphoneOn() {
+        return mAudioManager.isSpeakerphoneOn();
     }
 
-    private class AsyncPrepareTask extends AsyncTask<Void, Void, Exception> {
-        private int mClipPositionInMillis;
-
-        AsyncPrepareTask(int clipPositionInMillis) {
-            mClipPositionInMillis = clipPositionInMillis;
+    private static synchronized ScheduledExecutorService getScheduledExecutorServiceInstance() {
+        if (mScheduledExecutorService == null) {
+            mScheduledExecutorService = Executors.newScheduledThreadPool(NUMBER_OF_THREADS_IN_POOL);
         }
-
-        @Override
-        public Exception doInBackground(Void... params) {
-            try {
-                mPlayer.reset();
-                mPlayer.setDataSource(mView.getDataSourceContext(), mVoicemailUri);
-                mPlayer.setAudioStreamType(PLAYBACK_STREAM);
-                mPlayer.prepare();
-                return null;
-            } catch (Exception e) {
-                return e;
-            }
-        }
-
-        @Override
-        public void onPostExecute(Exception exception) {
-            mPrepareTask = null;
-            if (exception == null) {
-                final int duration = mPlayer.getDuration();
-                mDuration.set(duration);
-                int startPosition =
-                    constrain(mClipPositionInMillis, 0, duration);
-                mPlayer.seekTo(startPosition);
-                mView.setClipPosition(startPosition, duration);
-                try {
-                    // Grab audio focus here
-                    int result = getAudioManager().requestAudioFocus(
-                            VoicemailPlaybackPresenter.this,
-                            PLAYBACK_STREAM,
-                            AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
-
-                    if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
-                        throw new RejectedExecutionException("Could not capture audio focus.");
-                    }
-                    // Can throw RejectedExecutionException
-                    mPlayer.start();
-                    setPositionAndPlayingStatus(mPlayer.getCurrentPosition(), true);
-                    mView.playbackStarted();
-                    if (!mProximityWakeLock.isHeld()) {
-                        mProximityWakeLock.acquire();
-                    }
-                    // Only enable if we are not currently using the speaker phone.
-                    if (!mView.isSpeakerPhoneOn()) {
-                        enableProximitySensor();
-                    }
-                    // Can throw RejectedExecutionException
-                    mPositionUpdater.startUpdating(startPosition, duration);
-                } catch (RejectedExecutionException e) {
-                    handleError(e);
-                }
-            } else {
-                handleError(exception);
-            }
-        }
+        return mScheduledExecutorService;
     }
 
-    private AudioManager getAudioManager() {
-        if (mAudioManager == null) {
-            mAudioManager = (AudioManager)
-                    mView.getDataSourceContext().getSystemService(Context.AUDIO_SERVICE);
-        }
-        return mAudioManager;
-    }
-
-    @Override
-    public void onAudioFocusChange(int focusChange) {
-        boolean lostFocus = focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT ||
-                focusChange == AudioManager.AUDIOFOCUS_LOSS;
-        // Note: the below logic is the same as in {@code StartStopButtonListener}.
-        if (mPlayer.isPlaying() && lostFocus) {
-            setPositionAndPlayingStatus(mPlayer.getCurrentPosition(), false);
-            stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get());
-        } else if (!mPlayer.isPlaying() && focusChange == AudioManager.AUDIOFOCUS_GAIN) {
-            setPositionAndPlayingStatus(mPosition, true);
-            postSuccessfullyFetchedContent();
-        }
-    }
-
-
-    private void resetPrepareStartPlaying(final int clipPositionInMillis) {
-        if (mPrepareTask != null) {
-            mPrepareTask.cancel(false);
-            mPrepareTask = null;
-        }
-        mPrepareTask = mAsyncTaskExecutor.submit(Tasks.RESET_PREPARE_START_MEDIA_PLAYER,
-                new AsyncPrepareTask(clipPositionInMillis));
-    }
-
-    private void handleError(Exception e) {
-        mView.playbackError(e);
-        mPositionUpdater.stopUpdating();
-        mPlayer.release();
-        setPositionAndPlayingStatus(0, false);
-    }
-
-    public void handleCompletion(MediaPlayer mediaPlayer) {
-        stopPlaybackAtPosition(0, mDuration.get());
-    }
-
-    private void stopPlaybackAtPosition(int clipPosition, int duration) {
-        getAudioManager().abandonAudioFocus(this);
-        mPositionUpdater.stopUpdating();
-        mView.playbackStopped();
-        if (mProximityWakeLock.isHeld()) {
-            mProximityWakeLock.release();
-        }
-        // Always disable on stop.
-        disableProximitySensor(true /* waitForFarState */);
-        mView.setClipPosition(clipPosition, duration);
-        if (mPlayer.isPlaying()) {
-            mPlayer.pause();
-        }
-    }
-
-    private class PlaybackPositionListener implements SeekBar.OnSeekBarChangeListener {
-        private boolean mShouldResumePlaybackAfterSeeking;
-
-        @Override
-        public void onStartTrackingTouch(SeekBar arg0) {
-            if (mPlayer.isPlaying()) {
-                mShouldResumePlaybackAfterSeeking = true;
-                stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get());
-            } else {
-                mShouldResumePlaybackAfterSeeking = false;
-            }
-        }
-
-        @Override
-        public void onStopTrackingTouch(SeekBar arg0) {
-            if (mPlayer.isPlaying()) {
-                setPositionAndPlayingStatus(mPlayer.getCurrentPosition(), false);
-                stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get());
-            } else {
-                setPositionAndPlayingStatus(mView.getDesiredClipPosition(),
-                        mShouldResumePlaybackAfterSeeking);
-            }
-
-            if (mShouldResumePlaybackAfterSeeking) {
-                postSuccessfullyFetchedContent();
-            }
-        }
-
-        @Override
-        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
-            mView.setClipPosition(seekBar.getProgress(), seekBar.getMax());
-        }
-    }
-
-    private class SpeakerphoneListener implements View.OnClickListener {
-        @Override
-        public void onClick(View v) {
-            boolean previousState = mView.isSpeakerPhoneOn();
-            mView.setSpeakerPhoneOn(!previousState);
-            if (mPlayer.isPlaying() && previousState) {
-                // If we are currently playing and we are disabling the speaker phone, enable the
-                // sensor.
-                enableProximitySensor();
-            } else {
-                // If we are not currently playing, disable the sensor.
-                disableProximitySensor(true /* waitForFarState */);
-            }
-        }
-    }
-
-    private class StartStopButtonListener implements View.OnClickListener {
-        @Override
-        public void onClick(View arg0) {
-            if (mPlayer.isPlaying()) {
-                setPositionAndPlayingStatus(mPlayer.getCurrentPosition(), false);
-                stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get());
-            } else {
-                setPositionAndPlayingStatus(mPosition, true);
-                postSuccessfullyFetchedContent();
-            }
-        }
-    }
-
-    /**
-     * Controls the animation of the playback slider.
-     */
-    @ThreadSafe
-    private final class PositionUpdater implements Runnable {
-        private final ScheduledExecutorService mExecutorService;
-        private final int mPeriodMillis;
-        private final Object mLock = new Object();
-        @GuardedBy("mLock") private ScheduledFuture<?> mScheduledFuture;
-        private final Runnable mSetClipPostitionRunnable = new Runnable() {
-            @Override
-            public void run() {
-                int currentPosition = 0;
-                synchronized (mLock) {
-                    if (mScheduledFuture == null) {
-                        // This task has been canceled. Just stop now.
-                        return;
-                    }
-                    currentPosition = mPlayer.getCurrentPosition();
-                }
-                mView.setClipPosition(currentPosition, mDuration.get());
-            }
-        };
-
-        public PositionUpdater(ScheduledExecutorService executorService, int periodMillis) {
-            mExecutorService = executorService;
-            mPeriodMillis = periodMillis;
-        }
-
-        @Override
-        public void run() {
-            mView.runOnUiThread(mSetClipPostitionRunnable);
-        }
-
-        public void startUpdating(int beginPosition, int endPosition) {
-            synchronized (mLock) {
-                if (mScheduledFuture != null) {
-                    mScheduledFuture.cancel(false);
-                    mScheduledFuture = null;
-                }
-                mScheduledFuture = mExecutorService.scheduleAtFixedRate(this, 0, mPeriodMillis,
-                        TimeUnit.MILLISECONDS);
-            }
-        }
-
-        public void stopUpdating() {
-            synchronized (mLock) {
-                if (mScheduledFuture != null) {
-                    mScheduledFuture.cancel(false);
-                    mScheduledFuture = null;
-                }
-            }
-        }
-    }
-
-    public void onPause() {
-        if (mPlayer.isPlaying()) {
-            stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get());
-        }
-        if (mPrepareTask != null) {
-            mPrepareTask.cancel(false);
-            mPrepareTask = null;
-        }
-
-        disableProximitySensor(false /* waitForFarState */);
-    }
-
-    private static int constrain(int amount, int low, int high) {
-        return amount < low ? low : (amount > high ? high : amount);
-    }
 }
diff --git a/tests/src/com/android/dialer/CallDetailActivityTest.java b/tests/src/com/android/dialer/CallDetailActivityTest.java
index aca8f29..4dc9ebb 100644
--- a/tests/src/com/android/dialer/CallDetailActivityTest.java
+++ b/tests/src/com/android/dialer/CallDetailActivityTest.java
@@ -18,7 +18,6 @@
 
 import static com.android.dialer.calllog.CallLogAsyncTaskUtil.Tasks.GET_CALL_DETAILS;
 import static com.android.dialer.voicemail.VoicemailPlaybackPresenter.Tasks.CHECK_FOR_CONTENT;
-import static com.android.dialer.voicemail.VoicemailPlaybackPresenter.Tasks.PREPARE_MEDIA_PLAYER;
 
 import android.content.ContentResolver;
 import android.content.ContentUris;
@@ -118,10 +117,8 @@
         setActivityIntentForTestVoicemailEntry();
         startActivityUnderTest();
         mFakeAsyncTaskExecutor.runTask(CHECK_FOR_CONTENT);
-        // There should be exactly one background task ready to prepare the media player.
-        // Preparing the media player will have thrown an IOException since the file doesn't exist.
+        // The media player will have thrown an IOException since the file doesn't exist.
         // This should have put a failed to play message on screen, buffering is gone.
-        mFakeAsyncTaskExecutor.runTask(PREPARE_MEDIA_PLAYER);
         assertHasOneTextViewContaining("Couldn't play voicemail");
         assertZeroTextViewsContaining("Buffering");
     }
@@ -192,7 +189,6 @@
         setActivityIntentForRealFileVoicemailEntry();
         startActivityUnderTest();
         mFakeAsyncTaskExecutor.runTask(CHECK_FOR_CONTENT);
-        mFakeAsyncTaskExecutor.runTask(PREPARE_MEDIA_PLAYER);
         mTestUtils.clickButton(mActivityUnderTest, R.id.playback_speakerphone);
         mTestUtils.clickButton(mActivityUnderTest, R.id.playback_start_stop);
         Thread.sleep(2000);