am 112e70ce: am 7c461dc3: Add AsyncTaskUtil for call log actions.

* commit '112e70cedcffdbbe34ed636499fd4d796aeb5f9a':
  Add AsyncTaskUtil for call log actions.
diff --git a/src/com/android/dialer/CallDetailActivity.java b/src/com/android/dialer/CallDetailActivity.java
index 18cf753..db247ba 100644
--- a/src/com/android/dialer/CallDetailActivity.java
+++ b/src/com/android/dialer/CallDetailActivity.java
@@ -19,17 +19,12 @@
 import android.app.Activity;
 import android.content.ContentResolver;
 import android.content.ContentUris;
-import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.Resources;
-import android.database.Cursor;
 import android.net.Uri;
-import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.PowerManager;
-import android.provider.CallLog;
-import android.provider.CallLog.Calls;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.VoicemailContract.Voicemails;
 import android.telecom.PhoneAccount;
@@ -55,15 +50,14 @@
 import com.android.contacts.common.GeoUtil;
 import com.android.contacts.common.CallUtil;
 import com.android.dialer.calllog.CallDetailHistoryAdapter;
-import com.android.dialer.calllog.CallLogNotificationsService;
+import com.android.dialer.calllog.CallLogAsyncTaskUtil.CallLogAsyncTaskListener;
+import com.android.dialer.calllog.CallLogAsyncTaskUtil;
 import com.android.dialer.calllog.CallTypeHelper;
 import com.android.dialer.calllog.ContactInfo;
 import com.android.dialer.calllog.ContactInfoHelper;
 import com.android.dialer.calllog.PhoneAccountUtils;
 import com.android.dialer.calllog.PhoneNumberDisplayUtil;
 import com.android.dialer.calllog.PhoneNumberUtilsWrapper;
-import com.android.dialer.util.AsyncTaskExecutor;
-import com.android.dialer.util.AsyncTaskExecutors;
 import com.android.dialer.util.IntentUtil;
 import com.android.dialer.util.DialerUtils;
 import com.android.dialer.util.TelecomUtil;
