Merge "Fix shortcut action text length issues." into mnc-dev
diff --git a/res/layout/call_detail.xml b/res/layout/call_detail.xml
index 5d1607e..c077851 100644
--- a/res/layout/call_detail.xml
+++ b/res/layout/call_detail.xml
@@ -87,12 +87,6 @@
             </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/res/layout/call_log_list_item_actions.xml b/res/layout/call_log_list_item_actions.xml
index d3e18be..f1d0e9e 100644
--- a/res/layout/call_log_list_item_actions.xml
+++ b/res/layout/call_log_list_item_actions.xml
@@ -23,6 +23,11 @@
     android:visibility="visible"
     android:importantForAccessibility="1">
 
+    <com.android.dialer.voicemail.VoicemailPlaybackLayout
+        android:id="@+id/voicemail_playback_layout"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
     <LinearLayout
         android:id="@+id/video_call_action"
         style="@style/CallLogActionStyle">
@@ -38,20 +43,6 @@
     </LinearLayout>
 
     <LinearLayout
-        android:id="@+id/voicemail_action"
-        style="@style/CallLogActionStyle">
-
-        <ImageView
-            style="@style/CallLogActionIconStyle"
-            android:src="@drawable/ic_voicemail_24dp" />
-
-        <TextView
-            style="@style/CallLogActionTextStyle"
-            android:text="@string/call_log_action_voicemail" />
-
-    </LinearLayout>
-
-    <LinearLayout
         android:id="@+id/create_new_contact_action"
         style="@style/CallLogActionStyle">
 
diff --git a/res/layout/playback_layout.xml b/res/layout/voicemail_playback_layout.xml
similarity index 100%
rename from res/layout/playback_layout.xml
rename to res/layout/voicemail_playback_layout.xml
diff --git a/src/com/android/dialer/CallDetailActivity.java b/src/com/android/dialer/CallDetailActivity.java
index ce29049..6da7c79 100644
--- a/src/com/android/dialer/CallDetailActivity.java
+++ b/src/com/android/dialer/CallDetailActivity.java
@@ -61,8 +61,6 @@
 import com.android.dialer.util.IntentUtil;
 import com.android.dialer.util.DialerUtils;
 import com.android.dialer.util.TelecomUtil;
-import com.android.dialer.voicemail.VoicemailPlaybackLayout;
-import com.android.dialer.voicemail.VoicemailPlaybackPresenter;
 
 import java.util.List;
 
@@ -218,8 +216,6 @@
     /** Helper to load contact photos. */
     private ContactPhotoManager mContactPhotoManager;
 
-    private VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
-
     private Uri mVoicemailUri;
     private BidiFormatter mBidiFormatter = BidiFormatter.getInstance();
 
@@ -255,8 +251,6 @@
         mContactInfoHelper = new ContactInfoHelper(this, GeoUtil.getCurrentCountryIso(this));
         getActionBar().setDisplayHomeAsUpEnabled(true);
 
-        optionallyHandleVoicemail();
-
         if (getIntent().getBooleanExtra(EXTRA_FROM_NOTIFICATION, false)) {
             closeSystemDialogs();
         }
@@ -269,58 +263,6 @@
         CallLogAsyncTaskUtil.getCallDetails(this, getCallLogEntryUris(), mCallLogAsyncTaskListener);
     }
 
