Merge "Show transcriptions in voicemail notifications." into mnc-dev
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index eef8ed6..86bf4ff 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -286,6 +286,7 @@
 
         <service android:name="com.android.incallui.InCallServiceImpl"
                  android:permission="android.permission.BIND_INCALL_SERVICE" >
+            <meta-data android:name="android.telecom.IN_CALL_SERVICE_UI" android:value="true" />
             <intent-filter>
                 <action android:name="android.telecom.InCallService"/>
             </intent-filter>
diff --git a/res/layout/voicemail_promo_card.xml b/res/layout/voicemail_promo_card.xml
new file mode 100644
index 0000000..103fa30
--- /dev/null
+++ b/res/layout/voicemail_promo_card.xml
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2015 Google Inc. All Rights Reserved. -->
+
+<android.support.v7.widget.CardView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:card_view="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/promo_card"
+    style="@style/CallLogCardStyle"
+    android:orientation="vertical"
+    android:gravity="center_vertical"
+    card_view:cardBackgroundColor="@color/visual_voicemail_promo_card_background">
+
+    <LinearLayout
+        android:id="@+id/promo_card_content"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:paddingStart="@dimen/promo_card_start_padding"
+            android:paddingEnd="@dimen/promo_card_main_padding"
+            android:paddingTop="@dimen/promo_card_top_padding"
+            android:paddingBottom="@dimen/promo_card_main_padding"
+            android:orientation="horizontal"
+            android:gravity="top">
+
+            <ImageView
+                android:id="@+id/promo_card_icon"
+                android:layout_width="@dimen/promo_card_icon_size"
+                android:layout_height="@dimen/promo_card_icon_size"
+                android:layout_gravity="top"
+                android:src="@drawable/ic_voicemail_24dp"/>
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="@dimen/promo_card_main_padding"
+                android:orientation="vertical"
+                android:gravity="center_vertical">
+
+                <TextView
+                    android:id="@+id/promo_card_header"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginBottom="@dimen/promo_card_title_padding"
+                    android:layout_gravity="center_vertical"
+                    android:textColor="@color/background_dialer_white"
+                    android:textSize="@dimen/call_log_primary_text_size"
+                    android:textStyle="bold"
+                    android:text="@string/visual_voicemail_title"
+                    android:singleLine="false"/>
+
+                <TextView
+                    android:id="@+id/promo_card_details"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:textColor="@color/background_dialer_white"
+                    android:textSize="@dimen/call_log_secondary_text_size"
+                    android:text="@string/visual_voicemail_text"
+                    android:lineSpacingExtra="@dimen/promo_card_line_spacing"
+                    android:singleLine="false"/>
+            </LinearLayout>
+        </LinearLayout>
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:background="@color/visual_voicemail_promo_card_divider"/>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:paddingEnd="@dimen/promo_card_action_end_padding"
+            android:paddingTop="@dimen/promo_card_action_vertical_padding"
+            android:paddingBottom="@dimen/promo_card_action_vertical_padding"
+            android:orientation="horizontal"
+            android:gravity="end">
+
+            <TextView
+                android:id="@+id/settings_action"
+                style="@style/PromoCardActionStyle"
+                android:background="?android:attr/selectableItemBackground"
+                android:text="@string/visual_voicemail_settings"
+                android:nextFocusLeft="@+id/promo_card"
+                android:nextFocusRight="@+id/ok_action"
+                android:paddingEnd="@dimen/promo_card_action_between_padding"/>
+
+            <TextView
+                android:id="@+id/ok_action"
+                style="@style/PromoCardActionStyle"
+                android:background="?android:attr/selectableItemBackground"
+                android:text="@android:string/ok"
+                android:nextFocusLeft="@+id/settings_action"
+                android:nextFocusRight="@+id/promo_card"/>
+        </LinearLayout>
+    </LinearLayout>
+</android.support.v7.widget.CardView>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index f83c328..c3b0fb5 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -36,6 +36,11 @@
     <!-- Color of the text describing an unconsumed voicemail. -->
     <color name="call_log_voicemail_highlight_color">#33b5e5</color>
 
+    <!-- Background color of visual voicemail promo card. -->
+    <color name="visual_voicemail_promo_card_background">#673ab7</color>
+    <color name="visual_voicemail_promo_card_divider">#7d57c1</color>
+    <color name="promo_card_text">#ffffff</color>
+
     <!-- Tint of the recent card phone icon; 30% black -->
     <color name="call_log_list_item_primary_action_icon_tint">#4d000000</color>
 
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index bcde855..206b447 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -134,4 +134,15 @@
     <dimen name="preference_summary_line_spacing_extra">4dp</dimen>
 
     <dimen name="call_log_list_item_primary_action_dimen">36dp</dimen>
+
+    <!-- Dimensions for promo cards -->
+    <dimen name="promo_card_icon_size">24dp</dimen>
+    <dimen name="promo_card_start_padding">16dp</dimen>
+    <dimen name="promo_card_top_padding">21dp</dimen>
+    <dimen name="promo_card_main_padding">24dp</dimen>
+    <dimen name="promo_card_title_padding">12dp</dimen>
+    <dimen name="promo_card_action_vertical_padding">4dp</dimen>
+    <dimen name="promo_card_action_end_padding">4dp</dimen>
+    <dimen name="promo_card_action_between_padding">11dp</dimen>
+    <dimen name="promo_card_line_spacing">4dp</dimen>
 </resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index dab5c6a..7d5d42f 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -218,4 +218,18 @@
         <item name="cardCornerRadius">2dp</item>
         <item name="cardBackgroundColor">@color/background_dialer_call_log_list_item</item>
     </style>
+
+    <style name="PromoCardActionStyle">
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">@dimen/call_log_action_height</item>
+        <item name="android:gravity">end|center_vertical</item>
+        <item name="android:paddingStart">@dimen/call_log_action_horizontal_padding</item>
+        <item name="android:paddingEnd">@dimen/call_log_action_horizontal_padding</item>
+        <item name="android:textColor">@color/promo_card_text</item>
+        <item name="android:textSize">@dimen/call_log_list_item_actions_text_size</item>
+        <item name="android:fontFamily">"sans-serif-medium"</item>
+        <item name="android:focusable">true</item>
+        <item name="android:singleLine">true</item>
+        <item name="android:textAllCaps">true</item>
+    </style>
 </resources>
