Merge "Call-Blocking causing crash with incoming SIP call" into ub-contactsdialer-a-dev
diff --git a/proguard.flags b/proguard.flags
index 38d4050..185d5eb 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -12,4 +12,9 @@
 @com.android.dialer.NeededForReflection *;
 }
 
+# For design libraries
+-keep public class * extends android.support.design.widget.CoordinatorLayout$Behavior {
+    public <init>(android.content.Context, android.util.AttributeSet);
+}
+
 -verbose
diff --git a/res/layout/dialtacts_activity.xml b/res/layout/dialtacts_activity.xml
index 0f1f2bb..782d4f3 100644
--- a/res/layout/dialtacts_activity.xml
+++ b/res/layout/dialtacts_activity.xml
@@ -13,8 +13,9 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<FrameLayout
+<android.support.design.widget.CoordinatorLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:id="@+id/dialtacts_mainlayout"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
@@ -43,7 +44,8 @@
         android:layout_width="@dimen/floating_action_button_width"
         android:layout_height="@dimen/floating_action_button_height"
         android:layout_marginBottom="@dimen/floating_action_button_margin_bottom"
-        android:layout_gravity="center_horizontal|bottom">
+        android:layout_gravity="center_horizontal|bottom"
+        app:layout_behavior="com.android.dialer.FloatingActionButtonBehavior">
 
         <ImageButton
             android:id="@+id/floating_action_button"
@@ -68,4 +70,4 @@
             android:importantForAccessibility="no" />
     </FrameLayout>
 
-</FrameLayout>
+</android.support.design.widget.CoordinatorLayout>
diff --git a/res/layout/voicemail_playback_layout.xml b/res/layout/voicemail_playback_layout.xml
index 555d201..54493f1 100644
--- a/res/layout/voicemail_playback_layout.xml
+++ b/res/layout/voicemail_playback_layout.xml
@@ -71,7 +71,7 @@
 
                 <ImageButton android:id="@+id/playback_speakerphone"
                     style="@style/VoicemailPlaybackLayoutButtonStyle"
-                    android:src="@drawable/ic_speakerphone_on"
+                    android:src="@drawable/ic_volume_down_24dp"
                     android:tint="@color/voicemail_icon_tint"
                     android:contentDescription="@string/description_playback_speakerphone" />
 
diff --git a/src/com/android/dialer/CallDetailActivity.java b/src/com/android/dialer/CallDetailActivity.java
index 32eccc4..72a5012 100644
--- a/src/com/android/dialer/CallDetailActivity.java
+++ b/src/com/android/dialer/CallDetailActivity.java
@@ -16,32 +16,22 @@
 
 package com.android.dialer;
 
-import android.app.Activity;
-import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.Resources;
 import android.net.Uri;
 import android.os.Bundle;
-import android.os.PowerManager;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.VoicemailContract.Voicemails;
-import android.support.v7.app.ActionBar;
 import android.support.v7.app.AppCompatActivity;
-import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
-import android.telephony.TelephonyManager;
 import android.text.BidiFormatter;
 import android.text.TextDirectionHeuristics;
 import android.text.TextUtils;
-import android.util.Log;
-import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
-import android.widget.LinearLayout;
 import android.widget.ListView;
 import android.widget.QuickContactBadge;
 import android.widget.TextView;
@@ -49,7 +39,6 @@
 
 import com.android.contacts.common.ContactPhotoManager;
 import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
-import com.android.contacts.common.util.PermissionsUtil;
 import com.android.contacts.common.GeoUtil;
 import com.android.contacts.common.CallUtil;
 import com.android.contacts.common.util.UriUtils;
@@ -57,16 +46,12 @@
 import com.android.dialer.calllog.CallLogAsyncTaskUtil.CallLogAsyncTaskListener;
 import com.android.dialer.calllog.CallLogAsyncTaskUtil;
 import com.android.dialer.calllog.CallTypeHelper;
-import com.android.dialer.calllog.ContactInfo;
 import com.android.dialer.calllog.ContactInfoHelper;
 import com.android.dialer.calllog.PhoneAccountUtils;
-import com.android.dialer.calllog.PhoneNumberDisplayUtil;
-import com.android.dialer.util.DialerUtils;
-import com.android.dialer.util.IntentUtil;
+import com.android.dialer.util.IntentUtil.CallIntentBuilder;
 import com.android.dialer.util.PhoneNumberUtil;
 import com.android.dialer.util.TelecomUtil;
-
-import java.util.List;
+import com.android.incallui.Call.LogState;
 
 /**
  * Displays the details of a specific call log entry.
@@ -252,11 +237,14 @@
         mDefaultCountryIso = GeoUtil.getCurrentCountryIso(this);
         mContactPhotoManager = ContactPhotoManager.getInstance(this);
 
-        mCallButton = (View) findViewById(R.id.call_back_button);
+        mCallButton = findViewById(R.id.call_back_button);
         mCallButton.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View view) {
-                mContext.startActivity(IntentUtil.getCallIntent(mNumber));
+                mContext.startActivity(
+                        new CallIntentBuilder(mNumber)
+                                .setCallInitiationType(LogState.INITIATION_CALL_DETAILS)
+                                .build());
             }
         });
 
diff --git a/src/com/android/dialer/DialtactsActivity.java b/src/com/android/dialer/DialtactsActivity.java
index 4ade04a..dbff276 100644
--- a/src/com/android/dialer/DialtactsActivity.java
+++ b/src/com/android/dialer/DialtactsActivity.java
@@ -30,6 +30,7 @@
 import android.os.Trace;
 import android.provider.CallLog.Calls;
 import android.speech.RecognizerIntent;
+import android.support.design.widget.CoordinatorLayout;
 import android.support.v4.view.ViewPager;
 import android.support.v7.app.ActionBar;
 import android.telecom.PhoneAccount;
@@ -52,7 +53,6 @@
 import android.view.animation.AnimationUtils;
 import android.widget.AbsListView.OnScrollListener;
 import android.widget.EditText;
-import android.widget.FrameLayout;
 import android.widget.ImageButton;
 import android.widget.PopupMenu;
 import android.widget.TextView;
@@ -83,11 +83,13 @@
 import com.android.dialer.list.SpeedDialFragment;
 import com.android.dialer.settings.DialerSettingsActivity;
 import com.android.dialer.util.IntentUtil;
+import com.android.dialer.util.IntentUtil.CallIntentBuilder;
 import com.android.dialer.util.DialerUtils;
 import com.android.dialer.widget.ActionBarController;
 import com.android.dialer.widget.SearchEditTextLayout;
 import com.android.dialer.widget.SearchEditTextLayout.Callback;
 import com.android.dialerbind.DatabaseHelperManager;
+import com.android.incallui.Call.LogState;
 import com.android.phone.common.animation.AnimUtils;
 import com.android.phone.common.animation.AnimationListenerAdapter;
 
@@ -118,10 +120,6 @@
 
     public static final String SHARED_PREFS_NAME = "com.android.dialer_preferences";
 
-    /** @see #getCallOrigin() */
-    private static final String CALL_ORIGIN_DIALTACTS =
-            "com.android.dialer.DialtactsActivity";
-
     private static final String KEY_IN_REGULAR_SEARCH_UI = "in_regular_search_ui";
     private static final String KEY_IN_DIALPAD_SEARCH_UI = "in_dialpad_search_ui";
     private static final String KEY_SEARCH_QUERY = "search_query";
@@ -143,7 +141,7 @@
 
     private static final int FAB_SCALE_IN_DELAY_MS = 300;
 
-    private FrameLayout mParentLayout;
+    private CoordinatorLayout mParentLayout;
 
     /**
      * Fragment containing the dialpad that slides into view
@@ -468,7 +466,7 @@
         mSlideIn.setAnimationListener(mSlideInListener);
         mSlideOut.setAnimationListener(mSlideOutListener);
 
-        mParentLayout = (FrameLayout) findViewById(R.id.dialtacts_mainlayout);
+        mParentLayout = (CoordinatorLayout) findViewById(R.id.dialtacts_mainlayout);
         mParentLayout.setOnDragListener(new LayoutOnDragListener());
         floatingActionButtonContainer.getViewTreeObserver().addOnGlobalLayoutListener(
                 new ViewTreeObserver.OnGlobalLayoutListener() {
@@ -536,13 +534,21 @@
         mDialerDatabaseHelper.startSmartDialUpdateThread();
         mFloatingActionButtonController.align(getFabAlignment(), false /* animate */);
 
-        if (getIntent().hasExtra(EXTRA_SHOW_TAB)) {
+        if (Calls.CONTENT_TYPE.equals(getIntent().getType())) {
+            // Externally specified extras take precedence to EXTRA_SHOW_TAB, which is only
+            // used internally.
+            final Bundle extras = getIntent().getExtras();
+            if (extras != null
+                    && extras.getInt(Calls.EXTRA_CALL_TYPE_FILTER) == Calls.VOICEMAIL_TYPE) {
+                mListsFragment.showTab(ListsFragment.TAB_INDEX_VOICEMAIL);
+            } else {
+                mListsFragment.showTab(ListsFragment.TAB_INDEX_HISTORY);
+            }
+        } else if (getIntent().hasExtra(EXTRA_SHOW_TAB)) {
             int index = getIntent().getIntExtra(EXTRA_SHOW_TAB, ListsFragment.TAB_INDEX_SPEED_DIAL);
             if (index < mListsFragment.getTabCount()) {
                 mListsFragment.showTab(index);
             }
-        } else if (Calls.CONTENT_TYPE.equals(getIntent().getType())) {
-            mListsFragment.showTab(ListsFragment.TAB_INDEX_HISTORY);
         }
 
         setSearchBoxHint();
@@ -951,16 +957,6 @@
     }
 
     /**
-     * Returns an appropriate call origin for this Activity. May return null when no call origin
-     * should be used (e.g. when some 3rd party application launched the screen. Call origin is
-     * for remembering the tab in which the user made a phone call, so the external app's DIAL
-     * request should not be counted.)
-     */
-    public String getCallOrigin() {
-        return !isDialIntent(getIntent()) ? CALL_ORIGIN_DIALTACTS : null;
-    }
-
-    /**
      * Shows the search fragment
      */
     private void enterSearchUi(boolean smartDialSearch, String query, boolean animate) {
@@ -1248,29 +1244,26 @@
     }
 
     @Override
-    public void onPickPhoneNumberAction(Uri dataUri) {
-        // Specify call-origin so that users will see the previous tab instead of
-        // CallLog screen (search UI will be automatically exited).
-        PhoneNumberInteraction.startInteractionForPhoneCall(
-                DialtactsActivity.this, dataUri, getCallOrigin());
+    public void onPickPhoneNumberAction(Uri dataUri, int callInitiationType) {
         mClearSearchOnPause = true;
+        PhoneNumberInteraction.startInteractionForPhoneCall(
+                DialtactsActivity.this, dataUri, callInitiationType);
     }
 
     @Override
-    public void onCallNumberDirectly(String phoneNumber) {
-        onCallNumberDirectly(phoneNumber, false /* isVideoCall */);
-    }
-
-    @Override
-    public void onCallNumberDirectly(String phoneNumber, boolean isVideoCall) {
+    public void onCallNumberDirectly(String phoneNumber, boolean isVideoCall,
+            int callInitiationType) {
         if (phoneNumber == null) {
             // Invalid phone number, but let the call go through so that InCallUI can show
             // an error message.
             phoneNumber = "";
         }
-        Intent intent = isVideoCall ?
-                IntentUtil.getVideoCallIntent(phoneNumber, getCallOrigin()) :
-                IntentUtil.getCallIntent(phoneNumber, getCallOrigin());
+
+        final Intent intent = new CallIntentBuilder(phoneNumber)
+                .setIsVideoCall(isVideoCall)
+                .setCallInitiationType(callInitiationType)
+                .build();
+
         DialerUtils.startActivityWithErrorToast(this, intent);
         mClearSearchOnPause = true;
     }
diff --git a/src/com/android/dialer/FloatingActionButtonBehavior.java b/src/com/android/dialer/FloatingActionButtonBehavior.java
new file mode 100644
index 0000000..8a407bd
--- /dev/null
+++ b/src/com/android/dialer/FloatingActionButtonBehavior.java
@@ -0,0 +1,50 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.support.design.widget.CoordinatorLayout;
+import android.support.design.widget.Snackbar.SnackbarLayout;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+
+/**
+ * Implements custom behavior for the movement of the FAB in response to the Snackbar.
+ * Because we are not using the design framework FloatingActionButton widget, we need to manually
+ * implement the Material Design behavior of having the FAB translate upward and downward with
+ * the appearance and disappearance of a Snackbar.
+ */
+public class FloatingActionButtonBehavior extends CoordinatorLayout.Behavior<FrameLayout> {
+    public FloatingActionButtonBehavior(Context context, AttributeSet attrs) {
+    }
+
+    @Override
+    public boolean layoutDependsOn(CoordinatorLayout parent, FrameLayout child, View dependency) {
+        // This needs to return true to trigger the callback correctly.
+        return true;
+    }
+
+    @Override
+    public boolean onDependentViewChanged(CoordinatorLayout parent, FrameLayout child,
+            View dependency) {
+        if (dependency instanceof SnackbarLayout) {
+            float translationY = Math.min(0, dependency.getTranslationY() - dependency.getHeight());
+            child.setTranslationY(translationY);
+        }
+        return true;
+    }
+}
diff --git a/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java b/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java
index 5a62a7d..aa994d2 100644
--- a/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java
+++ b/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java
@@ -156,9 +156,13 @@
             boolean isVoicemail = PhoneNumberUtil.isVoicemailNumber(context, accountHandle, number);
             boolean shouldLookupNumber =
                     PhoneNumberUtil.canPlaceCallsTo(number, numberPresentation) && !isVoicemail;
-            ContactInfo info = shouldLookupNumber
-                            ? contactInfoHelper.lookupNumber(number, countryIso)
-                            : ContactInfo.EMPTY;
+
+            ContactInfo info = ContactInfo.EMPTY;
+            if (shouldLookupNumber) {
+                ContactInfo lookupInfo = contactInfoHelper.lookupNumber(number, countryIso);
+                info = lookupInfo != null ? lookupInfo : ContactInfo.EMPTY;
+            }
+
             PhoneCallDetails details = new PhoneCallDetails(
                     context, number, numberPresentation, info.formattedNumber, isVoicemail);
 
diff --git a/src/com/android/dialer/calllog/CallLogFragment.java b/src/com/android/dialer/calllog/CallLogFragment.java
index 8762f18..26e3965 100644
--- a/src/com/android/dialer/calllog/CallLogFragment.java
+++ b/src/com/android/dialer/calllog/CallLogFragment.java
@@ -34,6 +34,7 @@
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.Message;
 import android.provider.CallLog;
 import android.provider.CallLog.Calls;
 import android.provider.ContactsContract;
@@ -89,6 +90,10 @@
 
     private static final int READ_CALL_LOG_PERMISSION_REQUEST_CODE = 1;
 
+    private static final int EVENT_UPDATE_DISPLAY = 1;
+
+    private static final long MILLIS_IN_MINUTE = 60 * 1000;
+
     private RecyclerView mRecyclerView;
     private LinearLayoutManager mLayoutManager;
     private CallLogAdapter mAdapter;
@@ -106,6 +111,18 @@
     private boolean mCallLogFetched;
     private boolean mVoicemailStatusFetched;
 
+    private final Handler mDisplayUpdateHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case EVENT_UPDATE_DISPLAY:
+                    refreshData();
+                    rescheduleDisplayUpdate();
+                    break;
+            }
+        }
+    };
+
     private final Handler mHandler = new Handler();
 
     private class CustomContentObserver extends ContentObserver {
@@ -343,10 +360,14 @@
         mHasReadCallLogPermission = hasReadCallLogPermission;
         refreshData();
         mAdapter.startCache();
+
+        rescheduleDisplayUpdate();
     }
 
     @Override
     public void onPause() {
+        cancelDisplayUpdate();
+
         if (mVoicemailPlaybackPresenter != null) {
             mVoicemailPlaybackPresenter.onPause();
         }
@@ -517,4 +538,25 @@
             }
         }
     }