@@ -83,18 +77,7 @@
 public class CallDetailActivity extends Activity {
     private static final String TAG = "CallDetail";
 
-    private static final char LEFT_TO_RIGHT_EMBEDDING = '\u202A';
-    private static final char POP_DIRECTIONAL_FORMATTING = '\u202C';
-
-    /** The enumeration of {@link AsyncTask} objects used in this class. */
-    public enum Tasks {
-        MARK_VOICEMAIL_READ,
-        DELETE_VOICEMAIL_AND_FINISH,
-        REMOVE_FROM_CALL_LOG_AND_FINISH,
-        UPDATE_PHONE_CALL_DETAILS,
-    }
-
-    /** A long array extra containing ids of call log entries to display. */
+     /** A long array extra containing ids of call log entries to display. */
     public static final String EXTRA_CALL_LOG_IDS = "EXTRA_CALL_LOG_IDS";
     /** If we are started with a voicemail, we'll find the uri to play with this extra. */
     public static final String EXTRA_VOICEMAIL_URI = "EXTRA_VOICEMAIL_URI";
@@ -105,12 +88,128 @@
 
     public static final String VOICEMAIL_FRAGMENT_TAG = "voicemail_fragment";
 
+    private CallLogAsyncTaskListener mCallLogAsyncTaskListener = new CallLogAsyncTaskListener() {
+        @Override
+        public void onDeleteCall() {
+            finish();
+        }
+
+        @Override
+        public void onDeleteVoicemail() {
+            finish();
+        }
+
+        @Override
+        public void onGetCallDetails(PhoneCallDetails[] details) {
+            Context context = CallDetailActivity.this;
+
+            if (details == null) {
+                // Somewhere went wrong: we're going to bail out and show error to users.
+                Toast.makeText(context, R.string.toast_call_detail_error,
+                        Toast.LENGTH_SHORT).show();
+                finish();
+                return;
+            }
+
+            // We know that all calls are from the same number and the same contact, so pick the
+            // first.
+            PhoneCallDetails firstDetails = details[0];
+            mNumber = TextUtils.isEmpty(firstDetails.number) ?
+                    null : firstDetails.number.toString();
+            final int numberPresentation = firstDetails.numberPresentation;
+            final Uri contactUri = firstDetails.contactUri;
+            final Uri photoUri = firstDetails.photoUri;
+            final PhoneAccountHandle accountHandle = firstDetails.accountHandle;
+
+            // Cache the details about the phone number.
+            final boolean canPlaceCallsTo =
+                PhoneNumberUtilsWrapper.canPlaceCallsTo(mNumber, numberPresentation);
+            final PhoneNumberUtilsWrapper phoneUtils = new PhoneNumberUtilsWrapper(context);
+            final boolean isVoicemailNumber =
+                    phoneUtils.isVoicemailNumber(accountHandle, mNumber);
+            final boolean isSipNumber = PhoneNumberUtilsWrapper.isSipNumber(mNumber);
+
+            final CharSequence callLocationOrType = getNumberTypeOrLocation(firstDetails);
+
+            final CharSequence displayNumber = firstDetails.displayNumber;
+            final String displayNumberStr = mBidiFormatter.unicodeWrap(
+                    displayNumber.toString(), TextDirectionHeuristics.LTR);
+
+            if (!TextUtils.isEmpty(firstDetails.name)) {
+                mCallerName.setText(firstDetails.name);
+                mCallerNumber.setText(callLocationOrType + " " + displayNumberStr);
+            } else {
+                mCallerName.setText(displayNumberStr);
+                if (!TextUtils.isEmpty(callLocationOrType)) {
+                    mCallerNumber.setText(callLocationOrType);
+                    mCallerNumber.setVisibility(View.VISIBLE);
+                } else {
+                    mCallerNumber.setVisibility(View.GONE);
+                }
+            }
+
+            String accountLabel = PhoneAccountUtils.getAccountLabel(context, accountHandle);
+            if (!TextUtils.isEmpty(accountLabel)) {
+                mAccountLabel.setText(accountLabel);
+                mAccountLabel.setVisibility(View.VISIBLE);
+            } else {
+                mAccountLabel.setVisibility(View.GONE);
+            }
+
+            mHasEditNumberBeforeCallOption =
+                    canPlaceCallsTo && !isSipNumber && !isVoicemailNumber;
+            mHasTrashOption = hasVoicemail();
+            mHasRemoveFromCallLogOption = !hasVoicemail();
+            invalidateOptionsMenu();
+
+            ListView historyList = (ListView) findViewById(R.id.history);
+            historyList.setAdapter(
+                    new CallDetailHistoryAdapter(context, mInflater, mCallTypeHelper, details));
+
+            String lookupKey = contactUri == null ? null
+                    : ContactInfoHelper.getLookupKeyFromUri(contactUri);
+
+            final boolean isBusiness = mContactInfoHelper.isBusiness(firstDetails.sourceType);
+
+            final int contactType =
+                    isVoicemailNumber ? ContactPhotoManager.TYPE_VOICEMAIL :
+                    isBusiness ? ContactPhotoManager.TYPE_BUSINESS :
+                    ContactPhotoManager.TYPE_DEFAULT;
+
+            String nameForDefaultImage;
+            if (TextUtils.isEmpty(firstDetails.name)) {
+                nameForDefaultImage = firstDetails.displayNumber;
+            } else {
+                nameForDefaultImage = firstDetails.name.toString();
+            }
+
+            loadContactPhotos(
+                    contactUri, photoUri, nameForDefaultImage, lookupKey, contactType);
+            findViewById(R.id.call_detail).setVisibility(View.VISIBLE);
+        }
+
+        /**
+         * Determines the location geocode text for a call, or the phone number type
+         * (if available).
+         *
+         * @param details The call details.
+         * @return The phone number type or location.
+         */
+        private CharSequence getNumberTypeOrLocation(PhoneCallDetails details) {
+            if (!TextUtils.isEmpty(details.name)) {
+                return Phone.getTypeLabel(mResources, details.numberType,
+                        details.numberLabel);
+            } else {
+                return details.geocode;
+            }
+        }
+    };
+
     private CallTypeHelper mCallTypeHelper;
     private QuickContactBadge mQuickContactBadge;
     private TextView mCallerName;
     private TextView mCallerNumber;
     private TextView mAccountLabel;
-    private AsyncTaskExecutor mAsyncTaskExecutor;
     private ContactInfoHelper mContactInfoHelper;
 
     private String mNumber = null;
@@ -133,41 +232,12 @@
     /** Whether we should show "remove from call log" in the options menu. */
     private boolean mHasRemoveFromCallLogOption;
 
-    static final String[] CALL_LOG_PROJECTION = new String[] {
-        CallLog.Calls.DATE,
-        CallLog.Calls.DURATION,
-        CallLog.Calls.NUMBER,
-        CallLog.Calls.TYPE,
-        CallLog.Calls.COUNTRY_ISO,
-        CallLog.Calls.GEOCODED_LOCATION,
-        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
-    };
-
-    static final int DATE_COLUMN_INDEX = 0;
-    static final int DURATION_COLUMN_INDEX = 1;
-    static final int NUMBER_COLUMN_INDEX = 2;
-    static final int CALL_TYPE_COLUMN_INDEX = 3;
-    static final int COUNTRY_ISO_COLUMN_INDEX = 4;
-    static final int GEOCODED_LOCATION_COLUMN_INDEX = 5;
-    static final int NUMBER_PRESENTATION_COLUMN_INDEX = 6;
-    static final int ACCOUNT_COMPONENT_NAME = 7;
-    static final int ACCOUNT_ID = 8;
-    static final int FEATURES = 9;
-    static final int DATA_USAGE = 10;
-    static final int TRANSCRIPTION_COLUMN_INDEX = 11;
-
     @Override
     protected void onCreate(Bundle icicle) {
         super.onCreate(icicle);
 
         setContentView(R.layout.call_detail);
 
-        mAsyncTaskExecutor = AsyncTaskExecutors.createThreadPoolExecutor();
         mInflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
         mResources = getResources();
 
@@ -196,7 +266,8 @@
     @Override
     public void onResume() {
         super.onResume();
-        updateData(getCallLogEntryUris());
+
+        CallLogAsyncTaskUtil.getCallDetails(this, getCallLogEntryUris(), mCallLogAsyncTaskListener);
     }
 
     /**
@@ -237,7 +308,7 @@
             }
 
             voicemailContainer.setVisibility(View.VISIBLE);
-            markVoicemailAsRead(mVoicemailUri);
+            CallLogAsyncTaskUtil.markVoicemailAsRead(this, mVoicemailUri);
         }
     }
 
@@ -245,22 +316,6 @@
         return mVoicemailUri != null;
     }
 
-    private void markVoicemailAsRead(final Uri voicemailUri) {
-        mAsyncTaskExecutor.submit(Tasks.MARK_VOICEMAIL_READ, new AsyncTask<Void, Void, Void>() {
-            @Override
-            public Void doInBackground(Void... params) {
-                ContentValues values = new ContentValues();
-                values.put(Voicemails.IS_READ, true);
-                getContentResolver().update(voicemailUri, values,
-                        Voicemails.IS_READ + " = 0", null);
-                Intent intent = new Intent(getBaseContext(), CallLogNotificationsService.class);
-                intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD);
-                getBaseContext().startService(intent);
-                return null;
-            }
-        });
-    }
-
     /**
      * Returns the list of URIs to show.
      * <p>
@@ -285,207 +340,6 @@
         return uris;
     }
 
-    /**
-     * Update user interface with details of given call.
-     *
-     * @param callUris URIs into {@link android.provider.CallLog.Calls} of the calls to be displayed
-     */
-    private void updateData(final Uri... callUris) {
-        class UpdateContactDetailsTask extends AsyncTask<Void, Void, PhoneCallDetails[]> {
-            @Override
-            public PhoneCallDetails[] doInBackground(Void... params) {
-                // TODO: All phone calls correspond to the same person, so we can make a single
-                // lookup.
-                final int numCalls = callUris.length;
-                PhoneCallDetails[] details = new PhoneCallDetails[numCalls];
-                try {
-                    for (int index = 0; index < numCalls; ++index) {
-                        details[index] = getPhoneCallDetailsForUri(callUris[index]);
-                    }
-                    return details;
-                } catch (IllegalArgumentException e) {
-                    // Something went wrong reading in our primary data.
-                    Log.w(TAG, "invalid URI starting call details", e);
-                    return null;
-                }
-            }
-
-            @Override
-            public void onPostExecute(PhoneCallDetails[] details) {
-                Context context = CallDetailActivity.this;
-
-                if (details == null) {
-                    // Somewhere went wrong: we're going to bail out and show error to users.
-                    Toast.makeText(context, R.string.toast_call_detail_error,
-                            Toast.LENGTH_SHORT).show();
-                    finish();
-                    return;
-                }
-
-                // We know that all calls are from the same number and the same contact, so pick the
-                // first.
-                PhoneCallDetails firstDetails = details[0];
-                mNumber = TextUtils.isEmpty(firstDetails.number) ?
-                        null : firstDetails.number.toString();
-                final int numberPresentation = firstDetails.numberPresentation;
-                final Uri contactUri = firstDetails.contactUri;
-                final Uri photoUri = firstDetails.photoUri;
-                final PhoneAccountHandle accountHandle = firstDetails.accountHandle;
-
-                // Cache the details about the phone number.
-                final boolean canPlaceCallsTo =
-                    PhoneNumberUtilsWrapper.canPlaceCallsTo(mNumber, numberPresentation);
-                final PhoneNumberUtilsWrapper phoneUtils = new PhoneNumberUtilsWrapper(context);
-                final boolean isVoicemailNumber =
-                        phoneUtils.isVoicemailNumber(accountHandle, mNumber);
-                final boolean isSipNumber = PhoneNumberUtilsWrapper.isSipNumber(mNumber);
-
-                final CharSequence callLocationOrType = getNumberTypeOrLocation(firstDetails);
-
-                final CharSequence displayNumber = firstDetails.displayNumber;
-                final String displayNumberStr = mBidiFormatter.unicodeWrap(
-                        displayNumber.toString(), TextDirectionHeuristics.LTR);
-
-                if (!TextUtils.isEmpty(firstDetails.name)) {
-                    mCallerName.setText(firstDetails.name);
-                    mCallerNumber.setText(callLocationOrType + " " + displayNumberStr);
-                } else {
-                    mCallerName.setText(displayNumberStr);
-                    if (!TextUtils.isEmpty(callLocationOrType)) {
-                        mCallerNumber.setText(callLocationOrType);
-                        mCallerNumber.setVisibility(View.VISIBLE);
-                    } else {
-                        mCallerNumber.setVisibility(View.GONE);
-                    }
-                }
-
-                String accountLabel = PhoneAccountUtils.getAccountLabel(context, accountHandle);
-                if (!TextUtils.isEmpty(accountLabel)) {
-                    mAccountLabel.setText(accountLabel);
-                    mAccountLabel.setVisibility(View.VISIBLE);
-                } else {
-                    mAccountLabel.setVisibility(View.GONE);
-                }
-
-                mHasEditNumberBeforeCallOption =
-                        canPlaceCallsTo && !isSipNumber && !isVoicemailNumber;
-                mHasTrashOption = hasVoicemail();
-                mHasRemoveFromCallLogOption = !hasVoicemail();
-                invalidateOptionsMenu();
-
-                ListView historyList = (ListView) findViewById(R.id.history);
-                historyList.setAdapter(
-                        new CallDetailHistoryAdapter(context, mInflater, mCallTypeHelper, details));
-
-                String lookupKey = contactUri == null ? null
-                        : ContactInfoHelper.getLookupKeyFromUri(contactUri);
-
-                final boolean isBusiness = mContactInfoHelper.isBusiness(firstDetails.sourceType);
-
-                final int contactType =
-                        isVoicemailNumber? ContactPhotoManager.TYPE_VOICEMAIL :
-                        isBusiness ? ContactPhotoManager.TYPE_BUSINESS :
-                        ContactPhotoManager.TYPE_DEFAULT;
-
-                String nameForDefaultImage;
-                if (TextUtils.isEmpty(firstDetails.name)) {
-                    nameForDefaultImage = firstDetails.displayNumber.toString();
-                } else {
-                    nameForDefaultImage = firstDetails.name.toString();
-                }
-
-                loadContactPhotos(
-                        contactUri, photoUri, nameForDefaultImage, lookupKey, contactType);
-                findViewById(R.id.call_detail).setVisibility(View.VISIBLE);
-            }
-
-            /**
-             * Determines the location geocode text for a call, or the phone number type
-             * (if available).
-             *
-             * @param details The call details.
-             * @return The phone number type or location.
-             */
-            private CharSequence getNumberTypeOrLocation(PhoneCallDetails details) {
-                if (!TextUtils.isEmpty(details.name)) {
-                    return Phone.getTypeLabel(mResources, details.numberType,
-                            details.numberLabel);
-                } else {
-                    return details.geocode;
-                }
-            }
-        }
-        mAsyncTaskExecutor.submit(Tasks.UPDATE_PHONE_CALL_DETAILS, new UpdateContactDetailsTask());
-    }
-
-    /** Return the phone call details for a given call log URI. */
-    private PhoneCallDetails getPhoneCallDetailsForUri(Uri callUri) {
-        ContentResolver resolver = getContentResolver();
-        Cursor callCursor = resolver.query(callUri, CALL_LOG_PROJECTION, null, null, null);
-        try {
-            if (callCursor == null || !callCursor.moveToFirst()) {
-                throw new IllegalArgumentException("Cannot find content: " + callUri);
-            }
-
-            // Read call log specifics.
-            final String number = callCursor.getString(NUMBER_COLUMN_INDEX);
-            final int numberPresentation = callCursor.getInt(
-                    NUMBER_PRESENTATION_COLUMN_INDEX);
-            final long date = callCursor.getLong(DATE_COLUMN_INDEX);
-            final long duration = callCursor.getLong(DURATION_COLUMN_INDEX);
-            final int callType = callCursor.getInt(CALL_TYPE_COLUMN_INDEX);
-            String countryIso = callCursor.getString(COUNTRY_ISO_COLUMN_INDEX);
-            final String geocode = callCursor.getString(GEOCODED_LOCATION_COLUMN_INDEX);
-            final String transcription = callCursor.getString(TRANSCRIPTION_COLUMN_INDEX);
-
-            final PhoneAccountHandle accountHandle = PhoneAccountUtils.getAccount(
-                    callCursor.getString(ACCOUNT_COMPONENT_NAME),
-                    callCursor.getString(ACCOUNT_ID));
-
-            if (TextUtils.isEmpty(countryIso)) {
-                countryIso = mDefaultCountryIso;
-            }
-
-            // Formatted phone number.
-            final CharSequence formattedNumber;
-
-            // If this is not a regular number, there is no point in looking it up in the contacts.
-            ContactInfo info = ContactInfo.EMPTY;
-            final boolean isVoicemail = new PhoneNumberUtilsWrapper(this)
-                    .isVoicemailNumber(accountHandle, number);
-            if (PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation)
-                    && !isVoicemail) {
-                mContactInfoHelper.lookupNumber(number, countryIso);
-            }
-            if (info == null) {
-                formattedNumber = PhoneNumberDisplayUtil.getDisplayNumber(
-                        this,
-                        accountHandle,
-                        number,
-                        numberPresentation,
-                        null /* formattedNumber */,
-                        isVoicemail);
-            } else {
-                formattedNumber = info.formattedNumber;
-            }
-            final int features = callCursor.getInt(FEATURES);
-            Long dataUsage = null;
-            if (!callCursor.isNull(DATA_USAGE)) {
-                dataUsage = callCursor.getLong(DATA_USAGE);
-            }
-            return new PhoneCallDetails(this, number, numberPresentation,
-                    formattedNumber, countryIso, geocode,
-                    new int[]{ callType }, date, duration,
-                    info.name, info.type, info.label, info.lookupUri, info.photoUri,
-                    info.sourceType, accountHandle, features, dataUsage, transcription,
-                    isVoicemail);
-        } finally {
-            if (callCursor != null) {
-                callCursor.close();
-            }
-        }
-    }
-
     /** Load the contact photos and places them in the corresponding views. */
     private void loadContactPhotos(Uri contactUri, Uri photoUri, String displayName,
             String lookupKey, int contactType) {
@@ -525,22 +379,8 @@
             }
             callIds.append(ContentUris.parseId(callUri));
         }
