Merge "Fix layout so that all 4 buttons show on smaller screens." into nyc-dev
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index d23fca6..06f5795 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -158,6 +158,11 @@
             android:exported="false">
         </activity>
 
+        <activity android:name="com.android.dialer.voicemail.VoicemailArchiveActivity"
+            android:label="@string/voicemail_archive_activity_title"
+            android:theme="@style/DialtactsThemeWithoutActionBarOverlay">
+        </activity>
+
         <activity android:name="com.android.dialer.calllog.CallLogActivity"
             android:label="@string/call_log_activity_title"
             android:theme="@style/DialtactsThemeWithoutActionBarOverlay"
diff --git a/src/com/android/dialer/DialtactsActivity.java b/src/com/android/dialer/DialtactsActivity.java
index d12cf24..e775b0a 100644
--- a/src/com/android/dialer/DialtactsActivity.java
+++ b/src/com/android/dialer/DialtactsActivity.java
@@ -16,6 +16,7 @@
 
 package com.android.dialer;
 
+import com.android.dialer.voicemail.VoicemailArchiveActivity;
 import com.google.common.annotations.VisibleForTesting;
 
 import android.app.Fragment;
@@ -690,6 +691,10 @@
             handleMenuSettings();
             Logger.logScreenView(ScreenEvent.SETTINGS, this);
             return true;
+        } else if (resId == R.id.menu_archive) {
+            final Intent intent = new Intent(this, VoicemailArchiveActivity.class);
+            startActivity(intent);
+            return true;
         }
         return false;
     }
diff --git a/src/com/android/dialer/calllog/CallLogAdapter.java b/src/com/android/dialer/calllog/CallLogAdapter.java
index 6f96ee5..e97f8e2 100644
--- a/src/com/android/dialer/calllog/CallLogAdapter.java
+++ b/src/com/android/dialer/calllog/CallLogAdapter.java
@@ -16,7 +16,10 @@
 
 package com.android.dialer.calllog;
 
+import com.android.contacts.common.util.PermissionsUtil;
+
 import com.android.dialer.DialtactsActivity;
+import com.android.dialer.database.VoicemailArchiveContract;
 import com.google.common.annotations.VisibleForTesting;
 
 import android.content.Context;
@@ -72,6 +75,11 @@
                 VoicemailPlaybackPresenter.OnVoicemailDeletedListener,
                 ExtendedBlockingButtonRenderer.Listener {
 
+    // Types of activities the call log adapter is used for
+    public static final int ACTIVITY_TYPE_CALL_LOG = 1;
+    public static final int ACTIVITY_TYPE_ARCHIVE = 2;
+    public static final int ACTIVITY_TYPE_DIALTACTS = 3;
+
     /** Interface used to initiate a refresh of the content. */
     public interface CallFetcher {
         public void fetchCalls();
@@ -102,7 +110,7 @@
 
     protected ContactInfoCache mContactInfoCache;
 
-    private boolean mIsCallLogActivity;
+    private final int mActivityType;
 
     private static final String KEY_EXPANDED_POSITION = "expanded_position";
     private static final String KEY_EXPANDED_ROW_ID = "expanded_row_id";
@@ -172,7 +180,7 @@
             } else {
                 if (viewHolder.callType == CallLog.Calls.MISSED_TYPE) {
                     CallLogAsyncTaskUtil.markCallAsRead(mContext, viewHolder.callIds);
-                    if (!mIsCallLogActivity) {
+                    if (mActivityType == ACTIVITY_TYPE_DIALTACTS) {
                         ((DialtactsActivity) v.getContext()).updateTabUnreadCounts();
                     }
                 }
@@ -255,7 +263,7 @@
             CallFetcher callFetcher,
             ContactInfoHelper contactInfoHelper,
             VoicemailPlaybackPresenter voicemailPlaybackPresenter,
-            boolean isCallLogActivity) {
+            int activityType) {
         super(context);
 
         mContext = context;
@@ -265,7 +273,8 @@
         if (mVoicemailPlaybackPresenter != null) {
             mVoicemailPlaybackPresenter.setOnVoicemailDeletedListener(this);
         }
-        mIsCallLogActivity = isCallLogActivity;
+
+        mActivityType = activityType;
 
         mContactInfoCache = new ContactInfoCache(
                 mContactInfoHelper, mOnContactInfoChangedListener);
@@ -375,6 +384,11 @@
     }
 
     @Override
+    public void addVoicemailGroups(Cursor cursor) {
+        mCallLogGroupBuilder.addVoicemailGroups(cursor);
+    }
+
+    @Override
     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
         if (viewType == VIEW_TYPE_VOICEMAIL_PROMO_CARD) {
             return createVoicemailPromoCardViewHolder(parent);
@@ -415,7 +429,7 @@
 
                     @Override
                     public void onChangeFilteredNumberUndo() {}
-                });
+                }, mActivityType == ACTIVITY_TYPE_ARCHIVE);
 
         viewHolder.callLogEntryView.setTag(viewHolder);
         viewHolder.callLogEntryView.setAccessibilityDelegate(mAccessibilityDelegate);
@@ -479,7 +493,8 @@
 
         final String number = c.getString(CallLogQuery.NUMBER);
         final String postDialDigits = PhoneNumberDisplayUtil.canShowPostDial()
-                ? c.getString(CallLogQuery.POST_DIAL_DIGITS) : "";
+                && mActivityType != ACTIVITY_TYPE_ARCHIVE ?
+                c.getString(CallLogQuery.POST_DIAL_DIGITS) : "";
 
         final int numberPresentation = c.getInt(CallLogQuery.NUMBER_PRESENTATION);
         final PhoneAccountHandle accountHandle = PhoneAccountUtils.getAccount(
@@ -506,17 +521,13 @@
                 mContext, number, numberPresentation, formattedNumber,
                 postDialDigits, isVoicemailNumber);
         details.accountHandle = accountHandle;
-        details.callTypes = getCallTypes(c, count);
         details.countryIso = countryIso;
         details.date = c.getLong(CallLogQuery.DATE);
         details.duration = c.getLong(CallLogQuery.DURATION);
         details.features = getCallFeatures(c, count);
         details.geocode = c.getString(CallLogQuery.GEOCODED_LOCATION);
         details.transcription = c.getString(CallLogQuery.TRANSCRIPTION);
-        if (details.callTypes[0] == CallLog.Calls.VOICEMAIL_TYPE ||
-                details.callTypes[0] == CallLog.Calls.MISSED_TYPE) {
-            details.isRead = c.getInt(CallLogQuery.IS_READ) == 1;
-        }
+        details.callTypes = getCallTypes(c, count);
 
         if (!c.isNull(CallLogQuery.DATA_USAGE)) {
             details.dataUsage = c.getLong(CallLogQuery.DATA_USAGE);
@@ -543,9 +554,8 @@
         views.postDialDigits = details.postDialDigits;
         views.displayNumber = details.displayNumber;
         views.numberPresentation = numberPresentation;
-        views.callType = c.getInt(CallLogQuery.CALL_TYPE);
+
         views.accountHandle = accountHandle;
-        views.voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI);
         // Stash away the Ids of the calls so that we can support deleting a row in the call log.
         views.callIds = getCallIds(c, count);
         views.isBusiness = mContactInfoHelper.isBusiness(info.sourceType);
@@ -566,6 +576,21 @@
             views.dayGroupHeader.setVisibility(View.GONE);
         }
 
+        if (mActivityType == ACTIVITY_TYPE_ARCHIVE) {
+            views.callType = CallLog.Calls.VOICEMAIL_TYPE;
+            views.voicemailUri = VoicemailArchiveContract.VoicemailArchive.buildWithId(c.getInt(
+                    c.getColumnIndex(VoicemailArchiveContract.VoicemailArchive._ID)))
+                    .toString();
+
+        } else {
+            if (details.callTypes[0] == CallLog.Calls.VOICEMAIL_TYPE ||
+                    details.callTypes[0] == CallLog.Calls.MISSED_TYPE) {
+                details.isRead = c.getInt(CallLogQuery.IS_READ) == 1;
+            }
+            views.callType = c.getInt(CallLogQuery.CALL_TYPE);
+            views.voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI);
+        }
+
         mCallLogListItemHelper.setPhoneCallDetails(views, details);
 
         if (mCurrentlyExpandedRowId == views.rowId) {
@@ -613,7 +638,7 @@
     public Object getItem(int position) {
         return super.getItem(position - (mShowVoicemailPromoCard ? 1 : 0)
                 + ((mHiddenPosition != RecyclerView.NO_POSITION && position >= mHiddenPosition)
-                        ? 1 : 0));
+                ? 1 : 0));
     }
 
     @Override