-    @Override
-    public void onPause() {
-        if (mVoicemailPlaybackPresenter != null) {
-            mVoicemailPlaybackPresenter.onPause(isFinishing());
-        }
-        super.onPause();
-    }
-
-    @Override
-    public void onDestroy() {
-        if (mVoicemailPlaybackPresenter != null) {
-            mVoicemailPlaybackPresenter.onDestroy(isFinishing());
-        }
-        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>
-     * If the Intent used to start this Activity contains the suitable extras, then start voicemail
-     * playback.  If it doesn't, then don't inflate the voicemail ui.
-     */
-    private void optionallyHandleVoicemail() {
-        if (hasVoicemail()) {
-            VoicemailPlaybackLayout voicemailPlaybackLayout =
-                (VoicemailPlaybackLayout) findViewById(R.id.voicemail_playback_layout);
-
-            mVoicemailPlaybackPresenter = new VoicemailPlaybackPresenter(this);
-            mVoicemailPlaybackPresenter.setPlaybackView(
-                    voicemailPlaybackLayout, mVoicemailUri, false /* startPlayingImmediately */);
-
-            voicemailPlaybackLayout.setVisibility(View.VISIBLE);
-            CallLogAsyncTaskUtil.markVoicemailAsRead(this, mVoicemailUri);
-        }
-    }
-
     private boolean hasVoicemail() {
         return mVoicemailUri != null;
     }
diff --git a/src/com/android/dialer/calllog/CallLogAdapter.java b/src/com/android/dialer/calllog/CallLogAdapter.java
index c472ef2..6862f68 100644
--- a/src/com/android/dialer/calllog/CallLogAdapter.java
+++ b/src/com/android/dialer/calllog/CallLogAdapter.java
@@ -22,6 +22,7 @@
 import android.database.Cursor;
 import android.net.Uri;
 import android.support.v7.widget.RecyclerView;
+import android.os.Bundle;
 import android.os.Trace;
 import android.support.v7.widget.RecyclerView.ViewHolder;
 import android.telecom.PhoneAccountHandle;
@@ -41,6 +42,7 @@
 import com.android.dialer.contactinfo.ContactInfoCache;
 import com.android.dialer.contactinfo.ContactInfoCache.OnContactInfoChangedListener;
 import com.android.dialer.util.DialerUtils;
+import com.android.dialer.voicemail.VoicemailPlaybackPresenter;
 
 import com.google.common.annotations.VisibleForTesting;
 
@@ -67,6 +69,7 @@
 
     protected final Context mContext;
     private final ContactInfoHelper mContactInfoHelper;
+    private final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
     private final CallFetcher mCallFetcher;
     private final OnReportButtonClickListener mOnReportButtonClickListener;
     private ViewTreeObserver mViewTreeObserver = null;
@@ -75,6 +78,9 @@
 
     private boolean mIsShowingRecentsTab;
 
+    private static final String KEY_EXPANDED_POSITION = "expanded_position";
+    private static final String KEY_EXPANDED_ROW_ID = "expanded_row_id";
+
     // Tracks the position of the currently expanded list item.
     private int mCurrentlyExpandedPosition = RecyclerView.NO_POSITION;
     // Tracks the rowId of the currently expanded list item, so the position can be updated if there
@@ -200,6 +206,7 @@
             Context context,
             CallFetcher callFetcher,
             ContactInfoHelper contactInfoHelper,
+            VoicemailPlaybackPresenter voicemailPlaybackPresenter,
             boolean isShowingRecentsTab,
             OnReportButtonClickListener onReportButtonClickListener) {
         super(context);
@@ -207,6 +214,7 @@
         mContext = context;
         mCallFetcher = callFetcher;
         mContactInfoHelper = contactInfoHelper;
+        mVoicemailPlaybackPresenter = voicemailPlaybackPresenter;
         mIsShowingRecentsTab = isShowingRecentsTab;
         mOnReportButtonClickListener = onReportButtonClickListener;
 
@@ -226,6 +234,20 @@
         mCallLogGroupBuilder = new CallLogGroupBuilder(this);
     }
 
+    public void onSaveInstanceState(Bundle outState) {
+        outState.putInt(KEY_EXPANDED_POSITION, mCurrentlyExpandedPosition);
+        outState.putLong(KEY_EXPANDED_ROW_ID, mCurrentlyExpandedRowId);
+    }
+
+    public void onRestoreInstanceState(Bundle savedInstanceState) {
+        if (savedInstanceState != null) {
+            mCurrentlyExpandedPosition =
+                    savedInstanceState.getInt(KEY_EXPANDED_POSITION, RecyclerView.NO_POSITION);
+            mCurrentlyExpandedRowId =
+                    savedInstanceState.getLong(KEY_EXPANDED_ROW_ID, NO_EXPANDED_LIST_ITEM);
+        }
+    }
+
     /**
      * Requery on background thread when {@link Cursor} changes.
      */
@@ -296,7 +318,8 @@
                 mContext,
                 mActionListener,
                 mPhoneNumberUtilsWrapper,
-                mCallLogViewsHelper);
+                mCallLogViewsHelper,
+                mVoicemailPlaybackPresenter);
 
         viewHolder.callLogEntryView.setTag(viewHolder);
         viewHolder.callLogEntryView.setAccessibilityDelegate(mAccessibilityDelegate);
diff --git a/src/com/android/dialer/calllog/CallLogFragment.java b/src/com/android/dialer/calllog/CallLogFragment.java
index 36d9bb6..845f911 100644
--- a/src/com/android/dialer/calllog/CallLogFragment.java
+++ b/src/com/android/dialer/calllog/CallLogFragment.java
@@ -51,6 +51,7 @@
 import com.android.dialer.list.ListsFragment.HostInterface;
 import com.android.dialer.util.DialerUtils;
 import com.android.dialer.util.EmptyLoader;
+import com.android.dialer.voicemail.VoicemailPlaybackPresenter;
 import com.android.dialer.voicemail.VoicemailStatusHelper;
 import com.android.dialer.voicemail.VoicemailStatusHelper.StatusMessage;
 import com.android.dialer.voicemail.VoicemailStatusHelperImpl;
@@ -87,6 +88,7 @@
     private LinearLayoutManager mLayoutManager;
     private CallLogAdapter mAdapter;
     private CallLogQueryHandler mCallLogQueryHandler;
+    private VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
     private boolean mScrollToTop;
 
     /** Whether there is at least one voicemail source installed. */
@@ -187,6 +189,8 @@
                 mContactsObserver);
         resolver.registerContentObserver(Status.CONTENT_URI, true, mVoicemailStatusObserver);
         setHasOptionsMenu(true);
+
+        mVoicemailPlaybackPresenter = new VoicemailPlaybackPresenter(activity, state);
     }
 
     /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */
@@ -272,6 +276,7 @@
                 getActivity(),
                 this,
                 new ContactInfoHelper(getActivity(), currentCountryIso),
+                mVoicemailPlaybackPresenter,
                 isShowingRecentsTab,
                 this);
         mRecyclerView.setAdapter(mAdapter);
@@ -283,7 +288,9 @@
     @Override
     public void onViewCreated(View view, Bundle savedInstanceState) {
         super.onViewCreated(view, savedInstanceState);
+
         updateEmptyMessage(mCallTypeFilter);
+        mAdapter.onRestoreInstanceState(savedInstanceState);
     }
 
     /**
@@ -318,8 +325,9 @@
 
     @Override
     public void onPause() {
-        super.onPause();
+        mVoicemailPlaybackPresenter.onPause(getActivity().isFinishing());
         mAdapter.pauseCache();
+        super.onPause();
     }
 
     @Override
@@ -331,12 +339,14 @@
 
     @Override
     public void onDestroy() {
-        super.onDestroy();
         mAdapter.pauseCache();
         mAdapter.changeCursor(null);
+        mVoicemailPlaybackPresenter.onDestroy(getActivity().isFinishing());
+
         getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver);
         getActivity().getContentResolver().unregisterContentObserver(mContactsObserver);
         getActivity().getContentResolver().unregisterContentObserver(mVoicemailStatusObserver);
+        super.onDestroy();
     }
 
     @Override
@@ -345,6 +355,9 @@
         outState.putInt(KEY_FILTER_TYPE, mCallTypeFilter);
         outState.putInt(KEY_LOG_LIMIT, mLogLimit);
         outState.putLong(KEY_DATE_LIMIT, mDateLimit);
+
+        mAdapter.onSaveInstanceState(outState);
+        mVoicemailPlaybackPresenter.onSaveInstanceState(outState);
     }
 
     @Override
diff --git a/src/com/android/dialer/calllog/CallLogListItemHelper.java b/src/com/android/dialer/calllog/CallLogListItemHelper.java
index 147a192..4eb8797 100644
--- a/src/com/android/dialer/calllog/CallLogListItemHelper.java
+++ b/src/com/android/dialer/calllog/CallLogListItemHelper.java
@@ -91,10 +91,6 @@
                         mResources.getString(R.string.description_video_call_action),
                         nameOrNumber));
 
-        views.voicemailButtonView.setContentDescription(
-                TextUtils.expandTemplate(
-                        mResources.getString(R.string.description_voicemail_action), nameOrNumber));
-
         views.createNewContactButtonView.setContentDescription(
                 TextUtils.expandTemplate(
                         mResources.getString(R.string.description_create_new_contact_action),
diff --git a/src/com/android/dialer/calllog/CallLogListItemViewHolder.java b/src/com/android/dialer/calllog/CallLogListItemViewHolder.java
index ccd480e..5125247 100644
--- a/src/com/android/dialer/calllog/CallLogListItemViewHolder.java
+++ b/src/com/android/dialer/calllog/CallLogListItemViewHolder.java
@@ -41,6 +41,9 @@
 import com.android.dialer.PhoneCallDetailsHelper;
 import com.android.dialer.PhoneCallDetailsViews;
 import com.android.dialer.R;
+import com.android.dialer.calllog.CallLogAsyncTaskUtil;
+import com.android.dialer.voicemail.VoicemailPlaybackPresenter;
+import com.android.dialer.voicemail.VoicemailPlaybackLayout;
 
 /**
  * This is an object containing references to views contained by the call log list item. This
@@ -68,8 +71,8 @@
     /** The view containing call log item actions.  Null until the ViewStub is inflated. */
     public View actionsView;
     /** The button views below are assigned only when the action section is expanded. */
+    public VoicemailPlaybackLayout voicemailPlaybackView;
     public View videoCallButtonView;
-    public View voicemailButtonView;
     public View createNewContactButtonView;
     public View addToExistingContactButtonView;
     public View sendMessageView;
@@ -142,6 +145,7 @@
     private final View.OnClickListener mActionListener;
     private final PhoneNumberUtilsWrapper mPhoneNumberUtilsWrapper;
     private final CallLogListItemHelper mCallLogListItemHelper;
+    private final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
 
     private final int mPhotoSize;
 
@@ -150,6 +154,7 @@
             View.OnClickListener actionListener,
             PhoneNumberUtilsWrapper phoneNumberUtilsWrapper,
             CallLogListItemHelper callLogListItemHelper,
+            VoicemailPlaybackPresenter voicemailPlaybackPresenter,
             View rootView,
             QuickContactBadge quickContactView,
             View primaryActionView,
@@ -163,6 +168,7 @@
         mActionListener = actionListener;
         mPhoneNumberUtilsWrapper = phoneNumberUtilsWrapper;
         mCallLogListItemHelper = callLogListItemHelper;
+        mVoicemailPlaybackPresenter = voicemailPlaybackPresenter;
 
         this.rootView = rootView;
         this.quickContactView = quickContactView;
