Merge "Add files for voicemail archive tab." 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/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/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/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