+
+    /**
+     * Schedules an update to the relative call times (X mins ago).
+     */
+    private void rescheduleDisplayUpdate() {
+        if (!mDisplayUpdateHandler.hasMessages(EVENT_UPDATE_DISPLAY)) {
+            long time = System.currentTimeMillis();
+            // This value allows us to change the display relatively close to when the time changes
+            // from one minute to the next.
+            long millisUtilNextMinute = MILLIS_IN_MINUTE - (time % MILLIS_IN_MINUTE);
+            mDisplayUpdateHandler.sendEmptyMessageDelayed(
+                    EVENT_UPDATE_DISPLAY, millisUtilNextMinute);
+        }
+    }
+
+    /**
+     * Cancels any pending update requests to update the relative call times (X mins ago).
+     */
+    private void cancelDisplayUpdate() {
+        mDisplayUpdateHandler.removeMessages(EVENT_UPDATE_DISPLAY);
+    }
 }
diff --git a/src/com/android/dialer/calllog/IntentProvider.java b/src/com/android/dialer/calllog/IntentProvider.java
index a11d00b..773436b 100644
--- a/src/com/android/dialer/calllog/IntentProvider.java
+++ b/src/com/android/dialer/calllog/IntentProvider.java
@@ -21,17 +21,17 @@
 import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
-import android.provider.CallLog.Calls;
 import android.provider.ContactsContract;
 import android.telecom.PhoneAccountHandle;
 
+import com.android.contacts.common.CallUtil;
 import com.android.contacts.common.model.Contact;
 import com.android.contacts.common.model.ContactLoader;
 import com.android.dialer.CallDetailActivity;
-import com.android.dialer.DialtactsActivity;
-import com.android.dialer.PhoneCallDetails;
 import com.android.dialer.util.IntentUtil;
+import com.android.dialer.util.IntentUtil.CallIntentBuilder;
 import com.android.dialer.util.TelecomUtil;
+import com.android.incallui.Call.LogState;
 
 import java.util.ArrayList;
 
@@ -55,7 +55,10 @@
         return new IntentProvider() {
             @Override
             public Intent getIntent(Context context) {
-                return IntentUtil.getCallIntent(number, accountHandle);
+                return new CallIntentBuilder(number)
+                        .setPhoneAccountHandle(accountHandle)
+                        .setCallInitiationType(LogState.INITIATION_CALL_LOG)
+                        .build();
             }
         };
     }
@@ -69,7 +72,11 @@
         return new IntentProvider() {
             @Override
             public Intent getIntent(Context context) {
-                return IntentUtil.getVideoCallIntent(number, accountHandle);
+                return new CallIntentBuilder(number)
+                        .setPhoneAccountHandle(accountHandle)
+                        .setCallInitiationType(LogState.INITIATION_CALL_LOG)
+                        .setIsVideoCall(true)
+                        .build();
             }
         };
     }
@@ -78,7 +85,9 @@
         return new IntentProvider() {
             @Override
             public Intent getIntent(Context context) {
-                return IntentUtil.getVoicemailIntent();
+                return new CallIntentBuilder(CallUtil.getVoicemailUri())
+                        .setCallInitiationType(LogState.INITIATION_CALL_LOG)
+                        .build();
             }
         };
     }
diff --git a/src/com/android/dialer/dialpad/DialpadFragment.java b/src/com/android/dialer/dialpad/DialpadFragment.java
index 0bbf802..d2628da 100644
--- a/src/com/android/dialer/dialpad/DialpadFragment.java
+++ b/src/com/android/dialer/dialpad/DialpadFragment.java
@@ -81,7 +81,8 @@
 import com.android.dialer.SpecialCharSequenceMgr;
 import com.android.dialer.calllog.PhoneAccountUtils;
 import com.android.dialer.util.DialerUtils;
-import com.android.dialer.util.IntentUtil;
+import com.android.dialer.util.IntentUtil.CallIntentBuilder;
+import com.android.incallui.Call.LogState;
 import com.android.phone.common.CallLogAsync;
 import com.android.phone.common.animation.AnimUtils;
 import com.android.phone.common.dialpad.DialpadKeyButton;
@@ -1044,7 +1045,10 @@
     }
 
     public void callVoicemail() {
-        DialerUtils.startActivityWithErrorToast(getActivity(), IntentUtil.getVoicemailIntent());
+        DialerUtils.startActivityWithErrorToast(getActivity(),
+                new CallIntentBuilder(CallUtil.getVoicemailUri())
+                        .setCallInitiationType(LogState.INITIATION_DIALPAD)
+                        .build());
         hideAndClearDialpad(false);
     }
 
@@ -1140,9 +1144,9 @@
                 // Clear the digits just in case.
                 clearDialpad();
             } else {
-                final Intent intent = IntentUtil.getCallIntent(number,
-                        (getActivity() instanceof DialtactsActivity ?
-                                ((DialtactsActivity) getActivity()).getCallOrigin() : null));
+                final Intent intent = new CallIntentBuilder(number).
+                        setCallInitiationType(LogState.INITIATION_DIALPAD)
+                        .build();
                 DialerUtils.startActivityWithErrorToast(getActivity(), intent);
                 hideAndClearDialpad(false);
             }
@@ -1667,7 +1671,7 @@
     }
 
     private Intent newFlashIntent() {
-        final Intent intent = IntentUtil.getCallIntent(EMPTY_NUMBER);
+        final Intent intent = new CallIntentBuilder(EMPTY_NUMBER).build();
         intent.putExtra(EXTRA_SEND_EMPTY_FLASH, true);
         return intent;
     }
diff --git a/src/com/android/dialer/interactions/PhoneNumberInteraction.java b/src/com/android/dialer/interactions/PhoneNumberInteraction.java
index 6e218c0..96742fd 100644
--- a/src/com/android/dialer/interactions/PhoneNumberInteraction.java
+++ b/src/com/android/dialer/interactions/PhoneNumberInteraction.java
@@ -53,6 +53,8 @@
 import com.android.dialer.TransactionSafeActivity;
 import com.android.dialer.contact.ContactUpdateService;
 import com.android.dialer.util.IntentUtil;
+import com.android.dialer.util.IntentUtil.CallIntentBuilder;
+import com.android.incallui.Call.LogState;
 import com.android.dialer.util.DialerUtils;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -188,21 +190,20 @@
 
         private static final String ARG_PHONE_LIST = "phoneList";
         private static final String ARG_INTERACTION_TYPE = "interactionType";
-        private static final String ARG_CALL_ORIGIN = "callOrigin";
+        private static final String ARG_CALL_INITIATION_TYPE = "callInitiation";
 
         private int mInteractionType;
         private ListAdapter mPhonesAdapter;
         private List<PhoneItem> mPhoneList;
-        private String mCallOrigin;
+        private int mCallInitiationType;
 
-        public static void show(FragmentManager fragmentManager,
-                ArrayList<PhoneItem> phoneList, int interactionType,
-                String callOrigin) {
+        public static void show(FragmentManager fragmentManager, ArrayList<PhoneItem> phoneList,
+                int interactionType, int callInitiationType) {
             PhoneDisambiguationDialogFragment fragment = new PhoneDisambiguationDialogFragment();
             Bundle bundle = new Bundle();
             bundle.putParcelableArrayList(ARG_PHONE_LIST, phoneList);
-            bundle.putSerializable(ARG_INTERACTION_TYPE, interactionType);
-            bundle.putString(ARG_CALL_ORIGIN, callOrigin);
+            bundle.putInt(ARG_INTERACTION_TYPE, interactionType);
+            bundle.putInt(ARG_CALL_INITIATION_TYPE, callInitiationType);
             fragment.setArguments(bundle);
             fragment.show(fragmentManager, TAG);
         }
@@ -212,7 +213,7 @@
             final Activity activity = getActivity();
             mPhoneList = getArguments().getParcelableArrayList(ARG_PHONE_LIST);
             mInteractionType = getArguments().getInt(ARG_INTERACTION_TYPE);
-            mCallOrigin = getArguments().getString(ARG_CALL_ORIGIN);
+            mCallInitiationType = getArguments().getInt(ARG_CALL_INITIATION_TYPE);
 
             mPhonesAdapter = new PhoneItemAdapter(activity, mPhoneList, mInteractionType);
             final LayoutInflater inflater = activity.getLayoutInflater();
@@ -241,7 +242,7 @@
                 }
 
                 PhoneNumberInteraction.performAction(activity, phoneItem.phoneNumber,
-                        mInteractionType, mCallOrigin);
+                        mInteractionType, mCallInitiationType);
             } else {
                 dialog.dismiss();
             }
@@ -280,7 +281,7 @@
     private final OnDismissListener mDismissListener;
     private final int mInteractionType;
 
-    private final String mCallOrigin;
+    private final int mCallInitiationType;
     private boolean mUseDefault;
 
     private static final int UNKNOWN_CONTACT_ID = -1;
@@ -297,24 +298,25 @@
     @VisibleForTesting
     /* package */ PhoneNumberInteraction(Context context, int interactionType,
             DialogInterface.OnDismissListener dismissListener) {
-        this(context, interactionType, dismissListener, null);
+        this(context, interactionType, dismissListener, LogState.INITIATION_UNKNOWN);
     }
 
     private PhoneNumberInteraction(Context context, int interactionType,
-            DialogInterface.OnDismissListener dismissListener, String callOrigin) {
+            DialogInterface.OnDismissListener dismissListener, int callInitiationType) {
         mContext = context;
         mInteractionType = interactionType;
         mDismissListener = dismissListener;
-        mCallOrigin = callOrigin;
+        mCallInitiationType = callInitiationType;
     }
 
     private void performAction(String phoneNumber) {
-        PhoneNumberInteraction.performAction(mContext, phoneNumber, mInteractionType, mCallOrigin);
+        PhoneNumberInteraction.performAction(mContext, phoneNumber, mInteractionType,
+                mCallInitiationType);
     }
 
     private static void performAction(
             Context context, String phoneNumber, int interactionType,
-            String callOrigin) {
+            int callInitiationType) {
         Intent intent;
         switch (interactionType) {
             case ContactDisplayUtils.INTERACTION_SMS:
@@ -322,7 +324,9 @@
                         Intent.ACTION_SENDTO, Uri.fromParts("sms", phoneNumber, null));
                 break;
             default:
-                intent = IntentUtil.getCallIntent(phoneNumber, callOrigin);
+                intent = new CallIntentBuilder(phoneNumber)
+                        .setCallInitiationType(callInitiationType)
+                        .build();
                 break;
         }
         DialerUtils.startActivityWithErrorToast(context, intent);
@@ -447,54 +451,15 @@
     }
 
     /**
-     * Start call action using given contact Uri. If there are multiple candidates for the phone
-     * call, dialog is automatically shown and the user is asked to choose one.
-     *
      * @param activity that is calling this interaction. This must be of type
      * {@link TransactionSafeActivity} because we need to check on the activity state after the
      * phone numbers have been queried for.
-     * @param uri contact Uri (built from {@link Contacts#CONTENT_URI}) or data Uri
-     * (built from {@link Data#CONTENT_URI}). Contact Uri may show the disambiguation dialog while
-     * data Uri won't.
-     */
-    public static void startInteractionForPhoneCall(TransactionSafeActivity activity, Uri uri) {
-        (new PhoneNumberInteraction(activity, ContactDisplayUtils.INTERACTION_CALL, null))
-                .startInteraction(uri, true);
-    }
-
-    /**
-     * Start call action using given contact Uri. If there are multiple candidates for the phone
-     * call, dialog is automatically shown and the user is asked to choose one.
-     *
-     * @param activity that is calling this interaction. This must be of type
-     * {@link TransactionSafeActivity} because we need to check on the activity state after the
-     * phone numbers have been queried for.
-     * @param uri contact Uri (built from {@link Contacts#CONTENT_URI}) or data Uri
-     * (built from {@link Data#CONTENT_URI}). Contact Uri may show the disambiguation dialog while
-     * data Uri won't.
-     * @param useDefault Whether or not to use the primary(default) phone number. If true, the
-     * primary phone number will always be used by default if one is available. If false, a
-     * disambiguation dialog will be shown regardless of whether or not a primary phone number
-     * is available.
+     * @param callInitiationType Indicates the UI affordance that was used to initiate the call.
      */
     public static void startInteractionForPhoneCall(TransactionSafeActivity activity, Uri uri,
-            boolean useDefault) {
-        (new PhoneNumberInteraction(activity, ContactDisplayUtils.INTERACTION_CALL, null))
-                .startInteraction(uri, useDefault);
-    }
-
-    /**
-     * @param activity that is calling this interaction. This must be of type
-     * {@link TransactionSafeActivity} because we need to check on the activity state after the
-     * phone numbers have been queried for.
-     * @param callOrigin If non null, {@link PhoneConstants#EXTRA_CALL_ORIGIN} will be
-     * appended to the Intent initiating phone call. See comments in Phone package (PhoneApp)
-     * for more detail.
-     */
-    public static void startInteractionForPhoneCall(TransactionSafeActivity activity, Uri uri,
-            String callOrigin) {
-        (new PhoneNumberInteraction(activity, ContactDisplayUtils.INTERACTION_CALL, null, callOrigin))
-                .startInteraction(uri, true);
+            int callInitiationType) {
+        (new PhoneNumberInteraction(activity, ContactDisplayUtils.INTERACTION_CALL, null,
+                callInitiationType)).startInteraction(uri, true);
     }
 
     /**
@@ -521,7 +486,17 @@
 
     @VisibleForTesting
     /* package */ void showDisambiguationDialog(ArrayList<PhoneItem> phoneList) {
-        PhoneDisambiguationDialogFragment.show(((Activity)mContext).getFragmentManager(),
-                phoneList, mInteractionType, mCallOrigin);
+        final Activity activity = (Activity) mContext;
+        if (activity.isDestroyed()) {
+            // Check whether the activity is still running
+            return;
+        }
+        try {
+            PhoneDisambiguationDialogFragment.show(activity.getFragmentManager(),
+                    phoneList, mInteractionType, mCallInitiationType);
+        } catch (IllegalStateException e) {
+            // ignore to be safe. Shouldn't happen because we checked the
+            // activity wasn't destroyed, but to be safe.
+        }
     }
 }