@@ -622,7 +647,7 @@
     }
 
     protected boolean isCallLogActivity() {
-        return mIsCallLogActivity;
+        return mActivityType == ACTIVITY_TYPE_CALL_LOG;
     }
 
     /**
@@ -740,6 +765,9 @@
      * It position in the cursor is unchanged by this function.
      */
     private int[] getCallTypes(Cursor cursor, int count) {
+        if (mActivityType == ACTIVITY_TYPE_ARCHIVE) {
+            return new int[] {CallLog.Calls.VOICEMAIL_TYPE};
+        }
         int position = cursor.getPosition();
         int[] callTypes = new int[count];
         for (int index = 0; index < count; ++index) {
@@ -851,7 +879,8 @@
     private void maybeShowVoicemailPromoCard() {
         boolean showPromoCard = mPrefs.getBoolean(SHOW_VOICEMAIL_PROMO_CARD,
                 SHOW_VOICEMAIL_PROMO_CARD_DEFAULT);
-        mShowVoicemailPromoCard = (mVoicemailPlaybackPresenter != null) && showPromoCard;
+        mShowVoicemailPromoCard = mActivityType != ACTIVITY_TYPE_ARCHIVE &&
+                (mVoicemailPlaybackPresenter != null) && showPromoCard;
     }
 
     /**
diff --git a/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java b/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java
index 9825918..13de077 100644
--- a/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java
+++ b/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java
@@ -16,6 +16,7 @@
 
 package com.android.dialer.calllog;
 
+import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
@@ -32,6 +33,7 @@
 import com.android.contacts.common.util.PermissionsUtil;
 import com.android.dialer.DialtactsActivity;
 import com.android.dialer.PhoneCallDetails;
+import com.android.dialer.database.VoicemailArchiveContract;
 import com.android.dialer.util.AppCompatConstants;
 import com.android.dialer.util.AsyncTaskExecutor;
 import com.android.dialer.util.AsyncTaskExecutors;
@@ -413,16 +415,16 @@
     }
 
     /**
-     * Updates the duration of a voicemail call log entry.
+     * Updates the duration of a voicemail call log entry if the duration given is greater than 0,
+     * and if if the duration currently in the database is less than or equal to 0 (non-existent).
      */
     public static void updateVoicemailDuration(
             final Context context,
             final Uri voicemailUri,
-            final int duration) {
-        if (!PermissionsUtil.hasPhonePermissions(context)) {
+            final long duration) {
+        if (duration <= 0 || !PermissionsUtil.hasPhonePermissions(context)) {
             return;
         }
-
         if (sAsyncTaskExecutor == null) {
             initTaskExecutor();
         }
@@ -430,9 +432,18 @@
         sAsyncTaskExecutor.submit(Tasks.UPDATE_DURATION, new AsyncTask<Void, Void, Void>() {
             @Override
             public Void doInBackground(Void... params) {
-                ContentValues values = new ContentValues(1);
-                values.put(CallLog.Calls.DURATION, duration);
-                context.getContentResolver().update(voicemailUri, values, null, null);
+                ContentResolver contentResolver = context.getContentResolver();
+                Cursor cursor = contentResolver.query(
+                        voicemailUri,
+                        new String[] { VoicemailArchiveContract.VoicemailArchive.DURATION },
+                        null, null, null);
+                if (cursor != null && cursor.moveToFirst() && cursor.getInt(
+                        cursor.getColumnIndex(
+                                VoicemailArchiveContract.VoicemailArchive.DURATION)) <= 0) {
+                    ContentValues values = new ContentValues(1);
+                    values.put(CallLog.Calls.DURATION, duration);
+                    context.getContentResolver().update(voicemailUri, values, null, null);
+                }
                 return null;
             }
         });
diff --git a/src/com/android/dialer/calllog/CallLogFragment.java b/src/com/android/dialer/calllog/CallLogFragment.java
index 9cd1359..07299a2 100644
--- a/src/com/android/dialer/calllog/CallLogFragment.java
+++ b/src/com/android/dialer/calllog/CallLogFragment.java
@@ -300,13 +300,15 @@
         mEmptyListView.setImage(R.drawable.empty_call_log);
         mEmptyListView.setActionClickedListener(this);
 
+        int activityType = mIsCallLogActivity ? CallLogAdapter.ACTIVITY_TYPE_CALL_LOG :
+                CallLogAdapter.ACTIVITY_TYPE_DIALTACTS;
         String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
         mAdapter = ObjectFactory.newCallLogAdapter(
-                getActivity(),
-                this,
-                new ContactInfoHelper(getActivity(), currentCountryIso),
-                voicemailPlaybackPresenter,
-                mIsCallLogActivity);
+                        getActivity(),
+                        this,
+                        new ContactInfoHelper(getActivity(), currentCountryIso),
+                        voicemailPlaybackPresenter,
+                        activityType);
         mRecyclerView.setAdapter(mAdapter);
         fetchCalls();
     }
diff --git a/src/com/android/dialer/calllog/CallLogGroupBuilder.java b/src/com/android/dialer/calllog/CallLogGroupBuilder.java
index 194231b..950f634 100644
--- a/src/com/android/dialer/calllog/CallLogGroupBuilder.java
+++ b/src/com/android/dialer/calllog/CallLogGroupBuilder.java
@@ -187,6 +187,33 @@
         mGroupCreator.addGroup(count - groupSize, groupSize);
     }
 
+    /**
+     * Group cursor entries by date, with only one entry per group. This is used for listing
+     * voicemails in the archive tab.
+     */
+    public void addVoicemailGroups(Cursor cursor) {
+        if (cursor.getCount() == 0) {
+            return;
+        }
+
+        // Clear any previous day grouping information.
+        mGroupCreator.clearDayGroups();
+
+        // Get current system time, used for calculating which day group calls belong to.
+        long currentTime = System.currentTimeMillis();
+
+        // Reset cursor to start before the first row
+        cursor.moveToPosition(-1);
+
+        // Create an individual group for each voicemail
+        while (cursor.moveToNext()) {
+            mGroupCreator.addGroup(cursor.getPosition(), 1);
+            mGroupCreator.setDayGroup(cursor.getLong(CallLogQuery.ID),
+                    getDayGroup(cursor.getLong(CallLogQuery.DATE), currentTime));
+
+        }
+    }
+
     @VisibleForTesting
     boolean equalNumbers(String number1, String number2) {
         if (PhoneNumberHelper.isUriNumber(number1) || PhoneNumberHelper.isUriNumber(number2)) {
diff --git a/src/com/android/dialer/calllog/CallLogListItemViewHolder.java b/src/com/android/dialer/calllog/CallLogListItemViewHolder.java
index 6c25275..392672f 100644
--- a/src/com/android/dialer/calllog/CallLogListItemViewHolder.java
+++ b/src/com/android/dialer/calllog/CallLogListItemViewHolder.java
@@ -34,6 +34,7 @@
 import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewStub;
+import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.QuickContactBadge;
 import android.widget.TextView;
@@ -200,6 +201,11 @@
      */
     public boolean isBlocked;
 
+    /**
+     * Whether this is the archive tab or not.
+     */
+    public final boolean isArchiveTab;
+
     private final Context mContext;
     private final CallLogCache mCallLogCache;
     private final CallLogListItemHelper mCallLogListItemHelper;
@@ -230,7 +236,8 @@
             PhoneCallDetailsViews phoneCallDetailsViews,
             CardView callLogEntryView,
             TextView dayGroupHeader,
-            ImageView primaryActionButtonView) {
+            ImageView primaryActionButtonView,
+            boolean isArchiveTab) {
         super(rootView);
 
         mContext = context;
@@ -249,6 +256,7 @@
         this.dayGroupHeader = dayGroupHeader;
         this.primaryActionButtonView = primaryActionButtonView;
         this.workIconView = (ImageView) rootView.findViewById(R.id.work_profile_icon);
+        this.isArchiveTab = isArchiveTab;
         Resources resources = mContext.getResources();
         mPhotoSize = mContext.getResources().getDimensionPixelSize(R.dimen.contact_photo_size);
 
@@ -276,7 +284,9 @@
             CallLogListItemHelper callLogListItemHelper,
             VoicemailPlaybackPresenter voicemailPlaybackPresenter,
             FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler,
-            BlockNumberDialogFragment.Callback filteredNumberDialogCallback) {
+            BlockNumberDialogFragment.Callback filteredNumberDialogCallback,
+            boolean isArchiveTab) {
+
         return new CallLogListItemViewHolder(
                 context,
                 eventListener,
@@ -292,7 +302,8 @@
                 PhoneCallDetailsViews.fromView(view),
                 (CardView) view.findViewById(R.id.call_log_row),
                 (TextView) view.findViewById(R.id.call_log_day_group_label),
-                (ImageView) view.findViewById(R.id.primary_action_button));
+                (ImageView) view.findViewById(R.id.primary_action_button),
+                isArchiveTab);
     }
 
     @Override
@@ -397,6 +408,10 @@
 
             voicemailPlaybackView = (VoicemailPlaybackLayout) actionsView
                     .findViewById(R.id.voicemail_playback_layout);
+            if (isArchiveTab) {
+                voicemailPlaybackView.hideArchiveButton();
+            }
+
 
             callButtonView = actionsView.findViewById(R.id.call_action);
             callButtonView.setOnClickListener(this);
@@ -509,8 +524,10 @@
             mVoicemailPlaybackPresenter.setPlaybackView(
                     voicemailPlaybackView, uri, mVoicemailPrimaryActionButtonClicked);
             mVoicemailPrimaryActionButtonClicked = false;
-
-            CallLogAsyncTaskUtil.markVoicemailAsRead(mContext, uri);
+            // Only mark voicemail as read when not in archive tab
+            if (!isArchiveTab) {
+                CallLogAsyncTaskUtil.markVoicemailAsRead(mContext, uri);
+            }
         } else {
             voicemailPlaybackView.setVisibility(View.GONE);
         }
@@ -705,11 +722,12 @@
                 PhoneCallDetailsViews.createForTest(context),
                 new CardView(context),
                 new TextView(context),
-                new ImageView(context));
+                new ImageView(context),
+                false);
         viewHolder.detailsButtonView = new TextView(context);
         viewHolder.actionsView = new View(context);
         viewHolder.voicemailPlaybackView = new VoicemailPlaybackLayout(context);
-
+        viewHolder.workIconView = new ImageButton(context);
         return viewHolder;
     }
 }
\ No newline at end of file
diff --git a/src/com/android/dialer/calllog/CallLogQueryHandler.java b/src/com/android/dialer/calllog/CallLogQueryHandler.java
index 3b493cf..d1591e1 100644
--- a/src/com/android/dialer/calllog/CallLogQueryHandler.java
+++ b/src/com/android/dialer/calllog/CallLogQueryHandler.java
@@ -38,6 +38,7 @@
 import com.android.contacts.common.compat.SdkVersionOverride;
 import com.android.contacts.common.database.NoNullCursorAsyncQueryHandler;
 import com.android.contacts.common.util.PermissionsUtil;
+import com.android.dialer.database.VoicemailArchiveContract;
 import com.android.dialer.util.AppCompatConstants;
 import com.android.dialer.util.TelecomUtil;
 import com.android.dialer.voicemail.VoicemailStatusHelperImpl;
@@ -64,6 +65,8 @@
     private static final int QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN = 58;
     /** The token for the query to fetch the number of missed calls. */
     private static final int QUERY_MISSED_CALLS_UNREAD_COUNT_TOKEN = 59;
+    /** The oken for the query to fetch the archived voicemails. */
+    private static final int QUERY_VOICEMAIL_ARCHIVE = 60;
 
     private final int mLogLimit;
 
@@ -127,6 +130,17 @@
     }
 
     /**
+     * Fetch all the voicemails in the voicemail archive.
+     */
+    public void fetchVoicemailArchive() {
+        startQuery(QUERY_VOICEMAIL_ARCHIVE, null,
+                VoicemailArchiveContract.VoicemailArchive.CONTENT_URI,
+                null, VoicemailArchiveContract.VoicemailArchive.ARCHIVED + " = 1", null,
+                VoicemailArchiveContract.VoicemailArchive.DATE + " DESC");
+    }
+
+
+    /**
      * Fetches the list of calls from the call log for a given type.
      * This call ignores the new or old state.
      * <p>
@@ -253,7 +267,7 @@
             return;
         }
         try {
-            if (token == QUERY_CALLLOG_TOKEN) {
+            if (token == QUERY_CALLLOG_TOKEN || token == QUERY_VOICEMAIL_ARCHIVE) {
                 if (updateAdapterData(cursor)) {
                     cursor = null;
                 }
diff --git a/src/com/android/dialer/calllog/GroupingListAdapter.java b/src/com/android/dialer/calllog/GroupingListAdapter.java
index 70190df..0d06298 100644
--- a/src/com/android/dialer/calllog/GroupingListAdapter.java
+++ b/src/com/android/dialer/calllog/GroupingListAdapter.java
@@ -73,9 +73,19 @@
      */
     protected abstract void addGroups(Cursor cursor);
 