@@ -191,13 +197,15 @@
             Context context,
             View.OnClickListener actionListener,
             PhoneNumberUtilsWrapper phoneNumberUtilsWrapper,
-            CallLogListItemHelper callLogListItemHelper) {
+            CallLogListItemHelper callLogListItemHelper,
+            VoicemailPlaybackPresenter voicemailPlaybackPresenter) {
 
         return new CallLogListItemViewHolder(
                 context,
                 actionListener,
                 phoneNumberUtilsWrapper,
                 callLogListItemHelper,
+                voicemailPlaybackPresenter,
                 view,
                 (QuickContactBadge) view.findViewById(R.id.quick_contact_photo),
                 view.findViewById(R.id.primary_action_view),
@@ -220,12 +228,12 @@
         if (stub != null) {
             actionsView = (ViewGroup) stub.inflate();
 
+            voicemailPlaybackView = (VoicemailPlaybackLayout) actionsView
+                    .findViewById(R.id.voicemail_playback_layout);
+
             videoCallButtonView = actionsView.findViewById(R.id.video_call_action);
             videoCallButtonView.setOnClickListener(mActionListener);
 
-            voicemailButtonView = actionsView.findViewById(R.id.voicemail_action);
-            voicemailButtonView.setOnClickListener(mActionListener);
-
             createNewContactButtonView = actionsView.findViewById(R.id.create_new_contact_action);
             createNewContactButtonView.setOnClickListener(mActionListener);
 
@@ -302,29 +310,30 @@
             videoCallButtonView.setVisibility(View.GONE);
         }
 
-        // For voicemail calls, show the "VOICEMAIL" action button; hide otherwise.
-        if (callType == Calls.VOICEMAIL_TYPE) {
-            voicemailButtonView.setTag(
-                    IntentProvider.getPlayVoicemailIntentProvider(rowId, voicemailUri));
-            voicemailButtonView.setVisibility(View.VISIBLE);
+        // For voicemail calls, show the voicemail playback layout; hide otherwise.
+        if (callType == Calls.VOICEMAIL_TYPE && mVoicemailPlaybackPresenter != null) {
+            voicemailPlaybackView.setVisibility(View.VISIBLE);
 
-            detailsButtonView.setVisibility(View.GONE);
+            Uri uri = Uri.parse(voicemailUri);
+            mVoicemailPlaybackPresenter.setPlaybackView(
+                    voicemailPlaybackView, uri, false /* startPlayingImmediately */);
+
+            CallLogAsyncTaskUtil.markVoicemailAsRead(mContext, uri);
         } else {
-            voicemailButtonView.setTag(null);
-            voicemailButtonView.setVisibility(View.GONE);
-
-            detailsButtonView.setVisibility(View.VISIBLE);
-            detailsButtonView.setTag(
-                    IntentProvider.getCallDetailIntentProvider(rowId, callIds, null));
+            voicemailPlaybackView.setVisibility(View.GONE);
         }
 
+        detailsButtonView.setVisibility(View.VISIBLE);
+        detailsButtonView.setTag(
+                IntentProvider.getCallDetailIntentProvider(rowId, callIds, null));
+
         if (canBeReportedAsInvalid && !info.isBadData) {
             reportButtonView.setVisibility(View.VISIBLE);
         } else {
             reportButtonView.setVisibility(View.GONE);
         }
 
-        if (UriUtils.isEncodedContactUri(info.lookupUri)) {
+        if (info != null && UriUtils.isEncodedContactUri(info.lookupUri)) {
             createNewContactButtonView.setTag(IntentProvider.getAddContactIntentProvider(
                     info.lookupUri, info.name, info.number, info.type, true /* isNewContact */));
             createNewContactButtonView.setVisibility(View.VISIBLE);
@@ -420,6 +429,7 @@
                 null /* actionListener */,
                 phoneNumberUtilsWrapper,
                 new CallLogListItemHelper(phoneCallDetailsHelper, resources),
+                null /* voicemailPlaybackPresenter */,
                 new View(context),
                 new QuickContactBadge(context),
                 new View(context),
@@ -427,10 +437,10 @@
                 new CardView(context),
                 new TextView(context),
                 new View(context));
-        viewHolder.voicemailButtonView = new TextView(context);
         viewHolder.detailsButtonView = new TextView(context);
         viewHolder.reportButtonView = new TextView(context);
         viewHolder.actionsView = new View(context);
+        viewHolder.voicemailPlaybackView = new VoicemailPlaybackLayout(context);
 
         return viewHolder;
     }
diff --git a/src/com/android/dialer/list/ListsFragment.java b/src/com/android/dialer/list/ListsFragment.java
index a0e443c..49226d6 100644
--- a/src/com/android/dialer/list/ListsFragment.java
+++ b/src/com/android/dialer/list/ListsFragment.java
@@ -62,6 +62,9 @@
     // Oldest recents entry to display is 2 weeks old.
     private static final long OLDEST_RECENTS_DATE = 1000L * 60 * 60 * 24 * 14;
 
+    private static final String KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER =
+            "has_active_voicemail_provider";
+
     public interface HostInterface {
         public ActionBarController getActionBarController();
     }
@@ -161,6 +164,11 @@
         Trace.endSection();
 
         mVoicemailStatusHelper = new VoicemailStatusHelperImpl();
+
+        if (savedInstanceState != null) {
+            mHasActiveVoicemailProvider = savedInstanceState.getBoolean(
+                    KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER, false);
+        }
         Trace.endSection();
     }
 
@@ -227,6 +235,12 @@
     }
 
     @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putBoolean(KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER, mHasActiveVoicemailProvider);
+    }
+
+    @Override
     public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
         mTabIndex = getRtlPosition(position);
 