diff --git a/src/com/android/dialer/list/ListsFragment.java b/src/com/android/dialer/list/ListsFragment.java
index 09a4cb2..c80ab42 100644
--- a/src/com/android/dialer/list/ListsFragment.java
+++ b/src/com/android/dialer/list/ListsFragment.java
@@ -176,10 +176,6 @@
         Trace.beginSection(TAG + " onCreate");
         super.onCreate(savedInstanceState);
 
-        Trace.beginSection(TAG + " getCurrentCountryIso");
-        final String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
-        Trace.endSection();
-
         mVoicemailStatusHelper = new VoicemailStatusHelperImpl();
         mHasFetchedVoicemailStatus = false;
 
@@ -265,7 +261,7 @@
                 // Try to show the voicemail tab after the voicemail status returns.
                 mShowVoicemailTabAfterVoicemailStatusIsFetched = true;
             }
-        } else {
+        } else if (index < getTabCount()){
             mViewPager.setCurrentItem(getRtlPosition(index));
         }
     }
diff --git a/src/com/android/dialer/list/RegularSearchFragment.java b/src/com/android/dialer/list/RegularSearchFragment.java
index c715de8..ec771e8 100644
--- a/src/com/android/dialer/list/RegularSearchFragment.java
+++ b/src/com/android/dialer/list/RegularSearchFragment.java
@@ -15,7 +15,6 @@
  */
 package com.android.dialer.list;
 
-import static android.Manifest.permission.ACCESS_FINE_LOCATION;
 import static android.Manifest.permission.READ_CONTACTS;
 
 import android.app.Activity;
@@ -28,7 +27,7 @@
 import com.android.contacts.common.util.PermissionsUtil;
 import com.android.contacts.commonbind.analytics.AnalyticsUtil;
 import com.android.dialerbind.ObjectFactory;
-
+import com.android.incallui.Call.LogState;
 import com.android.dialer.R;
 import com.android.dialer.service.CachedNumberLookupService;
 import com.android.dialer.widget.EmptyContentView;
@@ -119,4 +118,10 @@
             }
         }
     }
+
+    @Override
+    protected int getCallInitiationType(boolean isRemoteDirectory) {
+        return isRemoteDirectory ? LogState.INITIATION_REMOTE_DIRECTORY
+                : LogState.INITIATION_REGULAR_SEARCH;
+    }
 }
diff --git a/src/com/android/dialer/list/SearchFragment.java b/src/com/android/dialer/list/SearchFragment.java
index 315cfb9..26ed117 100644
--- a/src/com/android/dialer/list/SearchFragment.java
+++ b/src/com/android/dialer/list/SearchFragment.java
@@ -53,6 +53,7 @@
 import com.android.dialer.util.DialerUtils;
 import com.android.dialer.util.IntentUtil;
 import com.android.dialer.widget.EmptyContentView;
+import com.android.incallui.Call.LogState;
 import com.android.phone.common.animation.AnimUtils;
 
 public class SearchFragment extends PhoneNumberPickerFragment {
@@ -250,7 +251,8 @@
                 number = adapter.getQueryString();
                 listener = getOnPhoneNumberPickerListener();
                 if (listener != null && !checkForProhibitedPhoneNumber(number)) {
-                    listener.onCallNumberDirectly(number);
+                    listener.onCallNumberDirectly(number, false /* isVideoCall */,
+                            getCallInitiationType(false /* isRemoteDirectory */));
                 }
                 break;
             case DialerPhoneNumberListAdapter.SHORTCUT_CREATE_NEW_CONTACT:
@@ -275,7 +277,8 @@
                 number = adapter.getQueryString();
                 listener = getOnPhoneNumberPickerListener();
                 if (listener != null && !checkForProhibitedPhoneNumber(number)) {
-                    listener.onCallNumberDirectly(number, true /* isVideoCall */);
+                    listener.onCallNumberDirectly(number, true /* isVideoCall */,
+                            getCallInitiationType(false /* isRemoteDirectory */));
                 }
                 break;
         }
diff --git a/src/com/android/dialer/list/SmartDialSearchFragment.java b/src/com/android/dialer/list/SmartDialSearchFragment.java
index 72d3abf..9aedfe8 100644
--- a/src/com/android/dialer/list/SmartDialSearchFragment.java
+++ b/src/com/android/dialer/list/SmartDialSearchFragment.java
@@ -30,6 +30,7 @@
 import com.android.dialer.dialpad.SmartDialCursorLoader;
 import com.android.dialer.R;
 import com.android.dialer.widget.EmptyContentView;
+import com.android.incallui.Call.LogState;
 
 import java.util.ArrayList;
 
@@ -116,6 +117,11 @@
         }
     }
 
+    @Override
+    protected int getCallInitiationType(boolean isRemoteDirectory) {
+        return LogState.INITIATION_SMART_DIAL;
+    }
+
     public boolean isShowingPermissionRequest() {
         return mEmptyView != null && mEmptyView.isShowingContent();
     }
diff --git a/src/com/android/dialer/list/SpeedDialFragment.java b/src/com/android/dialer/list/SpeedDialFragment.java
index 324caef..7b72e45 100644
--- a/src/com/android/dialer/list/SpeedDialFragment.java
+++ b/src/com/android/dialer/list/SpeedDialFragment.java
@@ -54,6 +54,7 @@
 import com.android.dialer.R;
 import com.android.dialer.util.DialerUtils;
 import com.android.dialer.widget.EmptyContentView;
+import com.android.incallui.Call.LogState;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -115,14 +116,16 @@
         @Override
         public void onContactSelected(Uri contactUri, Rect targetRect) {
             if (mPhoneNumberPickerActionListener != null) {
-                mPhoneNumberPickerActionListener.onPickPhoneNumberAction(contactUri);
+                mPhoneNumberPickerActionListener.onPickPhoneNumberAction(contactUri,
+                        LogState.INITIATION_SPEED_DIAL);
             }
         }
 
         @Override
         public void onCallNumberDirectly(String phoneNumber) {
             if (mPhoneNumberPickerActionListener != null) {
-                mPhoneNumberPickerActionListener.onCallNumberDirectly(phoneNumber);
+                mPhoneNumberPickerActionListener.onCallNumberDirectly(phoneNumber,
+                        false /* isVideoCall */, LogState.INITIATION_SPEED_DIAL);
             }
         }
 
diff --git a/src/com/android/dialer/logging/Logger.java b/src/com/android/dialer/logging/Logger.java
new file mode 100644
index 0000000..3007077
--- /dev/null
+++ b/src/com/android/dialer/logging/Logger.java
@@ -0,0 +1,58 @@
+/*
+ * 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.logging;
+
+import android.app.Activity;
+
+import com.android.dialerbind.ObjectFactory;
+import com.android.incallui.Call;
+
+public abstract class Logger {
+
+    public static Logger getInstance() {
+        return ObjectFactory.getLoggerInstance();
+    }
+
+    /**
+     * Logs a call event. PII like the call's number or caller details should never be logged.
+     *
+     * @param call to log.
+     */
+    public static void logCall(Call call) {
+        final Logger logger = getInstance();
+        if (logger != null) {
+            logger.logCallImpl(call);
+        }
+    }
+
+    /**
+     * Logs an event indicating that a screen/fragment was displayed.
+     *
+     * @param fragmentName of the displayed fragment.
+     * @param activity Parent activity of the fragment.
+     * @param tag Optional string used to provide additional information about the fragment.
+     */
+    public static void logScreenView(String fragmentName, Activity activity, String tag) {
+        final Logger logger = getInstance();
+        if (logger != null) {
+            logger.logScreenViewImpl(fragmentName, activity, tag);
+        }
+    }
+
+    public abstract void logCallImpl(Call call);
+    public abstract void logScreenViewImpl(String fragmentName, Activity activity, String tag);
+}
diff --git a/src/com/android/dialer/util/DialerUtils.java b/src/com/android/dialer/util/DialerUtils.java
index e25ada5..fbe14ba 100644
--- a/src/com/android/dialer/util/DialerUtils.java
+++ b/src/com/android/dialer/util/DialerUtils.java
@@ -81,7 +81,14 @@
                 // All dialer-initiated calls should pass the touch point to the InCallUI
                 Point touchPoint = TouchPointManager.getInstance().getPoint();
                 if (touchPoint.x != 0 || touchPoint.y != 0) {
-                    Bundle extras = new Bundle();
+                    Bundle extras;
+                    // Make sure to not accidentally clobber any existing extras
+                    if (intent.hasExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS)) {
+                        extras = intent.getParcelableExtra(
+                                TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS);
+                    } else {
+                        extras = new Bundle();
+                    }
                     extras.putParcelable(TouchPointManager.TOUCH_POINT, touchPoint);
                     intent.putExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, extras);
                 }
diff --git a/src/com/android/dialer/util/IntentUtil.java b/src/com/android/dialer/util/IntentUtil.java
index 2ce3bd1..5a4a80b 100644
--- a/src/com/android/dialer/util/IntentUtil.java
+++ b/src/com/android/dialer/util/IntentUtil.java
@@ -18,14 +18,13 @@
 
 import android.content.Intent;
 import android.net.Uri;
+import android.os.Bundle;
 import android.provider.ContactsContract;
-import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
 import android.telecom.VideoProfile;
 
 import com.android.contacts.common.CallUtil;
-import com.android.phone.common.PhoneConstants;
 
 /**
  * Utilities for creation of intents in Dialer, such as {@link Intent#ACTION_CALL}.
@@ -36,108 +35,65 @@
     private static final String SMS_URI_PREFIX = "sms:";
     private static final int NO_PHONE_TYPE = -1;
 
-    /**
-     * Return an Intent for making a phone call. Scheme (e.g. tel, sip) will be determined
-     * automatically.
-     */
-    public static Intent getCallIntent(String number) {
-        return getCallIntent(number, null, null);
+    public static final String EXTRA_CALL_INITIATION_TYPE
+            = "com.android.dialer.EXTRA_CALL_INITIATION_TYPE";
+
+    public static class CallIntentBuilder {
+        private Uri mUri;
+        private int mCallInitiationType;
+        private PhoneAccountHandle mPhoneAccountHandle;
+        private boolean mIsVideoCall = false;
+
+        public CallIntentBuilder(Uri uri) {
+            mUri = uri;
+        }
+
+        public CallIntentBuilder(String number) {
+            this(CallUtil.getCallUri(number));
+        }
+
+        public CallIntentBuilder setCallInitiationType(int initiationType) {
+            mCallInitiationType = initiationType;
+            return this;
+        }
+
+        public CallIntentBuilder setPhoneAccountHandle(PhoneAccountHandle accountHandle) {
+            mPhoneAccountHandle = accountHandle;
+            return this;
+        }
+
+        public CallIntentBuilder setIsVideoCall(boolean isVideoCall) {
+            mIsVideoCall = isVideoCall;
+            return this;
+        }
+
+        public Intent build() {
+            return getCallIntent(
+                    mUri,
+                    mPhoneAccountHandle,
+                    mIsVideoCall ? VideoProfile.STATE_BIDIRECTIONAL : VideoProfile.STATE_AUDIO_ONLY,
+                    mCallInitiationType);
+        }
     }
 
     /**
-     * Return an Intent for making a phone call. A given Uri will be used as is (without any
-     * sanity check).
-     */
-    public static Intent getCallIntent(Uri uri) {
-        return getCallIntent(uri, null, null);
-    }
-
-    /**
-     * A variant of {@link #getCallIntent(String)} but also accept a call origin.
-     * For more information about call origin, see comments in Phone package (PhoneApp).
-     */
-    public static Intent getCallIntent(String number, String callOrigin) {
-        return getCallIntent(CallUtil.getCallUri(number), callOrigin, null);
-    }
-
-    /**
-     * A variant of {@link #getCallIntent(String)} but also include {@code Account}.
-     */
-    public static Intent getCallIntent(String number, PhoneAccountHandle accountHandle) {
-        return getCallIntent(number, null, accountHandle);
-    }
-
-    /**
-     * A variant of {@link #getCallIntent(android.net.Uri)} but also include {@code Account}.
-     */
-    public static Intent getCallIntent(Uri uri, PhoneAccountHandle accountHandle) {
-        return getCallIntent(uri, null, accountHandle);
-    }
-
-    /**
-     * A variant of {@link #getCallIntent(String, String)} but also include {@code Account}.
+     * Create a call intent that can be used to place a call.
+     *
+     * @param uri Address to place the call to.
+     * @param accountHandle {@link PhoneAccountHandle} to place the call with.
+     * @param videoState Initial video state of the call.
+     * @param callIntiationType The UI affordance the call was initiated by.
+     * @return Call intent with provided extras and data.
      */
     public static Intent getCallIntent(
-            String number, String callOrigin, PhoneAccountHandle accountHandle) {
-        return getCallIntent(CallUtil.getCallUri(number), callOrigin, accountHandle);
-    }
-
-    /**
-     * A variant of {@link #getCallIntent(android.net.Uri)} but also accept a call
-     * origin and {@code Account}.
-     * For more information about call origin, see comments in Phone package (PhoneApp).
-     */
-    public static Intent getCallIntent(
-            Uri uri, String callOrigin, PhoneAccountHandle accountHandle) {
-        return getCallIntent(uri, callOrigin, accountHandle,
-                VideoProfile.STATE_AUDIO_ONLY);
-    }
-
-    /**
-     * A variant of {@link #getCallIntent(String, String)} for starting a video call.
-     */
-    public static Intent getVideoCallIntent(String number, String callOrigin) {
-        return getCallIntent(CallUtil.getCallUri(number), callOrigin, null,
-                VideoProfile.STATE_BIDIRECTIONAL);
-    }
-
-    /**
-     * A variant of {@link #getCallIntent(String, String, android.telecom.PhoneAccountHandle)} for
-     * starting a video call.
-     */
-    public static Intent getVideoCallIntent(
-            String number, String callOrigin, PhoneAccountHandle accountHandle) {
-        return getCallIntent(CallUtil.getCallUri(number), callOrigin, accountHandle,
-                VideoProfile.STATE_BIDIRECTIONAL);
-    }
-
-    /**
-     * A variant of {@link #getCallIntent(String, String, android.telecom.PhoneAccountHandle)} for
-     * starting a video call.
-     */
-    public static Intent getVideoCallIntent(String number, PhoneAccountHandle accountHandle) {
-        return getVideoCallIntent(number, null, accountHandle);
-    }
-
-    /**
-     * A variant of {@link #getCallIntent(android.net.Uri)} for calling Voicemail.
-     */
-    public static Intent getVoicemailIntent() {
-        return getCallIntent(Uri.fromParts(PhoneAccount.SCHEME_VOICEMAIL, "", null));
-    }
-
-    /**
-     * A variant of {@link #getCallIntent(android.net.Uri)} but also accept a call
-     * origin and {@code Account} and {@code VideoCallProfile} state.
-     * For more information about call origin, see comments in Phone package (PhoneApp).
-     */
-    public static Intent getCallIntent(
-            Uri uri, String callOrigin, PhoneAccountHandle accountHandle, int videoState) {
+            Uri uri, PhoneAccountHandle accountHandle, int videoState, int callIntiationType) {
         final Intent intent = new Intent(CALL_ACTION, uri);
         intent.putExtra(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, videoState);
-        if (callOrigin != null) {
-            intent.putExtra(PhoneConstants.EXTRA_CALL_ORIGIN, callOrigin);
-        }
+
+        final Bundle b = new Bundle();
+        b.putInt(EXTRA_CALL_INITIATION_TYPE, callIntiationType);
+        intent.putExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, b);
+
         if (accountHandle != null) {
             intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, accountHandle);
         }