diff --git a/src/com/android/dialer/DialtactsActivity.java b/src/com/android/dialer/DialtactsActivity.java
index fc0f1fb..b77e910 100644
--- a/src/com/android/dialer/DialtactsActivity.java
+++ b/src/com/android/dialer/DialtactsActivity.java
@@ -133,6 +133,7 @@
      * Just for backward compatibility. Should behave as same as {@link Intent#ACTION_DIAL}.
      */
     private static final String ACTION_TOUCH_DIALER = "com.android.phone.action.TOUCH_DIALER";
+    public static final String EXTRA_SHOW_TAB = "EXTRA_SHOW_TAB";
 
     private static final int ACTIVITY_REQUEST_CODE_VOICE_SEARCH = 1;
 
@@ -539,6 +540,7 @@
             }
             mIsRestarting = false;
         }
+
         prepareVoiceSearchButton();
         mDialerDatabaseHelper.startSmartDialUpdateThread();
         mFloatingActionButtonController.align(getFabAlignment(), false /* animate */);
@@ -903,6 +905,11 @@
         mStateSaved = false;
         displayFragment(newIntent);
 
+        if (newIntent.hasExtra(EXTRA_SHOW_TAB)) {
+            mListsFragment.showTab(
+                    getIntent().getIntExtra(EXTRA_SHOW_TAB, mListsFragment.TAB_INDEX_SPEED_DIAL));
+        }
+
         invalidateOptionsMenu();
     }
 
diff --git a/src/com/android/dialer/PhoneCallDetails.java b/src/com/android/dialer/PhoneCallDetails.java
index add6315..403c4e8 100644
--- a/src/com/android/dialer/PhoneCallDetails.java
+++ b/src/com/android/dialer/PhoneCallDetails.java
@@ -19,12 +19,9 @@
 import com.android.dialer.calllog.PhoneNumberDisplayUtil;
 
 import android.content.Context;
-import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.provider.CallLog.Calls;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.telecom.PhoneAccountHandle;
-import android.text.TextUtils;
 
 /**
  * The details of a phone call to be shown in the UI.
@@ -87,10 +84,19 @@
     // Voicemail transcription
     public String transcription;
 
+    // The display string for the number.
     public String displayNumber;
+
+    // Whether the contact number is a voicemail number.
     public boolean isVoicemail;
 
     /**
+     * If this is a voicemail, whether the message is read. For other types of calls, this defaults
+     * to {@code true}.
+     */
+    public boolean isRead = true;
+
+    /**
      * Constructor with required fields for the details of a call with a number associated with a
      * contact.
      */
@@ -104,7 +110,6 @@
         this.numberPresentation = numberPresentation;
         this.formattedNumber = formattedNumber;
         this.isVoicemail = isVoicemail;
-
         this.displayNumber = PhoneNumberDisplayUtil.getDisplayNumber(
                 context,
                 this.number,
diff --git a/src/com/android/dialer/PhoneCallDetailsHelper.java b/src/com/android/dialer/PhoneCallDetailsHelper.java
index 672a1c8..2dc0810 100644
--- a/src/com/android/dialer/PhoneCallDetailsHelper.java
+++ b/src/com/android/dialer/PhoneCallDetailsHelper.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.content.res.Resources;
+import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
 import android.provider.CallLog;
 import android.provider.CallLog.Calls;
@@ -140,6 +141,12 @@
             views.voicemailTranscriptionView.setText(null);
             views.voicemailTranscriptionView.setVisibility(View.GONE);
         }
+
+        // Bold if not read
+        Typeface typeface = details.isRead ? Typeface.SANS_SERIF : Typeface.DEFAULT_BOLD;
+        views.nameView.setTypeface(typeface);
+        views.voicemailTranscriptionView.setTypeface(typeface);
+        views.callLocationAndDate.setTypeface(typeface);
     }
 
     /**
diff --git a/src/com/android/dialer/calllog/CallLogAdapter.java b/src/com/android/dialer/calllog/CallLogAdapter.java
index 4304002..cc3c5ba 100644
--- a/src/com/android/dialer/calllog/CallLogAdapter.java
+++ b/src/com/android/dialer/calllog/CallLogAdapter.java
@@ -18,15 +18,20 @@
 
 import android.content.Context;
 import android.content.Intent;
+import android.content.SharedPreferences;
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.net.Uri;
 import android.support.v7.widget.RecyclerView;
 import android.os.Bundle;
 import android.os.Trace;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceManager;
+import android.provider.CallLog;
 import android.support.v7.widget.RecyclerView.ViewHolder;
 import android.telecom.PhoneAccountHandle;
 import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -61,6 +66,21 @@
     private static final int VIEW_TYPE_SHOW_CALL_HISTORY_LIST_ITEM = 10;
     private static final int NO_EXPANDED_LIST_ITEM = -1;
 
+    private static final int VOICEMAIL_PROMO_CARD_POSITION = 0;
+    /**
+     * View type for voicemail promo card.  Note: Numbering starts at 20 to avoid collision
+     * with {@link com.android.common.widget.GroupingListAdapter#ITEM_TYPE_IN_GROUP}, and
+     * {@link CallLogAdapter#VIEW_TYPE_SHOW_CALL_HISTORY_LIST_ITEM}.
+     */
+    private static final int VIEW_TYPE_VOICEMAIL_PROMO_CARD = 20;
+
+    /**
+     * The key for the show voicemail promo card preference which will determine whether the promo
+     * card was permanently dismissed or not.
+     */
+    private static final String SHOW_VOICEMAIL_PROMO_CARD = "show_voicemail_promo_card";
+    private static final boolean SHOW_VOICEMAIL_PROMO_CARD_DEFAULT = true;
+
     protected final Context mContext;
     private final ContactInfoHelper mContactInfoHelper;
     private final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
@@ -96,6 +116,10 @@
 
     private boolean mLoading = true;
 
+    private SharedPreferences mPrefs;
+
+    private boolean mShowPromoCard = false;
+
     /** Instance of helper class for managing views. */
     private final CallLogListItemHelper mCallLogViewsHelper;
 
@@ -116,8 +140,10 @@
                 return;
             }
 
-            // Always reset the voicemail playback state on expand or collapse.
-            mVoicemailPlaybackPresenter.reset();
+            if (mVoicemailPlaybackPresenter != null) {
+                // Always reset the voicemail playback state on expand or collapse.
+                mVoicemailPlaybackPresenter.reset();
+            }
 
             if (viewHolder.getAdapterPosition() == mCurrentlyExpandedPosition) {
                 // Hide actions, if the clicked item is the expanded item.
@@ -132,6 +158,30 @@
         }
     };
 