-        mAsyncTaskExecutor.submit(Tasks.REMOVE_FROM_CALL_LOG_AND_FINISH,
-                new AsyncTask<Void, Void, Void>() {
-                    @Override
-                    public Void doInBackground(Void... params) {
-                        getContentResolver().delete(
-                                TelecomUtil.getCallLogUri(CallDetailActivity.this),
-                                Calls._ID + " IN (" + callIds + ")", null);
-                        return null;
-                    }
 
-                    @Override
-                    public void onPostExecute(Void result) {
-                        finish();
-                    }
-                }
-        );
+        CallLogAsyncTaskUtil.deleteCalls(this, callIds.toString(), mCallLogAsyncTaskListener);
     }
 
     public void onMenuEditNumberBeforeCall(MenuItem menuItem) {
@@ -548,20 +388,7 @@
     }
 
     public void onMenuTrashVoicemail(MenuItem menuItem) {
-        mAsyncTaskExecutor.submit(Tasks.DELETE_VOICEMAIL_AND_FINISH,
-                new AsyncTask<Void, Void, Void>() {
-                    @Override
-                    public Void doInBackground(Void... params) {
-                        getContentResolver().delete(mVoicemailUri, null, null);
-                        return null;
-                    }
-
-                    @Override
-                    public void onPostExecute(Void result) {
-                        finish();
-                    }
-                }
-        );
+        CallLogAsyncTaskUtil.deleteVoicemail(this, mVoicemailUri, mCallLogAsyncTaskListener);
     }
 
     private void closeSystemDialogs() {
diff --git a/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java b/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java
new file mode 100644
index 0000000..20e213c
--- /dev/null
+++ b/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java
@@ -0,0 +1,300 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.calllog;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.CallLog;
+import android.provider.VoicemailContract.Voicemails;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.contacts.common.GeoUtil;
+import com.android.dialer.PhoneCallDetails;
+import com.android.dialer.util.AsyncTaskExecutor;
+import com.android.dialer.util.AsyncTaskExecutors;
+import com.android.dialer.util.TelecomUtil;
+
+public class CallLogAsyncTaskUtil {
+    private static String TAG = CallLogAsyncTaskUtil.class.getSimpleName();
+
+   /** The enumeration of {@link AsyncTask} objects used in this class. */
+    public enum Tasks {
+        DELETE_VOICEMAIL,
+        DELETE_CALL,
+        MARK_VOICEMAIL_READ,
+        GET_CALL_DETAILS,
+    }
+
+    private static class CallDetailQuery {
+        static final String[] CALL_LOG_PROJECTION = new String[] {
+            CallLog.Calls.DATE,
+            CallLog.Calls.DURATION,
+            CallLog.Calls.NUMBER,
+            CallLog.Calls.TYPE,
+            CallLog.Calls.COUNTRY_ISO,
+            CallLog.Calls.GEOCODED_LOCATION,
+            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
+        };
+
+        static final int DATE_COLUMN_INDEX = 0;
+        static final int DURATION_COLUMN_INDEX = 1;
+        static final int NUMBER_COLUMN_INDEX = 2;
+        static final int CALL_TYPE_COLUMN_INDEX = 3;
+        static final int COUNTRY_ISO_COLUMN_INDEX = 4;
+        static final int GEOCODED_LOCATION_COLUMN_INDEX = 5;
+        static final int NUMBER_PRESENTATION_COLUMN_INDEX = 6;
+        static final int ACCOUNT_COMPONENT_NAME = 7;
+        static final int ACCOUNT_ID = 8;
+        static final int FEATURES = 9;
+        static final int DATA_USAGE = 10;
+        static final int TRANSCRIPTION_COLUMN_INDEX = 11;
+    }
+
+    public interface CallLogAsyncTaskListener {
+        public void onDeleteCall();
+        public void onDeleteVoicemail();
+        public void onGetCallDetails(PhoneCallDetails[] details);
+    }
+
+    private static AsyncTaskExecutor sAsyncTaskExecutor;
+
+    private static void initTaskExecutor() {
+        sAsyncTaskExecutor = AsyncTaskExecutors.createThreadPoolExecutor();
+    }
+
+    public static void getCallDetails(
+            final Context context,
+            final Uri[] callUris,
+            final CallLogAsyncTaskListener callLogAsyncTaskListener) {
+        if (sAsyncTaskExecutor == null) {
+            initTaskExecutor();
+        }
+
+        sAsyncTaskExecutor.submit(Tasks.GET_CALL_DETAILS,
+                new AsyncTask<Void, Void, PhoneCallDetails[]>() {
+                    @Override
+                    public PhoneCallDetails[] doInBackground(Void... params) {
+                        // TODO: All calls correspond to the same person, so make a single lookup.
+                        final int numCalls = callUris.length;
+                        PhoneCallDetails[] details = new PhoneCallDetails[numCalls];
+                        try {
+                            for (int index = 0; index < numCalls; ++index) {
+                                details[index] =
+                                        getPhoneCallDetailsForUri(context, callUris[index]);
+                            }
+                            return details;
+                        } catch (IllegalArgumentException e) {
+                            // Something went wrong reading in our primary data.
+                            Log.w(TAG, "Invalid URI starting call details", e);
+                            return null;
+                        }
+                    }
+
+                    @Override
+                    public void onPostExecute(PhoneCallDetails[] phoneCallDetails) {
+                        if (callLogAsyncTaskListener != null) {
+                            callLogAsyncTaskListener.onGetCallDetails(phoneCallDetails);
+                        }
+                    }
+                });
+    }
+
+    /**
+     * Return the phone call details for a given call log URI.
+     */
+    private static PhoneCallDetails getPhoneCallDetailsForUri(Context context, Uri callUri) {
+        Cursor cursor = context.getContentResolver().query(
+                callUri, CallDetailQuery.CALL_LOG_PROJECTION, null, null, null);
+
+        try {
+            if (cursor == null || !cursor.moveToFirst()) {
+                throw new IllegalArgumentException("Cannot find content: " + callUri);
+            }
+
+            // Read call log.
+            final String number = cursor.getString(CallDetailQuery.NUMBER_COLUMN_INDEX);
+            final int numberPresentation =
+                    cursor.getInt(CallDetailQuery.NUMBER_PRESENTATION_COLUMN_INDEX);
+            final long date = cursor.getLong(CallDetailQuery.DATE_COLUMN_INDEX);
+            final long duration = cursor.getLong(CallDetailQuery.DURATION_COLUMN_INDEX);
+            final int callType = cursor.getInt(CallDetailQuery.CALL_TYPE_COLUMN_INDEX);
+            final String geocode = cursor.getString(CallDetailQuery.GEOCODED_LOCATION_COLUMN_INDEX);
+            final String transcription =
+                    cursor.getString(CallDetailQuery.TRANSCRIPTION_COLUMN_INDEX);
+
+            final PhoneAccountHandle accountHandle = PhoneAccountUtils.getAccount(
+                    cursor.getString(CallDetailQuery.ACCOUNT_COMPONENT_NAME),
+                    cursor.getString(CallDetailQuery.ACCOUNT_ID));
+
+            String countryIso = cursor.getString(CallDetailQuery.COUNTRY_ISO_COLUMN_INDEX);
+            if (TextUtils.isEmpty(countryIso)) {
+                countryIso = GeoUtil.getCurrentCountryIso(context);
+            }
+
+            // Formatted phone number.
+            final CharSequence formattedNumber;
+            // Read contact specifics.
+            final CharSequence nameText;
+            final int numberType;
+            final CharSequence numberLabel;
+            final Uri photoUri;
+            final Uri lookupUri;
+            int sourceType;
+
+            // If this is not a regular number, there is no point in looking it up in the contacts.
+            ContactInfoHelper contactInfoHelper =
+                    new ContactInfoHelper(context, GeoUtil.getCurrentCountryIso(context));
+            PhoneNumberUtilsWrapper phoneNumberUtilsWrapper =
+                    new PhoneNumberUtilsWrapper(context);
+            boolean isVoicemail = phoneNumberUtilsWrapper.isVoicemailNumber(accountHandle, number);
+            boolean shouldLookupNumber =
+                    PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation)
+                            && !isVoicemail;
+            ContactInfo info = shouldLookupNumber
+                            ? contactInfoHelper.lookupNumber(number, countryIso) : null;
+
+            if (info == null) {
+                formattedNumber = PhoneNumberDisplayUtil.getDisplayNumber(
+                        context, accountHandle, number, numberPresentation, null, isVoicemail);
+                nameText = "";
+                numberType = 0;
+                numberLabel = "";
+                photoUri = null;
+                lookupUri = null;
+                sourceType = 0;
+            } else {
+                formattedNumber = info.formattedNumber;
+                nameText = info.name;
+                numberType = info.type;
+                numberLabel = info.label;
+                photoUri = info.photoUri;
+                lookupUri = info.lookupUri;
+                sourceType = info.sourceType;
+            }
+
+
+            final int features = cursor.getInt(CallDetailQuery.FEATURES);
+
+            Long dataUsage = null;
+            if (!cursor.isNull(CallDetailQuery.DATA_USAGE)) {
+                dataUsage = cursor.getLong(CallDetailQuery.DATA_USAGE);
+            }
+
+            return new PhoneCallDetails(context, number, numberPresentation, formattedNumber,
+                    countryIso, geocode, new int[]{ callType }, date, duration, nameText,
+                    numberType, numberLabel, lookupUri, photoUri, sourceType, accountHandle,
+                    features, dataUsage, transcription, isVoicemail);
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+    }
+
+
+    /**
+     * Delete specified calls from the call log.
+     *
+     * @param context The context.
+     * @param callIds String of the callIds to delete from the call log, delimited by commas (",").
+     * @param callLogAsyncTaskListenerg The listener to invoke after the entries have been deleted.
+     */
+    public static void deleteCalls(
+            final Context context,
+            final String callIds,
+            final CallLogAsyncTaskListener callLogAsyncTaskListener) {
+        if (sAsyncTaskExecutor == null) {
+            initTaskExecutor();
+        }
+
+        sAsyncTaskExecutor.submit(Tasks.DELETE_CALL,
+                new AsyncTask<Void, Void, Void>() {
+                    @Override
+                    public Void doInBackground(Void... params) {
+                        context.getContentResolver().delete(
+                                TelecomUtil.getCallLogUri(context),
+                                CallLog.Calls._ID + " IN (" + callIds + ")", null);
+                        return null;
+                    }
+
+                    @Override
+                    public void onPostExecute(Void result) {
+                        if (callLogAsyncTaskListener != null) {
+                            callLogAsyncTaskListener.onDeleteCall();
+                        }
+                    }
+                });
+
+    }
+
+    public static void markVoicemailAsRead(final Context context, final Uri voicemailUri) {
+        if (sAsyncTaskExecutor == null) {
+            initTaskExecutor();
+        }
+
+        sAsyncTaskExecutor.submit(Tasks.MARK_VOICEMAIL_READ, new AsyncTask<Void, Void, Void>() {
+            @Override
+            public Void doInBackground(Void... params) {
+                ContentValues values = new ContentValues();
+                values.put(Voicemails.IS_READ, true);
+                context.getContentResolver().update(
+                        voicemailUri, values, Voicemails.IS_READ + " = 0", null);
+
+                Intent intent = new Intent(context, CallLogNotificationsService.class);
+                intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD);
+                context.startService(intent);
+                return null;
+            }
+        });
+    }
+
+    public static void deleteVoicemail(
+            final Context context,
+            final Uri voicemailUri,
+            final CallLogAsyncTaskListener callLogAsyncTaskListener) {
+        if (sAsyncTaskExecutor == null) {
+            initTaskExecutor();
+        }
+
+        sAsyncTaskExecutor.submit(Tasks.DELETE_VOICEMAIL,
+                new AsyncTask<Void, Void, Void>() {
+                    @Override
+                    public Void doInBackground(Void... params) {
+                        context.getContentResolver().delete(voicemailUri, null, null);
+                        return null;
+                    }
+
+                    @Override
+                    public void onPostExecute(Void result) {
+                        if (callLogAsyncTaskListener != null) {
+                            callLogAsyncTaskListener.onDeleteVoicemail();
+                        }
+                    }
+                });
+    }
+}
diff --git a/tests/src/com/android/dialer/CallDetailActivityTest.java b/tests/src/com/android/dialer/CallDetailActivityTest.java
index 15e90fc..aca8f29 100644
--- a/tests/src/com/android/dialer/CallDetailActivityTest.java
+++ b/tests/src/com/android/dialer/CallDetailActivityTest.java
@@ -16,7 +16,7 @@
 
 package com.android.dialer;
 
-import static com.android.dialer.CallDetailActivity.Tasks.UPDATE_PHONE_CALL_DETAILS;
+import static com.android.dialer.calllog.CallLogAsyncTaskUtil.Tasks.GET_CALL_DETAILS;
 import static com.android.dialer.voicemail.VoicemailPlaybackPresenter.Tasks.CHECK_FOR_CONTENT;
 import static com.android.dialer.voicemail.VoicemailPlaybackPresenter.Tasks.PREPARE_MEDIA_PLAYER;
 
@@ -303,7 +303,7 @@
         // We have to run all tasks, not just one.
         // This is because it seems that we can have onResume, onPause, onResume during the course
         // of a single unit test.
-        mFakeAsyncTaskExecutor.runAllTasks(UPDATE_PHONE_CALL_DETAILS);
+        mFakeAsyncTaskExecutor.runAllTasks(GET_CALL_DETAILS);
     }
 
     private AssetManager getAssets() {