+    protected abstract void addVoicemailGroups(Cursor cursor);
+
     protected abstract void onContentChanged();
 
     public void changeCursor(Cursor cursor) {
+        changeCursor(cursor, false);
+    }
+
+    public void changeCursorVoicemail(Cursor cursor) {
+        changeCursor(cursor, true);
+    }
+
+    public void changeCursor(Cursor cursor, boolean voicemail) {
         if (cursor == mCursor) {
             return;
         }
@@ -91,7 +101,11 @@
         mCursor = cursor;
 
         if (cursor != null) {
-            addGroups(mCursor);
+            if (voicemail) {
+                addVoicemailGroups(mCursor);
+            } else {
+                addGroups(mCursor);
+            }
 
             // Calculate the item count by subtracting group child counts from the cursor count.
             mItemCount = mGroupMetadata.size();
diff --git a/src/com/android/dialer/database/VoicemailArchiveProvider.java b/src/com/android/dialer/database/VoicemailArchiveProvider.java
index ae73670..79b7a76 100644
--- a/src/com/android/dialer/database/VoicemailArchiveProvider.java
+++ b/src/com/android/dialer/database/VoicemailArchiveProvider.java
@@ -115,11 +115,13 @@
         // Create the directory for archived voicemails if it doesn't already exist
         File directory = new File(getFilesDir(), VOICEMAIL_FOLDER);
         directory.mkdirs();
-
-        // Update the row's _data column with a file path in the voicemails folder
         Uri newUri = ContentUris.withAppendedId(uri, id);
-        File voicemailFile = new File(directory, Long.toString(id));
-        values.put(VoicemailArchiveContract.VoicemailArchive._DATA, voicemailFile.getPath());
+
+        // Create new file only if path is not provided to one
+        if (!values.containsKey(VoicemailArchiveContract.VoicemailArchive._DATA)) {
+            File voicemailFile = new File(directory, Long.toString(id));
+            values.put(VoicemailArchiveContract.VoicemailArchive._DATA, voicemailFile.getPath());
+        }
         update(newUri, values, null, null);
         return newUri;
     }
diff --git a/src/com/android/dialer/voicemail/VoicemailArchiveActivity.java b/src/com/android/dialer/voicemail/VoicemailArchiveActivity.java
new file mode 100644
index 0000000..16b947c
--- /dev/null
+++ b/src/com/android/dialer/voicemail/VoicemailArchiveActivity.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2016 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.Intent;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.MenuItem;
+import android.view.View;
+
+import com.android.contacts.common.GeoUtil;
+import com.android.dialer.DialtactsActivity;
+import com.android.dialer.R;
+import com.android.dialer.TransactionSafeActivity;
+import com.android.dialer.calllog.CallLogAdapter;
+import com.android.dialer.calllog.CallLogQueryHandler;
+import com.android.dialer.calllog.ContactInfoHelper;
+import com.android.dialer.widget.EmptyContentView;
+import com.android.dialerbind.ObjectFactory;
+
+/**
+ * This activity manages all the voicemails archived by the user.
+ */
+public class VoicemailArchiveActivity extends TransactionSafeActivity
+        implements CallLogAdapter.CallFetcher, CallLogQueryHandler.Listener {
+    private RecyclerView mRecyclerView;
+    private LinearLayoutManager mLayoutManager;
+    private EmptyContentView mEmptyListView;
+    private CallLogAdapter mAdapter;
+    private VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
+    private CallLogQueryHandler mCallLogQueryHandler;
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (!isSafeToCommitTransactions()) {
+            return true;
+        }
+
+        switch (item.getItemId()) {
+            case android.R.id.home:
+                Intent intent = new Intent(this, DialtactsActivity.class);
+                // Clears any activities between VoicemailArchiveActivity and DialtactsActivity
+                // on the activity stack and reuses the existing instance of DialtactsActivity
+                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+                startActivity(intent);
+                return true;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.call_log_fragment);
+
+        // Make window opaque to reduce overdraw
+        getWindow().setBackgroundDrawable(null);
+
+        ActionBar actionBar = getSupportActionBar();
+        actionBar.setDisplayShowHomeEnabled(true);
+        actionBar.setDisplayHomeAsUpEnabled(true);
+        actionBar.setDisplayShowTitleEnabled(true);
+        actionBar.setElevation(0);
+
+        mCallLogQueryHandler = new CallLogQueryHandler(this, getContentResolver(), this);
+        mVoicemailPlaybackPresenter = VoicemailArchivePlaybackPresenter
+                .getInstance(this, savedInstanceState);
+
+        mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
+        mRecyclerView.setHasFixedSize(true);
+        mLayoutManager = new LinearLayoutManager(this);
+        mRecyclerView.setLayoutManager(mLayoutManager);
+        mEmptyListView = (EmptyContentView) findViewById(R.id.empty_list_view);
+        mEmptyListView.setDescription(R.string.voicemail_archive_empty);
+        mEmptyListView.setImage(R.drawable.empty_call_log);
+
+        mAdapter = ObjectFactory.newCallLogAdapter(
+                this,
+                this,
+                new ContactInfoHelper(this, GeoUtil.getCurrentCountryIso(this)),
+                mVoicemailPlaybackPresenter,
+                CallLogAdapter.ACTIVITY_TYPE_ARCHIVE);
+        mRecyclerView.setAdapter(mAdapter);
+        fetchCalls();
+    }
+
+    @Override
+    protected void onPause() {
+        mVoicemailPlaybackPresenter.onPause();
+        mAdapter.onPause();
+        super.onPause();
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        mAdapter.onResume();
+        mVoicemailPlaybackPresenter.onResume();
+    }
+
+    @Override
+    public void onDestroy() {
+        mVoicemailPlaybackPresenter.onDestroy();
+        mAdapter.changeCursor(null);
+        super.onDestroy();
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        mVoicemailPlaybackPresenter.onSaveInstanceState(outState);
+    }
+
+    @Override
+    public void fetchCalls() {
+        mCallLogQueryHandler.fetchVoicemailArchive();
+    }
+
+    @Override
+    public void onVoicemailStatusFetched(Cursor statusCursor) {
+        // Do nothing
+    }
+
+    @Override
+    public void onVoicemailUnreadCountFetched(Cursor cursor) {
+        // Do nothing
+    }
+
+    @Override
+    public void onMissedCallsUnreadCountFetched(Cursor cursor) {
+        // Do nothing
+    }
+
+    @Override
+    public boolean onCallsFetched(Cursor cursor) {
+        mAdapter.changeCursorVoicemail(cursor);
+        boolean showListView = cursor != null && cursor.getCount() > 0;
+        mRecyclerView.setVisibility(showListView ? View.VISIBLE : View.GONE);
+        mEmptyListView.setVisibility(!showListView ? View.VISIBLE : View.GONE);
+        return true;
+    }
+}
diff --git a/src/com/android/dialer/voicemail/VoicemailArchivePlaybackPresenter.java b/src/com/android/dialer/voicemail/VoicemailArchivePlaybackPresenter.java
new file mode 100644
index 0000000..050b8ac
--- /dev/null
+++ b/src/com/android/dialer/voicemail/VoicemailArchivePlaybackPresenter.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.voicemail;
+
+import android.app.Activity;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.util.Log;
+import com.android.dialer.calllog.CallLogAsyncTaskUtil;
+import com.android.dialer.database.VoicemailArchiveContract;
+import java.io.FileNotFoundException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Similar to the {@link VoicemailPlaybackPresenter}, but for the archive voicemail tab. It checks
+ * whether the voicemail file exists locally before preparing it.
+ */
+public class VoicemailArchivePlaybackPresenter extends VoicemailPlaybackPresenter {
+    private static final String TAG = "VMPlaybackPresenter";
+    private static VoicemailPlaybackPresenter sInstance;
+
+    public VoicemailArchivePlaybackPresenter(Activity activity) {
+        super(activity);
+    }
+
+    public static VoicemailPlaybackPresenter getInstance(
+            Activity activity, Bundle savedInstanceState) {
+        if (sInstance == null) {
+            sInstance = new VoicemailArchivePlaybackPresenter(activity);
+        }
+
+        sInstance.init(activity, savedInstanceState);
+        return sInstance;
+    }
+
+    @Override
+    protected void checkForContent(final OnContentCheckedListener callback) {
+        mAsyncTaskExecutor.submit(Tasks.CHECK_FOR_CONTENT, new AsyncTask<Void, Void, Boolean>() {
+            @Override
+            public Boolean doInBackground(Void... params) {
+                try {
+                    // Check if the _data column of the archived voicemail is valid
+                    if (mVoicemailUri != null) {
+                        mContext.getContentResolver().openInputStream(mVoicemailUri);
+                        return true;
+                    }
+                } catch (FileNotFoundException e) {
+                    Log.d(TAG, "Voicemail file not found for " + mVoicemailUri);
+                    handleError(e);
+                }
+                return false;
+            }
+
+            @Override
+            public void onPostExecute(Boolean hasContent) {
+                callback.onContentChecked(hasContent);
+            }
+        });
+    }
+
+    @Override
+    protected boolean requestContent(int code) {
+        if (mContext == null || mVoicemailUri == null) {
+            return false;
+        }
+        prepareContent();
+        return true;
+    }
+}
diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java b/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java
index 19b592d..436fc79 100644
--- a/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java
+++ b/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java
@@ -16,40 +16,44 @@
 
 package com.android.dialer.voicemail;
 
-import android.app.Activity;
-import android.app.Fragment;
+import android.content.ContentUris;
 import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
 import android.graphics.drawable.Drawable;
-import android.media.MediaPlayer;
 import android.net.Uri;
-import android.os.Bundle;
+import android.os.AsyncTask;
 import android.os.Handler;
-import android.os.PowerManager;
-import android.provider.VoicemailContract;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.support.design.widget.Snackbar;
 import android.view.LayoutInflater;
 import android.view.View;
-import android.view.ViewGroup;
 import android.widget.ImageButton;
 import android.widget.LinearLayout;
 import android.widget.SeekBar;
 import android.widget.SeekBar.OnSeekBarChangeListener;
 import android.widget.TextView;
+import android.widget.Toast;
 
 import com.android.common.io.MoreCloseables;
 import com.android.dialer.PhoneCallDetails;
 import com.android.dialer.R;
 import com.android.dialer.calllog.CallLogAsyncTaskUtil;
 
+import com.android.dialer.database.VoicemailArchiveContract;
+import com.android.dialer.database.VoicemailArchiveContract.VoicemailArchive;
+import com.android.dialer.util.AsyncTaskExecutor;
+import com.android.dialer.util.AsyncTaskExecutors;
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Preconditions;
 
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Objects;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.ScheduledExecutorService;
 
+import javax.annotation.Nullable;
 import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.NotThreadSafe;
 import javax.annotation.concurrent.ThreadSafe;