+    /**
+     * Click handler used to dismiss the promo card when the user taps the "ok" button.
+     */
+    private final View.OnClickListener mOkActionListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View view) {
+            dismissVoicemailPromoCard();
+        }
+    };
+
+    /**
+     * Click handler used to send the user to the voicemail settings screen and then dismiss the
+     * promo card.
+     */
+    private final View.OnClickListener mVoicemailSettingsActionListener =
+            new View.OnClickListener() {
+        @Override
+        public void onClick(View view) {
+            Intent intent = new Intent(TelephonyManager.ACTION_CONFIGURE_VOICEMAIL);
+            mContext.startActivity(intent);
+            dismissVoicemailPromoCard();
+        }
+    };
+
     private void expandViewHolderActions(CallLogListItemViewHolder viewHolder) {
         // If another item is expanded, notify it that it has changed. Its actions will be
         // hidden when it is re-binded because we change mCurrentlyExpandedPosition below.
@@ -209,6 +259,8 @@
                 new PhoneCallDetailsHelper(mContext, resources, mPhoneNumberUtilsWrapper);
         mCallLogViewsHelper = new CallLogListItemHelper(phoneCallDetailsHelper, resources);
         mCallLogGroupBuilder = new CallLogGroupBuilder(this);
+        mPrefs = PreferenceManager.getDefaultSharedPreferences(context);
+        maybeShowVoicemailPromoCard();
     }
 
     public void onSaveInstanceState(Bundle outState) {
@@ -276,6 +328,8 @@
     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
         if (viewType == VIEW_TYPE_SHOW_CALL_HISTORY_LIST_ITEM) {
             return ShowCallHistoryViewHolder.create(mContext, parent);
+        } else if (viewType == VIEW_TYPE_VOICEMAIL_PROMO_CARD) {
+            return createVoicemailPromoCardViewHolder(parent);
         }
         return createCallLogEntryViewHolder(parent);
     }
@@ -289,7 +343,6 @@
     private ViewHolder createCallLogEntryViewHolder(ViewGroup parent) {
         LayoutInflater inflater = LayoutInflater.from(mContext);
         View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
-
         CallLogListItemViewHolder viewHolder = CallLogListItemViewHolder.create(
                 view,
                 mContext,
@@ -311,19 +364,52 @@
      * TODO: This gets called 20-30 times when Dialer starts up for a single call log entry and
      * should not. It invokes cross-process methods and the repeat execution can get costly.
      *
-     * @param callLogItemView the view corresponding to this entry
-     * @param count the number of entries in the current item, greater than 1 if it is a group
+     * @param ViewHolder The view corresponding to this entry.
+     * @param position The position of the entry.
      */
     public void onBindViewHolder(ViewHolder viewHolder, int position) {
-        if (getItemViewType(position) == VIEW_TYPE_SHOW_CALL_HISTORY_LIST_ITEM) {
-            return;
-        }
         Trace.beginSection("onBindViewHolder: " + position);
+
+        switch (getItemViewType(position)) {
+            case VIEW_TYPE_SHOW_CALL_HISTORY_LIST_ITEM:
+                break;
+            case VIEW_TYPE_VOICEMAIL_PROMO_CARD:
+                bindVoicemailPromoCardViewHolder(viewHolder);
+                break;
+            default:
+                bindCallLogListViewHolder(viewHolder, position);
+                break;
+        }
+
+        Trace.endSection();
+    }
+
+    /**
+     * Binds the promo card view holder.
+     *
+     * @param viewHolder The promo card view holder.
+     */
+    protected void bindVoicemailPromoCardViewHolder(ViewHolder viewHolder) {
+        PromoCardViewHolder promoCardViewHolder = (PromoCardViewHolder) viewHolder;
+
+        promoCardViewHolder.getSettingsTextView().setOnClickListener(
+                mVoicemailSettingsActionListener);
+        promoCardViewHolder.getOkTextView().setOnClickListener(mOkActionListener);
+    }
+
+    /**
+     * Binds the view holder for the call log list item view.
+     *
+     * @param viewHolder The call log list item view holder.
+     * @param position The position of the list item.
+     */
+
+    private void bindCallLogListViewHolder(ViewHolder viewHolder, int position) {
         Cursor c = (Cursor) getItem(position);
         if (c == null) {
-            Trace.endSection();
             return;
         }
+
         int count = getGroupSize(position);
 
         final String number = c.getString(CallLogQuery.NUMBER);
@@ -358,6 +444,9 @@
         details.features = getCallFeatures(c, count);
         details.geocode = c.getString(CallLogQuery.GEOCODED_LOCATION);
         details.transcription = c.getString(CallLogQuery.TRANSCRIPTION);
+        if (details.callTypes[0] == CallLog.Calls.VOICEMAIL_TYPE) {
+            details.isRead = c.getInt(CallLogQuery.IS_READ) == 1;
+        }
 
         if (!c.isNull(CallLogQuery.DATA_USAGE)) {
             details.dataUsage = c.getLong(CallLogQuery.DATA_USAGE);
@@ -423,22 +512,35 @@
             mViewTreeObserver = views.rootView.getViewTreeObserver();
             mViewTreeObserver.addOnPreDrawListener(this);
         }
-        Trace.endSection();
     }
 
     @Override
     public int getItemCount() {
-        return super.getItemCount() + (isShowingRecentsTab() ? 1 : 0);
+        return super.getItemCount() + ((isShowingRecentsTab() || mShowPromoCard) ? 1 : 0);
     }
 
     @Override
     public int getItemViewType(int position) {
         if (position == getItemCount() - 1 && isShowingRecentsTab()) {
             return VIEW_TYPE_SHOW_CALL_HISTORY_LIST_ITEM;
+        } else if (position == VOICEMAIL_PROMO_CARD_POSITION && mShowPromoCard) {
+            return VIEW_TYPE_VOICEMAIL_PROMO_CARD;
         }
         return super.getItemViewType(position);
     }
 
+    /**
+     * Retrieves an item at the specified position, taking into account the presence of a promo
+     * card.
+     *
+     * @param position The position to retrieve.
+     * @return The item at that position.
+     */
+    @Override
+    public Object getItem(int position) {
+        return super.getItem(position - (mShowPromoCard ? 1 : 0));
+    }
+
     protected boolean isShowingRecentsTab() {
         return mIsShowingRecentsTab;
     }
@@ -592,4 +694,37 @@
            return mContext.getResources().getString(R.string.call_log_header_other);
        }
     }