diff --git a/src/com/android/dialer/util/DialerUtils.java b/src/com/android/dialer/util/DialerUtils.java
index a04719a..a44c2ee 100644
--- a/src/com/android/dialer/util/DialerUtils.java
+++ b/src/com/android/dialer/util/DialerUtils.java
@@ -28,6 +28,8 @@
 import android.os.Bundle;
 import android.provider.Telephony;
 import android.telecom.TelecomManager;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
 import android.text.TextUtils;
 import android.view.View;
 import android.view.inputmethod.InputMethodManager;
@@ -41,6 +43,8 @@
 import com.android.incallui.CallCardFragment;
 import com.android.incallui.Log;
 
+import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 
@@ -158,8 +162,25 @@
      * @return Joined char sequences.
      */
     public static CharSequence join(Resources resources, Iterable<CharSequence> list) {
+        StringBuilder sb = new StringBuilder();
+        final BidiFormatter formatter = BidiFormatter.getInstance();
         final CharSequence separator = resources.getString(R.string.list_delimeter);
-        return TextUtils.join(separator, list);
+
+        Iterator<CharSequence> itr = list.iterator();
+        boolean firstTime = true;
+        while (itr.hasNext()) {
+            if (firstTime) {
+                firstTime = false;
+            } else {
+                sb.append(separator);
+            }
+            // Unicode wrap the elements of the list to respect RTL for individual strings.
+            sb.append(formatter.unicodeWrap(
+                    itr.next().toString(), TextDirectionHeuristics.FIRSTSTRONG_LTR));
+        }
+
+        // Unicode wrap the joined value, to respect locale's RTL ordering for the whole list.
+        return formatter.unicodeWrap(sb.toString());
     }
 
     /**
diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java b/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java
index 0e9ff3b..703004d 100644
--- a/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java
+++ b/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java
@@ -76,10 +76,9 @@
 
         public PositionUpdater(
                 MediaPlayer mediaPlayer,
-                int duration,
                 ScheduledExecutorService executorService) {
             mMediaPlayer = mediaPlayer;
-            mDuration = duration;
+            mDuration = mediaPlayer.getDuration();
             mExecutorService = executorService;
         }
 
@@ -167,6 +166,7 @@
             if (mPresenter == null) {
                 return;
             }
+
             if (mIsPlaying) {
                 mPresenter.pausePlayback();
             } else {
@@ -197,7 +197,7 @@
         mContext = context;
         LayoutInflater inflater =
                 (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-        inflater.inflate(R.layout.playback_layout, this);
+        inflater.inflate(R.layout.voicemail_playback_layout, this);
     }
 
     @Override
@@ -222,7 +222,6 @@
     @Override
     public void onPlaybackStarted(
             MediaPlayer mediaPlayer,
-            int duration,
             ScheduledExecutorService executorService) {
         mIsPlaying = true;
 
@@ -232,7 +231,7 @@
             onSpeakerphoneOn(mPresenter.isSpeakerphoneOn());
         }
 
-        mPositionUpdater = new PositionUpdater(mediaPlayer, duration, executorService);
+        mPositionUpdater = new PositionUpdater(mediaPlayer, executorService);
         mPositionUpdater.startUpdating();
     }
 
@@ -286,6 +285,7 @@
         if (mPlaybackSeek.getMax() != seekBarMax) {
             mPlaybackSeek.setMax(seekBarMax);
         }
+
         mPlaybackSeek.setProgress(seekBarPosition);
         mPlaybackPosition.setText(formatAsMinutesAndSeconds(seekBarMax - seekBarPosition));
     }
diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
index e8b0460..99d2734 100644
--- a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
+++ b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
@@ -78,9 +78,9 @@
         void disableUiElements();
         void enableUiElements();
         void onPlaybackError(Exception e);
-        void onPlaybackStarted(MediaPlayer mediaPlayer, int duration,
-                ScheduledExecutorService executorService);
+        void onPlaybackStarted(MediaPlayer mediaPlayer, ScheduledExecutorService executorService);
         void onPlaybackStopped();
+        void onSpeakerphoneOn(boolean on);
         void setClipPosition(int clipPositionInMillis, int clipLengthInMillis);
         void setFetchContentTimeout();
         void setIsBuffering();
@@ -103,6 +103,10 @@
     // Time to wait for content to be fetched before timing out.
     private static final long FETCH_CONTENT_TIMEOUT_MS = 20000;
 
+    private static final String VOICEMAIL_URI_KEY =
+            VoicemailPlaybackPresenter.class.getName() + ".VOICEMAIL_URI";
+    private static final String IS_PREPARED_KEY =
+            VoicemailPlaybackPresenter.class.getName() + ".IS_PREPARED";
     // 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";
@@ -111,22 +115,21 @@
             VoicemailPlaybackPresenter.class.getName() + ".CLIP_POSITION_KEY";
 
     /**
-     * The most recently calculated duration.
-     * <p>
-     * We cache this in a field since we don't want to keep requesting it from the player, as
-     * this can easily lead to throwing {@link IllegalStateException} (any time the player is
-     * released, it's illegal to ask for the duration).
+     * The most recently cached duration. We cache this since we don't want to keep requesting it
+     * from the player, as this can easily lead to throwing {@link IllegalStateException} (any time
+     * the player is released, it's illegal to ask for the duration).
      */
     private final AtomicInteger mDuration = new AtomicInteger(0);
 
     private Context mContext;