@@ -67,6 +71,12 @@
         CallLogAsyncTaskUtil.CallLogAsyncTaskListener {
     private static final String TAG = VoicemailPlaybackLayout.class.getSimpleName();
     private static final int VOICEMAIL_DELETE_DELAY_MS = 3000;
+    private static final int VOICEMAIL_ARCHIVE_DELAY_MS = 3000;
+
+    /** The enumeration of {@link AsyncTask} objects we use in this class. */
+    public enum Tasks {
+        QUERY_ARCHIVED_STATUS
+    }
 
     /**
      * Controls the animation of the playback slider.
@@ -202,7 +212,7 @@
             final Runnable deleteCallback = new Runnable() {
                 @Override
                 public void run() {
-                    if (mVoicemailUri == deleteUri) {
+                    if (Objects.equals(deleteUri, mVoicemailUri)) {
                         CallLogAsyncTaskUtil.deleteVoicemail(mContext, deleteUri,
                                 VoicemailPlaybackLayout.this);
                     }
@@ -214,8 +224,6 @@
             // window.
             handler.postDelayed(deleteCallback, VOICEMAIL_DELETE_DELAY_MS + 50);
 
-            final int actionTextColor =
-                    mContext.getResources().getColor(R.color.dialer_snackbar_action_text_color);
             Snackbar.make(VoicemailPlaybackLayout.this, R.string.snackbar_voicemail_deleted,
                             Snackbar.LENGTH_LONG)
                     .setDuration(VOICEMAIL_DELETE_DELAY_MS)
@@ -227,21 +235,44 @@
                                         handler.removeCallbacks(deleteCallback);
                                 }
                             })
-                    .setActionTextColor(actionTextColor)
+                    .setActionTextColor(
+                            mContext.getResources().getColor(
+                                    R.color.dialer_snackbar_action_text_color))
                     .show();
         }
     };
 
+    private final View.OnClickListener mArchiveButtonListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            if (mPresenter == null || isArchiving(mVoicemailUri)) {
+                return;
+            }
+            mIsArchiving.add(mVoicemailUri);
+            mPresenter.pausePlayback();
+            updateArchiveUI(mVoicemailUri);
+            disableUiElements();
+            mPresenter.archiveContent(mVoicemailUri, true);
+        }
+    };
+
     private Context mContext;
     private VoicemailPlaybackPresenter mPresenter;
     private Uri mVoicemailUri;
-
+    private final AsyncTaskExecutor mAsyncTaskExecutor =
+            AsyncTaskExecutors.createAsyncTaskExecutor();
     private boolean mIsPlaying = false;
+    /**
+     * Keeps track of which voicemails are currently being archived in order to update the voicemail
+     * card UI every time a user opens a new card.
+     */
+    private static final ArrayList<Uri> mIsArchiving = new ArrayList<>();
 
     private SeekBar mPlaybackSeek;
     private ImageButton mStartStopButton;
     private ImageButton mPlaybackSpeakerphone;
     private ImageButton mDeleteButton;
+    private ImageButton mArchiveButton;
     private TextView mStateText;
     private TextView mPositionText;
     private TextView mTotalDurationText;
@@ -256,7 +287,6 @@
 
     public VoicemailPlaybackLayout(Context context, AttributeSet attrs) {
         super(context, attrs);
-
         mContext = context;
         LayoutInflater inflater =
                 (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
@@ -267,6 +297,8 @@
     public void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri) {
         mPresenter = presenter;
         mVoicemailUri = voicemailUri;
+        updateArchiveUI(mVoicemailUri);
+        updateArchiveButton(mVoicemailUri);
     }
 
     @Override
@@ -277,6 +309,7 @@
         mStartStopButton = (ImageButton) findViewById(R.id.playback_start_stop);
         mPlaybackSpeakerphone = (ImageButton) findViewById(R.id.playback_speakerphone);
         mDeleteButton = (ImageButton) findViewById(R.id.delete_voicemail);
+        mArchiveButton =(ImageButton) findViewById(R.id.archive_voicemail);
         mStateText = (TextView) findViewById(R.id.playback_state_text);
         mPositionText = (TextView) findViewById(R.id.playback_position_text);
         mTotalDurationText = (TextView) findViewById(R.id.total_duration_text);
@@ -285,6 +318,7 @@
         mStartStopButton.setOnClickListener(mStartStopButtonListener);
         mPlaybackSpeakerphone.setOnClickListener(mSpeakerphoneListener);
         mDeleteButton.setOnClickListener(mDeleteButtonListener);
+        mArchiveButton.setOnClickListener(mArchiveButtonListener);
 
         mPositionText.setText(formatAsMinutesAndSeconds(0));
         mTotalDurationText.setText(formatAsMinutesAndSeconds(0));
@@ -358,7 +392,6 @@
 
         mPositionText.setText(formatAsMinutesAndSeconds(seekBarPositionMs));
         mTotalDurationText.setText(formatAsMinutesAndSeconds(durationMs));
-        mStateText.setText(null);
     }
 
     @Override
@@ -386,6 +419,7 @@
 
     @Override
     public void enableUiElements() {
+        mDeleteButton.setEnabled(true);
         mStartStopButton.setEnabled(true);
         mPlaybackSeek.setEnabled(true);
         mPlaybackSeek.setThumb(mVoicemailSeekHandleEnabled);
@@ -429,6 +463,134 @@
         return String.format("%02d:%02d", minutes, seconds);
     }
 
+    /**
+     * Called when a voicemail archive succeeded. If the expanded voicemail was being
+     * archived, update the card UI. Either way, display a snackbar linking user to archive.
+     */
+    @Override
+    public void onVoicemailArchiveSucceded(Uri voicemailUri) {
+        if (isArchiving(voicemailUri)) {
+            mIsArchiving.remove(voicemailUri);
+            if (Objects.equals(voicemailUri, mVoicemailUri)) {
+                onVoicemailArchiveResult();
+                hideArchiveButton();
+            }
+        }
+
+        Snackbar.make(this, R.string.snackbar_voicemail_archived,
+                Snackbar.LENGTH_LONG)
+                .setDuration(VOICEMAIL_ARCHIVE_DELAY_MS)
+                .setAction(R.string.snackbar_voicemail_archived_goto,
+                        new View.OnClickListener() {
+                            @Override
+                            public void onClick(View view) {
+                                Intent intent = new Intent(mContext,
+                                        VoicemailArchiveActivity.class);
+                                mContext.startActivity(intent);
+                            }
+                        })
+                .setActionTextColor(
+                        mContext.getResources().getColor(R.color.dialer_snackbar_action_text_color))
+                .show();
+    }
+
+    /**
+     * If a voicemail archive failed, and the expanded card was being archived, update the card UI.
+     * Either way, display a toast saying the voicemail archive failed.
+     */
+    @Override
+    public void onVoicemailArchiveFailed(Uri voicemailUri) {
+        if (isArchiving(voicemailUri)) {
+            mIsArchiving.remove(voicemailUri);
+            if (Objects.equals(voicemailUri, mVoicemailUri)) {
+                onVoicemailArchiveResult();
+            }
+        }
+        String toastStr = mContext.getString(R.string.voicemail_archive_failed);
+        Toast.makeText(mContext, toastStr, Toast.LENGTH_SHORT).show();
+    }
+
+    public void hideArchiveButton() {
+        mArchiveButton.setVisibility(View.GONE);
+        mArchiveButton.setClickable(false);
+        mArchiveButton.setEnabled(false);
+    }
+
+    /**
+     * Whenever a voicemail archive succeeds or fails, clear the text displayed in the voicemail
+     * card.
+     */
+    private void onVoicemailArchiveResult() {
+        enableUiElements();
+        mStateText.setText(null);
+        mArchiveButton.setColorFilter(null);
+    }
+
+    /**
+     * Whether or not the voicemail with the given uri is being archived.
+     */
+    private boolean isArchiving(@Nullable Uri uri) {
+        return uri != null && mIsArchiving.contains(uri);
+    }
+
+    /**
+     * Show the proper text and hide the archive button if the voicemail is still being archived.
+     */
+    private void updateArchiveUI(@Nullable Uri voicemailUri) {
+        if (!Objects.equals(voicemailUri, mVoicemailUri)) {
+            return;
+        }
+        if (isArchiving(voicemailUri)) {
+            // If expanded card was in the middle of archiving, disable buttons and display message
+            disableUiElements();
+            mDeleteButton.setEnabled(false);
+            mArchiveButton.setColorFilter(getResources().getColor(R.color.setting_disabled_color));
+            mStateText.setText(getString(R.string.voicemail_archiving_content));
+        } else {
+            onVoicemailArchiveResult();
+        }
+    }
+
+    /**
+     * Hides the archive button if the voicemail has already been archived, shows otherwise.
+     * @param voicemailUri the URI of the voicemail for which the archive button needs to be updated
+     */
+    private void updateArchiveButton(@Nullable final Uri voicemailUri) {
+        if (voicemailUri == null ||
+                !Objects.equals(voicemailUri, mVoicemailUri) || isArchiving(voicemailUri) ||
+                Objects.equals(voicemailUri.getAuthority(),VoicemailArchiveContract.AUTHORITY)) {
+            return;
+        }
+        mAsyncTaskExecutor.submit(Tasks.QUERY_ARCHIVED_STATUS,
+                new AsyncTask<Void, Void, Boolean>() {
+            @Override
+            public Boolean doInBackground(Void... params) {
+                Cursor cursor = mContext.getContentResolver().query(VoicemailArchive.CONTENT_URI,
+                        null, VoicemailArchive.SERVER_ID + "=" + ContentUris.parseId(mVoicemailUri)
+                        + " AND " + VoicemailArchive.ARCHIVED + "= 1", null, null);
+                boolean archived = cursor != null && cursor.getCount() > 0;
+                cursor.close();
+                return archived;
+            }
+
+            @Override
+            public void onPostExecute(Boolean archived) {
+                if (!Objects.equals(voicemailUri, mVoicemailUri)) {
+                    return;
+                }
+
+                if (archived) {
+                    hideArchiveButton();
+                } else {
+                    mArchiveButton.setVisibility(View.VISIBLE);
+                    mArchiveButton.setClickable(true);
+                    mArchiveButton.setEnabled(true);
+                }
+
+            }
+        });
+    }
+
     @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 fcb35e5..3151a5e 100644
--- a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
+++ b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
@@ -19,6 +19,9 @@
 import com.google.common.annotations.VisibleForTesting;
 
 import android.app.Activity;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
@@ -30,20 +33,30 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.PowerManager;
+import android.provider.CallLog;
 import android.provider.VoicemailContract;
 import android.support.annotation.Nullable;
 import android.util.Log;
 import android.view.WindowManager.LayoutParams;
 
-import com.android.common.io.MoreCloseables;
 import com.android.dialer.calllog.CallLogAsyncTaskUtil;
+import com.android.dialer.calllog.CallLogQuery;
+import com.android.dialer.database.VoicemailArchiveContract;
 import com.android.dialer.util.AsyncTaskExecutor;
 import com.android.dialer.util.AsyncTaskExecutors;
-
+import com.android.common.io.MoreCloseables;
+import com.android.dialer.util.TelecomUtil;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.io.ByteStreams;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.Executors;
 import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -81,6 +94,8 @@
         void setClipPosition(int clipPositionInMillis, int clipLengthInMillis);
         void setFetchContentTimeout();
         void setIsFetchingContent();
+        void onVoicemailArchiveSucceded(Uri voicemailUri);
+        void onVoicemailArchiveFailed(Uri voicemailUri);
         void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri);
         void resetSeekBar();
     }
@@ -95,10 +110,10 @@
     public enum Tasks {
         CHECK_FOR_CONTENT,
         CHECK_CONTENT_AFTER_CHANGE,
+        ARCHIVE_VOICEMAIL
     }
 