+
+    /**
+     * Determines if the voicemail promo card should be shown or not.  The voicemail promo card will
+     * be shown as the first item in the voicemail tab.
+     */
+    private void maybeShowVoicemailPromoCard() {
+        boolean showPromoCard = mPrefs.getBoolean(SHOW_VOICEMAIL_PROMO_CARD,
+                SHOW_VOICEMAIL_PROMO_CARD_DEFAULT);
+        mShowPromoCard = (mVoicemailPlaybackPresenter != null) && showPromoCard;
+    }
+
+    /**
+     * Dismisses the voicemail promo card and refreshes the call log.
+     */
+    private void dismissVoicemailPromoCard() {
+        mPrefs.edit().putBoolean(SHOW_VOICEMAIL_PROMO_CARD, false).apply();
+        mShowPromoCard = false;
+        notifyItemRemoved(VOICEMAIL_PROMO_CARD_POSITION);
+    }
+
+    /**
+     * Creates the view holder for the voicemail promo card.
+     *
+     * @param parent The parent view.
+     * @return The {@link ViewHolder}.
+     */
+    protected ViewHolder createVoicemailPromoCardViewHolder(ViewGroup parent) {
+        LayoutInflater inflater = LayoutInflater.from(mContext);
+        View view = inflater.inflate(R.layout.voicemail_promo_card, parent, false);
+
+        PromoCardViewHolder viewHolder = PromoCardViewHolder.create(view);
+        return viewHolder;
+    }
 }
diff --git a/src/com/android/dialer/calllog/CallLogFragment.java b/src/com/android/dialer/calllog/CallLogFragment.java
index 0f19f14..21ea97e 100644
--- a/src/com/android/dialer/calllog/CallLogFragment.java
+++ b/src/com/android/dialer/calllog/CallLogFragment.java
@@ -187,7 +187,10 @@
         resolver.registerContentObserver(Status.CONTENT_URI, true, mVoicemailStatusObserver);
         setHasOptionsMenu(true);
 
-        mVoicemailPlaybackPresenter = new VoicemailPlaybackPresenter(activity, state);
+        if (mCallTypeFilter == Calls.VOICEMAIL_TYPE) {
+            mVoicemailPlaybackPresenter = VoicemailPlaybackPresenter
+                    .getInstance(activity, state);
+        }
     }
 
     /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */
@@ -321,23 +324,28 @@
 
     @Override
     public void onPause() {
-        mVoicemailPlaybackPresenter.onPause(getActivity().isFinishing());
+        if (mVoicemailPlaybackPresenter != null) {
+            mVoicemailPlaybackPresenter.onPause();
+        }
         mAdapter.pauseCache();
         super.onPause();
     }
 
     @Override
     public void onStop() {
-        super.onStop();
-
         updateOnTransition(false /* onEntry */);
+
+        super.onStop();
     }
 
     @Override
     public void onDestroy() {
         mAdapter.pauseCache();
         mAdapter.changeCursor(null);
-        mVoicemailPlaybackPresenter.onDestroy(getActivity().isFinishing());
+
+        if (mVoicemailPlaybackPresenter != null) {
+            mVoicemailPlaybackPresenter.onDestroy();
+        }
 
         getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver);
         getActivity().getContentResolver().unregisterContentObserver(mContactsObserver);
@@ -353,7 +361,10 @@
         outState.putLong(KEY_DATE_LIMIT, mDateLimit);
 
         mAdapter.onSaveInstanceState(outState);
-        mVoicemailPlaybackPresenter.onSaveInstanceState(outState);
+
+        if (mVoicemailPlaybackPresenter != null) {
+            mVoicemailPlaybackPresenter.onSaveInstanceState(outState);
+        }
     }
 
     @Override
diff --git a/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java b/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java
index abdc38f..3d6eb0b 100644
--- a/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java
+++ b/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java
@@ -32,8 +32,10 @@
 import android.util.Log;
 
 import com.android.common.io.MoreCloseables;
+import com.android.dialer.DialtactsActivity;
 import com.android.dialer.R;
 import com.android.dialer.calllog.PhoneAccountUtils;
+import com.android.dialer.list.ListsFragment;
 import com.google.common.collect.Maps;
 
 import java.util.Map;
@@ -179,10 +181,10 @@
         final Intent contentIntent;
         // Open the call log.
         // TODO: Send to recents tab in Dialer instead.
-        contentIntent = new Intent(Intent.ACTION_VIEW, Calls.CONTENT_URI);
-        contentIntent.putExtra(Calls.EXTRA_CALL_TYPE_FILTER, Calls.VOICEMAIL_TYPE);
-        notificationBuilder.setContentIntent(
-                PendingIntent.getActivity(mContext, 0, contentIntent, 0));
+        contentIntent = new Intent(mContext, DialtactsActivity.class);
+        contentIntent.putExtra(DialtactsActivity.EXTRA_SHOW_TAB, ListsFragment.TAB_INDEX_VOICEMAIL);
+        notificationBuilder.setContentIntent(PendingIntent.getActivity(
+                mContext, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT));
 
         // The text to show in the ticker, describing the new event.
         if (callToNotify != null) {
diff --git a/src/com/android/dialer/calllog/PromoCardViewHolder.java b/src/com/android/dialer/calllog/PromoCardViewHolder.java
new file mode 100644
index 0000000..4c96027
--- /dev/null
+++ b/src/com/android/dialer/calllog/PromoCardViewHolder.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2015 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.calllog;
+
+import com.android.dialer.R;
+
+import android.support.v7.widget.CardView;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+
+/**
+ * View holder class for a promo card which will appear in the voicemail tab.
+ */
+public class PromoCardViewHolder extends RecyclerView.ViewHolder {
+    public static PromoCardViewHolder create(View rootView) {
+        return new PromoCardViewHolder(rootView);
+    }
+
+    /**
+     * The "Settings" button view.
+     */
+    private View mSettingsTextView;
+
+    /**
+     * The "Ok" button view.
+     */
+    private View mOkTextView;
+
+    /**
+     * Creates an instance of the {@link ViewHolder}.
+     *
+     * @param rootView The root view.
+     */
+    private PromoCardViewHolder(View rootView) {
+        super(rootView);
+
+        mSettingsTextView = rootView.findViewById(R.id.settings_action);
+        mOkTextView = rootView.findViewById(R.id.ok_action);
+    }
+
+    /**
+     * Retrieves the "Settings" button.
+     *
+     * @return The view.
+     */
+    public View getSettingsTextView() {
+        return mSettingsTextView;
+    }
+
+    /**
+     * Retrieves the "Ok" button.
+     *
+     * @return The view.
+     */
+    public View getOkTextView() {
+        return mOkTextView;
+    }
+}
diff --git a/src/com/android/dialer/dialpad/DialpadFragment.java b/src/com/android/dialer/dialpad/DialpadFragment.java
index 766175c..5b1e211 100644
--- a/src/com/android/dialer/dialpad/DialpadFragment.java
+++ b/src/com/android/dialer/dialpad/DialpadFragment.java
@@ -1643,12 +1643,17 @@
             if (mAnimate) {
                 dialpadView.animateShow();
             }
+            mFloatingActionButtonController.setVisible(false);
             mFloatingActionButtonController.scaleIn(mAnimate ? mDialpadSlideInDuration : 0);
             activity.onDialpadShown();
             mDigits.requestFocus();
         }