diff --git a/src/com/android/dialer/voicemail/VoicemailAudioManager.java b/src/com/android/dialer/voicemail/VoicemailAudioManager.java
new file mode 100644
index 0000000..e64e180
--- /dev/null
+++ b/src/com/android/dialer/voicemail/VoicemailAudioManager.java
@@ -0,0 +1,73 @@
+/*
+ * 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.voicemail;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.AudioManager.OnAudioFocusChangeListener;
+import android.util.Log;
+
+import java.util.concurrent.RejectedExecutionException;
+
+/**
+ * This class manages all audio changes for voicemail playback.
+ */
+final class VoicemailAudioManager implements OnAudioFocusChangeListener {
+    private static final String TAG = VoicemailAudioManager.class.getSimpleName();
+
+    public static final int PLAYBACK_STREAM = AudioManager.STREAM_VOICE_CALL;
+
+    private AudioManager mAudioManager;
+    private VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
+
+    public VoicemailAudioManager(Context context,
+            VoicemailPlaybackPresenter voicemailPlaybackPresenter) {
+        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        mVoicemailPlaybackPresenter = voicemailPlaybackPresenter;
+    }
+
+    public void requestAudioFocus() {
+        int result = mAudioManager.requestAudioFocus(
+                this,
+                PLAYBACK_STREAM,
+                AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+        if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+            throw new RejectedExecutionException("Could not capture audio focus.");
+        }
+    }
+
+    public void abandonAudioFocus() {
+        mAudioManager.abandonAudioFocus(this);
+    }
+
+    @Override
+    public void onAudioFocusChange(int focusChange) {
+        Log.d(TAG, "onAudioFocusChange: focusChange=" + focusChange);
+        mVoicemailPlaybackPresenter.onAudioFocusChange(focusChange == AudioManager.AUDIOFOCUS_GAIN);
+    }
+
+    public void turnOnSpeaker(boolean on) {
+        if (mAudioManager.isSpeakerphoneOn() != on) {
+            Log.i(TAG, "turning speaker phone on: " + on);
+            mAudioManager.setSpeakerphoneOn(on);
+        }
+    }
+
+    public boolean isSpeakerphoneOn() {
+        return mAudioManager.isSpeakerphoneOn();
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java b/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java
index 69c075f..14c5473 100644
--- a/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java
+++ b/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java
@@ -42,6 +42,7 @@
 import com.android.dialer.R;
 import com.android.dialer.calllog.CallLogAsyncTaskUtil;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 
 import java.util.concurrent.TimeUnit;
@@ -159,7 +160,7 @@
         @Override
         public void onClick(View v) {
             if (mPresenter != null) {
-                onSpeakerphoneOn(!mPresenter.isSpeakerphoneOn());
+                mPresenter.toggleSpeakerphone();
             }
         }
     };
@@ -286,10 +287,6 @@
 
         mStartStopButton.setImageResource(R.drawable.ic_pause);
 
-        if (mPresenter != null) {
-            onSpeakerphoneOn(mPresenter.isSpeakerphoneOn());
-        }
-
         if (mPositionUpdater != null) {
             mPositionUpdater.stopUpdating();
             mPositionUpdater = null;
@@ -321,10 +318,6 @@
     }
 
     public void onSpeakerphoneOn(boolean on) {
-        if (mPresenter != null) {
-            mPresenter.setSpeakerphoneOn(on);
-        }
-
         if (on) {
             mPlaybackSpeakerphone.setImageResource(R.drawable.ic_volume_up_24dp);
             // Speaker is now on, tapping button will turn it off.
@@ -373,7 +366,6 @@
     @Override
     public void disableUiElements() {
         mStartStopButton.setEnabled(false);
-        mPlaybackSpeakerphone.setEnabled(false);
         mPlaybackSeek.setProgress(0);
         mPlaybackSeek.setEnabled(false);
 
@@ -384,7 +376,6 @@
     @Override
     public void enableUiElements() {
         mStartStopButton.setEnabled(true);
-        mPlaybackSpeakerphone.setEnabled(true);
         mPlaybackSeek.setEnabled(true);
 
         mPositionText.setVisibility(View.VISIBLE);
@@ -421,4 +412,9 @@
         }
         return String.format("%02d:%02d", minutes, seconds);
     }
+
+    @VisibleForTesting
+    public String getStateText() {
+        return mStateText.getText().toString();
+    }
 }
diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
index 3f5a489..95622bf 100644
--- a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
+++ b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
@@ -23,7 +23,6 @@
 import android.database.ContentObserver;
 import android.database.Cursor;
 import android.media.AudioManager;
-import android.media.AudioManager.OnAudioFocusChangeListener;
 import android.media.MediaPlayer;
 import android.net.Uri;
 import android.os.AsyncTask;
@@ -60,7 +59,7 @@
  * {@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}. This
+ * instance can be reused for different such layouts, using {@link #setPlaybackView}. 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
@@ -68,8 +67,7 @@
  */
 @NotThreadSafe
 @VisibleForTesting
-public class VoicemailPlaybackPresenter
-        implements OnAudioFocusChangeListener, MediaPlayer.OnPreparedListener,
+public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListener,
                 MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener {
 
     private static final String TAG = VoicemailPlaybackPresenter.class.getSimpleName();
@@ -105,7 +103,6 @@
         VoicemailContract.Voicemails.HAS_CONTENT,
     };
 
-    public static final int PLAYBACK_STREAM = AudioManager.STREAM_VOICE_CALL;
     private static final int NUMBER_OF_THREADS_IN_POOL = 2;
     // Time to wait for content to be fetched before timing out.
     private static final long FETCH_CONTENT_TIMEOUT_MS = 20000;
@@ -120,6 +117,8 @@
     // If present in the saved instance bundle, indicates where to set the playback slider.
     private static final String CLIP_POSITION_KEY =
             VoicemailPlaybackPresenter.class.getName() + ".CLIP_POSITION_KEY";
+    private static final String IS_SPEAKERPHONE_ON_KEY =
+            VoicemailPlaybackPresenter.class.getName() + ".IS_SPEAKER_PHONE_ON";
 
     /**
      * The most recently cached duration. We cache this since we don't want to keep requesting it
@@ -141,6 +140,7 @@
     // MediaPlayer crashes on some method calls if not prepared but does not have a method which
     // exposes its prepared state. Store this locally, so we can check and prevent crashes.
     private boolean mIsPrepared;
+    private boolean mIsSpeakerphoneOn;
 
     private boolean mShouldResumePlaybackAfterSeeking;
     private int mInitialOrientation;
@@ -156,7 +156,7 @@
     private FetchResultHandler mFetchResultHandler;
     private Handler mHandler = new Handler();
     private PowerManager.WakeLock mProximityWakeLock;
-    private AudioManager mAudioManager;
+    private VoicemailAudioManager mVoicemailAudioManager;
 
     private OnVoicemailDeletedListener mOnVoicemailDeletedListener;
 
@@ -185,7 +185,7 @@
     private VoicemailPlaybackPresenter(Activity activity) {
         Context context = activity.getApplicationContext();
         mAsyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor();
-        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        mVoicemailAudioManager = new VoicemailAudioManager(context, this);
 
         PowerManager powerManager =
                 (PowerManager) context.getSystemService(Context.POWER_SERVICE);
@@ -203,7 +203,7 @@
         mContext = activity;
 
         mInitialOrientation = mContext.getResources().getConfiguration().orientation;
-        mActivity.setVolumeControlStream(VoicemailPlaybackPresenter.PLAYBACK_STREAM);
+        mActivity.setVolumeControlStream(VoicemailAudioManager.PLAYBACK_STREAM);
 
         if (savedInstanceState != null) {
             // Restores playback state when activity is recreated, such as after rotation.
@@ -211,6 +211,7 @@
             mIsPrepared = savedInstanceState.getBoolean(IS_PREPARED_KEY);
             mPosition = savedInstanceState.getInt(CLIP_POSITION_KEY, 0);
             mIsPlaying = savedInstanceState.getBoolean(IS_PLAYING_STATE_KEY, false);
+            mIsSpeakerphoneOn = savedInstanceState.getBoolean(IS_SPEAKERPHONE_ON_KEY, false);
         }
 
         if (mMediaPlayer == null) {
@@ -228,6 +229,7 @@
             outState.putBoolean(IS_PREPARED_KEY, mIsPrepared);
             outState.putInt(CLIP_POSITION_KEY, mView.getDesiredClipPosition());
             outState.putBoolean(IS_PLAYING_STATE_KEY, mIsPlaying);
+            outState.putBoolean(IS_SPEAKERPHONE_ON_KEY, mIsSpeakerphoneOn);
         }
     }
 
@@ -239,16 +241,21 @@
         mView = view;
         mView.setPresenter(this, voicemailUri);
 
+        // Handles cases where the same entry is binded again when scrolling in list, or where
+        // the MediaPlayer was retained after an orientation change.
         if (mMediaPlayer != null && mIsPrepared && voicemailUri.equals(mVoicemailUri)) {
-            // Handles case where MediaPlayer was retained after an orientation change.
             onPrepared(mMediaPlayer);
-            mView.onSpeakerphoneOn(isSpeakerphoneOn());
         } else {
             if (!voicemailUri.equals(mVoicemailUri)) {
+                mVoicemailUri = voicemailUri;
                 mPosition = 0;
+                // Default to earpiece.
+                setSpeakerphoneOn(false);
+            } else {
+                // Update the view to the current speakerphone state.
+                mView.onSpeakerphoneOn(mIsSpeakerphoneOn);
             }
 
-            mVoicemailUri = voicemailUri;
             mDuration.set(0);
 
             if (startPlayingImmediately) {
@@ -258,9 +265,6 @@
                 mIsPlaying = startPlayingImmediately;
                 checkForContent();
             }
-
-            // Default to earpiece.
-            mView.onSpeakerphoneOn(false);
         }
     }
 
@@ -496,7 +500,7 @@
 
             mMediaPlayer.reset();
             mMediaPlayer.setDataSource(mContext, mVoicemailUri);
-            mMediaPlayer.setAudioStreamType(PLAYBACK_STREAM);
+            mMediaPlayer.setAudioStreamType(VoicemailAudioManager.PLAYBACK_STREAM);
             mMediaPlayer.prepareAsync();
         } catch (IOException e) {
             handleError(e);
@@ -570,15 +574,22 @@
         }
     }
 
-    @Override
-    public void onAudioFocusChange(int focusChange) {
-        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 (!mIsPlaying && focusChange == AudioManager.AUDIOFOCUS_GAIN) {
+    /**
+     * Only play voicemail when audio focus is granted. When it is lost (usually by another
+     * application requesting focus), pause playback.
+     *
+     * @param gainedFocus {@code true} if the audio focus was gained, {@code} false otherwise.
+     */
+    public void onAudioFocusChange(boolean gainedFocus) {
+        if (mIsPlaying == gainedFocus) {
+            // Nothing new here, just exit.
+            return;
+        }
+
+        if (!mIsPlaying) {
             resumePlayback();
+        } else {
+            pausePlayback();
         }
     }
 
@@ -595,7 +606,6 @@
             // If we haven't downloaded the voicemail yet, attempt to download it.
             checkForContent();
             mIsPlaying = true;
-
             return;
         }
 
@@ -608,15 +618,10 @@
 
             try {
                 // Grab audio focus.
-                int result = mAudioManager.requestAudioFocus(
-                        this,
-                        PLAYBACK_STREAM,
-                        AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
-                if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
-                    throw new RejectedExecutionException("Could not capture audio focus.");
-                }
-
                 // Can throw RejectedExecutionException.
+                mVoicemailAudioManager.requestAudioFocus();
+
+                setSpeakerphoneOn(mIsSpeakerphoneOn);
                 mMediaPlayer.start();
             } catch (RejectedExecutionException e) {
                 handleError(e);
@@ -625,11 +630,6 @@
 
         Log.d(TAG, "Resumed playback at " + mPosition + ".");
         mView.onPlaybackStarted(mDuration.get(), getScheduledExecutorServiceInstance());
-        if (isSpeakerphoneOn()) {
-            mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
-        } else {
-            enableProximitySensor();
-        }
     }
 
     /**
@@ -653,7 +653,8 @@
         if (mView != null) {
             mView.onPlaybackStopped();
         }
-        mAudioManager.abandonAudioFocus(this);
+
+        mVoicemailAudioManager.abandonAudioFocus();
 
         if (mActivity != null) {
             mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
@@ -681,7 +682,7 @@
     }
 
     private void enableProximitySensor() {
-        if (mProximityWakeLock == null || isSpeakerphoneOn() || !mIsPrepared
+        if (mProximityWakeLock == null || mIsSpeakerphoneOn || !mIsPrepared
                 || mMediaPlayer == null || !mMediaPlayer.isPlaying()) {
             return;
         }
@@ -707,24 +708,32 @@
         }
     }
 
-    public void setSpeakerphoneOn(boolean on) {
-        mAudioManager.setSpeakerphoneOn(on);
-
-        if (on) {
-            disableProximitySensor(false /* waitForFarState */);
-            if (mIsPrepared && mMediaPlayer != null && mMediaPlayer.isPlaying()) {
-                mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
-            }
-        } else {
-            enableProximitySensor();
-            if (mActivity != null) {
-                mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
-            }
-        }
+    public void toggleSpeakerphone() {
+        setSpeakerphoneOn(!mIsSpeakerphoneOn);
     }
 
-    public boolean isSpeakerphoneOn() {
-        return mAudioManager.isSpeakerphoneOn();
+    private void setSpeakerphoneOn(boolean on) {
+        mView.onSpeakerphoneOn(on);
+        if (mIsSpeakerphoneOn == on) {
+            return;
+        }
+
+        mIsSpeakerphoneOn = on;
+        mVoicemailAudioManager.turnOnSpeaker(on);
+
+        if (mIsPlaying) {
+            if (on) {
+                disableProximitySensor(false /* waitForFarState */);
+                if (mIsPrepared && mMediaPlayer != null && mMediaPlayer.isPlaying()) {
+                    mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
+                }
+            } else {
+                enableProximitySensor();
+                if (mActivity != null) {
+                    mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
+                }
+            }
+        }
     }
 
     public void setOnVoicemailDeletedListener(OnVoicemailDeletedListener listener) {
@@ -767,4 +776,14 @@
     public boolean isPlaying() {
         return mIsPlaying;
     }
+
+    @VisibleForTesting
+    public boolean isSpeakerphoneOn() {
+        return mIsSpeakerphoneOn;
+    }
+
+    @VisibleForTesting
+    public void clearInstance() {
+        sInstance = null;
+    }
 }
diff --git a/src/com/android/dialerbind/ObjectFactory.java b/src/com/android/dialerbind/ObjectFactory.java
index d67ada0..c64afe8 100644
--- a/src/com/android/dialerbind/ObjectFactory.java
+++ b/src/com/android/dialerbind/ObjectFactory.java
@@ -22,6 +22,7 @@
 
 import com.android.dialer.calllog.CallLogAdapter;
 import com.android.dialer.calllog.ContactInfoHelper;
+import com.android.dialer.logging.Logger;
 import com.android.dialer.service.CachedNumberLookupService;
 import com.android.dialer.voicemail.VoicemailPlaybackPresenter;
 
@@ -59,4 +60,9 @@
                 voicemailPlaybackPresenter,
                 isCallLogActivity);
     }