-    private interface OnContentCheckedListener {
-
+    protected interface OnContentCheckedListener {
         void onContentChecked(boolean hasContent);
     }
 
@@ -123,6 +138,8 @@
             VoicemailPlaybackPresenter.class.getName() + ".CLIP_POSITION_KEY";
     private static final String IS_SPEAKERPHONE_ON_KEY =
             VoicemailPlaybackPresenter.class.getName() + ".IS_SPEAKER_PHONE_ON";
+    public static final int PLAYBACK_REQUEST = 0;
+    public static final int ARCHIVE_REQUEST = 1;
 
     /**
      * The most recently cached duration. We cache this since we don't want to keep requesting it
@@ -134,11 +151,11 @@
     private static VoicemailPlaybackPresenter sInstance;
 
     private Activity mActivity;
-    private Context mContext;
+    protected Context mContext;
     private PlaybackView mView;
-    private Uri mVoicemailUri;
+    protected Uri mVoicemailUri;
 
-    private MediaPlayer mMediaPlayer;
+    protected MediaPlayer mMediaPlayer;
     private int mPosition;
     private boolean mIsPlaying;
     // MediaPlayer crashes on some method calls if not prepared but does not have a method which
@@ -150,7 +167,7 @@
     private int mInitialOrientation;
 
     // Used to run async tasks that need to interact with the UI.
-    private AsyncTaskExecutor mAsyncTaskExecutor;
+    protected AsyncTaskExecutor mAsyncTaskExecutor;
     private static ScheduledExecutorService mScheduledExecutorService;
     /**
      * Used to handle the result of a successful or time-out fetch result.
@@ -158,6 +175,7 @@
      * This variable is thread-contained, accessed only on the ui thread.
      */
     private FetchResultHandler mFetchResultHandler;
+    private final List<FetchResultHandler> mArchiveResultHandlers = new ArrayList<>();
     private Handler mHandler = new Handler();
     private PowerManager.WakeLock mProximityWakeLock;
     private VoicemailAudioManager mVoicemailAudioManager;
@@ -186,11 +204,10 @@
     /**
      * Initialize variables which are activity-independent and state-independent.
      */
-    private VoicemailPlaybackPresenter(Activity activity) {
+    protected VoicemailPlaybackPresenter(Activity activity) {
         Context context = activity.getApplicationContext();
         mAsyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor();
         mVoicemailAudioManager = new VoicemailAudioManager(context, this);
-
         PowerManager powerManager =
                 (PowerManager) context.getSystemService(Context.POWER_SERVICE);
         if (powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
@@ -202,7 +219,7 @@
     /**
      * Update variables which are activity-dependent or state-dependent.
      */
-    private void init(Activity activity, Bundle savedInstanceState) {
+    protected void init(Activity activity, Bundle savedInstanceState) {
         mActivity = activity;
         mContext = activity;
 
@@ -274,11 +291,9 @@
                 public void onContentChecked(boolean hasContent) {
                     if (hasContent) {
                         prepareContent();
-                    } else {
-                        if (mView != null) {
-                            mView.resetSeekBar();
-                            mView.setClipPosition(0, mDuration.get());
-                        }
+                    } else if (mView != null) {
+                        mView.resetSeekBar();
+                        mView.setClipPosition(0, mDuration.get());
                     }
                 }
             });
@@ -377,6 +392,13 @@
             mScheduledExecutorService = null;
         }
 
+        if (!mArchiveResultHandlers.isEmpty()) {
+            for (FetchResultHandler fetchResultHandler : mArchiveResultHandlers) {
+                fetchResultHandler.destroy();
+            }
+            mArchiveResultHandlers.clear();
+        }
+
         if (mFetchResultHandler != null) {
             mFetchResultHandler.destroy();
             mFetchResultHandler = null;
@@ -386,7 +408,7 @@
     /**
      * Checks to see if we have content available for this voicemail.
      */
-    private void checkForContent(final OnContentCheckedListener callback) {
+    protected void checkForContent(final OnContentCheckedListener callback) {
         mAsyncTaskExecutor.submit(Tasks.CHECK_FOR_CONTENT, new AsyncTask<Void, Void, Boolean>() {
             @Override
             public Boolean doInBackground(Void... params) {
@@ -438,19 +460,27 @@
      *
      * @return whether issued request to fetch content
      */
-    private boolean requestContent() {
+    protected boolean requestContent(int code) {
         if (mContext == null || mVoicemailUri == null) {
             return false;
         }
 
-        if (mFetchResultHandler != null) {
-            mFetchResultHandler.destroy();
+        FetchResultHandler tempFetchResultHandler =
+                new FetchResultHandler(new Handler(), mVoicemailUri, code);
+
+        switch (code) {
+            case ARCHIVE_REQUEST:
+                mArchiveResultHandlers.add(tempFetchResultHandler);
+                break;
+            default:
+                if (mFetchResultHandler != null) {
+                    mFetchResultHandler.destroy();
+                }
+                mView.setIsFetchingContent();
+                mFetchResultHandler = tempFetchResultHandler;
+                break;
         }
 
-        mFetchResultHandler = new FetchResultHandler(new Handler(), mVoicemailUri);
-
-        mView.setIsFetchingContent();
-
         // Send voicemail fetch request.
         Intent intent = new Intent(VoicemailContract.ACTION_FETCH_VOICEMAIL, mVoicemailUri);
         mContext.sendBroadcast(intent);
@@ -461,14 +491,18 @@
     private class FetchResultHandler extends ContentObserver implements Runnable {
         private AtomicBoolean mIsWaitingForResult = new AtomicBoolean(true);
         private final Handler mFetchResultHandler;
+        private final Uri mVoicemailUri;
+        private final int mRequestCode;
+        private Uri mArchivedVoicemailUri;
 
-        public FetchResultHandler(Handler handler, Uri voicemailUri) {
+        public FetchResultHandler(Handler handler, Uri uri, int code) {
             super(handler);
             mFetchResultHandler = handler;
-
+            mRequestCode = code;
+            mVoicemailUri = uri;
             if (mContext != null) {
                 mContext.getContentResolver().registerContentObserver(
-                        voicemailUri, false, this);
+                        mVoicemailUri, false, this);
                 mFetchResultHandler.postDelayed(this, FETCH_CONTENT_TIMEOUT_MS);
             }
         }
@@ -481,7 +515,11 @@
             if (mIsWaitingForResult.getAndSet(false) && mContext != null) {
                 mContext.getContentResolver().unregisterContentObserver(this);
                 if (mView != null) {
-                    mView.setFetchContentTimeout();
+                    if (mRequestCode == ARCHIVE_REQUEST) {
+                        notifyUiOfArchiveResult(mVoicemailUri, false);
+                    } else {
+                        mView.setFetchContentTimeout();
+                    }
                 }
             }
         }
@@ -497,9 +535,16 @@
         public void onChange(boolean selfChange) {
             mAsyncTaskExecutor.submit(Tasks.CHECK_CONTENT_AFTER_CHANGE,
                     new AsyncTask<Void, Void, Boolean>() {
+
                 @Override
                 public Boolean doInBackground(Void... params) {
-                    return queryHasContent(mVoicemailUri);
+                    boolean hasContent = queryHasContent(mVoicemailUri);
+                    if (hasContent && mRequestCode == ARCHIVE_REQUEST) {
+                        mArchivedVoicemailUri =
+                                performArchiveVoicemailOnBackgroundThread(mVoicemailUri, true);
+                        return mArchivedVoicemailUri != null;
+                    }
+                    return hasContent;
                 }
 
                 @Override
@@ -507,7 +552,12 @@
                     if (hasContent && mContext != null && mIsWaitingForResult.getAndSet(false)) {
                         mContext.getContentResolver().unregisterContentObserver(
                                 FetchResultHandler.this);
-                        prepareContent();
+                        switch (mRequestCode) {
+                            case ARCHIVE_REQUEST:
+                                notifyUiOfArchiveResult(mVoicemailUri, true);
+                            default:
+                                prepareContent();
+                        }
                     }
                 }
             });
@@ -522,7 +572,7 @@
      * media player. If preparation is successful, the media player will {@link #onPrepared()},
      * and it will call {@link #onError()} otherwise.
      */
-    private void prepareContent() {
+    protected void prepareContent() {
         if (mView == null) {
             return;
         }
@@ -564,10 +614,8 @@
         mIsPrepared = true;
 
         // Update the duration in the database if it was not previously retrieved
-        if (mDuration.get() == 0) {
-            CallLogAsyncTaskUtil.updateVoicemailDuration(mContext, mVoicemailUri,
-                    mMediaPlayer.getDuration() / 1000);
-        }
+        CallLogAsyncTaskUtil.updateVoicemailDuration(mContext, mVoicemailUri,
+                TimeUnit.MILLISECONDS.toSeconds(mMediaPlayer.getDuration()));
 
         mDuration.set(mMediaPlayer.getDuration());
 
@@ -593,7 +641,7 @@
         return true;
     }
 
-    private void handleError(Exception e) {
+    protected void handleError(Exception e) {
         Log.d(TAG, "handleError: Could not play voicemail " + e);
 
         if (mIsPrepared) {
@@ -664,7 +712,7 @@
                     if (!hasContent) {
                         // No local content, download from server. Queue playing if the request was
                         // issued,
-                        mIsPlaying = requestContent();
+                        mIsPlaying = requestContent(PLAYBACK_REQUEST);
                     } else {
                         // Queue playing once the media play loaded the content.
                         mIsPlaying = true;
@@ -831,6 +879,17 @@
         return mIsPrepared && mMediaPlayer != null ? mMediaPlayer.getCurrentPosition() : 0;
     }
 
+    public void notifyUiOfArchiveResult(Uri voicemailUri, boolean archived) {
+        if (mView == null) {
+            return;
+        }
+        if (archived) {
+            mView.onVoicemailArchiveSucceded(voicemailUri);
+        } else {
+            mView.onVoicemailArchiveFailed(voicemailUri);
+        }
+    }
+
     /* package */ void onVoicemailDeleted() {
         // Trampoline the event notification to the interested listener.
         if (mOnVoicemailDeletedListener != null) {
@@ -859,6 +918,154 @@
         return mScheduledExecutorService;
     }
 
+    /**
+     * If voicemail has already been downloaded, go straight to archiving. Otherwise, request
+     * the voicemail content first.
+     */
+    public void archiveContent(Uri voicemailUri, boolean archivedByUser) {
+        if (!mIsPrepared) {
+            requestContent(ARCHIVE_REQUEST);
+        } else {
+            startArchiveVoicemailTask(voicemailUri, archivedByUser);
+        }
+    }
+
+    /**
+     * Asynchronous task used to archive a voicemail given its uri.
+     */
+    private void startArchiveVoicemailTask(final Uri voicemailUri, final boolean archivedByUser) {
+        mAsyncTaskExecutor.submit(Tasks.ARCHIVE_VOICEMAIL, new AsyncTask<Void, Void, Uri>() {
+            @Override
+            public Uri doInBackground(Void... params) {
+                return performArchiveVoicemailOnBackgroundThread(voicemailUri, archivedByUser);
+            }
+
+            @Override
+            public void onPostExecute(Uri archivedVoicemailUri) {
+                notifyUiOfArchiveResult(voicemailUri, archivedVoicemailUri != null);
+            }
+        });
+    }
+
+    /**
+     * Copy the voicemail information to the local dialer database, and copy
+     * the voicemail content to a local file in the dialer application's
+     * internal storage (voicemails directory).
+     *
+     * @param voicemailUri the uri of the voicemail to archive
+     * @return If archive was successful, archived voicemail URI, otherwise null.
+     */
+    private Uri performArchiveVoicemailOnBackgroundThread(Uri voicemailUri,
+                                                          boolean archivedByUser) {
+        Cursor callLogInfo = mContext.getContentResolver().query(
+                ContentUris.withAppendedId(CallLog.Calls.CONTENT_URI_WITH_VOICEMAIL,
+                        ContentUris.parseId(mVoicemailUri)),
+                CallLogQuery._PROJECTION, null, null, null);
+        Cursor contentInfo = mContext.getContentResolver().query(
+                voicemailUri, null, null, null, null);
+
+        if (callLogInfo == null || contentInfo == null) {
+            return null;
+        }
+
+        callLogInfo.moveToFirst();
+        contentInfo.moveToFirst();
+
+        // Create values to insert into database
+        ContentValues values = new ContentValues();
+        values.put(VoicemailArchiveContract.VoicemailArchive.NUMBER,
+                contentInfo.getString(contentInfo.getColumnIndex(
+                        VoicemailContract.Voicemails.NUMBER)));
+
+        values.put(VoicemailArchiveContract.VoicemailArchive.DATE,
+                contentInfo.getLong(contentInfo.getColumnIndex(
+                        VoicemailContract.Voicemails.DATE)));
+
+        values.put(VoicemailArchiveContract.VoicemailArchive.DURATION,
+                contentInfo.getLong(contentInfo.getColumnIndex(
+                        VoicemailContract.Voicemails.DURATION)));
+
+        values.put(VoicemailArchiveContract.VoicemailArchive.MIME_TYPE,
+                contentInfo.getString(contentInfo.getColumnIndex(
+                        VoicemailContract.Voicemails.MIME_TYPE)));
+
+        values.put(VoicemailArchiveContract.VoicemailArchive.COUNTRY_ISO,
+                callLogInfo.getString(CallLogQuery.COUNTRY_ISO));
+
+        values.put(VoicemailArchiveContract.VoicemailArchive.GEOCODED_LOCATION,
+                callLogInfo.getString(CallLogQuery.GEOCODED_LOCATION));
+
+        values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_NAME,
+                callLogInfo.getString(CallLogQuery.CACHED_NAME));
+
+        values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_NUMBER_TYPE,
+                callLogInfo.getInt(CallLogQuery.CACHED_NUMBER_TYPE));
+
+        values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_NUMBER_LABEL,
+                callLogInfo.getString(CallLogQuery.CACHED_NUMBER_LABEL));
+
+        values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_LOOKUP_URI,
+                callLogInfo.getString(CallLogQuery.CACHED_LOOKUP_URI));
+
+        values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_MATCHED_NUMBER,
+                callLogInfo.getString(CallLogQuery.CACHED_MATCHED_NUMBER));
+
+        values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_NORMALIZED_NUMBER,
+                callLogInfo.getString(CallLogQuery.CACHED_NORMALIZED_NUMBER));
+
+        values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_FORMATTED_NUMBER,
+                callLogInfo.getString(CallLogQuery.CACHED_FORMATTED_NUMBER));
+
+        values.put(VoicemailArchiveContract.VoicemailArchive.ARCHIVED, archivedByUser);
+
+        values.put(VoicemailArchiveContract.VoicemailArchive.NUMBER_PRESENTATION,
+                callLogInfo.getInt(CallLogQuery.NUMBER_PRESENTATION));
+
+        values.put(VoicemailArchiveContract.VoicemailArchive.ACCOUNT_COMPONENT_NAME,
+                callLogInfo.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME));
+
+        values.put(VoicemailArchiveContract.VoicemailArchive.ACCOUNT_ID,
+                callLogInfo.getString(CallLogQuery.ACCOUNT_ID));
+
+        values.put(VoicemailArchiveContract.VoicemailArchive.FEATURES,
+                callLogInfo.getInt(CallLogQuery.FEATURES));
+
+        values.put(VoicemailArchiveContract.VoicemailArchive.SERVER_ID,
+                contentInfo.getInt(contentInfo.getColumnIndex(
+                        VoicemailContract.Voicemails._ID)));
+
+        values.put(VoicemailArchiveContract.VoicemailArchive.TRANSCRIPTION,
+                contentInfo.getString(contentInfo.getColumnIndex(
+                        VoicemailContract.Voicemails.TRANSCRIPTION)));
+
+        values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_PHOTO_URI,
+                callLogInfo.getLong(CallLogQuery.CACHED_PHOTO_URI));
+
+        callLogInfo.close();
+        contentInfo.close();
+
+        // Insert info into dialer database
+        Uri archivedVoicemailUri = mContext.getContentResolver().insert(
+                        VoicemailArchiveContract.VoicemailArchive.CONTENT_URI, values);
+        try {
+            // Copy voicemail content to a local file
+            InputStream inputStream = mContext.getContentResolver()
+                    .openInputStream(voicemailUri);
+            OutputStream outputStream = mContext.getContentResolver()
+                    .openOutputStream(archivedVoicemailUri);
+
+            ByteStreams.copy(inputStream, outputStream);
+            inputStream.close();
+            outputStream.close();
+        } catch (IOException e) {
+            // Roll back insert if new file creation failed
+            mContext.getContentResolver().delete(archivedVoicemailUri, null, null);
+            Log.w(TAG, "Failed to copy voicemail content to temporary file");
+            return null;
+        }
+        return archivedVoicemailUri;
+    }
+
     @VisibleForTesting
     public boolean isPlaying() {
         return mIsPlaying;
diff --git a/src/com/android/dialerbind/ObjectFactory.java b/src/com/android/dialerbind/ObjectFactory.java
index 31255a2..4568c1b 100644
--- a/src/com/android/dialerbind/ObjectFactory.java
+++ b/src/com/android/dialerbind/ObjectFactory.java
@@ -62,13 +62,13 @@
             CallFetcher callFetcher,
             ContactInfoHelper contactInfoHelper,
             VoicemailPlaybackPresenter voicemailPlaybackPresenter,
-            boolean isCallLogActivity) {
+            int activityType) {
         return new CallLogAdapter(
                 context,
                 callFetcher,
                 contactInfoHelper,
                 voicemailPlaybackPresenter,
-                isCallLogActivity);
+                activityType);
     }
 
     public static Logger getLoggerInstance() {
diff --git a/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java b/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
index 80dfe35..e801311 100644
--- a/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
+++ b/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
@@ -31,6 +31,7 @@
 
 import com.android.contacts.common.preference.ContactsPreferences;
 import com.android.dialer.contactinfo.ContactInfoCache;
+import com.android.dialer.database.VoicemailArchiveContract;
 import com.android.dialer.util.AppCompatConstants;
 import com.android.dialer.util.TestConstants;
 import com.google.common.collect.Lists;
@@ -49,6 +50,7 @@
 public class CallLogAdapterTest extends AndroidTestCase {
     private static final String EMPTY_STRING = "";
     private static final int NO_VALUE_SET = -1;
+    private static final int ARCHIVE_TYPE = -2;
 
     private static final String TEST_CACHED_NAME_PRIMARY = "Cached Name";
     private static final String TEST_CACHED_NAME_ALTERNATIVE = "Name Cached";
@@ -74,12 +76,11 @@
 
     private View mView;
     private CallLogListItemViewHolder mViewHolder;
-    private Random mRandom;
+    private final Random mRandom = new Random();
 
     @Override
     protected void setUp() throws Exception {
         super.setUp();
-        mRandom = new Random();
 
         // Use a call fetcher that does not do anything.
         CallLogAdapter.CallFetcher fakeCallFetcher = new CallLogAdapter.CallFetcher() {
@@ -98,7 +99,8 @@
                     }
                 };
 
-        mAdapter = new TestCallLogAdapter(getContext(), fakeCallFetcher, fakeContactInfoHelper);
+        mAdapter = new TestCallLogAdapter(getContext(), fakeCallFetcher, fakeContactInfoHelper,
+                CallLogAdapter.ACTIVITY_TYPE_DIALTACTS);
 
         // The cursor used in the tests to store the entries to display.
         mCursor = new MatrixCursor(CallLogQuery._PROJECTION);
@@ -108,13 +110,6 @@
         mViewHolder = CallLogListItemViewHolder.createForTest(getContext());
     }
 
-    @Override
-    protected void tearDown() throws Exception {
-        mAdapter = null;
-        mCursor = null;
-        super.tearDown();
-    }
-
     @MediumTest
     public void testBindView_NumberOnlyNoCache() {
         createCallLogEntry();
@@ -547,6 +542,19 @@
         assertEquals(TEST_NUMBER_3, mViewHolder.number);
     }
 
+    public void testVoicemailArchive() {
+        setUpArchiveAdapter();
+        createVoicemailArchiveCallLogEntry();
+
+        mAdapter.changeCursorVoicemail(mCursor);
+        mAdapter.onBindViewHolder(mViewHolder, 0);
+
+        assertEquals(Uri.parse(mViewHolder.voicemailUri),
+                ContentUris.withAppendedId(
+                        VoicemailArchiveContract.VoicemailArchive.CONTENT_URI, 0));
+        assertNull(mViewHolder.primaryActionButtonView.getTag());
+    }
+
     private void createCallLogEntry() {
         createCallLogEntry(TEST_NUMBER);
     }
@@ -575,6 +583,10 @@
         createCallLogEntry(TEST_NUMBER, EMPTY_STRING, NO_VALUE_SET, Calls.VOICEMAIL_TYPE);
     }
 
+    private void createVoicemailArchiveCallLogEntry() {
+        createCallLogEntry(TEST_NUMBER, EMPTY_STRING, NO_VALUE_SET, ARCHIVE_TYPE);
+    }
+
     private void createCallLogEntry(String number, String postDialDigits, int presentation, int type) {
         Object[] values = getValues(number, postDialDigits, presentation, type);
         mCursor.addRow(values);
@@ -674,6 +686,10 @@
             values[CallLogQuery.VOICEMAIL_URI] = ContentUris.withAppendedId(
                     VoicemailContract.Voicemails.CONTENT_URI, mCursor.getCount());
         }
+        if (type == ARCHIVE_TYPE) {
+            values[CallLogQuery.VOICEMAIL_URI] = ContentUris.withAppendedId(
+                    VoicemailArchiveContract.VoicemailArchive.CONTENT_URI, mCursor.getCount());
+        }
 
         return values;
     }
@@ -739,11 +755,34 @@
         return Phone.getTypeLabel(getContext().getResources(), phoneType, "");
     }
 
+    private void setUpArchiveAdapter() {
+        // Use a call fetcher that does not do anything.
+        CallLogAdapter.CallFetcher fakeCallFetcher = new CallLogAdapter.CallFetcher() {
+            @Override
+            public void fetchCalls() {}
+        };
+
+        ContactInfoHelper fakeContactInfoHelper =
+                new ContactInfoHelper(getContext(), TEST_COUNTRY_ISO) {
+                    @Override
+                    public ContactInfo lookupNumber(String number, String countryIso) {
+                        ContactInfo info = new ContactInfo();
+                        info.number = number;
+                        info.formattedNumber = number;
+                        return info;
+                    }
+                };
+
+        mAdapter = new TestCallLogAdapter(getContext(), fakeCallFetcher, fakeContactInfoHelper,
+                CallLogAdapter.ACTIVITY_TYPE_ARCHIVE);
+    }
+
     /// Subclass of {@link CallLogAdapter} used in tests to intercept certain calls.
     private static final class TestCallLogAdapter extends CallLogAdapter {
         public TestCallLogAdapter(Context context, CallFetcher callFetcher,
-                ContactInfoHelper contactInfoHelper) {
-            super(context, callFetcher, contactInfoHelper, null, false);
+                ContactInfoHelper contactInfoHelper, int mActivity) {
+            super(context, callFetcher, contactInfoHelper, null,
+                    mActivity);
             mContactInfoCache = new TestContactInfoCache(
                     contactInfoHelper, mOnContactInfoChangedListener);
         }
diff --git a/tests/src/com/android/dialer/calllog/CallLogGroupBuilderTest.java b/tests/src/com/android/dialer/calllog/CallLogGroupBuilderTest.java
index 04463c2..c31c38e 100644
--- a/tests/src/com/android/dialer/calllog/CallLogGroupBuilderTest.java
+++ b/tests/src/com/android/dialer/calllog/CallLogGroupBuilderTest.java
@@ -129,6 +129,18 @@
                 AppCompatConstants.CALLS_VOICEMAIL_TYPE, AppCompatConstants.CALLS_OUTGOING_TYPE);
     }
 
+    public void testGrouping_VoicemailArchive() {
+        // Does not group with other types of calls, include voicemail themselves.
+        assertVoicemailsAreNotGrouped(
+                AppCompatConstants.CALLS_VOICEMAIL_TYPE, AppCompatConstants.CALLS_MISSED_TYPE);
+        assertVoicemailsAreNotGrouped(
+                AppCompatConstants.CALLS_VOICEMAIL_TYPE, AppCompatConstants.CALLS_VOICEMAIL_TYPE);
+        assertVoicemailsAreNotGrouped(
+                AppCompatConstants.CALLS_VOICEMAIL_TYPE, AppCompatConstants.CALLS_INCOMING_TYPE);
+        assertVoicemailsAreNotGrouped(
+                AppCompatConstants.CALLS_VOICEMAIL_TYPE, AppCompatConstants.CALLS_OUTGOING_TYPE);
+    }
+
     public void testGrouping_Missed() {
         // Groups with one or more missed calls.
         assertCallsAreGrouped(
@@ -198,6 +210,21 @@
 
     }
 
+    public void testAddGroups_Separate() {
+        addMultipleCallLogEntries(TEST_NUMBER1,
+                AppCompatConstants.CALLS_VOICEMAIL_TYPE,    // Group 1: 0
+                AppCompatConstants.CALLS_INCOMING_TYPE,     // Group 2: 1
+                AppCompatConstants.CALLS_OUTGOING_TYPE,     // Group 3: 2
+                AppCompatConstants.CALLS_MISSED_TYPE);      // Group 4: 3
+        mBuilder.addVoicemailGroups(mCursor);
+
+        assertEquals(4, mFakeGroupCreator.groups.size());
+        assertGroupIs(0, 1, mFakeGroupCreator.groups.get(0));
+        assertGroupIs(1, 1, mFakeGroupCreator.groups.get(1));
+        assertGroupIs(2, 1, mFakeGroupCreator.groups.get(2));
+        assertGroupIs(3, 1, mFakeGroupCreator.groups.get(3));
+    }
+
     public void testAddGroups_Mixed() {
         addMultipleCallLogEntries(TEST_NUMBER1,
                 AppCompatConstants.CALLS_VOICEMAIL_TYPE,   // Group 1: 0
@@ -326,6 +353,15 @@
         assertEquals(types.length, mFakeGroupCreator.groups.size());
     }
 
+    /** Asserts that voicemails are not grouped together with other types at all. */
+    private void assertVoicemailsAreNotGrouped(int... types) {
+        createCursor();
+        clearFakeGroupCreator();
+        addMultipleCallLogEntries(TEST_NUMBER1, types);
+        mBuilder.addVoicemailGroups(mCursor);
+        assertEquals(types.length, mFakeGroupCreator.groups.size());
+    }
+
     /** Adds a set of calls with the given types, all from the same number, in the old section. */
     private void addMultipleCallLogEntries(String number, int... types) {
         for (int type : types) {
diff --git a/tests/src/com/android/dialer/calllog/GroupingListAdapterTests.java b/tests/src/com/android/dialer/calllog/GroupingListAdapterTests.java
index 4d51f72..4d8cb9c 100644
--- a/tests/src/com/android/dialer/calllog/GroupingListAdapterTests.java
+++ b/tests/src/com/android/dialer/calllog/GroupingListAdapterTests.java
@@ -70,6 +70,11 @@
         }
 
         @Override
+        protected void addVoicemailGroups(Cursor c) {
+            // Do nothing.
+        }
+
+        @Override
         public void onContentChanged() {
             // Do nothing.
         }
diff --git a/tests/src/com/android/dialer/voicemail/VoicemailActivityInstrumentationTestCase2.java b/tests/src/com/android/dialer/voicemail/VoicemailActivityInstrumentationTestCase2.java
new file mode 100644
index 0000000..a992e8b
--- /dev/null
+++ b/tests/src/com/android/dialer/voicemail/VoicemailActivityInstrumentationTestCase2.java
@@ -0,0 +1,212 @@
+package com.android.dialer.voicemail;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.res.AssetManager;
+import android.net.Uri;
+import android.provider.VoicemailContract;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.Suppress;
+import android.view.View;
+
+import com.android.dialer.R;
+import com.android.dialer.util.AsyncTaskExecutors;
+import com.android.dialer.util.FakeAsyncTaskExecutor;
+import com.android.dialer.util.LocaleTestUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Locale;
+
+import static com.android.dialer.voicemail.VoicemailPlaybackPresenter.Tasks.CHECK_FOR_CONTENT;
+
+
+/**
+ * Common methods and attributes between {@link VoicemailArchiveTest} and
+ * {@link VoicemailPlaybackTest}.
+ */
+public class VoicemailActivityInstrumentationTestCase2<T extends Activity>
+        extends ActivityInstrumentationTestCase2<T> {
+    protected static final String TEST_ASSET_NAME = "quick_test_recording.mp3";
+    protected static final String MIME_TYPE = "audio/mp3";
+    protected static final String CONTACT_NUMBER = "+1412555555";
+    protected static final String VOICEMAIL_FILE_LOCATION = "/sdcard/sadlfj893w4j23o9sfu.mp3";
+
+    private T mActivity;
+    protected VoicemailPlaybackPresenter mPresenter;
+    private VoicemailPlaybackLayout mLayout;
+
+    protected Uri mVoicemailUri;
+    private LocaleTestUtils mLocaleTestUtils;
+    protected FakeAsyncTaskExecutor mFakeAsyncTaskExecutor;
+
+    public VoicemailActivityInstrumentationTestCase2(Class<T> activityClass) {
+        super(activityClass);
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        mFakeAsyncTaskExecutor = new FakeAsyncTaskExecutor(getInstrumentation());
+        AsyncTaskExecutors.setFactoryForTest(mFakeAsyncTaskExecutor.getFactory());
+
+        // Some of the tests rely on the text - safest to force a specific locale.
+        mLocaleTestUtils = new LocaleTestUtils(getInstrumentation().getTargetContext());
+        mLocaleTestUtils.setLocale(Locale.US);
+
+        mActivity = getActivity();
+        mLayout = new VoicemailPlaybackLayout(mActivity);
+        mLayout.onFinishInflate();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        cleanUpVoicemailUri();
+
+        mLocaleTestUtils.restoreLocale();
+        mLocaleTestUtils = null;
+
+        mPresenter.clearInstance();
+        AsyncTaskExecutors.setFactoryForTest(null);
+
+        mActivity = null;
+        mPresenter = null;
+        mLayout = null;
+
+        super.tearDown();
+    }
+
+    @Suppress
+    public void testFetchingVoicemail() throws Throwable {
+        setUriForUnfetchedVoicemailEntry();
+        setPlaybackViewForPresenter();
+
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mPresenter.resumePlayback();
+                assertStateTextContains("Loading voicemail");
+            }
+        });
+    }
+
+    @Suppress
+    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.
+        assertStateTextContains("Couldn't play voicemail");
+        assertStateTextNotContains("Buffering");
+    }
+
+    public void testClickingSpeakerphoneButton() throws Throwable {
+        setUriForRealFileVoicemailEntry();
+        setPlaybackViewForPresenter();
+
+        // Check that the speakerphone is false to start.
+        assertFalse(mPresenter.isSpeakerphoneOn());
+
+        View speakerphoneButton = mLayout.findViewById(R.id.playback_speakerphone);
+        speakerphoneButton.performClick();
+        assertTrue(mPresenter.isSpeakerphoneOn());
+    }
+
+    protected void cleanUpVoicemailUri() {
+        if (mVoicemailUri != null) {
+            getContentResolver().delete(VoicemailContract.Voicemails.CONTENT_URI,
+                    "_ID = ?", new String[] { String.valueOf(ContentUris.parseId(mVoicemailUri)) });
+            mVoicemailUri = null;
+        }
+    }
+
+    protected void setUriForRealFileVoicemailEntry() throws IOException {
+        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, 1);
+        String packageName = getInstrumentation().getTargetContext().getPackageName();
+        mVoicemailUri = getContentResolver().insert(
+                VoicemailContract.Voicemails.buildSourceUri(packageName), values);
+        AssetManager assets = getAssets();
+        try (InputStream inputStream = assets.open(TEST_ASSET_NAME);
+             OutputStream outputStream = getContentResolver().openOutputStream(mVoicemailUri)) {
+            copyBetweenStreams(inputStream, outputStream);
+        }
+    }
+
+    protected 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);
+    }
+
+    protected void setUriForInvalidVoicemailEntry() {
+        assertNull(mVoicemailUri);
+        ContentResolver contentResolver = getContentResolver();
+        ContentValues values = new ContentValues();
+        values.put(VoicemailContract.Voicemails.NUMBER, CONTACT_NUMBER);
+        values.put(VoicemailContract.Voicemails.HAS_CONTENT, 1);
+        // VoicemailContract.Voicemails._DATA
+        values.put("_data", VOICEMAIL_FILE_LOCATION);
+        mVoicemailUri = contentResolver.insert(VoicemailContract.Voicemails.CONTENT_URI, values);
+    }
+
+    protected void setPlaybackViewForPresenter() {
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mPresenter.setPlaybackView(mLayout, mVoicemailUri, false);
+            }
+        });
+    }
+
+    protected void copyBetweenStreams(InputStream in, OutputStream out) throws IOException {
+        byte[] buffer = new byte[1024];
+        int bytesRead;
+        while ((bytesRead = in.read(buffer)) > 0) {
+            out.write(buffer, 0, bytesRead);
+        }
+    }
+
+    protected void assertStateTextContains(String text) {
+        assertNotNull(mLayout);
+        assertTrue(mLayout.getStateText().contains(text));
+    }
+
+    protected void assertStateTextNotContains(String text) {
+        assertNotNull(mLayout);
+        assertFalse(mLayout.getStateText().contains(text));
+    }
+
+    protected ContentResolver getContentResolver() {
+        return getInstrumentation().getTargetContext().getContentResolver();
+    }
+
+    protected AssetManager getAssets() {
+        return getInstrumentation().getContext().getAssets();
+    }
+
+}
diff --git a/tests/src/com/android/dialer/voicemail/VoicemailArchiveTest.java b/tests/src/com/android/dialer/voicemail/VoicemailArchiveTest.java
new file mode 100644
index 0000000..8cbd344
--- /dev/null
+++ b/tests/src/com/android/dialer/voicemail/VoicemailArchiveTest.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2016 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.ContentUris;
+import android.content.ContentValues;
+
+import com.android.dialer.R;
+import com.android.dialer.database.VoicemailArchiveContract.VoicemailArchive;
+
+import android.content.res.AssetManager;
+import android.test.suitebuilder.annotation.Suppress;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Unit tests for {@link VoicemailArchiveActivity} and {@link VoicemailArchivePlaybackPresenter}.
+ */
+public class VoicemailArchiveTest
+        extends VoicemailActivityInstrumentationTestCase2<VoicemailArchiveActivity> {
+
+    public VoicemailArchiveTest() {
+        super(VoicemailArchiveActivity.class);
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mPresenter = VoicemailArchivePlaybackPresenter.getInstance(getActivity(), null);
+    }
+
+    @Override
+    public void testFetchingVoicemail() throws Throwable {
+        setUriForRealFileVoicemailEntry();
+        setPlaybackViewForPresenter();
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mPresenter.checkForContent(
+                        new VoicemailPlaybackPresenter.OnContentCheckedListener() {
+                            @Override
+                            public void onContentChecked(boolean hasContent) {
+                                mPresenter.resumePlayback();
+                                assertEquals(true, mPresenter.isPlaying());
+                            }
+                        });
+            }
+        });
+    }
+
+    @Override
+    public void testInvalidVoicemailShowsErrorMessage() throws Throwable {
+        setUriForInvalidVoicemailEntry();
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mPresenter.checkForContent(
+                        new VoicemailPlaybackPresenter.OnContentCheckedListener() {
+                            @Override
+                            public void onContentChecked(boolean hasContent) {
+                                assertStateTextContains("Couldn't play voicemail");
+                            }
+                        });
+            }
+        });
+    }
+
+    @Override
+    protected void setUriForInvalidVoicemailEntry() {
+        assertNull(mVoicemailUri);
+        ContentValues values = new ContentValues();
+        values.put(VoicemailArchive.NUMBER, CONTACT_NUMBER);
+        values.put(VoicemailArchive.DATE, String.valueOf(System.currentTimeMillis()));
+        values.put(VoicemailArchive.MIME_TYPE, MIME_TYPE);
+        values.put(VoicemailArchive._DATA, VOICEMAIL_FILE_LOCATION);
+        mVoicemailUri = getContentResolver().insert(VoicemailArchive.CONTENT_URI, values);
+    }
+
+    @Override
+    protected void setUriForRealFileVoicemailEntry() throws IOException {
+        assertNull(mVoicemailUri);
+        ContentValues values = new ContentValues();
+        values.put(VoicemailArchive.DATE, String.valueOf(System.currentTimeMillis()));
+        values.put(VoicemailArchive.NUMBER, CONTACT_NUMBER);
+        values.put(VoicemailArchive.MIME_TYPE, MIME_TYPE);
+        values.put(VoicemailArchive.DURATION, 0);
+        mVoicemailUri = getContentResolver().insert(VoicemailArchive.CONTENT_URI, values);
+        AssetManager assets = getAssets();
+        try (InputStream inputStream = assets.open(TEST_ASSET_NAME);
+             OutputStream outputStream = getContentResolver().openOutputStream(mVoicemailUri)) {
+            copyBetweenStreams(inputStream, outputStream);
+        }
+    }
+
+    @Override
+    protected void cleanUpVoicemailUri() {
+        if (mVoicemailUri != null) {
+            getContentResolver().delete(VoicemailArchive.CONTENT_URI,
+                    "_ID = ?", new String[] { String.valueOf(ContentUris.parseId(mVoicemailUri)) });
+            mVoicemailUri = null;
+        }
+    }
+}
diff --git a/tests/src/com/android/dialer/voicemail/VoicemailPlaybackTest.java b/tests/src/com/android/dialer/voicemail/VoicemailPlaybackTest.java
index 630789c..abd582b 100644
--- a/tests/src/com/android/dialer/voicemail/VoicemailPlaybackTest.java
+++ b/tests/src/com/android/dialer/voicemail/VoicemailPlaybackTest.java
@@ -16,54 +16,17 @@
 
 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;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.res.AssetManager;