-        if (hidden && mAnimate) {
-            mFloatingActionButtonController.scaleOut();
+        if (hidden) {
+            if (mAnimate) {
+                mFloatingActionButtonController.scaleOut();
+            } else {
+                mFloatingActionButtonController.setVisible(false);
+            }
         }
     }
 
diff --git a/src/com/android/dialer/list/AllContactsFragment.java b/src/com/android/dialer/list/AllContactsFragment.java
index 71c6980..d34250b 100644
--- a/src/com/android/dialer/list/AllContactsFragment.java
+++ b/src/com/android/dialer/list/AllContactsFragment.java
@@ -75,7 +75,7 @@
     public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
         super.onLoadFinished(loader, data);
 
-        if (data.getCount() == 0) {
+        if (data == null || data.getCount() == 0) {
             mEmptyListView.setVisibility(View.VISIBLE);
         }
     }
diff --git a/src/com/android/dialer/list/ListsFragment.java b/src/com/android/dialer/list/ListsFragment.java
index 0e3df52..e45da0c 100644
--- a/src/com/android/dialer/list/ListsFragment.java
+++ b/src/com/android/dialer/list/ListsFragment.java
@@ -84,6 +84,8 @@
 
     private SharedPreferences mPrefs;
     private boolean mHasActiveVoicemailProvider;
+    private boolean mHasFetchedVoicemailStatus;
+    private boolean mShowVoicemailTabAfterVoicemailStatusIsFetched;
 
     private VoicemailStatusHelper mVoicemailStatusHelper;
     private ArrayList<OnPageChangeListener> mOnPageChangeListeners =
@@ -167,6 +169,7 @@
         Trace.endSection();
 
         mVoicemailStatusHelper = new VoicemailStatusHelperImpl();
+        mHasFetchedVoicemailStatus = false;
 
         mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
         mHasActiveVoicemailProvider = mPrefs.getBoolean(
@@ -204,7 +207,7 @@
         mViewPager.setAdapter(mViewPagerAdapter);
         mViewPager.setOffscreenPageLimit(TAB_COUNT_WITH_VOICEMAIL - 1);
         mViewPager.setOnPageChangeListener(this);
-        mViewPager.setCurrentItem(getRtlPosition(TAB_INDEX_SPEED_DIAL));
+        showTab(TAB_INDEX_SPEED_DIAL);
 
         mTabTitles = new String[TAB_COUNT_WITH_VOICEMAIL];
         mTabTitles[TAB_INDEX_SPEED_DIAL] = getResources().getString(R.string.tab_speed_dial);
@@ -237,6 +240,24 @@
         }
     }
 
+    /**
+     * Shows the tab with the specified index. If the voicemail tab index is specified, but the
+     * voicemail status hasn't been fetched, it will try to show the tab after the voicemail status
+     * has been fetched.
+     */
+    public void showTab(int index) {
+        if (index == TAB_INDEX_VOICEMAIL) {
+            if (mHasActiveVoicemailProvider) {
+                mViewPager.setCurrentItem(getRtlPosition(TAB_INDEX_VOICEMAIL));
+            } else if (!mHasFetchedVoicemailStatus) {
+                // Try to show the voicemail tab after the voicemail status returns.
+                mShowVoicemailTabAfterVoicemailStatusIsFetched = true;
+            }
+        } else {
+            mViewPager.setCurrentItem(getRtlPosition(index));
+        }
+    }
+
     @Override
     public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
         mTabIndex = getRtlPosition(position);
@@ -252,6 +273,9 @@
     public void onPageSelected(int position) {
         mTabIndex = getRtlPosition(position);
 
+        // Show the tab which has been selected instead.
+        mShowVoicemailTabAfterVoicemailStatusIsFetched = false;
+
         final int count = mOnPageChangeListeners.size();
         for (int i = 0; i < count; i++) {
             mOnPageChangeListeners.get(i).onPageSelected(position);
@@ -269,6 +293,8 @@
 
     @Override
     public void onVoicemailStatusFetched(Cursor statusCursor) {
+        mHasFetchedVoicemailStatus = true;
+
         if (getActivity() == null || getActivity().isFinishing()) {
             return;
         }
@@ -285,6 +311,11 @@
                     .putBoolean(PREF_KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER, hasActiveVoicemailProvider)
                     .commit();
         }
+
+        if (mHasActiveVoicemailProvider && mShowVoicemailTabAfterVoicemailStatusIsFetched) {
+            mShowVoicemailTabAfterVoicemailStatusIsFetched = false;
+            showTab(TAB_INDEX_VOICEMAIL);
+        }
     }
 
     @Override
diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java b/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java
index 73f4b3b..ca487db 100644
--- a/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java
+++ b/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java
@@ -50,8 +50,9 @@
 import javax.annotation.concurrent.ThreadSafe;
 
 /**
- * Displays and plays a single voicemail.
- * <p>
+ * Displays and plays a single voicemail. See {@link VoicemailPlaybackPresenter} for
+ * details on the voicemail playback implementation.
+ *
  * 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.
  */
@@ -178,12 +179,13 @@
             if (mPresenter == null) {
                 return;
             }
-            CallLogAsyncTaskUtil.deleteVoicemail(mContext, mPresenter.getVoicemailUri(), null);
+            CallLogAsyncTaskUtil.deleteVoicemail(mContext, mVoicemailUri, null);
         }
     };
 
     private Context mContext;
     private VoicemailPlaybackPresenter mPresenter;
+    private Uri mVoicemailUri;
 
     private boolean mIsPlaying = false;
 
@@ -209,8 +211,9 @@
     }
 
     @Override
-    public void setPresenter(VoicemailPlaybackPresenter presenter) {
+    public void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri) {
         mPresenter = presenter;
+        mVoicemailUri = voicemailUri;
     }
 
     @Override
@@ -256,15 +259,13 @@
     }
 
     @Override
