Refactor voicemail archive code.

+Created new class for all AsyncTasks and listener interfaces.
+Separated the archive code into separate methods
+Added 7 tests

BUG=22797391

Change-Id: I789125328bf079846e34c5fd57fd0d1c67dc6266
diff --git a/src/com/android/dialer/voicemail/VoicemailAsyncTaskUtil.java b/src/com/android/dialer/voicemail/VoicemailAsyncTaskUtil.java
new file mode 100644
index 0000000..7abf9a7
--- /dev/null
+++ b/src/com/android/dialer/voicemail/VoicemailAsyncTaskUtil.java
@@ -0,0 +1,346 @@
+/*
+ * 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 com.android.contacts.common.testing.NeededForTesting;
+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.google.common.base.Preconditions;
+import com.google.common.io.ByteStreams;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.CallLog;
+import android.provider.VoicemailContract;
+import android.util.Log;
+import com.android.common.io.MoreCloseables;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import javax.annotation.Nullable;
+
+/**
+ * Class containing asynchronous tasks for voicemails.
+ */
+@NeededForTesting
+public class VoicemailAsyncTaskUtil {
+    private static final String TAG = "VoicemailAsyncTaskUtil";
+
+    /** The enumeration of {@link AsyncTask} objects we use in this class. */
+    public enum Tasks {
+        GET_VOICEMAIL_FILE_PATH,
+        SET_VOICEMAIL_ARCHIVE_STATUS,
+        ARCHIVE_VOICEMAIL_CONTENT
+    }
+
+    @NeededForTesting
+    public interface OnArchiveVoicemailListener {
+        /**
+         * Called after the voicemail has been archived.
+         *
+         * @param archivedVoicemailUri the URI of the archived voicemail
+         */
+        void onArchiveVoicemail(@Nullable Uri archivedVoicemailUri);
+    }
+
+    @NeededForTesting
+    public interface OnSetVoicemailArchiveStatusListener {
+        /**
+         * Called after the voicemail archived_by_user column is updated.
+         *
+         * @param success whether the update was successful or not
+         */
+        void onSetVoicemailArchiveStatus(boolean success);
+    }
+
+    @NeededForTesting
+    public interface OnGetArchivedVoicemailFilePathListener {
+        /**
+         * Called after the voicemail file path is obtained.
+         *
+         * @param filePath the file path of the archived voicemail
+         */
+        void onGetArchivedVoicemailFilePath(@Nullable String filePath);
+    }
+
+    private final ContentResolver mResolver;
+    private final AsyncTaskExecutor mAsyncTaskExecutor;
+
+    @NeededForTesting
+    public VoicemailAsyncTaskUtil(ContentResolver contentResolver) {
+        mResolver = Preconditions.checkNotNull(contentResolver);
+        mAsyncTaskExecutor = AsyncTaskExecutors.createThreadPoolExecutor();
+    }
+
+    /**
+     * Returns the archived voicemail file path.
+     */
+    @NeededForTesting
+    public void getVoicemailFilePath(
+            final OnGetArchivedVoicemailFilePathListener listener,
+            final Uri voicemailUri) {
+        Preconditions.checkNotNull(listener);
+        Preconditions.checkNotNull(voicemailUri);
+        mAsyncTaskExecutor.submit(Tasks.GET_VOICEMAIL_FILE_PATH,
+                new AsyncTask<Void, Void, String>() {
+                    @Nullable
+                    @Override
+                    protected String doInBackground(Void... params) {
+                        try (Cursor cursor = mResolver.query(voicemailUri,
+                                new String[]{VoicemailArchiveContract.VoicemailArchive._DATA},
+                                null, null, null)) {
+                            if (hasContent(cursor)) {
+                                return cursor.getString(cursor.getColumnIndex(
+                                        VoicemailArchiveContract.VoicemailArchive._DATA));
+                            }
+                        }
+                        return null;
+                    }
+
+                    @Override
+                    protected void onPostExecute(String filePath) {
+                        listener.onGetArchivedVoicemailFilePath(filePath);
+                    }
+                });
+    }
+
+    /**
+     * Updates the archived_by_user flag of the archived voicemail.
+     */
+    @NeededForTesting
+    public void setVoicemailArchiveStatus(
+            final OnSetVoicemailArchiveStatusListener listener,
+            final Uri voicemailUri,
+            final boolean archivedByUser) {
+        Preconditions.checkNotNull(listener);
+        Preconditions.checkNotNull(voicemailUri);
+        mAsyncTaskExecutor.submit(Tasks.SET_VOICEMAIL_ARCHIVE_STATUS,
+                new AsyncTask<Void, Void, Boolean>() {
+                    @Override
+                    protected Boolean doInBackground(Void... params) {
+                        ContentValues values = new ContentValues(1);
+                        values.put(VoicemailArchiveContract.VoicemailArchive.ARCHIVED,
+                                archivedByUser);
+                        return mResolver.update(voicemailUri, values, null, null) > 0;
+                    }
+
+                    @Override
+                    protected void onPostExecute(Boolean success) {
+                        listener.onSetVoicemailArchiveStatus(success);
+                    }
+                });
+    }
+
+    /**
+     * Checks if a voicemail has already been archived, if so, return the previously archived URI.
+     * Otherwise, copy the voicemail information to the local dialer database. If archive was
+     * successful, archived voicemail URI is returned to listener, otherwise null.
+     */
+    @NeededForTesting
+    public void archiveVoicemailContent(
+            final OnArchiveVoicemailListener listener,
+            final Uri voicemailUri) {
+        Preconditions.checkNotNull(listener);
+        Preconditions.checkNotNull(voicemailUri);
+        mAsyncTaskExecutor.submit(Tasks.ARCHIVE_VOICEMAIL_CONTENT,
+                new AsyncTask<Void, Void, Uri>() {
+                    @Nullable
+                    @Override
+                    protected Uri doInBackground(Void... params) {
+                        Uri archivedVoicemailUri = getArchivedVoicemailUri(voicemailUri);
+
+                        // If previously archived, return uri, otherwise archive everything.
+                        if (archivedVoicemailUri != null) {
+                            return archivedVoicemailUri;
+                        }
+
+                        // Combine call log and voicemail content info.
+                        ContentValues values = getVoicemailContentValues(voicemailUri);
+                        if (values == null) {
+                            return null;
+                        }
+
+                        Uri insertedVoicemailUri = mResolver.insert(
+                                VoicemailArchiveContract.VoicemailArchive.CONTENT_URI, values);
+                        if (insertedVoicemailUri == null) {
+                            return null;
+                        }
+
+                        // Copy voicemail content to a new file.
+                        boolean copiedFile = false;
+                        try (InputStream inputStream = mResolver.openInputStream(voicemailUri);
+                             OutputStream outputStream =
+                                     mResolver.openOutputStream(insertedVoicemailUri)) {
+                            if (inputStream != null && outputStream != null) {
+                                ByteStreams.copy(inputStream, outputStream);
+                                copiedFile = true;
+                                return insertedVoicemailUri;
+                            }
+                        } catch (IOException e) {
+                            Log.w(TAG, "Failed to copy voicemail content to new file: "
+                                    + e.toString());
+                        } finally {
+                            if (!copiedFile) {
+                                // Roll back insert if the voicemail content was not copied.
+                                mResolver.delete(insertedVoicemailUri, null, null);
+                            }
+                        }
+                        return null;
+                    }
+
+                    @Override
+                    protected void onPostExecute(Uri archivedVoicemailUri) {
+                        listener.onArchiveVoicemail(archivedVoicemailUri);
+                    }
+                });
+    }
+
+    /**
+     * Helper method to get the archived URI of a voicemail.
+     *
+     * @param voicemailUri a {@link android.provider.VoicemailContract.Voicemails#CONTENT_URI} URI.
+     * @return the URI of the archived voicemail or {@code null}
+     */
+    @Nullable
+    private Uri getArchivedVoicemailUri(Uri voicemailUri) {
+        try (Cursor cursor = getArchiveExistsCursor(voicemailUri)) {
+            if (hasContent(cursor)) {
+                return VoicemailArchiveContract.VoicemailArchive
+                        .buildWithId(cursor.getInt(cursor.getColumnIndex(
+                                VoicemailArchiveContract.VoicemailArchive._ID)));
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Helper method to make a copy of all the values needed to display a voicemail.
+     *
+     * @param voicemailUri a {@link VoicemailContract.Voicemails#CONTENT_URI} URI.
+     * @return the combined call log and voicemail values for the given URI, or {@code null}
+     */
+    @Nullable
+    private ContentValues getVoicemailContentValues(Uri voicemailUri) {
+        try (Cursor callLogInfo = getCallLogInfoCursor(voicemailUri);
+             Cursor contentInfo = getContentInfoCursor(voicemailUri)) {
+
+            if (hasContent(callLogInfo) && hasContent(contentInfo)) {
+                // Create values to insert into database.
+                ContentValues values = new ContentValues();
+
+                // Insert voicemail call log info.
+                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.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.CACHED_PHOTO_URI,
+                        callLogInfo.getString(CallLogQuery.CACHED_PHOTO_URI));
+
+                // Insert voicemail content info.
+                values.put(VoicemailArchiveContract.VoicemailArchive.SERVER_ID,
+                        contentInfo.getInt(contentInfo.getColumnIndex(
+                                VoicemailContract.Voicemails._ID)));
+                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.TRANSCRIPTION,
+                        contentInfo.getString(contentInfo.getColumnIndex(
+                                VoicemailContract.Voicemails.TRANSCRIPTION)));
+
+                // Achived is false by default because it is updated after insertion.
+                values.put(VoicemailArchiveContract.VoicemailArchive.ARCHIVED, false);
+
+                return values;
+            }
+        }
+        return null;
+    }
+
+    private boolean hasContent(@Nullable Cursor cursor) {
+        return cursor != null && cursor.moveToFirst();
+    }
+
+    @Nullable
+    private Cursor getCallLogInfoCursor(Uri voicemailUri) {
+        return mResolver.query(
+                ContentUris.withAppendedId(CallLog.Calls.CONTENT_URI_WITH_VOICEMAIL,
+                        ContentUris.parseId(voicemailUri)),
+                CallLogQuery._PROJECTION, null, null, null);
+    }
+
+    @Nullable
+    private Cursor getContentInfoCursor(Uri voicemailUri) {
+        return mResolver.query(voicemailUri,
+                new String[] {
+                        VoicemailContract.Voicemails._ID,
+                        VoicemailContract.Voicemails.NUMBER,
+                        VoicemailContract.Voicemails.DATE,
+                        VoicemailContract.Voicemails.DURATION,
+                        VoicemailContract.Voicemails.MIME_TYPE,
+                        VoicemailContract.Voicemails.TRANSCRIPTION,
+                }, null, null, null);
+    }
+
+    @Nullable
+    private Cursor getArchiveExistsCursor(Uri voicemailUri) {
+        return mResolver.query(VoicemailArchiveContract.VoicemailArchive.CONTENT_URI,
+                new String[] {VoicemailArchiveContract.VoicemailArchive._ID},
+                VoicemailArchiveContract.VoicemailArchive.SERVER_ID + "="
+                        + ContentUris.parseId(voicemailUri),
+                null,
+                null);
+    }
+}
diff --git a/tests/src/com/android/dialer/voicemail/VoicemailAsyncTaskUtilTest.java b/tests/src/com/android/dialer/voicemail/VoicemailAsyncTaskUtilTest.java
new file mode 100644
index 0000000..d98d9fa
--- /dev/null
+++ b/tests/src/com/android/dialer/voicemail/VoicemailAsyncTaskUtilTest.java
@@ -0,0 +1,388 @@
+/*
+ * 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 android.content.res.AssetFileDescriptor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+import android.provider.CallLog;
+import android.provider.VoicemailContract;
+import android.test.InstrumentationTestCase;
+import android.test.mock.MockContentResolver;
+
+import com.android.contacts.common.test.mocks.MockContentProvider;
+import com.android.contacts.common.test.mocks.MockContentProvider.Query;
+import com.android.dialer.calllog.CallLogQuery;
+import com.android.dialer.database.VoicemailArchiveContract;
+import com.android.dialer.util.AsyncTaskExecutors;
+import com.android.dialer.util.FakeAsyncTaskExecutor;
+import com.android.dialer.voicemail.VoicemailAsyncTaskUtil.OnGetArchivedVoicemailFilePathListener;
+import com.android.dialer.voicemail.VoicemailAsyncTaskUtil.OnArchiveVoicemailListener;
+import com.android.dialer.voicemail.VoicemailAsyncTaskUtil.OnSetVoicemailArchiveStatusListener;
+
+import org.junit.Rule;
+import org.junit.rules.TemporaryFolder;
+import static org.mockito.Mockito.*;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+public class VoicemailAsyncTaskUtilTest extends InstrumentationTestCase {
+    private static final String TEST_MIME_TYPE = "audio/mp3";
+    private static final String TEST_NUMBER = "+1412555555";
+    private static final String TEST_FILE_PATH = "TEST_PATH";
+    private static final String TEST_TRANSCRIPTION = "TEST_TRANSCRIPTION";
+    private static final long TEST_DATE = 0;
+    private static final long TEST_DURATION = 0;
+    private static final int TEST_SERVER_ID = 1;
+    private static final int TEST_ID = 2;
+    private static final Uri ARCHIVED_VOICEMAIL_URI =
+            VoicemailArchiveContract.VoicemailArchive.buildWithId(TEST_ID);
+    private static final Uri VOICEMAIL_URI = VoicemailContract.Voicemails.CONTENT_URI
+            .buildUpon().appendPath(Integer.toString(TEST_SERVER_ID)).build();
+    private static final String[] CALLLOG_QUERY_PROJECTION = new String[] {
+            CallLog.Calls._ID,
+            CallLog.Calls.NUMBER,
+            CallLog.Calls.DATE,
+            CallLog.Calls.DURATION,
+            CallLog.Calls.TYPE,
+            CallLog.Calls.COUNTRY_ISO,
+            CallLog.Calls.VOICEMAIL_URI,
+            CallLog.Calls.GEOCODED_LOCATION,
+            CallLog.Calls.CACHED_NAME,
+            CallLog.Calls.CACHED_NUMBER_TYPE,
+            CallLog.Calls.CACHED_NUMBER_LABEL,
+            CallLog.Calls.CACHED_LOOKUP_URI,
+            CallLog.Calls.CACHED_MATCHED_NUMBER,
+            CallLog.Calls.CACHED_NORMALIZED_NUMBER,
+            CallLog.Calls.CACHED_PHOTO_ID,
+            CallLog.Calls.CACHED_FORMATTED_NUMBER,
+            CallLog.Calls.IS_READ,
+            CallLog.Calls.NUMBER_PRESENTATION,
+            CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME,
+            CallLog.Calls.PHONE_ACCOUNT_ID,
+            CallLog.Calls.FEATURES,
+            CallLog.Calls.DATA_USAGE,
+            CallLog.Calls.TRANSCRIPTION,
+            CallLog.Calls.CACHED_PHOTO_URI
+    };
+    private static final String[] VOICEMAIL_PROJECTION = new String[] {
+            VoicemailContract.Voicemails._ID,
+            VoicemailContract.Voicemails.NUMBER,
+            VoicemailContract.Voicemails.DATE,
+            VoicemailContract.Voicemails.DURATION,
+            VoicemailContract.Voicemails.MIME_TYPE,
+            VoicemailContract.Voicemails.TRANSCRIPTION,
+    };
+
+    private final MockContentResolver mContentResolver = new MockContentResolver();
+    private final MockContentProvider mArchiveContentProvider = new TestVoicemailContentProvider();
+    private final MockContentProvider mVoicemailContentProvider =
+            new TestVoicemailContentProvider();
+    private final MockContentProvider mCallLogContentProvider = new MockContentProvider();
+    @Rule
+    private final TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+    private VoicemailAsyncTaskUtil mVoicemailAsyncTaskUtil;
+    private FakeAsyncTaskExecutor mFakeAsyncTaskExecutor;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mFakeAsyncTaskExecutor = new FakeAsyncTaskExecutor(getInstrumentation());
+        AsyncTaskExecutors.setFactoryForTest(mFakeAsyncTaskExecutor.getFactory());
+        mContentResolver.addProvider(VoicemailArchiveContract.AUTHORITY, mArchiveContentProvider);
+        mContentResolver.addProvider(VoicemailContract.AUTHORITY, mVoicemailContentProvider);
+        mContentResolver.addProvider(CallLog.AUTHORITY, mCallLogContentProvider);
+        mVoicemailAsyncTaskUtil = new VoicemailAsyncTaskUtil(mContentResolver);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        AsyncTaskExecutors.setFactoryForTest(null);
+        super.tearDown();
+    }
+
+    public void testGetVoicemailFilePath_VoicemailExists() throws Throwable {
+        newVoicemailArchiveQuery().withAnyProjection()
+                .returnRow(getCombinedContentValuesWithData());
+        final OnGetArchivedVoicemailFilePathListener listener =
+                mock(OnGetArchivedVoicemailFilePathListener.class);
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mVoicemailAsyncTaskUtil.getVoicemailFilePath(listener, ARCHIVED_VOICEMAIL_URI);
+            }
+        });
+        mFakeAsyncTaskExecutor.runTask(VoicemailAsyncTaskUtil.Tasks.GET_VOICEMAIL_FILE_PATH);
+        verify(listener).onGetArchivedVoicemailFilePath(TEST_FILE_PATH);
+        verifyQueries();
+    }
+
+    public void testGetVoicemailFilePath_VoicemailNotExists() throws Throwable {
+        newVoicemailArchiveFakeQuery().withAnyProjection().returnEmptyCursor();
+        final OnGetArchivedVoicemailFilePathListener listener =
+                mock(OnGetArchivedVoicemailFilePathListener.class);
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mVoicemailAsyncTaskUtil.getVoicemailFilePath(listener, getFakeVoicemailUri());
+            }
+        });
+        mFakeAsyncTaskExecutor.runTask(VoicemailAsyncTaskUtil.Tasks.GET_VOICEMAIL_FILE_PATH);
+        verify(listener).onGetArchivedVoicemailFilePath(null);
+        verifyQueries();
+    }
+
+    public void testSetVoicemailArchiveStatus_VoicemailNotExists() throws Throwable {
+        newVoicemailArchiveNotExistsUpdateQuery(true);
+        final OnSetVoicemailArchiveStatusListener listener =
+                mock(OnSetVoicemailArchiveStatusListener.class);
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mVoicemailAsyncTaskUtil.setVoicemailArchiveStatus(listener,
+                        getFakeVoicemailUri(), true);
+            }
+        });
+        mFakeAsyncTaskExecutor.runTask(VoicemailAsyncTaskUtil.Tasks.SET_VOICEMAIL_ARCHIVE_STATUS);
+        verify(listener).onSetVoicemailArchiveStatus(false);
+        verifyQueries();
+    }
+
+    public void testSetVoicemailArchiveStatus_VoicemailExists() throws Throwable {
+        newVoicemailArchiveExistsUpdateQuery(true);
+        final OnSetVoicemailArchiveStatusListener listener =
+                mock(OnSetVoicemailArchiveStatusListener.class);
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mVoicemailAsyncTaskUtil.setVoicemailArchiveStatus(listener,
+                        ARCHIVED_VOICEMAIL_URI, true);
+            }
+        });
+        mFakeAsyncTaskExecutor.runTask(VoicemailAsyncTaskUtil.Tasks.SET_VOICEMAIL_ARCHIVE_STATUS);
+        verify(listener).onSetVoicemailArchiveStatus(true);
+        verifyQueries();
+    }
+
+    public void testArchiveVoicemailContent_ArchiveNotExists() throws Throwable {
+        newVoicemailArchiveExistsQuery().returnEmptyCursor();
+        newQueryCallLogInfo().returnRow(getCallLogContentValues());
+        newVoicemailQuery().returnRow(getVoicemailContentValues());
+        newVoicemailArchiveInsert();
+        final OnArchiveVoicemailListener listener = mock(OnArchiveVoicemailListener.class);
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mVoicemailAsyncTaskUtil.archiveVoicemailContent(listener, VOICEMAIL_URI);
+            }
+        });
+        mFakeAsyncTaskExecutor.runTask(VoicemailAsyncTaskUtil.Tasks.ARCHIVE_VOICEMAIL_CONTENT);
+        verify(listener).onArchiveVoicemail(ARCHIVED_VOICEMAIL_URI);
+        verifyQueries();
+    }
+
+    public void testArchiveVoicemailContent_ArchiveExists() throws Throwable {
+        newVoicemailArchiveExistsQuery().returnRow(getCombinedValuesWithId());
+        final OnArchiveVoicemailListener listener = mock(OnArchiveVoicemailListener.class);
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mVoicemailAsyncTaskUtil.archiveVoicemailContent(listener, VOICEMAIL_URI);
+            }
+        });
+        mFakeAsyncTaskExecutor.runTask(VoicemailAsyncTaskUtil.Tasks.ARCHIVE_VOICEMAIL_CONTENT);
+        verify(listener).onArchiveVoicemail(ARCHIVED_VOICEMAIL_URI);
+        verifyQueries();
+    }
+
+    public void testArchiveVoicemailContent_CallLogInfoNotExists() throws Throwable {
+        newVoicemailArchiveExistsQuery().returnEmptyCursor();
+        newQueryCallLogInfo().returnEmptyCursor();
+        newVoicemailQuery().returnEmptyCursor();
+        final OnArchiveVoicemailListener listener = mock(OnArchiveVoicemailListener.class);
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mVoicemailAsyncTaskUtil.archiveVoicemailContent(listener, VOICEMAIL_URI);
+            }
+        });
+        mFakeAsyncTaskExecutor.runTask(VoicemailAsyncTaskUtil.Tasks.ARCHIVE_VOICEMAIL_CONTENT);
+        verify(listener).onArchiveVoicemail(null);
+        verifyQueries();
+    }
+
+    private Query newVoicemailArchiveQuery() {
+        return mArchiveContentProvider.expectQuery(ARCHIVED_VOICEMAIL_URI);
+    }
+
+    private Query newVoicemailArchiveFakeQuery() {
+        return mArchiveContentProvider.expectQuery(getFakeVoicemailUri());
+    }
+
+    private Query newQueryCallLogInfo() {
+        return mCallLogContentProvider.expectQuery(ContentUris.withAppendedId(
+                CallLog.Calls.CONTENT_URI_WITH_VOICEMAIL, ContentUris.parseId(VOICEMAIL_URI)))
+                .withProjection(CALLLOG_QUERY_PROJECTION);
+    }
+
+    private Query newVoicemailQuery() {
+        return mVoicemailContentProvider.expectQuery(VOICEMAIL_URI).withAnySelection()
+                .withProjection(VOICEMAIL_PROJECTION);
+    }
+
+    private Query newVoicemailArchiveExistsQuery() {
+        return mArchiveContentProvider.expectQuery(
+                VoicemailArchiveContract.VoicemailArchive.CONTENT_URI)
+                .withSelection(VoicemailArchiveContract.VoicemailArchive.SERVER_ID + "="
+                        + ContentUris.parseId(VOICEMAIL_URI), (String[]) null)
+                .withProjection(VoicemailArchiveContract.VoicemailArchive._ID);
+    }
+
+    private void newVoicemailArchiveInsert() {
+        mArchiveContentProvider.expectInsert(
+                VoicemailArchiveContract.VoicemailArchive.CONTENT_URI, getCombinedContentValues(),
+                ARCHIVED_VOICEMAIL_URI);
+    }
+
+    private void newVoicemailArchiveNotExistsUpdateQuery(boolean status) {
+        mArchiveContentProvider.expectUpdate(getFakeVoicemailUri(),
+                getArchiveStatusUpdateValues(status), null, null).returnRowsAffected(0);
+    }
+
+    private void newVoicemailArchiveExistsUpdateQuery(boolean status) {
+        mArchiveContentProvider.expectUpdate(ARCHIVED_VOICEMAIL_URI,
+                getArchiveStatusUpdateValues(status), null, null).returnRowsAffected(1);
+    }
+
+    private static ContentValues getCallLogContentValues() {
+        ContentValues values = new ContentValues();
+        values.put(CALLLOG_QUERY_PROJECTION[CallLogQuery.GEOCODED_LOCATION], "");
+        values.put(CALLLOG_QUERY_PROJECTION[CallLogQuery.CACHED_NAME], "");
+        values.put(CALLLOG_QUERY_PROJECTION[CallLogQuery.COUNTRY_ISO], "");
+        values.put(CALLLOG_QUERY_PROJECTION[CallLogQuery.CACHED_NUMBER_TYPE], 0);
+        values.put(CALLLOG_QUERY_PROJECTION[CallLogQuery.CACHED_NUMBER_LABEL], "");
+        values.put(CALLLOG_QUERY_PROJECTION[CallLogQuery.CACHED_LOOKUP_URI], "");
+        values.put(CALLLOG_QUERY_PROJECTION[CallLogQuery.CACHED_MATCHED_NUMBER], "");
+        values.put(CALLLOG_QUERY_PROJECTION[CallLogQuery.CACHED_NORMALIZED_NUMBER], "");
+        values.put(CALLLOG_QUERY_PROJECTION[CallLogQuery.CACHED_FORMATTED_NUMBER], "");
+        values.put(CALLLOG_QUERY_PROJECTION[CallLogQuery.NUMBER_PRESENTATION], 0);
+        values.put(CALLLOG_QUERY_PROJECTION[CallLogQuery.ACCOUNT_COMPONENT_NAME], "");
+        values.put(CALLLOG_QUERY_PROJECTION[CallLogQuery.ACCOUNT_ID], "");
+        values.put(CALLLOG_QUERY_PROJECTION[CallLogQuery.FEATURES], 0);
+        values.put(CALLLOG_QUERY_PROJECTION[23], "");
+        return values;
+    }
+
+    private static ContentValues getVoicemailContentValues() {
+        ContentValues values = new ContentValues();
+        values.put(VoicemailContract.Voicemails.NUMBER, TEST_NUMBER);
+        values.put(VoicemailContract.Voicemails.DATE, TEST_DATE);
+        values.put(VoicemailContract.Voicemails.DURATION, TEST_DURATION);
+        values.put(VoicemailContract.Voicemails.MIME_TYPE, TEST_MIME_TYPE);
+        values.put(VoicemailContract.Voicemails._ID, TEST_SERVER_ID);
+        values.put(VoicemailContract.Voicemails.TRANSCRIPTION, TEST_TRANSCRIPTION);
+        values.put("_data", TEST_FILE_PATH);    // VoicemailContract.Voicemails._DATA
+        return values;
+    }
+
+    private static ContentValues getCombinedContentValues() {
+        ContentValues values = new ContentValues();
+        // Call log info
+        values.put(VoicemailArchiveContract.VoicemailArchive.GEOCODED_LOCATION, "");
+        values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_NAME, "");
+        values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_NUMBER_TYPE, 0);
+        values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_NUMBER_LABEL, "");
+        values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_LOOKUP_URI, "");
+        values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_MATCHED_NUMBER, "");
+        values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_NORMALIZED_NUMBER, "");
+        values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_FORMATTED_NUMBER, "");
+        values.put(VoicemailArchiveContract.VoicemailArchive.NUMBER_PRESENTATION, 0);
+        values.put(VoicemailArchiveContract.VoicemailArchive.ACCOUNT_COMPONENT_NAME, "");
+        values.put(VoicemailArchiveContract.VoicemailArchive.ACCOUNT_ID, "");
+        values.put(VoicemailArchiveContract.VoicemailArchive.FEATURES, 0);
+        values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_PHOTO_URI, "");
+        values.put(VoicemailArchiveContract.VoicemailArchive.COUNTRY_ISO, "");
+
+        // Voicemail content info
+        values.put(VoicemailArchiveContract.VoicemailArchive.NUMBER, TEST_NUMBER);
+        values.put(VoicemailArchiveContract.VoicemailArchive.DATE, TEST_DATE);
+        values.put(VoicemailArchiveContract.VoicemailArchive.DURATION, TEST_DURATION);
+        values.put(VoicemailArchiveContract.VoicemailArchive.MIME_TYPE, TEST_MIME_TYPE);
+        values.put(VoicemailArchiveContract.VoicemailArchive.SERVER_ID, TEST_SERVER_ID);
+        values.put(VoicemailArchiveContract.VoicemailArchive.TRANSCRIPTION, TEST_TRANSCRIPTION);
+
+        // Custom fields
+        values.put(VoicemailArchiveContract.VoicemailArchive.ARCHIVED, false);
+        return values;
+    }
+
+    private static ContentValues getCombinedContentValuesWithData() {
+        ContentValues values = getCombinedContentValues();
+        values.put(VoicemailArchiveContract.VoicemailArchive._DATA, TEST_FILE_PATH);
+        return values;
+    }
+
+    private static ContentValues getCombinedValuesWithId() {
+        ContentValues values = getCombinedContentValuesWithData();
+        values.put(VoicemailArchiveContract.VoicemailArchive._ID, TEST_ID);
+        return values;
+    }
+
+    private static ContentValues getArchiveStatusUpdateValues(boolean status) {
+        ContentValues values = new ContentValues();
+        values.put(VoicemailArchiveContract.VoicemailArchive.ARCHIVED, status);
+        return values;
+    }
+
+    private static Uri getFakeVoicemailUri() {
+        return VoicemailArchiveContract.VoicemailArchive.buildWithId(0);
+    }
+
+    private void verifyQueries() {
+        mArchiveContentProvider.verify();
+        mCallLogContentProvider.verify();
+        mVoicemailContentProvider.verify();
+    }
+
+    private class TestVoicemailContentProvider extends MockContentProvider {
+        @Override
+        public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
+            int modeBits = ParcelFileDescriptor.parseMode(mode);
+            try {
+                return ParcelFileDescriptor.open(mTemporaryFolder.newFile(), modeBits);
+            } catch (IOException e) {
+                return null;
+            }
+        }
+
+        @Override
+        public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts) {
+            try {
+                return new AssetFileDescriptor(openFile(uri, "r"), 0,
+                        AssetFileDescriptor.UNKNOWN_LENGTH);
+            } catch (FileNotFoundException e) {
+                return null;
+            }
+        }
+    }
+}
+