+
+    public static Logger getLoggerInstance() {
+        // no-op
+        return null;
+    }
 }
diff --git a/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java b/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
index de6f198..83d098f 100644
--- a/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
+++ b/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
@@ -22,14 +22,16 @@
 import android.database.MatrixCursor;
 import android.net.Uri;
 import android.provider.CallLog.Calls;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.VoicemailContract;
 import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.telephony.PhoneNumberUtils;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.test.suitebuilder.annotation.MediumTest;
+import android.test.suitebuilder.annotation.LargeTest;
 import android.text.TextUtils;
 import android.view.View;
-import android.widget.LinearLayout;
 
 import com.android.dialer.contactinfo.ContactInfoCache;
 import com.android.dialer.contactinfo.ContactInfoCache.OnContactInfoChangedListener;
@@ -42,24 +44,32 @@
 
 /**
  * Unit tests for {@link CallLogAdapter}.
+ *
+ * adb shell am instrument \
+ *     -e com.android.dialer.calllog.CallLogAdapterTest \
+ *     -w com.android.dialer.tests/android.test.InstrumentationTestRunner
  */
-@SmallTest
 public class CallLogAdapterTest extends AndroidTestCase {
+    private static final String EMPTY_STRING = "";
     private static final int NO_VALUE_SET = -1;
 
+    private static final String TEST_CACHED_NAME = "name";
+    private static final String TEST_CACHED_NUMBER_LABEL = "label";
+    private static final int TEST_CACHED_NUMBER_TYPE = 1;
+    private static final String TEST_COUNTRY_ISO = "US";
+    private static final String TEST_DEFAULT_CUSTOM_LABEL = "myLabel";
+    private static final Uri TEST_LOOKUP_URI = Uri.parse("content://contacts/2");
+
+    private static final String TEST_NUMBER = "12125551000";
     private static final String TEST_NUMBER_1 = "12345678";
     private static final String TEST_NUMBER_2 = "87654321";
     private static final String TEST_NUMBER_3 = "18273645";
-    private static final String TEST_NAME = "name";
-    private static final String TEST_NUMBER_LABEL = "label";
-    private static final int TEST_NUMBER_TYPE = 1;
-    private static final String TEST_COUNTRY_ISO = "US";
+    private static final String TEST_FORMATTED_NUMBER = "1 212-555-1000";
 
     // The object under test.
     private TestCallLogAdapter mAdapter;
 
     private MatrixCursor mCursor;
-    private int mCursorSize;
 
     private View mView;
     private CallLogListItemViewHolder mViewHolder;
@@ -88,117 +98,188 @@
                 };
 
         mAdapter = new TestCallLogAdapter(getContext(), fakeCallFetcher, fakeContactInfoHelper);
+
         // The cursor used in the tests to store the entries to display.
         mCursor = new MatrixCursor(CallLogQuery._PROJECTION);
         mCursor.moveToFirst();
+
         // The views into which to store the data.
-        mView = new LinearLayout(getContext());
         mViewHolder = CallLogListItemViewHolder.createForTest(getContext());
-        mView.setTag(mViewHolder);
     }
 
     @Override
     protected void tearDown() throws Exception {
         mAdapter = null;
         mCursor = null;
-        mView = null;
         super.tearDown();
     }
 
     @MediumTest
     public void testBindView_NumberOnlyNoCache() {
         createCallLogEntry();
-        mAdapter.changeCursor(mCursor);
 
+        mAdapter.changeCursor(mCursor);
         mAdapter.onBindViewHolder(mViewHolder, 0);
-        assertNameIs(mViewHolder, TEST_NUMBER_1);
+
+        assertNameIs(mViewHolder, TEST_NUMBER);
     }
 
     @MediumTest
     public void testBindView_PrivateCall() {
         createPrivateCallLogEntry();
-        mAdapter.changeCursor(mCursor);
 
+        mAdapter.changeCursor(mCursor);
         mAdapter.onBindViewHolder(mViewHolder, 0);
+
         assertEquals(Calls.PRESENTATION_RESTRICTED, mViewHolder.numberPresentation);
+        assertNull(mViewHolder.primaryActionButtonView.getTag());
+    }
+
+    @MediumTest
+    public void testBindView_UnknownCall() {
+        createUnknownCallLogEntry();
+
+        mAdapter.changeCursor(mCursor);
+        mAdapter.onBindViewHolder(mViewHolder, 0);
+
+        assertEquals(Calls.PRESENTATION_UNKNOWN, mViewHolder.numberPresentation);
+        assertNull(mViewHolder.primaryActionButtonView.getTag());
     }
 
     @MediumTest
     public void testBindView_WithoutQuickContactBadge() {
         createCallLogEntry();
-        mAdapter.changeCursor(mCursor);
 
+        mAdapter.changeCursor(mCursor);
         mAdapter.onBindViewHolder(mViewHolder, 0);
+
         assertFalse(mViewHolder.quickContactView.isEnabled());
     }
 
     @MediumTest
     public void testBindView_CallButton() {
         createCallLogEntry();
-        mAdapter.changeCursor(mCursor);
 
+        mAdapter.changeCursor(mCursor);
         mAdapter.onBindViewHolder(mViewHolder, 0);
 
         // The primaryActionView tag is set when the ViewHolder is binded. If it is possible
         // to place a call to the phone number, a call intent will have been created which
         // starts a phone call to the entry's number.
-        IntentProvider intentProvider =
-                (IntentProvider) mViewHolder.primaryActionButtonView.getTag();
-        Intent intent = intentProvider.getIntent(getContext());
-        assertEquals(TestConstants.CALL_INTENT_ACTION, intent.getAction());
-        assertEquals(Uri.parse("tel:" + TEST_NUMBER_1), intent.getData());
+        assertHasCallAction(mViewHolder);
     }
 
     @MediumTest
     public void testBindView_VoicemailUri() {
         createVoicemailCallLogEntry();
-        mAdapter.changeCursor(mCursor);
 
+        mAdapter.changeCursor(mCursor);
         mAdapter.onBindViewHolder(mViewHolder, 0);
+
         assertEquals(Uri.parse(mViewHolder.voicemailUri),
                 ContentUris.withAppendedId(VoicemailContract.Voicemails.CONTENT_URI, 0));
+        assertNull(mViewHolder.primaryActionButtonView.getTag());
     }
 