-    public void onPlaybackError(Exception e) {
+    public void onPlaybackError() {
         if (mPositionUpdater != null) {
             mPositionUpdater.stopUpdating();
         }
 
         disableUiElements();
         mPlaybackPosition.setText(getString(R.string.voicemail_playback_error));
-
-        Log.e(TAG, "Could not play voicemail", e);
     }
 
 
diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
index 60425e4..4cd8c4d 100644
--- a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
+++ b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
@@ -56,10 +56,13 @@
 import javax.annotation.concurrent.ThreadSafe;
 
 /**
- * Contains the controlling logic for a voicemail playback UI.
+ * Contains the controlling logic for a voicemail playback in the call log. It is closely coupled
+ * to assumptions about the behaviors and lifecycle of the call log, in particular in the
+ * {@link CallLogFragment} and {@link CallLogAdapter}.
  * <p>
  * This controls a single {@link com.android.dialer.voicemail.VoicemailPlaybackLayout}. A single
- * instance can be reused for different such layouts, using {@link #setVoicemailPlaybackView}.
+ * instance can be reused for different such layouts, using {@link #setVoicemailPlaybackView}. This
+ * is to facilitate reuse across different voicemail call log entries.
  * <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.
@@ -77,7 +80,7 @@
         int getDesiredClipPosition();
         void disableUiElements();
         void enableUiElements();
-        void onPlaybackError(Exception e);
+        void onPlaybackError();
         void onPlaybackStarted(int duration, ScheduledExecutorService executorService);
         void onPlaybackStopped();
         void onSpeakerphoneOn(boolean on);
@@ -85,7 +88,7 @@
         void setFetchContentTimeout();
         void setIsBuffering();
         void setIsFetchingContent();
-        void setPresenter(VoicemailPlaybackPresenter presenter);
+        void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri);
     }
 
     /** The enumeration of {@link AsyncTask} objects we use in this class. */
@@ -121,12 +124,14 @@
      */
     private final AtomicInteger mDuration = new AtomicInteger(0);
 
+    private static VoicemailPlaybackPresenter sInstance;
+
     private Activity mActivity;
     private Context mContext;
     private PlaybackView mView;
-    private static MediaPlayer mMediaPlayer;
-
     private Uri mVoicemailUri;
+
+    private MediaPlayer mMediaPlayer;
     private int mPosition;
     private boolean mIsPlaying;
     // MediaPlayer crashes on some method calls if not prepared but does not have a method which
@@ -134,9 +139,10 @@
     private boolean mIsPrepared;
 
     private boolean mShouldResumePlaybackAfterSeeking;
+    private int mInitialOrientation;
 
     // Used to run async tasks that need to interact with the UI.
-    private final AsyncTaskExecutor mAsyncTaskExecutor;
+    private AsyncTaskExecutor mAsyncTaskExecutor;
     private static ScheduledExecutorService mScheduledExecutorService;
     /**
      * Used to handle the result of a successful or time-out fetch result.
@@ -148,12 +154,49 @@
     private PowerManager.WakeLock mProximityWakeLock;
     private AudioManager mAudioManager;
 
-    public VoicemailPlaybackPresenter(Activity activity, Bundle savedInstanceState) {
+    /**
+     * Obtain singleton instance of this class. Use a single instance to provide a consistent
+     * listener to the AudioManager when requesting and abandoning audio focus.
+     *
+     * Otherwise, after rotation the previous listener will still be active but a new listener
+     * will be provided to calls to the AudioManager, which is bad. For example, abandoning
+     * audio focus with the new listeners results in an AUDIO_FOCUS_GAIN callback to the
+     * previous listener, which is the opposite of the intended behavior.
+     */
+    public static VoicemailPlaybackPresenter getInstance(
+            Activity activity, Bundle savedInstanceState) {
+        if (sInstance == null) {
+            sInstance = new VoicemailPlaybackPresenter(activity);
+        }
+
+        sInstance.init(activity, savedInstanceState);
+        return sInstance;
+    }
+
+    /**
+     * Initialize variables which are activity-independent and state-independent.
+     */
+    private VoicemailPlaybackPresenter(Activity activity) {
+        Context context = activity.getApplicationContext();
+        mAsyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor();
+        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+
+        PowerManager powerManager =
+                (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+        if (powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
+            mProximityWakeLock = powerManager.newWakeLock(
+                    PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);
+        }
+    }
+
+    /**
+     * Update variables which are activity-dependent or state-dependent.
+     */
+    private void init(Activity activity, Bundle savedInstanceState) {
         mActivity = activity;
         mContext = activity;
-        mAsyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor();
-        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
 
+        mInitialOrientation = mContext.getResources().getConfiguration().orientation;
         mActivity.setVolumeControlStream(VoicemailPlaybackPresenter.PLAYBACK_STREAM);
 
         if (savedInstanceState != null) {
@@ -163,27 +206,58 @@
             mPosition = savedInstanceState.getInt(CLIP_POSITION_KEY, 0);
             mIsPlaying = savedInstanceState.getBoolean(IS_PLAYING_STATE_KEY, false);
         }
-
-        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);
-        }
-
-        // mMediaPlayer is static to enable seamless playback during rotation. If we do not create
-        // a new MediaPlayer, we still need to update listeners to the current Presenter instance.
-        if (mMediaPlayer == null) {
-            mMediaPlayer = new MediaPlayer();
-            mIsPrepared = false;
-        }
-        mMediaPlayer.setOnPreparedListener(this);
-        mMediaPlayer.setOnErrorListener(this);
-        mMediaPlayer.setOnCompletionListener(this);
     }
 
+    /**
+     * Must be invoked when the parent Activity is saving it state.
+     */
+    public void onSaveInstanceState(Bundle outState) {
+        if (mView != null) {
+            outState.putParcelable(VOICEMAIL_URI_KEY, mVoicemailUri);
+            outState.putBoolean(IS_PREPARED_KEY, mIsPrepared);
+            outState.putInt(CLIP_POSITION_KEY, mView.getDesiredClipPosition());
+            outState.putBoolean(IS_PLAYING_STATE_KEY, mIsPlaying);
+        }
+    }
+
+    /**
+     * Specify the view which this presenter controls and the voicemail to prepare to play.
+     */
+    public void setPlaybackView(
+            PlaybackView view, Uri voicemailUri, boolean startPlayingImmediately) {
+        mView = view;
+        mView.setPresenter(this, voicemailUri);
+
+        if (mMediaPlayer != null && voicemailUri.equals(mVoicemailUri)) {
+            // Handles case where MediaPlayer was retained after an orientation change.
+            onPrepared(mMediaPlayer);
+            mView.onSpeakerphoneOn(isSpeakerphoneOn());
+        } else {
+            if (!voicemailUri.equals(mVoicemailUri)) {
+                mPosition = 0;
+            }
+
+            mVoicemailUri = voicemailUri;
+            mDuration.set(0);
+            mIsPlaying = startPlayingImmediately;
+
+            checkForContent();
+
+            // Default to earpiece.
+            mView.onSpeakerphoneOn(false);
+        }
+    }
+
+    /**
+     * Reset the presenter for playback.
+     */
     public void reset() {
-        pausePlayback();
+        if (mMediaPlayer != null) {
+            mMediaPlayer.release();
+            mMediaPlayer = null;
+        }
+
+        disableProximitySensor(false /* waitForFarState */);
 
         mView = null;
         mVoicemailUri = null;
@@ -195,49 +269,32 @@
     }
 
     /**
-     * Specify the view which this presenter controls and the voicemail for playback.
+     * Must be invoked when the parent activity is paused.
      */