-    private MediaPlayer mMediaPlayer;
     private PlaybackView mView;
+    private static MediaPlayer mMediaPlayer;
 
     private Uri mVoicemailUri;
     private int mPosition;
     private boolean mIsPrepared;
     private boolean mIsPlaying;
+
     private boolean mShouldResumePlaybackAfterSeeking;
 
     // Used to run async tasks that need to interact with the UI.
@@ -141,11 +144,19 @@
     private PowerManager.WakeLock mProximityWakeLock;
     private AudioManager mAudioManager;
 
-    public VoicemailPlaybackPresenter(Activity activity) {
+    public VoicemailPlaybackPresenter(Activity activity, Bundle savedInstanceState) {
         mContext = activity;
         mAsyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor();
         mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
 
+        if (savedInstanceState != null) {
+            // Restores playback state when activity is recreated, such as after rotation.
+            mVoicemailUri = (Uri) savedInstanceState.getParcelable(VOICEMAIL_URI_KEY);
+            mIsPrepared = savedInstanceState.getBoolean(IS_PREPARED_KEY);
+            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)) {
@@ -153,12 +164,15 @@
                     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();
-            mMediaPlayer.setOnPreparedListener(this);
-            mMediaPlayer.setOnErrorListener(this);
-            mMediaPlayer.setOnCompletionListener(this);
+            mIsPrepared = false;
         }
+        mMediaPlayer.setOnPreparedListener(this);
+        mMediaPlayer.setOnErrorListener(this);
+        mMediaPlayer.setOnCompletionListener(this);
 
         activity.setVolumeControlStream(PLAYBACK_STREAM);
     }
@@ -169,21 +183,29 @@
     public void setPlaybackView(
             PlaybackView view, Uri voicemailUri, boolean startPlayingImmediately) {
         mView = view;
-        mVoicemailUri = voicemailUri;
-
         mView.setPresenter(this);
 
-        if (!mMediaPlayer.isPlaying()) {
-            setPosition(0, startPlayingImmediately);
-            mIsPrepared = false;
+        mView.onSpeakerphoneOn(isSpeakerphoneOn());
+
+        if (mVoicemailUri != null && mVoicemailUri.equals(voicemailUri)) {
+            // Handles rotation case where playback view is set for the same voicemail.
+            if (mIsPrepared) {
+                onPrepared(mMediaPlayer);
+            } else {
+                checkForContent();
+            }
+        } else {
+            mVoicemailUri = voicemailUri;
+            mPosition = 0;
+            mIsPlaying = startPlayingImmediately;
+            checkForContent();
         }
-        checkForContent();
     }
 
     public void onPause(boolean isFinishing) {
         // Do not pause for orientation changes.
         if (mMediaPlayer.isPlaying() && isFinishing) {
-            pausePlayback(mMediaPlayer.getCurrentPosition(), mIsPlaying);
+            pausePlayback();
         }
 
         disableProximitySensor(false /* waitForFarState */);
@@ -210,16 +232,11 @@
     }
 
     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);
+        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);
         }
     }
 