-import android.net.Uri;
-import android.provider.VoicemailContract;
-import android.test.ActivityInstrumentationTestCase2;
-import android.test.suitebuilder.annotation.LargeTest;
 import android.test.suitebuilder.annotation.Suppress;
-import android.view.View;
-import android.widget.TextView;
 
-import com.android.dialer.R;
 import com.android.dialer.calllog.CallLogActivity;
-import com.android.dialer.util.AsyncTaskExecutors;
-import com.android.dialer.util.FakeAsyncTaskExecutor;
-import com.android.dialer.util.LocaleTestUtils;
-import com.android.dialer.voicemail.VoicemailPlaybackLayout;
-import com.android.dialer.voicemail.VoicemailPlaybackPresenter;
 
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.List;
-import java.util.Locale;
+import static com.android.dialer.voicemail.VoicemailPlaybackPresenter.Tasks.CHECK_FOR_CONTENT;
 
 /**
- * Unit tests for the {@link VoicemailPlaybackPresenter} and {@link VoicemailPlaybackLayout}.
+ * Unit tests for {@link VoicemailPlaybackPresenter} and {@link VoicemailPlaybackLayout}.
  */
-@LargeTest
-public class VoicemailPlaybackTest extends ActivityInstrumentationTestCase2<CallLogActivity> {
-    private static final String TEST_ASSET_NAME = "quick_test_recording.mp3";
-    private static final String MIME_TYPE = "audio/mp3";
-    private static final String CONTACT_NUMBER = "+1412555555";
-    private static final String VOICEMAIL_FILE_LOCATION = "/sdcard/sadlfj893w4j23o9sfu.mp3";
-
-    private Activity mActivity;
-    private VoicemailPlaybackPresenter mPresenter;
-    private VoicemailPlaybackLayout mLayout;
-
-    private Uri mVoicemailUri;
-    private LocaleTestUtils mLocaleTestUtils;
-    private FakeAsyncTaskExecutor mFakeAsyncTaskExecutor;
+public class VoicemailPlaybackTest
+        extends VoicemailActivityInstrumentationTestCase2<CallLogActivity> {
 
     public VoicemailPlaybackTest() {
         super(CallLogActivity.class);
@@ -72,49 +35,7 @@
     @Override
     public void setUp() throws Exception {
         super.setUp();
-
-        mFakeAsyncTaskExecutor = new FakeAsyncTaskExecutor(getInstrumentation());
-        AsyncTaskExecutors.setFactoryForTest(mFakeAsyncTaskExecutor.getFactory());
-
-        // Some of the tests rely on the text - safest to force a specific locale.
-        mLocaleTestUtils = new LocaleTestUtils(getInstrumentation().getTargetContext());
-        mLocaleTestUtils.setLocale(Locale.US);
-
-        mActivity = getActivity();
-        mLayout = new VoicemailPlaybackLayout(mActivity);
-        mLayout.onFinishInflate();
-
-        mPresenter = VoicemailPlaybackPresenter.getInstance(mActivity, null);
-    }
-
-    @Override
-    protected void tearDown() throws Exception {
-        cleanUpVoicemailUri();
-
-        mLocaleTestUtils.restoreLocale();
-        mLocaleTestUtils = null;
-
-        mPresenter.clearInstance();
-        AsyncTaskExecutors.setFactoryForTest(null);
-
-        mActivity = null;
-        mPresenter = null;
-        mLayout = null;
-
-        super.tearDown();
-    }
-
-    public void testFetchingVoicemail() throws Throwable {
-        setUriForUnfetchedVoicemailEntry();
-        setPlaybackViewForPresenter();
-
-        getInstrumentation().runOnMainSync(new Runnable() {
-            @Override
-            public void run() {
-                mPresenter.resumePlayback();
-                assertStateTextContains("Loading voicemail");
-            }
-        });
+        mPresenter = VoicemailPlaybackPresenter.getInstance(getActivity(), null);
     }
 
     @Suppress
@@ -133,119 +54,4 @@
 
         assertStateTextContains("Loading voicemail");
     }
-
-    @Suppress
-    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.
-        assertStateTextContains("Couldn't play voicemail");
-        assertStateTextNotContains("Buffering");
-    }
-
-    public void testClickingSpeakerphoneButton() throws Throwable {
-        setUriForRealFileVoicemailEntry();
-        setPlaybackViewForPresenter();
-
-        // Check that the speakerphone is false to start.
-        assertFalse(mPresenter.isSpeakerphoneOn());
-
-        View speakerphoneButton = mLayout.findViewById(R.id.playback_speakerphone);
-        speakerphoneButton.performClick();
-        assertTrue(mPresenter.isSpeakerphoneOn());
-    }
-
-    private void cleanUpVoicemailUri() {
-        if (mVoicemailUri != null) {
-            getContentResolver().delete(VoicemailContract.Voicemails.CONTENT_URI,
-                    "_ID = ?", new String[] { String.valueOf(ContentUris.parseId(mVoicemailUri)) });
-            mVoicemailUri = null;
-        }
-    }
-
-    private void setUriForRealFileVoicemailEntry() throws IOException {
-        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, 1);
-        String packageName = getInstrumentation().getTargetContext().getPackageName();
-        mVoicemailUri = getContentResolver().insert(
-                VoicemailContract.Voicemails.buildSourceUri(packageName), values);
-        AssetManager assets = getAssets();
-        try (InputStream inputStream = assets.open(TEST_ASSET_NAME);
-             OutputStream outputStream = getContentResolver().openOutputStream(mVoicemailUri)) {
-            copyBetweenStreams(inputStream, outputStream);
-        }
-    }
-
-    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();
-        ContentValues values = new ContentValues();
-        values.put(VoicemailContract.Voicemails.NUMBER, CONTACT_NUMBER);
-        values.put(VoicemailContract.Voicemails.HAS_CONTENT, 1);
-        // VoicemailContract.Voicemails._DATA
-        values.put("_data", VOICEMAIL_FILE_LOCATION);
-        mVoicemailUri = contentResolver.insert(VoicemailContract.Voicemails.CONTENT_URI, values);
-    }
-
-    private void setPlaybackViewForPresenter() {
-        getInstrumentation().runOnMainSync(new Runnable() {
-            @Override
-            public void run() {
-                mPresenter.setPlaybackView(mLayout, mVoicemailUri, false);
-            }
-        });
-    }
-
-    public void copyBetweenStreams(InputStream in, OutputStream out) throws IOException {
-        byte[] buffer = new byte[1024];
-        int bytesRead;
-        while ((bytesRead = in.read(buffer)) > 0) {
-            out.write(buffer, 0, bytesRead);
-        }
-    }
-
-    private void assertStateTextContains(String text) {
-        assertNotNull(mLayout);
-        assertTrue(mLayout.getStateText().contains(text));
-    }
-
-    private void assertStateTextNotContains(String text) {
-        assertNotNull(mLayout);
-        assertFalse(mLayout.getStateText().contains(text));
-    }
-
-    private ContentResolver getContentResolver() {
-        return getInstrumentation().getTargetContext().getContentResolver();
-    }
-
-    private AssetManager getAssets() {
-        return getInstrumentation().getContext().getAssets();
-    }
-}
+}
\ No newline at end of file