-    public void setPlaybackView(
-            PlaybackView view, Uri voicemailUri, boolean startPlayingImmediately) {
-        mView = view;
-        mView.setPresenter(this);
-
-        if (mVoicemailUri != null && mVoicemailUri.equals(voicemailUri)) {
-            // Handles rotation case where playback view is set for the same voicemail.
-            if (mIsPrepared) {
-                onPrepared(mMediaPlayer);
-            } else {
-                checkForContent();
-            }
-            mView.onSpeakerphoneOn(isSpeakerphoneOn());
-        } else {
-            mVoicemailUri = voicemailUri;
-            mPosition = 0;
-            mDuration.set(0);
-            mIsPlaying = startPlayingImmediately;
-
-            // Default to earpiece.
-            mView.onSpeakerphoneOn(false);
-
-            checkForContent();
+    public void onPause() {
+        if (mContext != null && mIsPrepared
+                && mInitialOrientation != mContext.getResources().getConfiguration().orientation) {
+            // If an orientation change triggers the pause, retain the MediaPlayer.
+            Log.d(TAG, "onPause: Orientation changed.");
+            return;
         }
-    }
 
-    public void onPause(boolean isFinishing) {
-        // Do not pause for orientation changes.
-        if (mIsPrepared && mMediaPlayer.isPlaying() && isFinishing) {
-            pausePlayback();
+        // Release the media player, otherwise there may be failures.
+        if (mMediaPlayer != null) {
+            mMediaPlayer.release();
+            mMediaPlayer = null;
         }
 
         disableProximitySensor(false /* waitForFarState */);
     }
 
-    public void onDestroy(boolean isFinishing) {
-        // Do not release for orientation changes.
-        if (mIsPrepared && isFinishing) {
-            mMediaPlayer.release();
-            mIsPrepared = false;
-        }
+    /**
+     * Must be invoked when the parent activity is destroyed.
+     */
+    public void onDestroy() {
+        // Clear references to avoid leaks from the singleton instance.
+        mActivity = null;
+        mContext = null;
 
         if (mScheduledExecutorService != null) {
             mScheduledExecutorService.shutdown();
@@ -248,17 +305,6 @@
             mFetchResultHandler.destroy();
             mFetchResultHandler = null;
         }
-
-        disableProximitySensor(false /* waitForFarState */);
-    }
-
-    public void onSaveInstanceState(Bundle outState) {
-        if (mView != null) {
-            outState.putParcelable(VOICEMAIL_URI_KEY, mVoicemailUri);
-            outState.putBoolean(IS_PREPARED_KEY, mIsPrepared);
-            outState.putInt(CLIP_POSITION_KEY, mView.getDesiredClipPosition());
-            outState.putBoolean(IS_PLAYING_STATE_KEY, mIsPlaying);
-        }
     }
 
     /**
@@ -268,7 +314,7 @@
      * voicemail we've been asked to play has any content available.
      * <p>
      * 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
+     * the DB is set. If set, we proceed to {@link #prepareContent()} method. If not set, make
      * a request to fetch the content asynchronously via {@link #requestContent()}.
      */
     private void checkForContent() {
@@ -282,7 +328,7 @@
             @Override
             public void onPostExecute(Boolean hasContent) {
                 if (hasContent) {
-                    prepareToPlayContent();
+                    prepareContent();
                 } else {
                     requestContent();
                 }
@@ -314,7 +360,7 @@
      * 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 #prepareToPlayContent()}. If the has_content field does not
+     * proceed to {@link #prepareContent()}. 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.
      */
@@ -323,8 +369,7 @@
             mFetchResultHandler.destroy();
         }
 
-        mFetchResultHandler = new FetchResultHandler(new Handler());
-        mFetchResultHandler.registerContentObserver(mVoicemailUri);
+        mFetchResultHandler = new FetchResultHandler(new Handler(), mVoicemailUri);
 
         // Send voicemail fetch request.
         Intent intent = new Intent(VoicemailContract.ACTION_FETCH_VOICEMAIL, mVoicemailUri);
@@ -336,17 +381,13 @@
         private AtomicBoolean mIsWaitingForResult = new AtomicBoolean(true);
         private final Handler mFetchResultHandler;
 
-        public FetchResultHandler(Handler handler) {
+        public FetchResultHandler(Handler handler, Uri voicemailUri) {
             super(handler);
             mFetchResultHandler = handler;
-        }
 
-        public void registerContentObserver(Uri voicemailUri) {
-            if (mIsWaitingForResult.get()) {
-                mContext.getContentResolver().registerContentObserver(
-                        voicemailUri, false, this);
-                mFetchResultHandler.postDelayed(this, FETCH_CONTENT_TIMEOUT_MS);
-            }
+            mContext.getContentResolver().registerContentObserver(
+                    voicemailUri, false, this);
+            mFetchResultHandler.postDelayed(this, FETCH_CONTENT_TIMEOUT_MS);
         }
 
         /**
@@ -354,7 +395,7 @@
          */
         @Override
         public void run() {
-            if (mIsWaitingForResult.getAndSet(false)) {
+            if (mIsWaitingForResult.getAndSet(false) && mContext != null) {
                 mContext.getContentResolver().unregisterContentObserver(this);
                 if (mView != null) {
                     mView.setFetchContentTimeout();
@@ -363,7 +404,7 @@
         }
 
         public void destroy() {
-            if (mIsWaitingForResult.getAndSet(false)) {
+            if (mIsWaitingForResult.getAndSet(false) && mContext != null) {
                 mContext.getContentResolver().unregisterContentObserver(this);
                 mFetchResultHandler.removeCallbacks(this);
             }
@@ -380,12 +421,10 @@
 
                 @Override
                 public void onPostExecute(Boolean hasContent) {
-                    if (hasContent) {
-                        if (mIsWaitingForResult.getAndSet(false)) {
-                            mContext.getContentResolver().unregisterContentObserver(
-                                    FetchResultHandler.this);
-                            prepareToPlayContent();
-                        }
+                    if (hasContent && mContext != null && mIsWaitingForResult.getAndSet(false)) {
+                        mContext.getContentResolver().unregisterContentObserver(
+                                FetchResultHandler.this);
+                        prepareContent();
                     }
                 }
             });
@@ -400,15 +439,27 @@
      * media player. If preparation is successful, the media player will {@link #onPrepared()},
      * and it will call {@link #onError()} otherwise.
      */
-    private void prepareToPlayContent() {
+    private void prepareContent() {
         if (mView == null) {
             return;
         }
-        mIsPrepared = false;
+        Log.d(TAG, "prepareContent");
+
+        // Release the previous media player, otherwise there may be failures.
+        if (mMediaPlayer != null) {
+            mMediaPlayer.release();
+            mMediaPlayer = null;
+        }
 
         mView.setIsBuffering();
+        mIsPrepared = false;
 
         try {
+            mMediaPlayer = new MediaPlayer();
+            mMediaPlayer.setOnPreparedListener(this);
+            mMediaPlayer.setOnErrorListener(this);
+            mMediaPlayer.setOnCompletionListener(this);
+
             mMediaPlayer.reset();
             mMediaPlayer.setDataSource(mContext, mVoicemailUri);
             mMediaPlayer.setAudioStreamType(PLAYBACK_STREAM);
@@ -426,12 +477,15 @@
         if (mView == null) {
             return;
         }
+        Log.d(TAG, "onPrepared");
         mIsPrepared = true;
 
         mDuration.set(mMediaPlayer.getDuration());
 
         mView.enableUiElements();
+        Log.d(TAG, "onPrepared: mPosition=" + mPosition);
         mView.setClipPosition(mPosition, mDuration.get());
+        mMediaPlayer.seekTo(mPosition);
 
         if (mIsPlaying) {
             resumePlayback();
@@ -451,12 +505,15 @@
     }
 
     private void handleError(Exception e) {
+        Log.d(TAG, "handleError: Could not play voicemail " + e);
+
         if (mIsPrepared) {
             mMediaPlayer.release();
+            mMediaPlayer = null;
             mIsPrepared = false;
         }
 
-        mView.onPlaybackError(e);
+        mView.onPlaybackError();
 
         mPosition = 0;
         mIsPlaying = false;
@@ -476,15 +533,12 @@
 
     @Override
     public void onAudioFocusChange(int focusChange) {
-        if (!mIsPrepared) {
-            return;
-        }
-
-        boolean lostFocus = focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT ||
-                focusChange == AudioManager.AUDIOFOCUS_LOSS;
-        if (mMediaPlayer.isPlaying() && lostFocus) {
+        Log.d(TAG, "onAudioFocusChange: focusChange=" + focusChange);
+        boolean lostFocus = focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT
+                || focusChange == AudioManager.AUDIOFOCUS_LOSS;
+        if (mIsPlaying && focusChange == AudioManager.AUDIOFOCUS_LOSS) {
             pausePlayback();
-        } else if (!mMediaPlayer.isPlaying() && focusChange == AudioManager.AUDIOFOCUS_GAIN) {
+        } else if (!mIsPlaying && focusChange == AudioManager.AUDIOFOCUS_GAIN) {
             resumePlayback();
         }
     }
@@ -506,25 +560,25 @@
             mMediaPlayer.seekTo(mPosition);
 
             try {
-                // Grab audio focus here
+                // Grab audio focus.
                 int result = mAudioManager.requestAudioFocus(
-                        VoicemailPlaybackPresenter.this,
+                        this,
                         PLAYBACK_STREAM,
                         AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
-
                 if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
                     throw new RejectedExecutionException("Could not capture audio focus.");
                 }
 
-                // Can throw RejectedExecutionException
+                // Can throw RejectedExecutionException.
                 mMediaPlayer.start();
             } catch (RejectedExecutionException e) {
                 handleError(e);
             }
         }
 
-        enableProximitySensor();
+        Log.d(TAG, "Resumed playback at " + mPosition + ".");
         mView.onPlaybackStarted(mDuration.get(), getScheduledExecutorServiceInstance());
+        enableProximitySensor();
     }
 
     /**
@@ -535,17 +589,17 @@
             return;
         }
 
-        mPosition = mMediaPlayer.getCurrentPosition();
         mIsPlaying = false;
 
         if (mMediaPlayer.isPlaying()) {
             mMediaPlayer.pause();
         }
 
-        mAudioManager.abandonAudioFocus(this);
-        mView.onPlaybackStopped();
+        mPosition = mMediaPlayer.getCurrentPosition();
+        Log.d(TAG, "Paused playback at " + mPosition + ".");
 
-        // Always disable the proximity sensor on stop.
+        mView.onPlaybackStopped();
+        mAudioManager.abandonAudioFocus(this);
         disableProximitySensor(true /* waitForFarState */);
     }
 
@@ -567,6 +621,9 @@
     }
 
     private void enableProximitySensor() {
+        // Disable until proximity sensor behavior in onPause is fixed: b/21932251.
+
+        /*
         if (mProximityWakeLock == null || isSpeakerphoneOn() || !mIsPrepared
                 || !mMediaPlayer.isPlaying()) {
             return;
@@ -578,6 +635,7 @@
         } else {
             Log.i(TAG, "Proximity wake lock already acquired");
         }
+        */
     }
 
     private void disableProximitySensor(boolean waitForFarState) {
@@ -606,12 +664,8 @@
         return mAudioManager.isSpeakerphoneOn();
     }
 
-    public Uri getVoicemailUri() {
-        return mVoicemailUri;
-    }
-
     public int getMediaPlayerPosition() {
-        return mIsPrepared ? mMediaPlayer.getCurrentPosition() : 0;
+        return mIsPrepared && mMediaPlayer != null ? mMediaPlayer.getCurrentPosition() : 0;
     }
 
     private static synchronized ScheduledExecutorService getScheduledExecutorServiceInstance() {
diff --git a/tests/src/com/android/dialer/voicemail/VoicemailPlaybackTest.java b/tests/src/com/android/dialer/voicemail/VoicemailPlaybackTest.java
index ea341a3..58b4f55 100644
--- a/tests/src/com/android/dialer/voicemail/VoicemailPlaybackTest.java
+++ b/tests/src/com/android/dialer/voicemail/VoicemailPlaybackTest.java
@@ -85,7 +85,7 @@
         mLayout = new VoicemailPlaybackLayout(mActivity);
         mLayout.onFinishInflate();
 
-        mPresenter = new VoicemailPlaybackPresenter(mActivity, null);
+        mPresenter = VoicemailPlaybackPresenter.getInstance(mActivity, null);
     }
 
     @Override