@@ -355,6 +372,7 @@
      * and it will call {@link #onError()} otherwise.
      */
     private void prepareToPlayContent() {
+        mIsPrepared = false;
         mView.setIsBuffering();
 
         try {
@@ -373,8 +391,10 @@
     @Override
     public void onPrepared(MediaPlayer mp) {
         mIsPrepared = true;
+        mDuration.set(mMediaPlayer.getDuration());
 
         mView.enableUiElements();
+        mView.setClipPosition(mPosition, mDuration.get());
 
         if (mIsPlaying) {
             resumePlayback();
@@ -400,7 +420,9 @@
         }
 
         mView.onPlaybackError(e);
-        setPosition(0, false);
+
+        mPosition = 0;
+        mIsPlaying = false;
     }
 
     /**
@@ -408,7 +430,11 @@
      */
     @Override
     public void onCompletion(MediaPlayer mediaPlayer) {
-        pausePlayback(0, false);
+        pausePlayback();
+
+        // Reset the seekbar position to the beginning.
+        mPosition = 0;
+        mView.setClipPosition(0, mDuration.get());
     }
 
     @Override
@@ -423,56 +449,45 @@
     }
 
     /**
-     * Sets the position and playing state for when playback is resumed.
-     */
-    private void setPosition(int position, boolean isPlaying) {
-        mPosition = position;
-        mIsPlaying = isPlaying;
-    }
-
-    /**
-     * Resumes voicemail playback at the clip position stored by the presenter.
+     * Resumes voicemail playback at the clip position stored by the presenter. Null-op if already
+     * playing.
      */
     public void resumePlayback() {
-        final int duration = mMediaPlayer.getDuration();
-        mDuration.set(duration);
+        mIsPlaying = true;
 
-        // Clamp the start position between 0 and the duration.
-        int startPosition = Math.max(0, Math.min(mPosition, duration));
-        mMediaPlayer.seekTo(startPosition);
-        setPosition(startPosition, true);
+        if (!mMediaPlayer.isPlaying()) {
+            // Clamp the start position between 0 and the duration.
+            mPosition = Math.max(0, Math.min(mPosition, mDuration.get()));
+            mMediaPlayer.seekTo(mPosition);
 
-        try {
-            // Grab audio focus here
-            int result = mAudioManager.requestAudioFocus(
-                    VoicemailPlaybackPresenter.this,
-                    PLAYBACK_STREAM,
-                    AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+            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 (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+                    throw new RejectedExecutionException("Could not capture audio focus.");
+                }
+
+                // Can throw RejectedExecutionException
+                mMediaPlayer.start();
+            } catch (RejectedExecutionException e) {
+                handleError(e);
             }
-
-            // Can throw RejectedExecutionException
-            mMediaPlayer.start();
-
-            mView.onPlaybackStarted(mMediaPlayer, duration, getScheduledExecutorServiceInstance());
-            enableProximitySensor();
-        } catch (RejectedExecutionException e) {
-            handleError(e);
         }
-    }
 
-    public void pausePlayback() {
-        pausePlayback(mMediaPlayer.getCurrentPosition(), false);
+        enableProximitySensor();
+        mView.onPlaybackStarted(mMediaPlayer, getScheduledExecutorServiceInstance());
     }
 
     /**
-     * {@link isPlaying} may be set to {@code true} so voicemail playback can be resumed after a
-     * rotation.
+     * Pauses voicemail playback at the current position. Null-op if already paused.
      */
-    private void pausePlayback(int position, boolean isPlaying) {
-        setPosition(position, isPlaying);
+    public void pausePlayback() {
+        mPosition = mMediaPlayer.getCurrentPosition();
+        mIsPlaying = false;
 
         if (mMediaPlayer.isPlaying()) {
             mMediaPlayer.pause();
@@ -483,9 +498,6 @@
 
         // Always disable the proximity sensor on stop.
         disableProximitySensor(true /* waitForFarState */);
-
-        int duration = mDuration.get();
-        mView.setClipPosition(position, duration);
     }
 
     /**
@@ -498,11 +510,11 @@
     }
 
     public void resumePlaybackAfterSeeking(int desiredPosition) {
-        setPosition(desiredPosition, mShouldResumePlaybackAfterSeeking);
+        mPosition = desiredPosition;
         if (mShouldResumePlaybackAfterSeeking) {
+            mShouldResumePlaybackAfterSeeking = false;
             resumePlayback();
         }
-        mShouldResumePlaybackAfterSeeking = false;
     }
 
     private void enableProximitySensor() {
diff --git a/src/com/android/dialerbind/ObjectFactory.java b/src/com/android/dialerbind/ObjectFactory.java
index 12607d9..43b237c 100644
--- a/src/com/android/dialerbind/ObjectFactory.java
+++ b/src/com/android/dialerbind/ObjectFactory.java
@@ -25,6 +25,7 @@
 import com.android.dialer.calllog.CallLogAdapter.OnReportButtonClickListener;
 import com.android.dialer.calllog.ContactInfoHelper;
 import com.android.dialer.service.CachedNumberLookupService;
+import com.android.dialer.voicemail.VoicemailPlaybackPresenter;
 
 /**
  * Default static binding for various objects.
@@ -47,12 +48,14 @@
             Context context,
             CallFetcher callFetcher,
             ContactInfoHelper contactInfoHelper,
+            VoicemailPlaybackPresenter voicemailPlaybackPresenter,
             boolean isShowingRecentsTab,
             OnReportButtonClickListener onReportButtonClickListener) {
         return new CallLogAdapter(
                 context,
                 callFetcher,
                 contactInfoHelper,
+                voicemailPlaybackPresenter,
                 isShowingRecentsTab,
                 onReportButtonClickListener);
     }
diff --git a/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java b/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
index 5f09cb7..14e0aaf 100644
--- a/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
+++ b/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
@@ -204,7 +204,7 @@
     private static final class TestCallLogAdapter extends CallLogAdapter {
         public TestCallLogAdapter(Context context, CallFetcher callFetcher,
                 ContactInfoHelper contactInfoHelper) {
-            super(context, callFetcher, contactInfoHelper, false, null);
+            super(context, callFetcher, contactInfoHelper, null, false, null);
             mContactInfoCache = new TestContactInfoCache(
                     contactInfoHelper, mOnContactInfoChangedListener);
         }
diff --git a/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java b/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java
index 0c19799..71554d6 100644
--- a/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java
+++ b/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java
@@ -335,28 +335,15 @@
     }
 
     @MediumTest
-    public void testBindView_PlayButton() {
+    public void testBindView_VoicemailUri() {
         mCursor.moveToFirst();
         insertVoicemail(TEST_NUMBER, Calls.PRESENTATION_ALLOWED, NOW, 0);
         CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder)
                 mAdapter.onCreateViewHolder(mParentView, /* viewType */ 0);
         bindViewForTest(viewHolder);
 
-        IntentProvider intentProvider = (IntentProvider) viewHolder.voicemailButtonView.getTag();
-        Intent intent = intentProvider.getIntent(mActivity);
-        // Starts the call detail activity.
-        assertEquals(new ComponentName(mActivity, CallDetailActivity.class),
-                intent.getComponent());
-        // With the given entry.
-        assertEquals(ContentUris.withAppendedId(Calls.CONTENT_URI_WITH_VOICEMAIL, 1),
-                intent.getData());
-        // With the URI of the voicemail.
-        assertEquals(
-                ContentUris.withAppendedId(VoicemailContract.Voicemails.CONTENT_URI, 1),
-                intent.getParcelableExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI));
-        // And starts playback.
-        assertTrue(
-                intent.getBooleanExtra(CallDetailActivity.EXTRA_VOICEMAIL_START_PLAYBACK, false));
+        assertEquals(Uri.parse(viewHolder.voicemailUri),
+                ContentUris.withAppendedId(VoicemailContract.Voicemails.CONTENT_URI, 1));
     }
 
     /** Returns the label associated with a given phone type. */
@@ -453,11 +440,17 @@
      * unit tests can access the buttons contained within.
      *
      * @param view The current call log row.
-     * @param position The position of hte item.
+     * @param position The position of the item.
      */
-    private void bindViewForTest(CallLogListItemViewHolder viewHolder, int position) {
+    private void bindViewForTest(final CallLogListItemViewHolder viewHolder, int position) {
         mAdapter.onBindViewHolder(viewHolder, position);
-        viewHolder.inflateActionViewStub(null);
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                viewHolder.inflateActionViewStub(null);
+            }
+        });
+        getInstrumentation().waitForIdleSync();
         viewHolder.updateCallButton();
     }
 