-    public void testBindView_NoCallLogCacheNorMemoryCache_EnqueueRequest() {
-        createCallLogEntry();
+    @MediumTest
+    public void testPresentationAfterRebindingViewHolders() {
+        final int increment = 10;
+        final int size = increment * 4;
 
-        // Bind the views of a single row.
+        // Instantiate list of ViewHolders.
+        CallLogListItemViewHolder[] holders = new CallLogListItemViewHolder[size];
+        for (int i = 0; i < size; i++) {
+            holders[i] = CallLogListItemViewHolder.createForTest(getContext());
+        }
+
+        // Add first set of entries to the cursor.
+        for (int i = 0; i < increment; i++) {
+            createCallLogEntry();
+            createPrivateCallLogEntry();
+            createCallLogEntry();
+            createUnknownCallLogEntry();
+        }
+
         mAdapter.changeCursor(mCursor);
-        mAdapter.onBindViewHolder(mViewHolder, 0);
+
+        // Verify correct appearance for presentation.
+        for (int i = 0; i < size; i++) {
+            mAdapter.onBindViewHolder(holders[i], i);
+            if (holders[i].numberPresentation == Calls.PRESENTATION_ALLOWED) {
+                assertHasCallAction(holders[i]);
+            } else {
+                assertNull(holders[i].primaryActionButtonView.getTag());
+                assertEquals(holders[i].number, EMPTY_STRING);
+            }
+        }
+
+        // Append the rest of the entries to the cursor. Keep the first set of ViewHolders
+        // so they are updated and not buitl from scratch. This checks for bugs which may
+        // be evident only after the call log is updated.
+        for (int i = 0; i < increment; i++) {
+            createPrivateCallLogEntry();
+            createCallLogEntry();
+            createUnknownCallLogEntry();
+            createCallLogEntry();
+        }
+
+        mCursor.move(size);
+
+        // Verify correct appearnce for presentation.
+        for (int i = 0; i < size; i++) {
+            mAdapter.onBindViewHolder(holders[i], i + size);
+            if (holders[i].numberPresentation == Calls.PRESENTATION_ALLOWED) {
+                assertHasCallAction(holders[i]);
+            } else {
+                assertNull(holders[i].primaryActionButtonView.getTag());
+                assertEquals(holders[i].number, EMPTY_STRING);
+            }
+        }
+    }
+
+   @MediumTest
+   public void testBindView_NoCallLogCacheNorMemoryCache_EnqueueRequest() {
+       createCallLogEntry();
+
+       // Bind the views of a single row.
+       mAdapter.changeCursor(mCursor);
+       mAdapter.onBindViewHolder(mViewHolder, 0);
+
+       // There is one request for contact details.
+       assertEquals(1, mAdapter.getContactInfoCache().requests.size());
+
+       TestContactInfoCache.Request request = mAdapter.getContactInfoCache().requests.get(0);
+       // It is for the number we need to show.
+       assertEquals(TEST_NUMBER, request.number);
+       // It has the right country.
+       assertEquals(TEST_COUNTRY_ISO, request.countryIso);
+       // Since there is nothing in the cache, it is an immediate request.
+       assertTrue("should be immediate", request.immediate);
+   }
+
+   @MediumTest
+   public void testBindView_CallLogCacheButNoMemoryCache_EnqueueRequest() {
+       createCallLogEntryWithCachedValues(false);
+
+       // Bind the views of a single row.
+       mAdapter.changeCursor(mCursor);
+       mAdapter.onBindViewHolder(mViewHolder, 0);
 
         // There is one request for contact details.
         assertEquals(1, mAdapter.getContactInfoCache().requests.size());
 
         TestContactInfoCache.Request request = mAdapter.getContactInfoCache().requests.get(0);
-        // It is for the number we need to show.
-        assertEquals(TEST_NUMBER_1, request.number);
-        // It has the right country.
-        assertEquals(TEST_COUNTRY_ISO, request.countryIso);
-        // Since there is nothing in the cache, it is an immediate request.
-        assertTrue("should be immediate", request.immediate);
-    }
 
-    public void testBindView_CallLogCacheButNoMemoryCache_EnqueueRequest() {
-        mCursor.addRow(createCallLogEntryWithCachedValues());
-
-        // Bind the views of a single row.
-        mAdapter.changeCursor(mCursor);
-        mAdapter.onBindViewHolder(mViewHolder, 0);
-
-        // There is one request for contact details.
-        assertEquals(1, mAdapter.getContactInfoCache().requests.size());
-
-        TestContactInfoCache.Request request = mAdapter.getContactInfoCache().requests.get(0);
         // The values passed to the request, match the ones in the call log cache.
-        assertEquals(TEST_NAME, request.callLogInfo.name);
-        assertEquals(1, request.callLogInfo.type);
-        assertEquals(TEST_NUMBER_LABEL, request.callLogInfo.label);
+        assertEquals(TEST_CACHED_NAME, request.callLogInfo.name);
+        assertEquals(TEST_CACHED_NUMBER_TYPE, request.callLogInfo.type);
+        assertEquals(TEST_CACHED_NUMBER_LABEL, request.callLogInfo.label);
     }
 
-
+    @MediumTest
     public void testBindView_NoCallLogButMemoryCache_EnqueueRequest() {
         createCallLogEntry();
-        mAdapter.injectContactInfoForTest(TEST_NUMBER_1, TEST_COUNTRY_ISO, createContactInfo());
+        mAdapter.injectContactInfoForTest(TEST_NUMBER, TEST_COUNTRY_ISO, createContactInfo());
 
         // Bind the views of a single row.
         mAdapter.changeCursor(mCursor);
@@ -212,9 +293,9 @@
         assertFalse("should not be immediate", request.immediate);
     }
 
+    @MediumTest
     public void testBindView_BothCallLogAndMemoryCache_NoEnqueueRequest() {
-        mCursor.addRow(createCallLogEntryWithCachedValues());
-        mAdapter.injectContactInfoForTest(TEST_NUMBER_1, TEST_COUNTRY_ISO, createContactInfo());
+        createCallLogEntryWithCachedValues(true);
 
         // Bind the views of a single row.
         mAdapter.changeCursor(mCursor);
@@ -224,13 +305,14 @@
         assertEquals(0, mAdapter.getContactInfoCache().requests.size());
     }
 
-    public void testBindView_MismatchBetwenCallLogAndMemoryCache_EnqueueRequest() {
-        mCursor.addRow(createCallLogEntryWithCachedValues());
+    @MediumTest
+    public void testBindView_MismatchBetweenCallLogAndMemoryCache_EnqueueRequest() {
+        createCallLogEntryWithCachedValues(false);
 
         // Contact info contains a different name.
         ContactInfo info = createContactInfo();
         info.name = "new name";
-        mAdapter.injectContactInfoForTest(TEST_NUMBER_1, TEST_COUNTRY_ISO, info);
+        mAdapter.injectContactInfoForTest(TEST_NUMBER, TEST_COUNTRY_ISO, info);
 
         // Bind the views of a single row.
         mAdapter.changeCursor(mCursor);
@@ -244,6 +326,98 @@
         assertFalse("should not be immediate", request.immediate);
     }
 
+    @MediumTest
+    public void testBindView_WithCachedName() {
+        createCallLogEntryWithCachedValues(
+                "John Doe",
+                Phone.TYPE_HOME,
+                TEST_CACHED_NUMBER_LABEL);
+
+        mAdapter.changeCursor(mCursor);
+        mAdapter.onBindViewHolder(mViewHolder, 0);
+
+        assertNameIs(mViewHolder, "John Doe");
+        assertLabel(mViewHolder, TEST_FORMATTED_NUMBER, getTypeLabel(Phone.TYPE_HOME));
+    }
+
+    @MediumTest
+    public void testBindView_UriNumber() {
+        createCallLogEntryWithCachedValues(
+                "sip:johndoe@gmail.com",
+                Calls.INCOMING_TYPE,
+                "John Doe",
+                Phone.TYPE_HOME,
+                TEST_DEFAULT_CUSTOM_LABEL,
+                EMPTY_STRING,
+                false /* inject */);
+
+        mAdapter.changeCursor(mCursor);
+        mAdapter.onBindViewHolder(mViewHolder, 0);
+
+        assertNameIs(mViewHolder, "John Doe");
+        assertLabel(mViewHolder, "sip:johndoe@gmail.com", "sip:johndoe@gmail.com");
+    }
+
+    @MediumTest
+    public void testBindView_HomeLabel() {
+        createCallLogEntryWithCachedValues(
+                "John Doe",
+                Phone.TYPE_HOME,
+                TEST_CACHED_NUMBER_LABEL);
+
+        mAdapter.changeCursor(mCursor);
+        mAdapter.onBindViewHolder(mViewHolder, 0);
+
+        assertNameIs(mViewHolder, "John Doe");
+        assertLabel(mViewHolder, TEST_FORMATTED_NUMBER, getTypeLabel(Phone.TYPE_HOME));
+    }
+
+    @MediumTest
+    public void testBindView_WorkLabel() {
+        createCallLogEntryWithCachedValues(
+                "John Doe",
+                Phone.TYPE_WORK,
+                TEST_CACHED_NUMBER_LABEL);
+
+        mAdapter.changeCursor(mCursor);
+        mAdapter.onBindViewHolder(mViewHolder, 0);
+
+        assertNameIs(mViewHolder, "John Doe");
+        assertLabel(mViewHolder, TEST_FORMATTED_NUMBER, getTypeLabel(Phone.TYPE_WORK));
+    }
+
+    @MediumTest
+    public void testBindView_CustomLabel() {
+        createCallLogEntryWithCachedValues(
+                "John Doe",
+                Phone.TYPE_CUSTOM,
+                TEST_DEFAULT_CUSTOM_LABEL);
+
+        mAdapter.changeCursor(mCursor);
+        mAdapter.onBindViewHolder(mViewHolder, 0);
+
+        assertNameIs(mViewHolder, "John Doe");
+        assertLabel(mViewHolder, TEST_FORMATTED_NUMBER, TEST_DEFAULT_CUSTOM_LABEL);
+    }
+
+    @MediumTest
+    public void testBindView_NumberOnlyDbCachedFormattedNumber() {
+        createCallLogEntryWithCachedValues(
+                TEST_NUMBER,
+                Calls.INCOMING_TYPE,
+                EMPTY_STRING,
+                TEST_CACHED_NUMBER_TYPE,
+                TEST_CACHED_NUMBER_LABEL,
+                TEST_FORMATTED_NUMBER,
+                false /* inject */);
+
+        mAdapter.changeCursor(mCursor);
+        mAdapter.onBindViewHolder(mViewHolder, 0);
+
+        assertNameIs(mViewHolder, TEST_FORMATTED_NUMBER);
+    }
+
+    @MediumTest
     public void testBindVoicemailPromoCard() {
         createCallLogEntry(TEST_NUMBER_1);
         createCallLogEntry(TEST_NUMBER_1);
@@ -271,51 +445,106 @@
         assertEquals(TEST_NUMBER_3, mViewHolder.number);
     }
 
-    /** Returns a contact info with default values. */
-    private ContactInfo createContactInfo() {
-        ContactInfo info = new ContactInfo();
-        info.number = TEST_NUMBER_1;
-        info.name = TEST_NAME;
-        info.type = TEST_NUMBER_TYPE;
-        info.label = TEST_NUMBER_LABEL;
-        return info;
-    }
-
-    /** Returns a call log entry without cached values. */
     private void createCallLogEntry() {
-        createCallLogEntry(TEST_NUMBER_1);
+        createCallLogEntry(TEST_NUMBER);
     }
 
-    private void  createCallLogEntry(String testNumber) {
-        createCallLogEntry(testNumber, NO_VALUE_SET, NO_VALUE_SET, NO_VALUE_SET, NO_VALUE_SET);
+    private void createCallLogEntry(String testNumber) {
+        createCallLogEntry(testNumber, NO_VALUE_SET, NO_VALUE_SET);
     }
 
     private void createPrivateCallLogEntry() {
-        createCallLogEntry("", Calls.PRESENTATION_RESTRICTED, NO_VALUE_SET, 0, Calls.INCOMING_TYPE);
+        createCallLogEntry(EMPTY_STRING, Calls.PRESENTATION_RESTRICTED, Calls.INCOMING_TYPE);
+    }
+
+    private void createUnknownCallLogEntry() {
+        createCallLogEntry(EMPTY_STRING, Calls.PRESENTATION_UNKNOWN, Calls.INCOMING_TYPE);
     }
 
     private void createVoicemailCallLogEntry() {
-        createCallLogEntry(TEST_NUMBER_1, NO_VALUE_SET, NO_VALUE_SET, NO_VALUE_SET,
-                Calls.VOICEMAIL_TYPE, true /* isVoicemail */);
+        createCallLogEntry(TEST_NUMBER, NO_VALUE_SET, Calls.VOICEMAIL_TYPE);
     }
 
-    private void createCallLogEntry(
-            String number, int presentation, long date, int duration, int type) {
-        createCallLogEntry(number, presentation, date, duration, type, false /* isVoicemail */);
+    private void createCallLogEntry(String number, int presentation, int type) {
+        Object[] values = getValues(number, presentation, type);
+        mCursor.addRow(values);
     }
 
-    private void createCallLogEntry(
+    private void createCallLogEntryWithCachedValues(boolean inject) {
+        createCallLogEntryWithCachedValues(
+                TEST_NUMBER,
+                NO_VALUE_SET,
+                TEST_CACHED_NAME,
+                TEST_CACHED_NUMBER_TYPE,
+                TEST_CACHED_NUMBER_LABEL,
+                EMPTY_STRING,
+                inject);
+    }
+
+    private void createCallLogEntryWithCachedValues(
+            String cachedName, int cachedNumberType, String cachedNumberLabel) {
+        createCallLogEntryWithCachedValues(
+                TEST_NUMBER,
+                NO_VALUE_SET,
+                cachedName,
+                cachedNumberType,
+                cachedNumberLabel,
+                EMPTY_STRING,
+                false /* inject */);
+    }
+
+    /**
+     * Inserts a new call log entry
+     *
+     * It includes the values for the cached contact associated with the number.
+     *
+     * @param number The phone number.
+     * @param type Either Call.OUTGOING_TYPE or Call.INCOMING_TYPE or Call.MISSED_TYPE.
+     * @param cachedName The name of the contact with this number
+     * @param cachedNumberType The type of the number, from the contact with this number.
+     * @param cachedNumberLabel The label of the number, from the contact with this number.
+     * @param cachedFormattedNumber The formatted number, from the contact with this number.
+     * @param inject Whether to inject the contact info into the adapter's ContactInfoCache.
+    */
+    private void createCallLogEntryWithCachedValues(
+            String number,
+            int type,
+            String cachedName,
+            int cachedNumberType,
+            String cachedNumberLabel,
+            String cachedFormattedNumber,
+            boolean inject) {
+        Object[] values = getValues(number, NO_VALUE_SET, type);
+        values[CallLogQuery.CACHED_NAME] = cachedName;
+        values[CallLogQuery.CACHED_NUMBER_TYPE] = cachedNumberType;
+        values[CallLogQuery.CACHED_NUMBER_LABEL] = cachedNumberLabel;
+        values[CallLogQuery.CACHED_FORMATTED_NUMBER] = cachedFormattedNumber;
+
+        mCursor.addRow(values);
+
+        if (inject) {
+            ContactInfo contactInfo =
+                    createContactInfo(cachedName, cachedNumberType, cachedNumberLabel);
+            mAdapter.injectContactInfoForTest(number, TEST_COUNTRY_ISO, contactInfo);
+        }
+    }
+
+    /**
+     * @param number The phone number.
+     * @param presentation Number representing display rules for "allowed",
+     *               "payphone", "restricted", or "unknown".
+     * @param date In millisec since epoch. Use NOW to use the current time.
+     */
+    private Object[] getValues(
             String number,
             int presentation,
-            long date,
-            int duration,
-            int type,
-            boolean isVoicemail) {
+            int type) {
         Object[] values = CallLogQueryTestUtils.createTestValues();
 
-        values[CallLogQuery.ID] = mCursorSize;
+        values[CallLogQuery.ID] = mCursor.getCount();
         values[CallLogQuery.COUNTRY_ISO] = TEST_COUNTRY_ISO;
-        values[CallLogQuery.DATE] = date != NO_VALUE_SET ? date : new Date().getTime();
+        values[CallLogQuery.DATE] = new Date().getTime();
+        values[CallLogQuery.DURATION] = mRandom.nextInt(10 * 60);
 
         if (!TextUtils.isEmpty(number)) {
             values[CallLogQuery.NUMBER] = number;
@@ -323,38 +552,68 @@
         if (presentation != NO_VALUE_SET) {
             values[CallLogQuery.NUMBER_PRESENTATION] = presentation;
         }
-        if (duration != NO_VALUE_SET) {
-            values[CallLogQuery.DURATION] = (duration < 0) ? mRandom.nextInt(10 * 60) : duration;
-        }
         if (type != NO_VALUE_SET) {
             values[CallLogQuery.CALL_TYPE] = type;
         }
-        if (isVoicemail) {
-            values[CallLogQuery.VOICEMAIL_URI] =
-                ContentUris.withAppendedId(VoicemailContract.Voicemails.CONTENT_URI, mCursorSize);
+        if (type == Calls.VOICEMAIL_TYPE) {
+            values[CallLogQuery.VOICEMAIL_URI] = ContentUris.withAppendedId(
+                    VoicemailContract.Voicemails.CONTENT_URI, mCursor.getCount());
         }
 
-        mCursor.addRow(values);
-        mCursorSize++;
-    }
-
-    // Returns a call log entry with a cached values.
-    private Object[] createCallLogEntryWithCachedValues() {
-        Object[] values = CallLogQueryTestUtils.createTestValues();
-        values[CallLogQuery.NUMBER] = TEST_NUMBER_1;
-        values[CallLogQuery.COUNTRY_ISO] = TEST_COUNTRY_ISO;
-        values[CallLogQuery.CACHED_NAME] = TEST_NAME;
-        values[CallLogQuery.CACHED_NUMBER_TYPE] = TEST_NUMBER_TYPE;
-        values[CallLogQuery.CACHED_NUMBER_LABEL] = TEST_NUMBER_LABEL;
         return values;
     }
 
-    // Asserts that the name text view is shown and contains the given text./
+    private ContactInfo createContactInfo() {
+        return createContactInfo(
+                TEST_CACHED_NAME,
+                TEST_CACHED_NUMBER_TYPE,
+                TEST_CACHED_NUMBER_LABEL);
+    }
+
+    /** Returns a contact info with default values. */
+    private ContactInfo createContactInfo(String name, int type, String label) {
+        ContactInfo info = new ContactInfo();
+        info.number = TEST_NUMBER;
+        info.name = name;
+        info.type = type;
+        info.label = label;
+        info.formattedNumber = TEST_FORMATTED_NUMBER;
+        info.normalizedNumber = TEST_NUMBER;
+        info.lookupUri = TEST_LOOKUP_URI;
+        return info;
+    }
+
+    // Asserts that the name text view is shown and contains the given text.
     private void assertNameIs(CallLogListItemViewHolder viewHolder, String name) {
         assertEquals(View.VISIBLE, viewHolder.phoneCallDetailsViews.nameView.getVisibility());
         assertEquals(name, viewHolder.phoneCallDetailsViews.nameView.getText());
     }
 
+    // Asserts that the label text view contains the given text.
+    private void assertLabel(
+            CallLogListItemViewHolder viewHolder, CharSequence number, CharSequence label) {
+        if (label != null) {
+            assertTrue(viewHolder.phoneCallDetailsViews.callLocationAndDate.getText()
+                    .toString().contains(label));
+        }
+    }
+
+    private void assertHasCallAction(CallLogListItemViewHolder viewHolder) {
+        // The primaryActionView tag is set when the ViewHolder is binded. If it is possible
+        // to place a call to the phone number, a call intent will have been created which
+        // starts a phone call to the entry's number.
+        IntentProvider intentProvider =
+                (IntentProvider) viewHolder.primaryActionButtonView.getTag();
+        Intent intent = intentProvider.getIntent(getContext());
+        assertEquals(TestConstants.CALL_INTENT_ACTION, intent.getAction());
+        assertEquals(Uri.parse("tel:" + TEST_NUMBER), intent.getData());
+    }
+
+    /** Returns the label associated with a given phone type. */
+    private CharSequence getTypeLabel(int phoneType) {
+        return Phone.getTypeLabel(getContext().getResources(), phoneType, "");
+    }
+
     /// Subclass of {@link CallLogAdapter} used in tests to intercept certain calls.
     private static final class TestCallLogAdapter extends CallLogAdapter {
         public TestCallLogAdapter(Context context, CallFetcher callFetcher,
diff --git a/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java b/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java
deleted file mode 100644
index aa4ad80..0000000
--- a/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java
+++ /dev/null
@@ -1,545 +0,0 @@
-/*
- * Copyright (C) 2009 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 android.app.FragmentManager;
-import android.app.FragmentTransaction;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.database.MatrixCursor;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
-import android.net.Uri;
-import android.provider.CallLog.Calls;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.VoicemailContract;
-import android.telephony.PhoneNumberUtils;
-import android.telephony.TelephonyManager;
-import android.test.ActivityInstrumentationTestCase2;
-import android.test.suitebuilder.annotation.LargeTest;
-import android.test.suitebuilder.annotation.MediumTest;
-import android.util.Log;
-import android.view.View;
-import android.widget.FrameLayout;
-
-import com.android.contacts.common.test.FragmentTestActivity;
-import com.android.dialer.CallDetailActivity;
-import com.android.dialer.R;
-import com.android.dialer.util.TestConstants;
-
-import java.util.Date;
-import java.util.Formatter;
-import java.util.HashMap;
-import java.util.Random;
-
-/**
- * Tests for the contact call list activity.
- *
- * Running all tests:
- *
- *   runtest contacts
- * or
- *   adb shell am instrument \
- *     -w com.android.dialer.tests/android.test.InstrumentationTestRunner
- */
-@LargeTest
-public class CallLogFragmentTest extends ActivityInstrumentationTestCase2<FragmentTestActivity> {
-    private static final int RAND_DURATION = -1;
-    private static final long NOW = -1L;
-
-    /** A test value for the URI of a contact. */
-    private static final Uri TEST_LOOKUP_URI = Uri.parse("content://contacts/2");
-    /** A test value for the country ISO of the phone number in the call log. */
-    private static final String TEST_COUNTRY_ISO = "US";
-    /** A phone number to be used in tests. */
-    private static final String TEST_NUMBER = "12125551000";
-    /** The formatted version of {@link #TEST_NUMBER}. */
-    private static final String TEST_FORMATTED_NUMBER = "1 212-555-1000";
-
-    private static final String TEST_DEFAULT_CUSTOM_LABEL = "myLabel";
-
-    /** The activity in which we are hosting the fragment. */
-    private FragmentTestActivity mActivity;
-    private CallLogFragment mFragment;
-    private FrameLayout mParentView;
-    /**
-     * The adapter used by the fragment to build the rows in the call log. We use it with our own in
-     * memory database.
-     */
-    private CallLogAdapter mAdapter;
-    private String mVoicemail;
-
-    // In memory array to hold the rows corresponding to the 'calls' table.
-    private MatrixCursor mCursor;
-    private int mIndex;  // Of the next row.
-
-    private Random mRnd;
-
-    // An item in the call list. All the methods performing checks use it.
-    private CallLogListItemViewHolder mItem;
-
-    // The list of view holderss representing the data in the DB, in reverse order from the DB.
-    private CallLogListItemViewHolder[] mList;
-
-    public CallLogFragmentTest() {
-        super(FragmentTestActivity.class);
-        mIndex = 1;
-        mRnd = new Random();
-    }
-
-    @Override
-    public void setUp() {
-        mActivity = getActivity();
-        // Needed by the CallLogFragment.
-        mActivity.setTheme(R.style.DialtactsTheme);
-
-        // Create the fragment and load it into the activity.
-        mFragment = new CallLogFragment();
-        FragmentManager fragmentManager = mActivity.getFragmentManager();
-        FragmentTransaction transaction = fragmentManager.beginTransaction();
-        transaction.add(FragmentTestActivity.LAYOUT_ID, mFragment);
-        transaction.commitAllowingStateLoss();
-        // Wait for the fragment to be loaded.
-        getInstrumentation().waitForIdleSync();
-
-        final TelephonyManager telephonyManager =
-                (TelephonyManager) mActivity.getSystemService(Context.TELEPHONY_SERVICE);
-        mVoicemail = telephonyManager.getVoiceMailNumber();
-        mAdapter = mFragment.getAdapter();
-        // Do not process requests for details during tests. This would start a background thread,
-        // which makes the tests flaky.
-        mAdapter.disableRequestProcessingForTest();
-        mAdapter.pauseCache();
-        mParentView = new FrameLayout(mActivity);
-        mCursor = new MatrixCursor(CallLogQuery._PROJECTION);
-
-        getInstrumentation().runOnMainSync(new Runnable() {
-            @Override
-            public void run() {
-                mAdapter.changeCursor(mCursor);
-            }
-        });
-        getInstrumentation().waitForIdleSync();
-    }
-
-    /**
-     * Checks that the call icon is not visible for private and
-     * unknown numbers.
-     * Use 2 passes, one where new viewHolder are created and one where
-     * half of the total viewHolder are updated and the other half created.
-     */
-    @MediumTest
-    public void testCallViewIsNotVisibleForPrivateAndUnknownNumbers() {
-        final int SIZE = 50;
-        mList = new CallLogListItemViewHolder[SIZE];
-
-        // Insert the first batch of entries.
-        mCursor.moveToFirst();
-        insertRandomEntries(SIZE / 2);
-        int startOfSecondBatch = mCursor.getPosition();
-
-        buildViewListFromDb();
-        checkCallStatus();
-
-        // Append the rest of the entries. We keep the first set of
-        // viewHolder around so they get updated and not built from
-        // scratch, this exposes some bugs that are not there when the
-        // call log is launched for the 1st time but show up when the
-        // call log gets updated afterwards.
-        mCursor.move(startOfSecondBatch);
-        insertRandomEntries(SIZE / 2);
-
-        buildViewListFromDb();
-        checkCallStatus();
-    }
-
-    @MediumTest
-    public void testBindView_NumberOnlyDbCachedFormattedNumber() {
-        mCursor.moveToFirst();
-        Object[] values = getValuesToInsert(TEST_NUMBER,
-                Calls.PRESENTATION_ALLOWED, NOW, 0, Calls.INCOMING_TYPE);
-        values[CallLogQuery.CACHED_FORMATTED_NUMBER] = TEST_FORMATTED_NUMBER;
-        insertValues(values);
-        CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder)
-                mAdapter.onCreateViewHolder(mParentView, /* viewType */ 0);
-        bindViewForTest(viewHolder);
-
-        assertNameIs(viewHolder, TEST_FORMATTED_NUMBER);
-    }
-
-    @MediumTest
-    public void testBindView_WithCachedName() {
-        mCursor.moveToFirst();
-        insertWithCachedValues(TEST_NUMBER, NOW, 0, Calls.INCOMING_TYPE,
-                "John Doe", Phone.TYPE_HOME, TEST_DEFAULT_CUSTOM_LABEL);
-        CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder)
-                mAdapter.onCreateViewHolder(mParentView, /* viewType */ 0);
-        bindViewForTest(viewHolder);
-
-        assertNameIs(viewHolder, "John Doe");
-        assertLabel(viewHolder, TEST_FORMATTED_NUMBER, getTypeLabel(Phone.TYPE_HOME));
-    }
-
-    @MediumTest
-    public void testBindView_UriNumber() {
-        mCursor.moveToFirst();
-        insertWithCachedValues("sip:johndoe@gmail.com", NOW, 0, Calls.INCOMING_TYPE,
-                "John Doe", Phone.TYPE_HOME, TEST_DEFAULT_CUSTOM_LABEL);
-        CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder)
-                mAdapter.onCreateViewHolder(mParentView, /* viewType */ 0);
-        bindViewForTest(viewHolder);
-
-        assertNameIs(viewHolder, "John Doe");
-        assertLabel(viewHolder, "sip:johndoe@gmail.com", "sip:johndoe@gmail.com");
-    }
-
-    @MediumTest
-    public void testBindView_HomeLabel() {
-        mCursor.moveToFirst();
-        insertWithCachedValues(TEST_NUMBER, NOW, 0, Calls.INCOMING_TYPE,
-                "John Doe", Phone.TYPE_HOME, TEST_DEFAULT_CUSTOM_LABEL);
-        CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder)
-                mAdapter.onCreateViewHolder(mParentView, /* viewType */ 0);
-        bindViewForTest(viewHolder);
-
-        assertNameIs(viewHolder, "John Doe");
-        assertLabel(viewHolder, TEST_FORMATTED_NUMBER, getTypeLabel(Phone.TYPE_HOME));
-    }
-
-    @MediumTest
-    public void testBindView_WorkLabel() {
-        mCursor.moveToFirst();
-        insertWithCachedValues(TEST_NUMBER, NOW, 0, Calls.INCOMING_TYPE,
-                "John Doe", Phone.TYPE_WORK, TEST_DEFAULT_CUSTOM_LABEL);
-        CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder)
-                mAdapter.onCreateViewHolder(mParentView, /* viewType */ 0);
-        bindViewForTest(viewHolder);
-
-        assertNameIs(viewHolder, "John Doe");
-        assertLabel(viewHolder, TEST_FORMATTED_NUMBER, getTypeLabel(Phone.TYPE_WORK));
-    }
-
-    @MediumTest
-    public void testBindView_CustomLabel() {
-        mCursor.moveToFirst();
-        String numberLabel = "My label";
-        insertWithCachedValues(TEST_NUMBER, NOW, 0, Calls.INCOMING_TYPE,
-                "John Doe", Phone.TYPE_CUSTOM, numberLabel);
-        CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder)
-                mAdapter.onCreateViewHolder(mParentView, /* viewType */ 0);
-        bindViewForTest(viewHolder);
-
-        assertNameIs(viewHolder, "John Doe");
-        assertLabel(viewHolder, TEST_FORMATTED_NUMBER, numberLabel);
-    }
-
-
-    /** Returns the label associated with a given phone type. */
-    private CharSequence getTypeLabel(int phoneType) {
-        return Phone.getTypeLabel(getActivity().getResources(), phoneType, "");
-    }
-
-    //
-    // HELPERS to check conditions on the DB/viewHolder
-    //
-    /**
-     * Go over the viewHolder in the list and check to ensure that
-     * callable numbers have an associated call intent, where numbers
-     * which are not callable have a null intent.
-     */
-    private void checkCallStatus() {
-        for (int i = 0; i < mList.length; i++) {
-            if (null == mList[i]) {
-                break;
-            }
-            mItem = (CallLogListItemViewHolder) mList[i];
-            int presentation = getPhoneNumberPresentationForListEntry(i);
-            if (presentation == Calls.PRESENTATION_RESTRICTED ||
-                    presentation == Calls.PRESENTATION_UNKNOWN) {
-                //If number is not callable, the primary action view should have a null tag.
-                assertNull(mItem.primaryActionButtonView.getTag());
-            } else {
-                //If the number is callable, the primary action view should have a non-null tag.
-                assertNotNull(mItem.primaryActionButtonView.getTag());
-
-                IntentProvider intentProvider =
-                        (IntentProvider) mItem.primaryActionButtonView.getTag();
-                Intent callIntent = intentProvider.getIntent(mActivity);
-
-                //The intent should be to make the call
-                assertEquals(TestConstants.CALL_INTENT_ACTION, callIntent.getAction());
-            }
-        }
-    }
-
-    //
-    // HELPERS to setup the tests.
-    //
-
-    /**
-     * Get the Bitmap from the icons in the contacts package.
-     */
-    private Bitmap getBitmap(String resName) {
-        Resources r = mActivity.getResources();
-        int resid = r.getIdentifier(resName, "drawable",
-                getInstrumentation().getTargetContext().getPackageName());
-        BitmapDrawable d = (BitmapDrawable) r.getDrawable(resid);
-        assertNotNull(d);
-        return d.getBitmap();
-    }
-
-    //
-    // HELPERS to build/update the call entries (viewHolder) from the DB.
-    //
-
-    /**
-     * Read the DB and foreach call either update the existing view if
-     * one exists already otherwise create one.
-     * The list is build from a DESC view of the DB (last inserted entry is first).
-     */
-    private void buildViewListFromDb() {
-        int i = 0;
-        mCursor.moveToLast();
-        while (!mCursor.isBeforeFirst()) {
-            if (null == mList[i]) {
-                mList[i] = (CallLogListItemViewHolder)
-                        mAdapter.onCreateViewHolder(mParentView, /* itemType */ 0);
-            }
-            // Bind to the proper position, despite iterating in reverse.
-            bindViewForTest(mList[i], mCursor.getCount() - i - 1);
-            mCursor.moveToPrevious();
-            i++;
-        }
-    }
-
-    /** Returns the number presentation associated with the given entry in {{@link #mList}. */
-    private int getPhoneNumberPresentationForListEntry(int index) {
-        // The entries are added backward, so count from the end of the cursor.
-        mCursor.moveToPosition(mCursor.getCount() - index - 1);
-        return mCursor.getInt(CallLogQuery.NUMBER_PRESENTATION);
-    }
-
-    //
-    // HELPERS to insert numbers in the call log DB.
-    //
-
-    /**
-     * Bind a call log entry view for testing purposes.  Also inflates the action view stub so
-     * unit tests can access the buttons contained within.
-     *
-     * @param view The current call log row.
-     * @param position The position of the item.
-     */
-    private void bindViewForTest(final CallLogListItemViewHolder viewHolder, int position) {
-        mAdapter.onBindViewHolder(viewHolder, position);
-        getInstrumentation().runOnMainSync(new Runnable() {
-            @Override
-            public void run() {
-                viewHolder.inflateActionViewStub();
-            }
-        });
-        getInstrumentation().waitForIdleSync();
-    }
-
-    private void bindViewForTest(CallLogListItemViewHolder viewHolder) {
-        bindViewForTest(viewHolder, /* position */ 0);
-    }
-
-    /**
-     * Insert a certain number of random numbers in the DB. Makes sure
-     * there is at least one private and one unknown number in the DB.
-     * @param num Of entries to be inserted.
-     */
-    private void insertRandomEntries(int num) {
-        if (num < 10) {
-            throw new IllegalArgumentException("num should be >= 10");
-        }
-        boolean privateOrUnknownOrVm[];
-        privateOrUnknownOrVm = insertRandomRange(0, num - 2);
-
-        if (privateOrUnknownOrVm[0] && privateOrUnknownOrVm[1]) {
-            insertRandomRange(num - 2, num);
-        } else {
-            insertPrivate(NOW, RAND_DURATION);
-            insertUnknown(NOW, RAND_DURATION);
-        }
-    }
-
-    /**
-     * Insert a new call entry in the test DB.
-     *
-     * It includes the values for the cached contact associated with the number.
-     *
-     * @param number The phone number.
-     * @param date In millisec since epoch. Use NOW to use the current time.
-     * @param duration In seconds of the call. Use RAND_DURATION to pick a random one.
-     * @param type Either Call.OUTGOING_TYPE or Call.INCOMING_TYPE or Call.MISSED_TYPE.
-     * @param cachedName the name of the contact with this number
-     * @param cachedNumberType the type of the number, from the contact with this number
-     * @param cachedNumberLabel the label of the number, from the contact with this number
-     */
-    private void insertWithCachedValues(String number, long date, int duration, int type,
-            String cachedName, int cachedNumberType, String cachedNumberLabel) {
-        insert(number, Calls.PRESENTATION_ALLOWED, date, duration, type);
-        ContactInfo contactInfo = new ContactInfo();
-        contactInfo.lookupUri = TEST_LOOKUP_URI;
-        contactInfo.name = cachedName;
-        contactInfo.type = cachedNumberType;
-        contactInfo.label = cachedNumberLabel;
-        String formattedNumber = PhoneNumberUtils.formatNumber(number, TEST_COUNTRY_ISO);
-        if (formattedNumber == null) {
-            formattedNumber = number;
-        }
-        contactInfo.formattedNumber = formattedNumber;
-        contactInfo.normalizedNumber = number;
-        contactInfo.photoId = 0;
-        mAdapter.injectContactInfoForTest(number, TEST_COUNTRY_ISO, contactInfo);
-    }
-
-    /**
-     * Insert a new call entry in the test DB.
-     * @param number The phone number.
-     * @param presentation Number representing display rules for "allowed",
-     *               "payphone", "restricted", or "unknown".
-     * @param date In millisec since epoch. Use NOW to use the current time.
-     * @param duration In seconds of the call. Use RAND_DURATION to pick a random one.
-     * @param type Either Call.OUTGOING_TYPE or Call.INCOMING_TYPE or Call.MISSED_TYPE.
-     */
-    private void insert(String number, int presentation, long date, int duration, int type) {
-        insertValues(getValuesToInsert(number, presentation, date, duration, type));
-    }
-
-    /** Inserts the given values in the cursor. */
-    private void insertValues(Object[] values) {
-        mCursor.addRow(values);
-        ++mIndex;
-    }
-
-    /**
-     * Returns the values for a new call entry.
-     *
-     * @param number The phone number.
-     * @param presentation Number representing display rules for "allowed",
-     *               "payphone", "restricted", or "unknown".
-     * @param date In millisec since epoch. Use NOW to use the current time.
-     * @param duration In seconds of the call. Use RAND_DURATION to pick a random one.
-     * @param type Either Call.OUTGOING_TYPE or Call.INCOMING_TYPE or Call.MISSED_TYPE.
-     */
-    private Object[] getValuesToInsert(String number, int presentation,
-            long date, int duration, int type) {
-        Object[] values = CallLogQueryTestUtils.createTestValues();
-        values[CallLogQuery.ID] = mIndex;
-        values[CallLogQuery.NUMBER] = number;
-        values[CallLogQuery.NUMBER_PRESENTATION] = presentation;
-        values[CallLogQuery.DATE] = date == NOW ? new Date().getTime() : date;
-        values[CallLogQuery.DURATION] = duration < 0 ? mRnd.nextInt(10 * 60) : duration;
-        if (mVoicemail != null && mVoicemail.equals(number)) {
-            assertEquals(Calls.OUTGOING_TYPE, type);
-        }
-        values[CallLogQuery.CALL_TYPE] = type;
-        values[CallLogQuery.COUNTRY_ISO] = TEST_COUNTRY_ISO;
-        return values;
-    }
-
-    /**
-     * Insert a new private call entry in the test DB.
-     * @param date In millisec since epoch. Use NOW to use the current time.
-     * @param duration In seconds of the call. Use RAND_DURATION to pick a random one.
-     */
-    private void insertPrivate(long date, int duration) {
-        insert("", Calls.PRESENTATION_RESTRICTED, date, duration, Calls.INCOMING_TYPE);
-    }
-
-    /**
-     * Insert a new unknown call entry in the test DB.
-     * @param date In millisec since epoch. Use NOW to use the current time.
-     * @param duration In seconds of the call. Use RAND_DURATION to pick a random one.
-     */
-    private void insertUnknown(long date, int duration) {
-        insert("", Calls.PRESENTATION_UNKNOWN, date, duration, Calls.INCOMING_TYPE);
-    }
-
-    /**
-     * Insert a new call to voicemail entry in the test DB.
-     * @param date In millisec since epoch. Use NOW to use the current time.
-     * @param duration In seconds of the call. Use RAND_DURATION to pick a random one.
-     */
-    private void insertCalltoVoicemail(long date, int duration) {
-        // mVoicemail may be null
-        if (mVoicemail != null) {
-            insert(mVoicemail, Calls.PRESENTATION_ALLOWED, date, duration, Calls.OUTGOING_TYPE);
-        }
-    }
-
-    /**
-     * Insert a range [start, end) of random numbers in the DB. For
-     * each row, there is a 1/10 probability that the number will be
-     * marked as PRIVATE or UNKNOWN or VOICEMAIL. For regular numbers, a number is
-     * inserted, its last 4 digits will be the number of the iteration
-     * in the range.
-     * @param start Of the range.
-     * @param end Of the range (excluded).
-     * @return An array with 2 booleans [0 = private number, 1 =
-     * unknown number, 2 = voicemail] to indicate if at least one
-     * private or unknown or voicemail number has been inserted. Since
-     * the numbers are random some tests may want to enforce the
-     * insertion of such numbers.
-     */
-    // TODO: Should insert numbers with contact entries too.
-    private boolean[] insertRandomRange(int start, int end) {
-        boolean[] privateOrUnknownOrVm = new boolean[] {false, false, false};
-
-        for (int i = start; i < end; i++ ) {
-            int type = mRnd.nextInt(10);
-
-            if (0 == type) {
-                insertPrivate(NOW, RAND_DURATION);
-                privateOrUnknownOrVm[0] = true;
-            } else if (1 == type) {
-                insertUnknown(NOW, RAND_DURATION);
-                privateOrUnknownOrVm[1] = true;
-            } else if (2 == type) {
-                insertCalltoVoicemail(NOW, RAND_DURATION);
-                privateOrUnknownOrVm[2] = true;
-            } else {
-                int inout = mRnd.nextBoolean() ? Calls.OUTGOING_TYPE :  Calls.INCOMING_TYPE;
-                final Formatter formatter = new Formatter();
-                String number = formatter.format("1800123%04d", i).toString();
-                formatter.close();
-                insert(number, Calls.PRESENTATION_ALLOWED, NOW, RAND_DURATION, inout);
-            }
-        }
-        return privateOrUnknownOrVm;
-    }
-
-    /** Asserts that the name text view is shown and contains the given text. */
-    private void assertNameIs(CallLogListItemViewHolder viewHolder, String name) {
-        assertEquals(View.VISIBLE, viewHolder.phoneCallDetailsViews.nameView.getVisibility());
-        assertEquals(name, viewHolder.phoneCallDetailsViews.nameView.getText().toString());
-    }
-
-    /** Asserts that the label text view contains the given text. */
-    private void assertLabel(CallLogListItemViewHolder viewHolder, CharSequence number,
-            CharSequence label) {
-        if (label != null) {
-            assertTrue(viewHolder.phoneCallDetailsViews.callLocationAndDate.getText()
-                    .toString().contains(label));
-        }
-    }
-}
diff --git a/tests/src/com/android/dialer/calllog/CallLogGroupBuilderTest.java b/tests/src/com/android/dialer/calllog/CallLogGroupBuilderTest.java
index 95558bc..9bdcd40 100644
--- a/tests/src/com/android/dialer/calllog/CallLogGroupBuilderTest.java
+++ b/tests/src/com/android/dialer/calllog/CallLogGroupBuilderTest.java
@@ -66,14 +66,14 @@
     public void testAddGroups_OneCall() {
         addCallLogEntry(TEST_NUMBER1, Calls.INCOMING_TYPE);
         mBuilder.addGroups(mCursor);
-        assertEquals(0, mFakeGroupCreator.groups.size());
+        assertEquals(1, mFakeGroupCreator.groups.size());
     }
 
     public void testAddGroups_TwoCallsNotMatching() {
         addCallLogEntry(TEST_NUMBER1, Calls.INCOMING_TYPE);
         addCallLogEntry(TEST_NUMBER2, Calls.INCOMING_TYPE);
         mBuilder.addGroups(mCursor);
-        assertEquals(0, mFakeGroupCreator.groups.size());
+        assertEquals(2, mFakeGroupCreator.groups.size());
     }
 
     public void testAddGroups_ThreeCallsMatching() {
@@ -136,21 +136,25 @@
 
     public void testAddGroups_Mixed() {
         addMultipleCallLogEntries(TEST_NUMBER1,
-                Calls.VOICEMAIL_TYPE,  // Stand-alone
-                Calls.INCOMING_TYPE,  // Group 1: 1-4
+                Calls.VOICEMAIL_TYPE,   // Group 1:Stand-alone
+                Calls.INCOMING_TYPE,    // Group 2: 1-4
                 Calls.OUTGOING_TYPE,
                 Calls.MISSED_TYPE,
                 Calls.MISSED_TYPE,
-                Calls.VOICEMAIL_TYPE,  // Stand-alone
-                Calls.INCOMING_TYPE,  // Stand-alone
-                Calls.VOICEMAIL_TYPE,  // Stand-alone
-                Calls.MISSED_TYPE, // Group 2: 8-10
+                Calls.VOICEMAIL_TYPE,   // Group 3: Stand-alone
+                Calls.INCOMING_TYPE,    // Group 4: Stand-alone
+                Calls.VOICEMAIL_TYPE,   // Group 5: Stand-alone
+                Calls.MISSED_TYPE,      // Group 6: 8-10
                 Calls.MISSED_TYPE,
                 Calls.OUTGOING_TYPE);
         mBuilder.addGroups(mCursor);
-        assertEquals(2, mFakeGroupCreator.groups.size());
-        assertGroupIs(1, 4, mFakeGroupCreator.groups.get(0));
-        assertGroupIs(8, 3, mFakeGroupCreator.groups.get(1));
+        assertEquals(6, mFakeGroupCreator.groups.size());
+        assertGroupIs(0, 1, mFakeGroupCreator.groups.get(0));
+        assertGroupIs(1, 4, mFakeGroupCreator.groups.get(1));
+        assertGroupIs(5, 1, mFakeGroupCreator.groups.get(2));
+        assertGroupIs(6, 1, mFakeGroupCreator.groups.get(3));
+        assertGroupIs(7, 1, mFakeGroupCreator.groups.get(4));
+        assertGroupIs(8, 3, mFakeGroupCreator.groups.get(5));
     }
 
     public void testEqualPhoneNumbers() {
@@ -237,7 +241,7 @@
         clearFakeGroupCreator();
         addMultipleCallLogEntries(TEST_NUMBER1, types);
         mBuilder.addGroups(mCursor);
-        assertEquals(0, mFakeGroupCreator.groups.size());
+        assertEquals(types.length, mFakeGroupCreator.groups.size());
     }
 
     /** Adds a set of calls with the given types, all from the same number, in the old section. */
diff --git a/tests/src/com/android/dialer/voicemail/VoicemailPlaybackTest.java b/tests/src/com/android/dialer/voicemail/VoicemailPlaybackTest.java
index b9c70d3..420a17c 100644
--- a/tests/src/com/android/dialer/voicemail/VoicemailPlaybackTest.java
+++ b/tests/src/com/android/dialer/voicemail/VoicemailPlaybackTest.java
@@ -17,6 +17,7 @@
 package com.android.dialer.voicemail;
 
 import static com.android.dialer.voicemail.VoicemailPlaybackPresenter.Tasks.CHECK_FOR_CONTENT;
+import static com.android.dialer.voicemail.VoicemailPlaybackPresenter.Tasks.CHECK_CONTENT_AFTER_CHANGE;
 
 import android.app.Activity;
 import android.content.ContentResolver;
@@ -31,7 +32,6 @@
 import android.view.View;
 import android.widget.TextView;
 
-import com.android.contacts.common.test.IntegrationTestUtils;
 import com.android.dialer.R;
 import com.android.dialer.calllog.CallLogActivity;
 import com.android.dialer.util.AsyncTaskExecutors;
@@ -61,7 +61,6 @@
     private VoicemailPlaybackLayout mLayout;
 
     private Uri mVoicemailUri;
-    private IntegrationTestUtils mTestUtils;
     private LocaleTestUtils mLocaleTestUtils;
     private FakeAsyncTaskExecutor mFakeAsyncTaskExecutor;
 
@@ -75,7 +74,6 @@
 
         mFakeAsyncTaskExecutor = new FakeAsyncTaskExecutor(getInstrumentation());
         AsyncTaskExecutors.setFactoryForTest(mFakeAsyncTaskExecutor.getFactory());
-        mTestUtils = new IntegrationTestUtils(getInstrumentation());
 
         // Some of the tests rely on the text - safest to force a specific locale.
         mLocaleTestUtils = new LocaleTestUtils(getInstrumentation().getTargetContext());
@@ -95,56 +93,73 @@
         mLocaleTestUtils.restoreLocale();
         mLocaleTestUtils = null;
 
-        mLayout = null;
-        mPresenter = null;
-        mTestUtils = null;
+        mPresenter.clearInstance();
         AsyncTaskExecutors.setFactoryForTest(null);
 
+        mActivity = null;
+        mPresenter = null;
+        mLayout = null;
+
         super.tearDown();
     }
 
     public void testFetchingVoicemail() throws Throwable {
-        setUriForRealFileVoicemailEntry();
+        setUriForUnfetchedVoicemailEntry();
         setPlaybackViewForPresenter();
-        assertHasOneTextViewContaining("Loading voicemail");
+
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mPresenter.resumePlayback();
+            }
+        });
+        mFakeAsyncTaskExecutor.runTask(CHECK_FOR_CONTENT);
+        getInstrumentation().waitForIdleSync();
+
+        assertStateTextContains("Loading voicemail");
     }
 
     public void testWhenCheckForContentCompletes() throws Throwable {
         setUriForRealFileVoicemailEntry();
         setPlaybackViewForPresenter();
 
-        // There is a background check that is testing to see if we have the content available.
-        // Once that task completes, we shouldn't be showing the fetching message.
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mPresenter.resumePlayback();
+            }
+        });
         mFakeAsyncTaskExecutor.runTask(CHECK_FOR_CONTENT);
         getInstrumentation().waitForIdleSync();
 