diff --git a/tests/src/com/android/dialer/calllog/CallLogListItemHelperTest.java b/tests/src/com/android/dialer/calllog/CallLogListItemHelperTest.java
index 085ec9b..a7d9b99 100644
--- a/tests/src/com/android/dialer/calllog/CallLogListItemHelperTest.java
+++ b/tests/src/com/android/dialer/calllog/CallLogListItemHelperTest.java
@@ -99,23 +99,23 @@
     public void testSetPhoneCallDetails_VoicemailNumber() {
         setPhoneCallDetailsWithNumber(TEST_VOICEMAIL_NUMBER,
                 Calls.PRESENTATION_ALLOWED, TEST_VOICEMAIL_NUMBER);
-        assertEquals(View.VISIBLE, mViewHolder.voicemailButtonView.getVisibility());
+        assertEquals(View.VISIBLE, mViewHolder.voicemailPlaybackView.getVisibility());
     }
 
     public void testSetPhoneCallDetails_ReadVoicemail() {
         setPhoneCallDetailsWithTypes(Calls.VOICEMAIL_TYPE);
-        assertEquals(View.VISIBLE, mViewHolder.voicemailButtonView.getVisibility());
+        assertEquals(View.VISIBLE, mViewHolder.voicemailPlaybackView.getVisibility());
     }
 
     public void testSetPhoneCallDetails_UnreadVoicemail() {
         setUnreadPhoneCallDetailsWithTypes(Calls.VOICEMAIL_TYPE);
-        assertEquals(View.VISIBLE, mViewHolder.voicemailButtonView.getVisibility());
+        assertEquals(View.VISIBLE, mViewHolder.voicemailPlaybackView.getVisibility());
     }
 
     public void testSetPhoneCallDetails_VoicemailFromUnknown() {
         setPhoneCallDetailsWithNumberAndType("", Calls.PRESENTATION_UNKNOWN,
                 "", Calls.VOICEMAIL_TYPE);
-        assertEquals(View.VISIBLE, mViewHolder.voicemailButtonView.getVisibility());
+        assertEquals(View.VISIBLE, mViewHolder.voicemailPlaybackView.getVisibility());
     }
 
     /**
@@ -470,7 +470,7 @@
 
     /** Sets the details of a phone call using the specified call type. */
     private void setPhoneCallDetailsWithTypes(int... types) {
-        mHelper.setPhoneCallDetails(getContext() ,mViewHolder,
+        mHelper.setPhoneCallDetails(getContext(), mViewHolder,
                 new PhoneCallDetails(
                         mContext,
                         TEST_NUMBER,