-        assertHasOneTextViewContaining("Buffering");
-        assertHasZeroTextViewsContaining("Loading voicemail");
+        // Since the content is already fetched, don't show the loading message.
+        assertStateTextNotContains("Loading voicemail");
     }
 
     public void testInvalidVoicemailShowsErrorMessage() throws Throwable {
         setUriForInvalidVoicemailEntry();
         setPlaybackViewForPresenter();
 
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mPresenter.resumePlayback();
+            }
+        });
         mFakeAsyncTaskExecutor.runTask(CHECK_FOR_CONTENT);
         getInstrumentation().waitForIdleSync();
 
         // The media player will have thrown an IOException since the file doesn't exist.
         // This should have put a failed to play message on screen, buffering is gone.
-        assertHasOneTextViewContaining("Couldn't play voicemail");
-        assertHasZeroTextViewsContaining("Buffering");
+        assertStateTextContains("Couldn't play voicemail");
+        assertStateTextNotContains("Buffering");
     }
 
     public void testClickingSpeakerphoneButton() throws Throwable {
         setUriForRealFileVoicemailEntry();
         setPlaybackViewForPresenter();
 
-        // Wait for check for content to complete.
-        mFakeAsyncTaskExecutor.runTask(CHECK_FOR_CONTENT);
-        getInstrumentation().waitForIdleSync();
-
-        // Force the speakerphone to false to start.
-        mPresenter.setSpeakerphoneOn(false);
+        // Check that the speakerphone is false to start.
         assertFalse(mPresenter.isSpeakerphoneOn());
 
         View speakerphoneButton = mLayout.findViewById(R.id.playback_speakerphone);
@@ -177,6 +192,18 @@
         }
     }
 
+    private void setUriForUnfetchedVoicemailEntry() {
+        assertNull(mVoicemailUri);
+        ContentValues values = new ContentValues();
+        values.put(VoicemailContract.Voicemails.DATE, String.valueOf(System.currentTimeMillis()));
+        values.put(VoicemailContract.Voicemails.NUMBER, CONTACT_NUMBER);
+        values.put(VoicemailContract.Voicemails.MIME_TYPE, MIME_TYPE);
+        values.put(VoicemailContract.Voicemails.HAS_CONTENT, 0);
+        String packageName = getInstrumentation().getTargetContext().getPackageName();
+        mVoicemailUri = getContentResolver().insert(
+                VoicemailContract.Voicemails.buildSourceUri(packageName), values);
+    }
+
     private void setUriForInvalidVoicemailEntry() {
         assertNull(mVoicemailUri);
         ContentResolver contentResolver = getContentResolver();
@@ -205,18 +232,14 @@
         }
     }
 
-    private void assertHasOneTextViewContaining(String text) throws Throwable {
+    private void assertStateTextContains(String text) throws Throwable {
         assertNotNull(mLayout);
-        List<TextView> views = mTestUtils.getTextViewsWithString(mLayout, text);
-        assertEquals("There should have been one TextView with text '" + text + "' but found "
-                + views, 1, views.size());
+        assertTrue(mLayout.getStateText().contains(text));
     }
 
-    private void assertHasZeroTextViewsContaining(String text) throws Throwable {
+    private void assertStateTextNotContains(String text) throws Throwable {
         assertNotNull(mLayout);
-        List<TextView> views = mTestUtils.getTextViewsWithString(mLayout, text);
-        assertEquals("There should have been no TextViews with text '" + text + "' but found "
-                + views, 0,  views.size());
+        assertFalse(mLayout.getStateText().contains(text));
     }
 
     private ContentResolver getContentResolver() {