Merge "Make update items/photos highlight when they're clickable"
diff --git a/res/drawable-hdpi/change_photo_box_normal_holo_light.9.png b/res/drawable-hdpi/change_photo_box_normal_holo_light.9.png
new file mode 100644
index 0000000..f026cc8
--- /dev/null
+++ b/res/drawable-hdpi/change_photo_box_normal_holo_light.9.png
Binary files differ
diff --git a/res/drawable-hdpi/change_photo_box_pressed_holo_light.9.png b/res/drawable-hdpi/change_photo_box_pressed_holo_light.9.png
new file mode 100644
index 0000000..a0770ea
--- /dev/null
+++ b/res/drawable-hdpi/change_photo_box_pressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_menu_expander_maximized_holo_light.png b/res/drawable-hdpi/ic_menu_expander_maximized_holo_light.png
index 3c47e9e..41c6eda 100644
--- a/res/drawable-hdpi/ic_menu_expander_maximized_holo_light.png
+++ b/res/drawable-hdpi/ic_menu_expander_maximized_holo_light.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_menu_expander_minimized_holo_light.png b/res/drawable-hdpi/ic_menu_expander_minimized_holo_light.png
index 6b8c6b0..a3b22bc 100644
--- a/res/drawable-hdpi/ic_menu_expander_minimized_holo_light.png
+++ b/res/drawable-hdpi/ic_menu_expander_minimized_holo_light.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_menu_remove_field_holo_light.png b/res/drawable-hdpi/ic_menu_remove_field_holo_light.png
index d8172ee..03fd2fb 100644
--- a/res/drawable-hdpi/ic_menu_remove_field_holo_light.png
+++ b/res/drawable-hdpi/ic_menu_remove_field_holo_light.png
Binary files differ
diff --git a/res/drawable-mdpi/change_photo_box_normal_holo_light.9.png b/res/drawable-mdpi/change_photo_box_normal_holo_light.9.png
new file mode 100644
index 0000000..591e6d5
--- /dev/null
+++ b/res/drawable-mdpi/change_photo_box_normal_holo_light.9.png
Binary files differ
diff --git a/res/drawable-mdpi/change_photo_box_pressed_holo_light.9.png b/res/drawable-mdpi/change_photo_box_pressed_holo_light.9.png
new file mode 100644
index 0000000..5d5eee2
--- /dev/null
+++ b/res/drawable-mdpi/change_photo_box_pressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_menu_expander_maximized_holo_light.png b/res/drawable-mdpi/ic_menu_expander_maximized_holo_light.png
index 3dee74d..342867c 100644
--- a/res/drawable-mdpi/ic_menu_expander_maximized_holo_light.png
+++ b/res/drawable-mdpi/ic_menu_expander_maximized_holo_light.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_menu_expander_minimized_holo_light.png b/res/drawable-mdpi/ic_menu_expander_minimized_holo_light.png
index 6dba137..f447069 100644
--- a/res/drawable-mdpi/ic_menu_expander_minimized_holo_light.png
+++ b/res/drawable-mdpi/ic_menu_expander_minimized_holo_light.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_menu_remove_field_holo_light.png b/res/drawable-mdpi/ic_menu_remove_field_holo_light.png
index cb993fd..8c44e70 100644
--- a/res/drawable-mdpi/ic_menu_remove_field_holo_light.png
+++ b/res/drawable-mdpi/ic_menu_remove_field_holo_light.png
Binary files differ
diff --git a/res/drawable-xhdpi/change_photo_box_normal_holo_light.9.png b/res/drawable-xhdpi/change_photo_box_normal_holo_light.9.png
new file mode 100644
index 0000000..7e1e97f
--- /dev/null
+++ b/res/drawable-xhdpi/change_photo_box_normal_holo_light.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/change_photo_box_pressed_holo_light.9.png b/res/drawable-xhdpi/change_photo_box_pressed_holo_light.9.png
new file mode 100644
index 0000000..e98266f
--- /dev/null
+++ b/res/drawable-xhdpi/change_photo_box_pressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_menu_expander_maximized_holo_light.png b/res/drawable-xhdpi/ic_menu_expander_maximized_holo_light.png
index 490371c..6a5ef9b 100644
--- a/res/drawable-xhdpi/ic_menu_expander_maximized_holo_light.png
+++ b/res/drawable-xhdpi/ic_menu_expander_maximized_holo_light.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_menu_expander_minimized_holo_light.png b/res/drawable-xhdpi/ic_menu_expander_minimized_holo_light.png
index 0e660ea..cb53db8 100644
--- a/res/drawable-xhdpi/ic_menu_expander_minimized_holo_light.png
+++ b/res/drawable-xhdpi/ic_menu_expander_minimized_holo_light.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_menu_remove_field_holo_light.png b/res/drawable-xhdpi/ic_menu_remove_field_holo_light.png
index 705833e..65a6b7b 100644
--- a/res/drawable-xhdpi/ic_menu_remove_field_holo_light.png
+++ b/res/drawable-xhdpi/ic_menu_remove_field_holo_light.png
Binary files differ
diff --git a/res/layout-sw580dp-w1000dp/contact_detail_fragment.xml b/res/layout-sw580dp-w1000dp/contact_detail_fragment.xml
index b8328ef..a15d98f2 100644
--- a/res/layout-sw580dp-w1000dp/contact_detail_fragment.xml
+++ b/res/layout-sw580dp-w1000dp/contact_detail_fragment.xml
@@ -46,6 +46,7 @@
         <ListView android:id="@android:id/list"
             android:layout_width="0dip"
             android:layout_height="match_parent"
+            android:fadingEdge="none"
             android:layout_weight="1"
             android:divider="@null"/>
 
diff --git a/res/layout-sw580dp-w1000dp/contact_detail_list_item.xml b/res/layout-sw580dp-w1000dp/contact_detail_list_item.xml
index b57e85c..0d94622 100644
--- a/res/layout-sw580dp-w1000dp/contact_detail_list_item.xml
+++ b/res/layout-sw580dp-w1000dp/contact_detail_list_item.xml
@@ -63,16 +63,17 @@
 
         <ImageView
             android:id="@+id/presence_icon"
-            android:layout_width="32dip"
-            android:layout_height="@dimen/detail_min_line_item_height"
-            android:layout_marginLeft="5dip"
-            android:gravity="center"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginRight="4dip"
+            android:layout_gravity="center_vertical"
             android:scaleType="centerInside" />
 
         <TextView
             android:id="@+id/kind"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
             android:visibility="gone" />
 
         <TextView
@@ -80,6 +81,7 @@
             style="@style/ContactDetailItemType"
             android:layout_width="wrap_content"
             android:layout_height="match_parent"
+            android:layout_gravity="center_vertical"
             android:paddingRight="16dip" />
 
         <View
diff --git a/res/layout-sw580dp-w1000dp/contact_detail_updates_fragment.xml b/res/layout-sw580dp-w1000dp/contact_detail_updates_fragment.xml
index 0e89d24..71c2267 100644
--- a/res/layout-sw580dp-w1000dp/contact_detail_updates_fragment.xml
+++ b/res/layout-sw580dp-w1000dp/contact_detail_updates_fragment.xml
@@ -19,4 +19,5 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:background="@color/background_social_updates"
+    android:fadingEdge="none"
     android:divider="@null"/>
diff --git a/res/layout-sw580dp/contact_detail_fragment.xml b/res/layout-sw580dp/contact_detail_fragment.xml
index f05dc56..9b997e1 100644
--- a/res/layout-sw580dp/contact_detail_fragment.xml
+++ b/res/layout-sw580dp/contact_detail_fragment.xml
@@ -33,6 +33,7 @@
         android:layout_weight="1"
         android:layout_width="match_parent"
         android:layout_height="0dip"
+        android:fadingEdge="none"
         android:cacheColorHint="#00000000"
         android:divider="@null"
     />
diff --git a/res/layout-sw580dp/contact_detail_updates_fragment.xml b/res/layout-sw580dp/contact_detail_updates_fragment.xml
index d73275e..6002151 100644
--- a/res/layout-sw580dp/contact_detail_updates_fragment.xml
+++ b/res/layout-sw580dp/contact_detail_updates_fragment.xml
@@ -25,6 +25,7 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:background="@color/background_social_updates"
+        android:fadingEdge="none"
         android:divider="@null"/>
 
     <View
diff --git a/res/layout-sw580dp/contact_editor_fragment.xml b/res/layout-sw580dp/contact_editor_fragment.xml
index 988be2a..572de4f 100644
--- a/res/layout-sw580dp/contact_editor_fragment.xml
+++ b/res/layout-sw580dp/contact_editor_fragment.xml
@@ -26,6 +26,7 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:orientation="vertical"
+        android:fadingEdge="none"
         ex:layout_wideParentWidth="800dip"
         ex:layout_wideMarginLeft="128dip"
         ex:layout_wideMarginRight="128dip"
diff --git a/res/layout-w470dp/contact_detail_fragment.xml b/res/layout-w470dp/contact_detail_fragment.xml
index 4f26cc5..b523ff8 100644
--- a/res/layout-w470dp/contact_detail_fragment.xml
+++ b/res/layout-w470dp/contact_detail_fragment.xml
@@ -49,6 +49,7 @@
         <ListView android:id="@android:id/list"
             android:layout_width="0dip"
             android:layout_height="match_parent"
+            android:fadingEdge="none"
             android:layout_weight="1"
             android:divider="@null"/>
 
@@ -57,6 +58,7 @@
     <ScrollView android:id="@android:id/empty"
         android:layout_width="match_parent"
         android:layout_height="0px"
+        android:fadingEdge="none"
         android:visibility="gone">
         <TextView android:id="@+id/emptyText"
             android:layout_width="match_parent"
diff --git a/res/layout-w470dp/contact_detail_updates_fragment.xml b/res/layout-w470dp/contact_detail_updates_fragment.xml
index 6081cfd..44207fc 100644
--- a/res/layout-w470dp/contact_detail_updates_fragment.xml
+++ b/res/layout-w470dp/contact_detail_updates_fragment.xml
@@ -24,6 +24,7 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:background="@color/background_social_updates"
+        android:fadingEdge="none"
         android:divider="@null"/>
 
     <View
diff --git a/res/layout/call_log_fragment.xml b/res/layout/call_log_fragment.xml
index ff7dd4b..7cfcef3 100644
--- a/res/layout/call_log_fragment.xml
+++ b/res/layout/call_log_fragment.xml
@@ -37,6 +37,7 @@
         <ListView android:id="@android:id/list"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
+            android:fadingEdge="none"
             android:scrollbarStyle="outsideOverlay"
         />
         <TextView android:id="@android:id/empty"
diff --git a/res/layout/contact_detail_fragment.xml b/res/layout/contact_detail_fragment.xml
index 581dc03..a7b509d 100644
--- a/res/layout/contact_detail_fragment.xml
+++ b/res/layout/contact_detail_fragment.xml
@@ -24,6 +24,7 @@
         android:layout_width="match_parent"
         android:layout_height="0px"
         android:layout_weight="1"
+        android:fadingEdge="none"
         android:background="@color/background_primary"
         android:divider="@null"/>
 
@@ -31,6 +32,7 @@
         android:layout_width="match_parent"
         android:layout_height="0px"
         android:layout_weight="1"
+        android:fadingEdge="none"
         android:visibility="gone">
 
         <TextView android:id="@+id/emptyText"
diff --git a/res/layout/contact_detail_list_item.xml b/res/layout/contact_detail_list_item.xml
index e292f39..4d6ed5f 100644
--- a/res/layout/contact_detail_list_item.xml
+++ b/res/layout/contact_detail_list_item.xml
@@ -64,6 +64,15 @@
                 android:layout_height="wrap_content"
                 android:orientation="horizontal">
 
+                <ImageView
+                    android:id="@+id/presence_icon"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginRight="4dip"
+                    android:layout_gravity="center_vertical"
+                    android:gravity="center"
+                    android:scaleType="centerInside" />
+
                 <TextView
                     android:id="@+id/type"
                     style="@style/ContactDetailItemType"
@@ -90,14 +99,6 @@
                 android:visibility="gone" />
 
         </LinearLayout>
-
-        <ImageView
-            android:id="@+id/presence_icon"
-            android:layout_width="32dip"
-            android:layout_height="wrap_content"
-            android:layout_marginLeft="5dip"
-            android:gravity="center"
-            android:scaleType="centerInside" />
     </com.android.contacts.detail.PrimaryActionViewContainer>
 
     <View
diff --git a/res/layout/contact_editor_fragment.xml b/res/layout/contact_editor_fragment.xml
index f3989e1..913a5e0 100644
--- a/res/layout/contact_editor_fragment.xml
+++ b/res/layout/contact_editor_fragment.xml
@@ -24,6 +24,7 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:fillViewport="true"
+        android:fadingEdge="none"
     >
 
         <LinearLayout android:id="@+id/editors"
diff --git a/res/layout/contact_tile_list.xml b/res/layout/contact_tile_list.xml
index 1df1377..b1469f3 100644
--- a/res/layout/contact_tile_list.xml
+++ b/res/layout/contact_tile_list.xml
@@ -24,6 +24,7 @@
         android:id="@+id/contact_tile_list"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
+        android:fadingEdge="none"
         android:divider="@null" />
 
     <TextView
diff --git a/res/layout/contact_tile_starred.xml b/res/layout/contact_tile_starred.xml
index 757abea..3f8d91d 100644
--- a/res/layout/contact_tile_starred.xml
+++ b/res/layout/contact_tile_starred.xml
@@ -60,6 +60,7 @@
                 android:textColor="@color/people_contact_tile_status_color"
                 android:singleLine="true"
                 android:drawablePadding="4dip"
+                android:paddingBottom="4dip"
                 android:fadingEdge="horizontal"
                 android:fadingEdgeLength="3dip"
                 android:ellipsize="marquee" />
diff --git a/res/layout/contacts_list_content.xml b/res/layout/contacts_list_content.xml
index cc0ebf4..4223f54 100644
--- a/res/layout/contacts_list_content.xml
+++ b/res/layout/contacts_list_content.xml
@@ -64,6 +64,7 @@
         android:layout_marginLeft="?attr/contact_browser_list_padding_left"
         android:layout_marginRight="?attr/contact_browser_list_padding_right"
         android:fastScrollEnabled="true"
+        android:fadingEdge="none"
         android:layout_weight="1" />
 
    <ViewStub
diff --git a/res/layout/group_browse_list_fragment.xml b/res/layout/group_browse_list_fragment.xml
index e3d98f0..c390e67 100644
--- a/res/layout/group_browse_list_fragment.xml
+++ b/res/layout/group_browse_list_fragment.xml
@@ -30,6 +30,7 @@
       android:paddingRight="16dip"
       android:scrollbarStyle="outsideOverlay"
       android:layout_weight="1"
+      android:fadingEdge="none"
       android:cacheColorHint="@android:color/transparent"
       android:divider="@null" />
 
diff --git a/res/layout/raw_contact_editor_view.xml b/res/layout/raw_contact_editor_view.xml
index af95e04..66feb27 100644
--- a/res/layout/raw_contact_editor_view.xml
+++ b/res/layout/raw_contact_editor_view.xml
@@ -56,7 +56,7 @@
                 android:id="@+id/stub_photo"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:layout_marginRight="8dip">
+                android:layout_marginRight="16dip">
 
                 <include
                     android:id="@+id/edit_photo"
diff --git a/res/layout/updates_title.xml b/res/layout/updates_title.xml
index 4b706cc..78fe178 100644
--- a/res/layout/updates_title.xml
+++ b/res/layout/updates_title.xml
@@ -31,6 +31,7 @@
         android:text="@string/recent_updates"
         android:textColor="@color/detail_kind_title_color"
         android:textStyle="bold"
+        android:textAllCaps="true"
         android:singleLine="true"
         android:ellipsize="end"
         android:paddingLeft="8dip"
diff --git a/res/values-sw580dp/dimens.xml b/res/values-sw580dp/dimens.xml
index 42a8314..440929a 100644
--- a/res/values-sw580dp/dimens.xml
+++ b/res/values-sw580dp/dimens.xml
@@ -31,6 +31,8 @@
     <dimen name="action_bar_search_spacing">12dip</dimen>
     <dimen name="shortcut_icon_size">64dip</dimen>
     <dimen name="list_section_height">37dip</dimen>
+    <dimen name="detail_update_section_side_padding">0dip</dimen>
+    <dimen name="detail_update_section_item_horizontal_padding">8dip</dimen>
     <dimen name="detail_update_section_item_vertical_padding">32dip</dimen>
     <dimen name="detail_update_section_item_last_row_extra_vertical_padding">16dip</dimen>
     <dimen name="search_view_width">400dip</dimen>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 418d632..03a9134 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -168,6 +168,7 @@
     <style name="ContactsActionBarStyle" parent="@android:style/Widget.Holo.Light.ActionBar.Solid.Inverse">
         <item name="android:background">@drawable/ab_solid_custom_blue_inverse_holo</item>
         <item name="android:backgroundStacked">@drawable/ab_solid_custom_blue_inverse_holo</item>
+        <item name="android:displayOptions"></item>
     </style>
 
     <style name="ContactsActionBarTabView" parent="@android:style/Widget.Holo.ActionBar.TabView">
@@ -287,6 +288,7 @@
     <style name="DialtactsActionBarStyle" parent="android:Widget.Holo.ActionBar">
         <item name="android:backgroundSplit">@drawable/ab_bottom_opaque_dark_holo</item>
         <item name="android:backgroundStacked">@drawable/ab_stacked_opaque_dark_holo</item>
+        <item name="android:displayOptions"></item>
     </style>
 
 </resources>
diff --git a/src/com/android/contacts/CallDetailActivity.java b/src/com/android/contacts/CallDetailActivity.java
index 85e0ff7..4941121 100644
--- a/src/com/android/contacts/CallDetailActivity.java
+++ b/src/com/android/contacts/CallDetailActivity.java
@@ -20,9 +20,8 @@
 import com.android.contacts.calllog.CallDetailHistoryAdapter;
 import com.android.contacts.calllog.CallTypeHelper;
 import com.android.contacts.calllog.PhoneNumberHelper;
-import com.android.contacts.util.AbstractBackgroundTask;
-import com.android.contacts.util.BackgroundTask;
-import com.android.contacts.util.BackgroundTaskService;
+import com.android.contacts.util.AsyncTaskExecutor;
+import com.android.contacts.util.AsyncTaskExecutors;
 import com.android.contacts.voicemail.VoicemailPlaybackFragment;
 import com.android.contacts.voicemail.VoicemailStatusHelper;
 import com.android.contacts.voicemail.VoicemailStatusHelper.StatusMessage;
@@ -73,6 +72,14 @@
 public class CallDetailActivity extends Activity {
     private static final String TAG = "CallDetail";
 
+    /** 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. */
     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. */
@@ -87,7 +94,7 @@
     private ImageView mMainActionView;
     private ImageButton mMainActionPushLayerView;
     private ImageView mContactBackgroundView;
-    private BackgroundTaskService mBackgroundTaskService;
+    private AsyncTaskExecutor mAsyncTaskExecutor;
 
     private String mNumber = null;
     private String mDefaultCountryIso;
@@ -161,8 +168,7 @@
 
         setContentView(R.layout.call_detail);
 
-        mBackgroundTaskService = (BackgroundTaskService) getApplicationContext().getSystemService(
-                BackgroundTaskService.BACKGROUND_TASK_SERVICE);
+        mAsyncTaskExecutor = AsyncTaskExecutors.createThreadPoolExecutor();
         mInflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
         mResources = getResources();
 
@@ -231,14 +237,15 @@
     }
 
     private void markVoicemailAsRead(final Uri voicemailUri) {
-        mBackgroundTaskService.submit(new AbstractBackgroundTask() {
+        mAsyncTaskExecutor.submit(Tasks.MARK_VOICEMAIL_READ, new AsyncTask<Void, Void, Void>() {
             @Override
-            public void doInBackground() {
+            public Void doInBackground(Void... params) {
                 ContentValues values = new ContentValues();
                 values.put(Voicemails.IS_READ, true);
                 getContentResolver().update(voicemailUri, values, null, null);
+                return null;
             }
-        }, AsyncTask.THREAD_POOL_EXECUTOR);
+        });
     }
 
     /**
@@ -288,28 +295,27 @@
      * @param callUris URIs into {@link CallLog.Calls} of the calls to be displayed
      */
     private void updateData(final Uri... callUris) {
-        mBackgroundTaskService.submit(new BackgroundTask() {
-            private PhoneCallDetails[] details;
-
+        class UpdateContactDetailsTask extends AsyncTask<Void, Void, PhoneCallDetails[]> {
             @Override
-            public void doInBackground() {
+            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;
-                details = new PhoneCallDetails[numCalls];
+                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);
-                    details = null;
+                    return null;
                 }
             }
 
             @Override
-            public void onPostExecute() {
+            public void onPostExecute(PhoneCallDetails[] details) {
                 if (details == null) {
                     // Somewhere went wrong: we're going to bail out and show error to users.
                     Toast.makeText(CallDetailActivity.this, R.string.toast_call_detail_error,
@@ -461,7 +467,8 @@
                 loadContactPhotos(photoUri);
                 findViewById(R.id.call_detail).setVisibility(View.VISIBLE);
             }
-        });
+        }
+        mAsyncTaskExecutor.submit(Tasks.UPDATE_PHONE_CALL_DETAILS, new UpdateContactDetailsTask());
     }
 
     /** Return the phone call details for a given call log URI. */
@@ -707,34 +714,40 @@
             }
             callIds.append(ContentUris.parseId(callUri));
         }
-        mBackgroundTaskService.submit(new BackgroundTask() {
-            @Override
-            public void doInBackground() {
-                getContentResolver().delete(Calls.CONTENT_URI_WITH_VOICEMAIL,
-                        Calls._ID + " IN (" + callIds + ")", null);
-            }
-            @Override
-            public void onPostExecute() {
-                finish();
-            }
-        });
+        mAsyncTaskExecutor.submit(Tasks.REMOVE_FROM_CALL_LOG_AND_FINISH,
+                new AsyncTask<Void, Void, Void>() {
+                    @Override
+                    public Void doInBackground(Void... params) {
+                        getContentResolver().delete(Calls.CONTENT_URI_WITH_VOICEMAIL,
+                                Calls._ID + " IN (" + callIds + ")", null);
+                        return null;
+                    }
+
+                    @Override
+                    public void onPostExecute(Void result) {
+                        finish();
+                    }
+                });
     }
+
     public void onMenuEditNumberBeforeCall(MenuItem menuItem) {
         startActivity(new Intent(Intent.ACTION_DIAL, mPhoneNumberHelper.getCallUri(mNumber)));
     }
 
     public void onMenuTrashVoicemail(MenuItem menuItem) {
         final Uri voicemailUri = getVoicemailUri();
-        mBackgroundTaskService.submit(new BackgroundTask() {
-            @Override
-            public void doInBackground() {
-                getContentResolver().delete(voicemailUri, null, null);
-            }
-            @Override
-            public void onPostExecute() {
-                finish();
-            }
-        });
+        mAsyncTaskExecutor.submit(Tasks.DELETE_VOICEMAIL_AND_FINISH,
+                new AsyncTask<Void, Void, Void>() {
+                    @Override
+                    public Void doInBackground(Void... params) {
+                        getContentResolver().delete(voicemailUri, null, null);
+                        return null;
+                    }
+                    @Override
+                    public void onPostExecute(Void result) {
+                        finish();
+                    }
+                });
     }
 
     private void configureActionBar() {
diff --git a/src/com/android/contacts/ContactsApplication.java b/src/com/android/contacts/ContactsApplication.java
index 0aba332..1c8c080 100644
--- a/src/com/android/contacts/ContactsApplication.java
+++ b/src/com/android/contacts/ContactsApplication.java
@@ -16,11 +16,8 @@
 
 package com.android.contacts;
 
-import static com.android.contacts.util.BackgroundTaskService.createAsyncTaskBackgroundTaskService;
-
 import com.android.contacts.model.AccountTypeManager;
 import com.android.contacts.test.InjectedServices;
-import com.android.contacts.util.BackgroundTaskService;
 import com.google.common.annotations.VisibleForTesting;
 
 import android.app.Application;
@@ -36,7 +33,6 @@
     private static InjectedServices sInjectedServices;
     private AccountTypeManager mAccountTypeManager;
     private ContactPhotoManager mContactPhotoManager;
-    private BackgroundTaskService mBackgroundTaskService;
 
     /**
      * Overrides the system services with mocks for testing.
@@ -97,13 +93,6 @@
             return mContactPhotoManager;
         }
 
-        if (BackgroundTaskService.BACKGROUND_TASK_SERVICE.equals(name)) {
-            if (mBackgroundTaskService == null) {
-                mBackgroundTaskService = createAsyncTaskBackgroundTaskService();
-            }
-            return mBackgroundTaskService;
-        }
-
         return super.getSystemService(name);
     }
 
diff --git a/src/com/android/contacts/calllog/CallLogAdapter.java b/src/com/android/contacts/calllog/CallLogAdapter.java
index 7e934b6..28e2e90 100644
--- a/src/com/android/contacts/calllog/CallLogAdapter.java
+++ b/src/com/android/contacts/calllog/CallLogAdapter.java
@@ -24,6 +24,7 @@
 import com.android.contacts.util.ExpirableCache;
 import com.google.common.annotations.VisibleForTesting;
 
+import android.content.ContentValues;
 import android.content.Context;
 import android.content.res.Resources;
 import android.database.Cursor;
@@ -37,6 +38,7 @@
 import android.provider.ContactsContract.PhoneLookup;
 import android.telephony.PhoneNumberUtils;
 import android.text.TextUtils;
+import android.util.Pair;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -47,7 +49,7 @@
 /**
  * Adapter class to fill in data for the Call Log.
  */
-public final class CallLogAdapter extends GroupingListAdapter
+public class CallLogAdapter extends GroupingListAdapter
         implements Runnable, ViewTreeObserver.OnPreDrawListener, CallLogGroupBuilder.GroupCreator {
     /** Interface used to initiate a refresh of the content. */
     public interface CallFetcher {
@@ -75,10 +77,13 @@
     /**
      * List of requests to update contact details.
      * <p>
+     * Each request is made of a phone number to look up, and the contact info currently stored in
+     * the call log for this number.
+     * <p>
      * The requests are added when displaying the contacts and are processed by a background
      * thread.
      */
-    private final LinkedList<String> mRequests;
+    private final LinkedList<Pair<String, ContactInfo>> mRequests;
 
     private volatile boolean mDone;
     private boolean mLoading = true;
@@ -155,7 +160,7 @@
         mCallFetcher = callFetcher;
 
         mContactInfoCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE);
-        mRequests = new LinkedList<String>();
+        mRequests = new LinkedList<Pair<String,ContactInfo>>();
         mPreDrawListener = null;
 
         Resources resources = mContext.getResources();
@@ -230,10 +235,21 @@
         mPreDrawListener = null;
     }
 
-    private void enqueueRequest(String number, boolean immediate) {
+    /**
+     * Enqueues a request to look up the contact details for the given phone number.
+     * <p>
+     * It also provides the current contact info stored in the call log for this number.
+     * <p>
+     * If the {@code immediate} parameter is true, it will start immediately the thread that looks
+     * up the contact information (if it has not been already started). Otherwise, it will be
+     * started with a delay. See {@link #START_PROCESSING_REQUESTS_DELAY_MILLIS}.
+     */
+    @VisibleForTesting
+    void enqueueRequest(String number, ContactInfo callLogInfo, boolean immediate) {
+        Pair<String, ContactInfo> request = new Pair<String, ContactInfo>(number, callLogInfo);
         synchronized (mRequests) {
-            if (!mRequests.contains(number)) {
-                mRequests.add(number);
+            if (!mRequests.contains(request)) {
+                mRequests.add(request);
                 mRequests.notifyAll();
             }
         }
@@ -378,12 +394,15 @@
     /**
      * Queries the appropriate content provider for the contact associated with the number.
      * <p>
+     * Upon completion it also updates the cache in the call log, if it is different from
+     * {@code callLogInfo}.
+     * <p>
      * The number might be either a SIP address or a phone number.
      * <p>
      * It returns true if it updated the content of the cache and we should therefore tell the
      * view to update its content.
      */
-    private boolean queryContactInfo(String number) {
+    private boolean queryContactInfo(String number, ContactInfo callLogInfo) {
         final ContactInfo info;
 
         // Determine the contact info.
@@ -411,6 +430,9 @@
         // Store the data in the cache so that the UI thread can use to display it. Store it
         // even if it has not changed so that it is marked as not expired.
         mContactInfoCache.put(number, info);
+        // Update the call log even if the cache it is up-to-date: it is possible that the cache
+        // contains the value from a different call log entry.
+        updateCallLogContactInfoCache(number, info, callLogInfo);
         return updated;
     }
 
@@ -423,9 +445,12 @@
         boolean needNotify = false;
         while (!mDone) {
             String number = null;
+            ContactInfo callLogInfo = null;
             synchronized (mRequests) {
                 if (!mRequests.isEmpty()) {
-                    number = mRequests.removeFirst();
+                    Pair<String, ContactInfo> request = mRequests.removeFirst();
+                    number = request.first;
+                    callLogInfo  = request.second;
                 } else {
                     if (needNotify) {
                         needNotify = false;
@@ -439,7 +464,7 @@
                     }
                 }
             }
-            if (!mDone && number != null && queryContactInfo(number)) {
+            if (!mDone && number != null && queryContactInfo(number, callLogInfo)) {
                 needNotify = true;
             }
         }
@@ -571,14 +596,20 @@
             info = ContactInfo.EMPTY;
             mContactInfoCache.put(number, info);
             // Request the contact details immediately since they are currently missing.
-            enqueueRequest(number, true);
+            enqueueRequest(number, cachedContactInfo, true);
             // Format the phone number in the call log as best as we can.
             formattedNumber = formatPhoneNumber(number, null, countryIso);
         } else {
             if (cachedInfo.isExpired()) {
                 // The contact info is no longer up to date, we should request it. However, we
                 // do not need to request them immediately.
-                enqueueRequest(number, false);
+                enqueueRequest(number, cachedContactInfo, false);
+            } else  if (!callLogInfoMatches(cachedContactInfo, info)) {
+                // The call log information does not match the one we have, look it up again.
+                // We could simply update the call log directly, but that needs to be done in a
+                // background thread, so it is easier to simply request a new lookup, which will, as
+                // a side-effect, update the call log.
+                enqueueRequest(number, cachedContactInfo, false);
             }
 
             if (info != ContactInfo.EMPTY) {
@@ -629,6 +660,52 @@
         }
     }
 
+    /** Checks whether the contact info from the call log matches the one from the contacts db. */
+    private boolean callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info) {
+        // The call log only contains a subset of the fields in the contacts db.
+        // Only check those.
+        return TextUtils.equals(callLogInfo.name, info.name)
+                && callLogInfo.type == info.type
+                && TextUtils.equals(callLogInfo.label, info.label);
+    }
+
+    /** Stores the updated contact info in the call log if it is different from the current one. */
+    private void updateCallLogContactInfoCache(String number, ContactInfo updatedInfo,
+            ContactInfo callLogInfo) {
+        final ContentValues values = new ContentValues();
+        boolean needsUpdate = false;
+
+        if (callLogInfo != null) {
+            if (!TextUtils.equals(updatedInfo.name, callLogInfo.name)) {
+                values.put(Calls.CACHED_NAME, updatedInfo.name);
+                needsUpdate = true;
+            }
+
+            if (updatedInfo.type != callLogInfo.type) {
+                values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
+                needsUpdate = true;
+            }
+
+            if (!TextUtils.equals(updatedInfo.label, callLogInfo.label)) {
+                values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
+                needsUpdate = true;
+            }
+        } else {
+            needsUpdate = true;
+        }
+
+        if (!needsUpdate) {
+            return;
+        }
+
+        StringBuilder where = new StringBuilder();
+        where.append(Calls.NUMBER);
+        where.append(" = ?");
+
+        mContext.getContentResolver().update(Calls.CONTENT_URI_WITH_VOICEMAIL, values,
+                where.toString(), new String[]{ number });
+    }
+
     /** Returns the contact information as stored in the call log. */
     private ContactInfo getContactInfoFromCallLog(Cursor c) {
         ContactInfo info = new ContactInfo();
diff --git a/src/com/android/contacts/detail/ContactDetailFragment.java b/src/com/android/contacts/detail/ContactDetailFragment.java
index ca99c79..4d50a39 100644
--- a/src/com/android/contacts/detail/ContactDetailFragment.java
+++ b/src/com/android/contacts/detail/ContactDetailFragment.java
@@ -43,6 +43,7 @@
 import com.android.contacts.util.PhoneCapabilityTester;
 import com.android.contacts.widget.TransitionAnimationView;
 import com.android.internal.telephony.ITelephony;
+import com.google.common.annotations.VisibleForTesting;
 
 import android.app.Activity;
 import android.app.Fragment;
@@ -122,8 +123,6 @@
 
     private static final String TAG = "ContactDetailFragment";
 
-    private static final int LOADER_DETAILS = 1;
-
     private interface ContextMenuIds {
         static final int COPY_TEXT = 0;
         static final int CLEAR_DEFAULT = 1;
@@ -132,7 +131,6 @@
 
     private static final String KEY_CONTACT_URI = "contactUri";
     private static final String KEY_LIST_STATE = "liststate";
-    private static final String LOADER_ARG_CONTACT_URI = "contactUri";
 
     private Context mContext;
     private View mView;
@@ -605,7 +603,7 @@
                         final DetailViewEntry imEntry = DetailViewEntry.fromValues(mContext, imMime,
                                 imKind, dataId, entryValues, mContactData.isDirectoryEntry(),
                                 mContactData.getDirectoryId());
-                        buildImActions(imEntry, entryValues);
+                        buildImActions(mContext, imEntry, entryValues);
                         imEntry.applyStatus(status, false);
                         mImEntries.add(imEntry);
                     }
@@ -617,7 +615,7 @@
                     mPostalEntries.add(entry);
                 } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
                     // Build IM entries
-                    buildImActions(entry, entryValues);
+                    buildImActions(mContext, entry, entryValues);
 
                     // Apply presence and status details when available
                     final DataStatus status = mContactData.getStatuses().get(entry.id);
@@ -936,11 +934,11 @@
     }
 
     /**
-     * Build {@link Intent} to launch an action for the given {@link Im} or
-     * {@link Email} row. If the result is non-null, it either contains one or two Intents
-     * (e.g. [Text, Videochat] or just [Text])
+     * Writes the Instant Messaging action into the given entry value.
      */
-    public static void buildImActions(DetailViewEntry entry, ContentValues values) {
+    @VisibleForTesting
+    public static void buildImActions(Context context, DetailViewEntry entry,
+            ContentValues values) {
         final boolean isEmail = Email.CONTENT_ITEM_TYPE.equals(values.getAsString(Data.MIMETYPE));
 
         if (!isEmail && !isProtocolValid(values)) {
@@ -958,6 +956,8 @@
             final Integer chatCapabilityObj = values.getAsInteger(Im.CHAT_CAPABILITY);
             final int chatCapability = chatCapabilityObj == null ? 0 : chatCapabilityObj;
             entry.chatCapability = chatCapability;
+            entry.typeString = Im.getProtocolLabel(context.getResources(), Im.PROTOCOL_GOOGLE_TALK,
+                    null).toString();
             if ((chatCapability & Im.CAPABILITY_HAS_CAMERA) != 0) {
                 entry.actionIcon = R.drawable.sym_action_talk_holo_light;
                 entry.intent =
diff --git a/src/com/android/contacts/editor/ContactEditorFragment.java b/src/com/android/contacts/editor/ContactEditorFragment.java
index e1b4e9f..5a96e7f 100644
--- a/src/com/android/contacts/editor/ContactEditorFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorFragment.java
@@ -61,11 +61,9 @@
 import android.os.SystemClock;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.Event;
-import android.provider.ContactsContract.CommonDataKinds.Note;
 import android.provider.ContactsContract.CommonDataKinds.Organization;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
-import android.provider.ContactsContract.CommonDataKinds.Website;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.DisplayPhoto;
 import android.provider.ContactsContract.Groups;
@@ -79,9 +77,6 @@
 import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.ViewGroup.LayoutParams;
-import android.view.ViewGroup.MarginLayoutParams;
-import android.view.ViewStub;
 import android.widget.AdapterView;
 import android.widget.AdapterView.OnItemClickListener;
 import android.widget.BaseAdapter;
@@ -102,7 +97,7 @@
         AggregationSuggestionEngine.Listener, AggregationSuggestionView.Listener,
         ExternalRawContactEditorView.Listener {
 
-    private static final String TAG = "ContactEditorFragment";
+    private static final String TAG = ContactEditorFragment.class.getSimpleName();
 
     private static final int LOADER_DATA = 1;
     private static final int LOADER_GROUPS = 2;
diff --git a/src/com/android/contacts/list/ContactTileAdapter.java b/src/com/android/contacts/list/ContactTileAdapter.java
index b7434b1..2f0f24b 100644
--- a/src/com/android/contacts/list/ContactTileAdapter.java
+++ b/src/com/android/contacts/list/ContactTileAdapter.java
@@ -122,7 +122,7 @@
 
         // Converting padding in dips to padding in pixels
         mPaddingInPixels = mContext.getResources()
-                .getDimensionPixelOffset(R.dimen.contact_tile_divider_padding);
+                .getDimensionPixelSize(R.dimen.contact_tile_divider_padding);
 
         bindColumnIndices();
     }
diff --git a/src/com/android/contacts/list/ContactTileView.java b/src/com/android/contacts/list/ContactTileView.java
index bfc4a2e..7355dbf 100644
--- a/src/com/android/contacts/list/ContactTileView.java
+++ b/src/com/android/contacts/list/ContactTileView.java
@@ -16,13 +16,11 @@
 package com.android.contacts.list;
 
 import com.android.contacts.ContactPhotoManager;
-import com.android.contacts.ContactStatusUtil;
 import com.android.contacts.R;
 import com.android.contacts.list.ContactTileAdapter.ContactEntry;
 
 import android.content.Context;
 import android.net.Uri;
-import android.provider.ContactsContract.StatusUpdates;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.View;
@@ -91,6 +89,7 @@
      * fields in {@link ContactEntry}
      */
     public void loadFromContact(ContactEntry entry) {
+
         if (entry != null) {
             mName.setText(entry.name);
             mLookupUri = entry.lookupKey;
diff --git a/src/com/android/contacts/util/AbstractBackgroundTask.java b/src/com/android/contacts/util/AbstractBackgroundTask.java
deleted file mode 100644
index c492e7c..0000000
--- a/src/com/android/contacts/util/AbstractBackgroundTask.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2011 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.contacts.util;
-
-import com.android.contacts.util.BackgroundTask;
-
-/**
- * Base class you can use if you only want to override the {@link #doInBackground()} method.
- */
-public abstract class AbstractBackgroundTask implements BackgroundTask {
-    @Override
-    public void onPostExecute() {
-        // No action necessary.
-    }
-}
diff --git a/src/com/android/contacts/util/AsyncTaskExecutor.java b/src/com/android/contacts/util/AsyncTaskExecutor.java
new file mode 100644
index 0000000..f202949
--- /dev/null
+++ b/src/com/android/contacts/util/AsyncTaskExecutor.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2011 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.contacts.util;
+
+import android.os.AsyncTask;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Interface used to submit {@link AsyncTask} objects to run in the background.
+ * <p>
+ * This interface has a direct parallel with the {@link Executor} interface. It exists to decouple
+ * the mechanics of AsyncTask submission from the description of how that AsyncTask will execute.
+ * <p>
+ * One immediate benefit of this approach is that testing becomes much easier, since it is easy to
+ * introduce a mock or fake AsyncTaskExecutor in unit/integration tests, and thus inspect which
+ * tasks have been submitted and control their execution in an orderly manner.
+ * <p>
+ * Another benefit in due course will be the management of the submitted tasks. An extension to this
+ * interface is planned to allow Activities to easily cancel all the submitted tasks that are still
+ * pending in the onDestroy() method of the Activity.
+ */
+public interface AsyncTaskExecutor {
+    /**
+     * Executes the given AsyncTask with the default Executor.
+     * <p>
+     * This method <b>must only be called from the ui thread</b>.
+     * <p>
+     * The identifier supplied is any Object that can be used to identify the task later. Most
+     * commonly this will be an enum which the tests can also refer to. {@code null} is also
+     * accepted, though of course this won't help in identifying the task later.
+     */
+    <T> AsyncTask<T, ?, ?> submit(Object identifier, AsyncTask<T, ?, ?> task, T... params);
+}
diff --git a/src/com/android/contacts/util/AsyncTaskExecutors.java b/src/com/android/contacts/util/AsyncTaskExecutors.java
new file mode 100644
index 0000000..539dee7
--- /dev/null
+++ b/src/com/android/contacts/util/AsyncTaskExecutors.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2011 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.contacts.util;
+
+import com.android.contacts.test.NeededForTesting;
+import com.google.common.base.Preconditions;
+
+import android.os.AsyncTask;
+import android.os.Looper;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Factory methods for creating AsyncTaskExecutors.
+ * <p>
+ * All of the factory methods on this class check first to see if you have set a static
+ * {@link AsyncTaskExecutorFactory} set through the
+ * {@link #setFactoryForTest(AsyncTaskExecutorFactory)} method, and if so delegate to that instead,
+ * which is one way of injecting dependencies for testing classes whose construction cannot be
+ * controlled such as {@link android.app.Activity}.
+ */
+public final class AsyncTaskExecutors {
+    /**
+     * A single instance of the {@link AsyncTaskExecutorFactory}, to which we delegate if it is
+     * non-null, for injecting when testing.
+     */
+    private static AsyncTaskExecutorFactory mInjectedAsyncTaskExecutorFactory = null;
+
+    /**
+     * Creates an AsyncTaskExecutor that submits tasks to run with
+     * {@link AsyncTask#SERIAL_EXECUTOR}.
+     */
+    public static AsyncTaskExecutor createAsyncTaskExecutor() {
+        synchronized (AsyncTaskExecutors.class) {
+            if (mInjectedAsyncTaskExecutorFactory != null) {
+                return mInjectedAsyncTaskExecutorFactory.createAsyncTaskExeuctor();
+            }
+            return new SimpleAsyncTaskExecutor(AsyncTask.SERIAL_EXECUTOR);
+        }
+    }
+
+    /**
+     * Creates an AsyncTaskExecutor that submits tasks to run with
+     * {@link AsyncTask#THREAD_POOL_EXECUTOR}.
+     */
+    public static AsyncTaskExecutor createThreadPoolExecutor() {
+        synchronized (AsyncTaskExecutors.class) {
+            if (mInjectedAsyncTaskExecutorFactory != null) {
+                return mInjectedAsyncTaskExecutorFactory.createAsyncTaskExeuctor();
+            }
+            return new SimpleAsyncTaskExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+        }
+    }
+
+    /** Interface for creating AsyncTaskExecutor objects. */
+    public interface AsyncTaskExecutorFactory {
+        AsyncTaskExecutor createAsyncTaskExeuctor();
+    }
+
+    @NeededForTesting
+    public static void setFactoryForTest(AsyncTaskExecutorFactory factory) {
+        synchronized (AsyncTaskExecutors.class) {
+            mInjectedAsyncTaskExecutorFactory = factory;
+        }
+    }
+
+    public static void checkCalledFromUiThread() {
+        Preconditions.checkState(Thread.currentThread() == Looper.getMainLooper().getThread(),
+                "submit method must be called from ui thread, was: " + Thread.currentThread());
+    }
+
+    private static class SimpleAsyncTaskExecutor implements AsyncTaskExecutor {
+        private final Executor mExecutor;
+
+        public SimpleAsyncTaskExecutor(Executor executor) {
+            mExecutor = executor;
+        }
+
+        @Override
+        public <T> AsyncTask<T, ?, ?> submit(Object identifer, AsyncTask<T, ?, ?> task,
+                T... params) {
+            checkCalledFromUiThread();
+            return task.executeOnExecutor(mExecutor, params);
+        }
+    }
+}
diff --git a/src/com/android/contacts/util/BackgroundTask.java b/src/com/android/contacts/util/BackgroundTask.java
deleted file mode 100644
index ba791fb..0000000
--- a/src/com/android/contacts/util/BackgroundTask.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2011 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.contacts.util;
-
-/**
- * Simple interface to improve the testability of code using AsyncTasks.
- * <p>
- * Provides a trivial replacement for no-arg versions of AsyncTask clients.  We may extend this
- * to add more functionality as we require.
- * <p>
- * The same memory-visibility guarantees are made here as are made for AsyncTask objects, namely
- * that fields set in {@link #doInBackground()} are visible to {@link #onPostExecute()}.
- */
-public interface BackgroundTask {
-    public void doInBackground();
-    public void onPostExecute();
-}
diff --git a/src/com/android/contacts/util/BackgroundTaskService.java b/src/com/android/contacts/util/BackgroundTaskService.java
deleted file mode 100644
index 310a178..0000000
--- a/src/com/android/contacts/util/BackgroundTaskService.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (C) 2011 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.contacts.util;
-
-import android.os.AsyncTask;
-
-import java.util.concurrent.Executor;
-
-/**
- * Service used to submit tasks to run in the background.
- * <p>
- * BackgroundTaskService makes the same memory-visibility guarantees that AsyncTask which it
- * emulates makes, namely that fields set in the {@link BackgroundTask#doInBackground()} method
- * will be visible to the {@link BackgroundTask#onPostExecute()} method.
- * <p>
- * You are not expected to derive from this class unless you are writing your own test
- * implementation, or you are absolutely sure that the instance in
- * {@link #createAsyncTaskBackgroundTaskService()} doesn't do what you need.
- */
-public abstract class BackgroundTaskService {
-    public static final String BACKGROUND_TASK_SERVICE = BackgroundTaskService.class.getName();
-
-    /**
-     * Executes the given BackgroundTask with the default Executor.
-     * <p>
-     * All {@link BackgroundTask#doInBackground()} tasks will be guaranteed to happen serially.
-     * If this is not what you want, see {@link #submit(BackgroundTask, Executor)}.
-     */
-    public abstract void submit(BackgroundTask task);
-
-    /**
-     * Executes the BackgroundTask with the supplied Executor.
-     * <p>
-     * The main use-case for this method will be to allow submitted tasks to perform their
-     * {@link BackgroundTask#doInBackground()} methods concurrently.
-     */
-    public abstract void submit(BackgroundTask task, Executor executor);
-
-    /**
-     * Creates a concrete BackgroundTaskService whose default Executor is
-     * {@link AsyncTask#SERIAL_EXECUTOR}.
-     */
-    public static BackgroundTaskService createAsyncTaskBackgroundTaskService() {
-        return new AsyncTaskBackgroundTaskService();
-    }
-
-    private static final class AsyncTaskBackgroundTaskService extends BackgroundTaskService {
-        @Override
-        public void submit(BackgroundTask task) {
-            submit(task, AsyncTask.SERIAL_EXECUTOR);
-        }
-
-        @Override
-        public void submit(final BackgroundTask task, Executor executor) {
-            new AsyncTask<Void, Void, Void>() {
-                @Override
-                protected Void doInBackground(Void... params) {
-                    task.doInBackground();
-                    return null;
-                }
-
-                @Override
-                protected void onPostExecute(Void result) {
-                    task.onPostExecute();
-                }
-            }.executeOnExecutor(executor);
-        }
-    }
-}
diff --git a/src/com/android/contacts/voicemail/VoicemailPlaybackFragment.java b/src/com/android/contacts/voicemail/VoicemailPlaybackFragment.java
index 5e53b76..5eb0ddf 100644
--- a/src/com/android/contacts/voicemail/VoicemailPlaybackFragment.java
+++ b/src/com/android/contacts/voicemail/VoicemailPlaybackFragment.java
@@ -21,7 +21,7 @@
 
 import com.android.common.io.MoreCloseables;
 import com.android.contacts.R;
-import com.android.contacts.util.BackgroundTaskService;
+import com.android.contacts.util.AsyncTaskExecutors;
 import com.android.ex.variablespeed.MediaPlayerProxy;
 import com.android.ex.variablespeed.VariableSpeed;
 import com.google.common.base.Preconditions;
@@ -93,15 +93,11 @@
         boolean startPlayback = arguments.getBoolean(EXTRA_VOICEMAIL_START_PLAYBACK, false);
         mPresenter = new VoicemailPlaybackPresenter(createPlaybackViewImpl(),
                 createMediaPlayer(mScheduledExecutorService), voicemailUri,
-                mScheduledExecutorService, startPlayback, getBackgroundTaskService());
+                mScheduledExecutorService, startPlayback,
+                AsyncTaskExecutors.createAsyncTaskExecutor());
         mPresenter.onCreate(savedInstanceState);
     }
 
-    private BackgroundTaskService getBackgroundTaskService() {
-        return (BackgroundTaskService) getActivity().getApplicationContext().getSystemService(
-                BackgroundTaskService.BACKGROUND_TASK_SERVICE);
-    }
-
     @Override
     public void onSaveInstanceState(Bundle outState) {
         mPresenter.onSaveInstanceState(outState);
diff --git a/src/com/android/contacts/voicemail/VoicemailPlaybackPresenter.java b/src/com/android/contacts/voicemail/VoicemailPlaybackPresenter.java
index bd01991..d3e4bef 100644
--- a/src/com/android/contacts/voicemail/VoicemailPlaybackPresenter.java
+++ b/src/com/android/contacts/voicemail/VoicemailPlaybackPresenter.java
@@ -19,10 +19,10 @@
 import static android.util.MathUtils.constrain;
 
 import com.android.contacts.R;
-import com.android.contacts.util.BackgroundTask;
-import com.android.contacts.util.BackgroundTaskService;
+import com.android.contacts.util.AsyncTaskExecutor;
 import com.android.ex.variablespeed.MediaPlayerProxy;
 import com.android.ex.variablespeed.SingleThreadedMediaPlayerProxy;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 
 import android.content.Context;
@@ -57,7 +57,8 @@
  * the main ui thread.
  */
 @NotThreadSafe
-/*package*/ class VoicemailPlaybackPresenter {
+@VisibleForTesting
+public class VoicemailPlaybackPresenter {
     /** Contract describing the behaviour we need from the ui we are controlling. */
     public interface PlaybackView {
         Context getDataSourceContext();
@@ -87,6 +88,13 @@
         void unregisterContentObserver(ContentObserver observer);
     }
 
+    /** The enumeration of {@link AsyncTask} objects we use in this class. */
+    public enum Tasks {
+        CHECK_FOR_CONTENT,
+        CHECK_CONTENT_AFTER_CHANGE,
+        PREPARE_MEDIA_PLAYER,
+    }
+
     /** Update rate for the slider, 30fps. */
     private static final int SLIDER_UPDATE_PERIOD_MILLIS = 1000 / 30;
     /** Time our ui will wait for content to be fetched before reporting not available. */
@@ -143,8 +151,8 @@
     private final Uri mVoicemailUri;
     /** Start playing in onCreate iff this is true. */
     private final boolean mStartPlayingImmediately;
-    /** Used to run background tasks that need to interact with the ui. */
-    private final BackgroundTaskService mBackgroundTaskService;
+    /** Used to run async tasks that need to interact with the ui. */
+    private final AsyncTaskExecutor mAsyncTaskExecutor;
 
     /**
      * Used to handle the result of a successful or time-out fetch result.
@@ -155,12 +163,12 @@
 
     public VoicemailPlaybackPresenter(PlaybackView view, MediaPlayerProxy player,
             Uri voicemailUri, ScheduledExecutorService executorService,
-            boolean startPlayingImmediately, BackgroundTaskService backgroundTaskService) {
+            boolean startPlayingImmediately, AsyncTaskExecutor asyncTaskExecutor) {
         mView = view;
         mPlayer = player;
         mVoicemailUri = voicemailUri;
         mStartPlayingImmediately = startPlayingImmediately;
-        mBackgroundTaskService = backgroundTaskService;
+        mAsyncTaskExecutor = asyncTaskExecutor;
         mPositionUpdater = new PositionUpdater(executorService, SLIDER_UPDATE_PERIOD_MILLIS);
     }
 
@@ -181,23 +189,21 @@
      */
     private void checkThatWeHaveContent() {
         mView.setIsFetchingContent();
-        mBackgroundTaskService.submit(new BackgroundTask() {
-            private boolean mHasContent = false;
-
+        mAsyncTaskExecutor.submit(Tasks.CHECK_FOR_CONTENT, new AsyncTask<Void, Void, Boolean>() {
             @Override
-            public void doInBackground() {
-                mHasContent = mView.queryHasContent(mVoicemailUri);
+            public Boolean doInBackground(Void... params) {
+                return mView.queryHasContent(mVoicemailUri);
             }
 
             @Override
-            public void onPostExecute() {
-                if (mHasContent) {
+            public void onPostExecute(Boolean hasContent) {
+                if (hasContent) {
                     postSuccessfullyFetchedContent();
                 } else {
                     makeRequestForContent();
                 }
             }
-        }, AsyncTask.THREAD_POOL_EXECUTOR);
+        });
     }
 
     /**
@@ -253,24 +259,23 @@
 
         @Override
         public void onChange(boolean selfChange) {
-            mBackgroundTaskService.submit(new BackgroundTask() {
-                private boolean mHasContent = false;
-
+            mAsyncTaskExecutor.submit(Tasks.CHECK_CONTENT_AFTER_CHANGE,
+                    new AsyncTask<Void, Void, Boolean>() {
                 @Override
-                public void doInBackground() {
-                    mHasContent = mView.queryHasContent(mVoicemailUri);
+                public Boolean doInBackground(Void... params) {
+                    return mView.queryHasContent(mVoicemailUri);
                 }
 
                 @Override
-                public void onPostExecute() {
-                    if (mHasContent) {
+                public void onPostExecute(Boolean hasContent) {
+                    if (hasContent) {
                         if (mResultStillPending.getAndSet(false)) {
                             mView.unregisterContentObserver(FetchResultHandler.this);
                             postSuccessfullyFetchedContent();
                         }
                     }
                 }
-            }, AsyncTask.THREAD_POOL_EXECUTOR);
+            });
         }
     }
 
@@ -286,29 +291,29 @@
      */
     private void postSuccessfullyFetchedContent() {
         mView.setIsBuffering();
-        mBackgroundTaskService.submit(new BackgroundTask() {
-            private Exception mException;
+        mAsyncTaskExecutor.submit(Tasks.PREPARE_MEDIA_PLAYER,
+                new AsyncTask<Void, Void, Exception>() {
+                    @Override
+                    public Exception doInBackground(Void... params) {
+                        try {
+                            mPlayer.reset();
+                            mPlayer.setDataSource(mView.getDataSourceContext(), mVoicemailUri);
+                            mPlayer.prepare();
+                            return null;
+                        } catch (IOException e) {
+                            return e;
+                        }
+                    }
 
-            @Override
-            public void doInBackground() {
-                try {
-                    mPlayer.reset();
-                    mPlayer.setDataSource(mView.getDataSourceContext(), mVoicemailUri);
-                    mPlayer.prepare();
-                } catch (IOException e) {
-                    mException = e;
-                }
-            }
-
-            @Override
-            public void onPostExecute() {
-                if (mException == null) {
-                    postSuccessfulPrepareActions();
-                } else {
-                    mView.playbackError(mException);
-                }
-            }
-        }, AsyncTask.THREAD_POOL_EXECUTOR);
+                    @Override
+                    public void onPostExecute(Exception exception) {
+                        if (exception == null) {
+                            postSuccessfulPrepareActions();
+                        } else {
+                            mView.playbackError(exception);
+                        }
+                    }
+                });
     }
 
     /**
diff --git a/tests/src/com/android/contacts/CallDetailActivityTest.java b/tests/src/com/android/contacts/CallDetailActivityTest.java
index c060af5..6a8fcb3 100644
--- a/tests/src/com/android/contacts/CallDetailActivityTest.java
+++ b/tests/src/com/android/contacts/CallDetailActivityTest.java
@@ -16,23 +16,24 @@
 
 package com.android.contacts;
 
-import com.android.contacts.test.InjectedServices;
-import com.android.contacts.util.BackgroundTaskService;
+import static com.android.contacts.CallDetailActivity.Tasks.UPDATE_PHONE_CALL_DETAILS;
+import static com.android.contacts.voicemail.VoicemailPlaybackPresenter.Tasks.CHECK_FOR_CONTENT;
+import static com.android.contacts.voicemail.VoicemailPlaybackPresenter.Tasks.PREPARE_MEDIA_PLAYER;
+
+import com.android.contacts.util.AsyncTaskExecutors;
+import com.android.contacts.util.FakeAsyncTaskExecutor;
 import com.android.contacts.util.IntegrationTestUtils;
 import com.android.contacts.util.LocaleTestUtils;
-import com.android.contacts.util.FakeBackgroundTaskService;
 import com.android.internal.view.menu.ContextMenuBuilder;
 import com.google.common.base.Preconditions;
-import com.google.common.util.concurrent.Executors;
 
-import android.app.Activity;
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Intent;
 import android.net.Uri;
 import android.provider.CallLog;
-import android.provider.VoicemailContract.Voicemails;
+import android.provider.VoicemailContract;
 import android.test.ActivityInstrumentationTestCase2;
 import android.test.suitebuilder.annotation.LargeTest;
 import android.test.suitebuilder.annotation.Suppress;
@@ -47,12 +48,12 @@
  */
 @LargeTest
 public class CallDetailActivityTest extends ActivityInstrumentationTestCase2<CallDetailActivity> {
-    private static final String FAKE_VOICEMAIL_URI_STRING = ContentUris.withAppendedId(
-            Voicemails.CONTENT_URI, Integer.MAX_VALUE).toString();
-    private Uri mUri;
+    private Uri mCallLogUri;
+    private Uri mVoicemailUri;
     private IntegrationTestUtils mTestUtils;
     private LocaleTestUtils mLocaleTestUtils;
-    private FakeBackgroundTaskService mMockBackgroundTaskService;
+    private FakeAsyncTaskExecutor mFakeAsyncTaskExecutor;
+    private CallDetailActivity mActivityUnderTest;
 
     public CallDetailActivityTest() {
         super(CallDetailActivity.class);
@@ -61,11 +62,8 @@
     @Override
     protected void setUp() throws Exception {
         super.setUp();
-        InjectedServices injectedServices = new InjectedServices();
-        mMockBackgroundTaskService = new FakeBackgroundTaskService();
-        injectedServices.setSystemService(BackgroundTaskService.BACKGROUND_TASK_SERVICE,
-                mMockBackgroundTaskService);
-        ContactsApplication.injectServices(injectedServices);
+        mFakeAsyncTaskExecutor = new FakeAsyncTaskExecutor(getInstrumentation());
+        AsyncTaskExecutors.setFactoryForTest(mFakeAsyncTaskExecutor.getFactory());
         // I don't like the default of focus-mode for tests, the green focus border makes the
         // screenshots look weak.
         setActivityInitialTouchMode(true);
@@ -82,45 +80,58 @@
         mLocaleTestUtils = null;
         cleanUpUri();
         mTestUtils = null;
-        ContactsApplication.injectServices(null);
+        AsyncTaskExecutors.setFactoryForTest(null);
         super.tearDown();
     }
 
-    public void testInitialActivityStartsWithBuffering() throws Throwable {
+    public void testInitialActivityStartsWithFetchingVoicemail() throws Throwable {
         setActivityIntentForTestVoicemailEntry();
-        CallDetailActivity activity = getActivity();
-        // When the activity first starts, we will show "buffering..." on the screen.
+        startActivityUnderTest();
+        // When the activity first starts, we will show "Fetching voicemail" on the screen.
         // The duration should not be visible.
-        assertHasOneTextViewContaining(activity, "buffering...");
-        assertZeroTextViewsContaining(activity, "00:00");
+        assertHasOneTextViewContaining("Fetching voicemail");
+        assertZeroTextViewsContaining("00:00");
+    }
+
+    public void testWhenCheckForContentCompletes_UiShowsBuffering() throws Throwable {
+        setActivityIntentForTestVoicemailEntry();
+        startActivityUnderTest();
+        // There is a background check that is testing to see if we have the content available.
+        // Once that task completes, we shouldn't be showing the fetching message, we should
+        // be showing "Buffering".
+        mFakeAsyncTaskExecutor.runTask(CHECK_FOR_CONTENT);
+        assertHasOneTextViewContaining("Buffering");
+        assertZeroTextViewsContaining("Fetching voicemail");
     }
 
     public void testInvalidVoicemailShowsErrorMessage() throws Throwable {
         setActivityIntentForTestVoicemailEntry();
-        CallDetailActivity activity = getActivity();
-        // If we run all the background tasks, one of them should have prepared the media player.
+        startActivityUnderTest();
+        mFakeAsyncTaskExecutor.runTask(CHECK_FOR_CONTENT);
+        // There should be exactly one background task ready to prepare the media player.
         // Preparing 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.
-        runAllBackgroundTasks();
-        assertHasOneTextViewContaining(activity, "failed to play voicemail");
-        assertZeroTextViewsContaining(activity, "buffering...");
+        mFakeAsyncTaskExecutor.runTask(PREPARE_MEDIA_PLAYER);
+        assertHasOneTextViewContaining("Couldn't play voicemail");
+        assertZeroTextViewsContaining("Buffering");
     }
 
     public void testOnResumeDoesNotCreateManyFragments() throws Throwable {
         // There was a bug where every time the activity was resumed, a new fragment was created.
-        // Before the fix, this was failing reproducibly with at least 3 "buffering..." views.
+        // Before the fix, this was failing reproducibly with at least 3 "Buffering" views.
         setActivityIntentForTestVoicemailEntry();
-        final CallDetailActivity activity = getActivity();
+        startActivityUnderTest();
+        mFakeAsyncTaskExecutor.runTask(CHECK_FOR_CONTENT);
         getInstrumentation().runOnMainSync(new Runnable() {
             @Override
             public void run() {
-                getInstrumentation().callActivityOnPause(activity);
-                getInstrumentation().callActivityOnResume(activity);
-                getInstrumentation().callActivityOnPause(activity);
-                getInstrumentation().callActivityOnResume(activity);
+                getInstrumentation().callActivityOnPause(mActivityUnderTest);
+                getInstrumentation().callActivityOnResume(mActivityUnderTest);
+                getInstrumentation().callActivityOnPause(mActivityUnderTest);
+                getInstrumentation().callActivityOnResume(mActivityUnderTest);
             }
         });
-        assertHasOneTextViewContaining(activity, "buffering...");
+        assertHasOneTextViewContaining("Buffering");
     }
 
     /**
@@ -132,15 +143,15 @@
      */
     public void testClickIncreaseRateButtonWithInvalidVoicemailDoesNotCrash() throws Throwable {
         setActivityIntentForTestVoicemailEntry();
-        Activity activity = getActivity();
-        mTestUtils.clickButton(activity, R.id.playback_start_stop);
-        mTestUtils.clickButton(activity, R.id.rate_increase_button);
+        startActivityUnderTest();
+        mTestUtils.clickButton(mActivityUnderTest, R.id.playback_start_stop);
+        mTestUtils.clickButton(mActivityUnderTest, R.id.rate_increase_button);
     }
 
     /** Test for bug where missing Extras on intent used to start Activity causes NPE. */
-    public void testCallLogUriWithMissingExtrasShouldNotCauseNPE() throws Exception {
+    public void testCallLogUriWithMissingExtrasShouldNotCauseNPE() throws Throwable {
         setActivityIntentForTestCallEntry();
-        getActivity();
+        startActivityUnderTest();
     }
 
     /**
@@ -150,20 +161,20 @@
      */
     public void testVoicemailDoesNotHaveRemoveFromCallLog() throws Throwable {
         setActivityIntentForTestVoicemailEntry();
-        CallDetailActivity activity = getActivity();
-        Menu menu = new ContextMenuBuilder(activity);
-        activity.onCreateOptionsMenu(menu);
-        activity.onPrepareOptionsMenu(menu);
+        startActivityUnderTest();
+        Menu menu = new ContextMenuBuilder(mActivityUnderTest);
+        mActivityUnderTest.onCreateOptionsMenu(menu);
+        mActivityUnderTest.onPrepareOptionsMenu(menu);
         assertFalse(menu.findItem(R.id.menu_remove_from_call_log).isVisible());
     }
 
     /** Test to check that I haven't broken the remove-from-call-log entry from regular calls. */
     public void testRegularCallDoesHaveRemoveFromCallLog() throws Throwable {
         setActivityIntentForTestCallEntry();
-        CallDetailActivity activity = getActivity();
-        Menu menu = new ContextMenuBuilder(activity);
-        activity.onCreateOptionsMenu(menu);
-        activity.onPrepareOptionsMenu(menu);
+        startActivityUnderTest();
+        Menu menu = new ContextMenuBuilder(mActivityUnderTest);
+        mActivityUnderTest.onCreateOptionsMenu(menu);
+        mActivityUnderTest.onPrepareOptionsMenu(menu);
         assertTrue(menu.findItem(R.id.menu_remove_from_call_log).isVisible());
     }
 
@@ -175,16 +186,16 @@
     @Suppress
     public void testVoicemailPlaybackRateDisplayedOnUi() throws Throwable {
         setActivityIntentForTestVoicemailEntry();
-        CallDetailActivity activity = getActivity();
+        startActivityUnderTest();
         // Find the TextView containing the duration.  It should be initially displaying "00:00".
-        List<TextView> views = mTestUtils.getTextViewsWithString(activity, "00:00");
+        List<TextView> views = mTestUtils.getTextViewsWithString(mActivityUnderTest, "00:00");
         assertEquals(1, views.size());
         TextView timeDisplay = views.get(0);
         // Hit the plus button.  At this point we should be displaying "fast speed".
-        mTestUtils.clickButton(activity, R.id.rate_increase_button);
+        mTestUtils.clickButton(mActivityUnderTest, R.id.rate_increase_button);
         assertEquals("fast speed", mTestUtils.getText(timeDisplay));
         // Hit the minus button.  We should be back to "normal" speed.
-        mTestUtils.clickButton(activity, R.id.rate_decrease_button);
+        mTestUtils.clickButton(mActivityUnderTest, R.id.rate_decrease_button);
         assertEquals("normal speed", mTestUtils.getText(timeDisplay));
         // Wait for one and a half seconds.  The timer will be back.
         Thread.sleep(1500);
@@ -192,39 +203,39 @@
     }
 
     private void setActivityIntentForTestCallEntry() {
-        createTestCallEntry(false);
-        setActivityIntent(new Intent(Intent.ACTION_VIEW, mUri));
+        Preconditions.checkState(mCallLogUri == null, "mUri should be null");
+        ContentResolver contentResolver = getContentResolver();
+        ContentValues values = new ContentValues();
+        values.put(CallLog.Calls.NUMBER, "01234567890");
+        values.put(CallLog.Calls.TYPE, CallLog.Calls.INCOMING_TYPE);
+        mCallLogUri = contentResolver.insert(CallLog.Calls.CONTENT_URI, values);
+        setActivityIntent(new Intent(Intent.ACTION_VIEW, mCallLogUri));
     }
 
     private void setActivityIntentForTestVoicemailEntry() {
-        createTestCallEntry(true);
-        Intent intent = new Intent(Intent.ACTION_VIEW, mUri);
-        Uri voicemailUri = Uri.parse(FAKE_VOICEMAIL_URI_STRING);
-        intent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI, voicemailUri);
+        Preconditions.checkState(mVoicemailUri == null, "mUri should be null");
+        ContentResolver contentResolver = getContentResolver();
+        ContentValues values = new ContentValues();
+        values.put(VoicemailContract.Voicemails.NUMBER, "01234567890");
+        values.put(VoicemailContract.Voicemails.HAS_CONTENT, 1);
+        mVoicemailUri = contentResolver.insert(VoicemailContract.Voicemails.CONTENT_URI, values);
+        Uri callLogUri = ContentUris.withAppendedId(CallLog.Calls.CONTENT_URI_WITH_VOICEMAIL,
+                ContentUris.parseId(mVoicemailUri));
+        Intent intent = new Intent(Intent.ACTION_VIEW, callLogUri);
+        intent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI, mVoicemailUri);
         setActivityIntent(intent);
     }
 
-    /** Inserts an entry into the call log. */
-    private void createTestCallEntry(boolean isVoicemail) {
-        Preconditions.checkState(mUri == null, "mUri should be null");
-        ContentResolver contentResolver = getContentResolver();
-        ContentValues contentValues = new ContentValues();
-        contentValues.put(CallLog.Calls.NUMBER, "01234567890");
-        if (isVoicemail) {
-            contentValues.put(CallLog.Calls.TYPE, CallLog.Calls.VOICEMAIL_TYPE);
-            contentValues.put(CallLog.Calls.VOICEMAIL_URI, FAKE_VOICEMAIL_URI_STRING);
-        } else {
-            contentValues.put(CallLog.Calls.TYPE, CallLog.Calls.INCOMING_TYPE);
-        }
-        contentValues.put(CallLog.Calls.VOICEMAIL_URI, FAKE_VOICEMAIL_URI_STRING);
-        mUri = contentResolver.insert(CallLog.Calls.CONTENT_URI_WITH_VOICEMAIL, contentValues);
-    }
-
     private void cleanUpUri() {
-        if (mUri != null) {
+        if (mVoicemailUri != null) {
+            getContentResolver().delete(VoicemailContract.Voicemails.CONTENT_URI,
+                    "_ID = ?", new String[] { String.valueOf(ContentUris.parseId(mVoicemailUri)) });
+            mVoicemailUri = null;
+        }
+        if (mCallLogUri != null) {
             getContentResolver().delete(CallLog.Calls.CONTENT_URI_WITH_VOICEMAIL,
-                    "_ID = ?", new String[] { String.valueOf(ContentUris.parseId(mUri)) });
-            mUri = null;
+                    "_ID = ?", new String[] { String.valueOf(ContentUris.parseId(mCallLogUri)) });
+            mCallLogUri = null;
         }
     }
 
@@ -232,23 +243,28 @@
         return getInstrumentation().getTargetContext().getContentResolver();
     }
 
-    private TextView assertHasOneTextViewContaining(Activity activity, String text)
-            throws Throwable {
-        List<TextView> views = mTestUtils.getTextViewsWithString(activity, text);
+    private TextView assertHasOneTextViewContaining(String text) throws Throwable {
+        Preconditions.checkNotNull(mActivityUnderTest, "forget to call startActivityUnderTest()?");
+        List<TextView> views = mTestUtils.getTextViewsWithString(mActivityUnderTest, text);
         assertEquals("There should have been one TextView with text '" + text + "' but found "
                 + views, 1, views.size());
         return views.get(0);
     }
 
-    private void assertZeroTextViewsContaining(Activity activity, String text) throws Throwable {
-        List<TextView> views = mTestUtils.getTextViewsWithString(activity, text);
+    private void assertZeroTextViewsContaining(String text) throws Throwable {
+        Preconditions.checkNotNull(mActivityUnderTest, "forget to call startActivityUnderTest()?");
+        List<TextView> views = mTestUtils.getTextViewsWithString(mActivityUnderTest, text);
         assertEquals("There should have been no TextViews with text '" + text + "' but found "
                 + views, 0,  views.size());
     }
 
-    private void runAllBackgroundTasks() {
-        mMockBackgroundTaskService.runAllBackgroundTasks(
-                Executors.sameThreadExecutor(),
-                FakeBackgroundTaskService.createMainSyncExecutor(getInstrumentation()));
+    private void startActivityUnderTest() throws Throwable {
+        Preconditions.checkState(mActivityUnderTest == null, "must only start the activity once");
+        mActivityUnderTest = getActivity();
+        assertNotNull("activity should not be null", mActivityUnderTest);
+        // 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);
     }
 }
diff --git a/tests/src/com/android/contacts/calllog/CallLogAdapterTest.java b/tests/src/com/android/contacts/calllog/CallLogAdapterTest.java
new file mode 100644
index 0000000..28db896
--- /dev/null
+++ b/tests/src/com/android/contacts/calllog/CallLogAdapterTest.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2011 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.contacts.calllog;
+
+import com.google.common.collect.Lists;
+
+import android.content.Context;
+import android.database.MatrixCursor;
+import android.test.AndroidTestCase;
+import android.view.View;
+
+import java.util.List;
+
+/**
+ * Unit tests for {@link CallLogAdapter}.
+ */
+public class CallLogAdapterTest extends AndroidTestCase {
+    private static final String TEST_NUMBER = "12345678";
+    private static final String TEST_NAME = "name";
+    private static final String TEST_NUMBER_LABEL = "label";
+    private static final int TEST_NUMBER_TYPE = 1;
+    private static final String TEST_COUNTRY_ISO = "US";
+    private static final String TEST_VOICEMAIL_NUMBER = "111";
+
+    /** The object under test. */
+    private TestCallLogAdapter mAdapter;
+
+    private MatrixCursor mCursor;
+    private View mView;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        // Use a call fetcher that does not do anything.
+        CallLogAdapter.CallFetcher fakeCallFetcher = new CallLogAdapter.CallFetcher() {
+            @Override
+            public void startCallsQuery() {}
+        };
+
+        mAdapter = new TestCallLogAdapter(getContext(), fakeCallFetcher, TEST_COUNTRY_ISO,
+                TEST_VOICEMAIL_NUMBER);
+        // The cursor used in the tests to store the entries to display.
+        mCursor = new MatrixCursor(CallLogQuery.EXTENDED_PROJECTION);
+        mCursor.moveToFirst();
+        // The views into which to store the data.
+        mView = new View(getContext());
+        mView.setTag(CallLogListItemViews.createForTest(getContext()));
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mAdapter = null;
+        mCursor = null;
+        mView = null;
+        super.tearDown();
+    }
+
+    public void testBindView_NoCallLogCacheNorMemoryCache_EnqueueRequest() {
+        mCursor.addRow(createCallLogEntry());
+
+        // Bind the views of a single row.
+        mAdapter.bindStandAloneView(mView, getContext(), mCursor);
+
+        // There is one request for contact details.
+        assertEquals(1, mAdapter.requests.size());
+
+        TestCallLogAdapter.Request request = mAdapter.requests.get(0);
+        // It is for the number we need to show.
+        assertEquals(TEST_NUMBER, request.number);
+        // Since there is nothing in the cache, it is an immediate request.
+        assertTrue("should be immediate", request.immediate);
+    }
+
+    public void testBindView_CallLogCacheButNoMemoryCache_EnqueueRequest() {
+        mCursor.addRow(createCallLogEntryWithCachedValues());
+
+        // Bind the views of a single row.
+        mAdapter.bindStandAloneView(mView, getContext(), mCursor);
+
+        // There is one request for contact details.
+        assertEquals(1, mAdapter.requests.size());
+
+        TestCallLogAdapter.Request request = mAdapter.requests.get(0);
+        // The values passed to the request, match the ones in the call log cache.
+        assertEquals(TEST_NAME, request.callLogInfo.name);
+        assertEquals(1, request.callLogInfo.type);
+        assertEquals(TEST_NUMBER_LABEL, request.callLogInfo.label);
+    }
+
+
+    public void testBindView_NoCallLogButMemoryCache_EnqueueRequest() {
+        mCursor.addRow(createCallLogEntry());
+        mAdapter.injectContactInfoForTest(TEST_NUMBER, createContactInfo());
+
+        // Bind the views of a single row.
+        mAdapter.bindStandAloneView(mView, getContext(), mCursor);
+
+        // There is one request for contact details.
+        assertEquals(1, mAdapter.requests.size());
+
+        TestCallLogAdapter.Request request = mAdapter.requests.get(0);
+        // Since there is something in the cache, it is not an immediate request.
+        assertFalse("should not be immediate", request.immediate);
+    }
+
+    public void testBindView_BothCallLogAndMemoryCache_NoEnqueueRequest() {
+        mCursor.addRow(createCallLogEntryWithCachedValues());
+        mAdapter.injectContactInfoForTest(TEST_NUMBER, createContactInfo());
+
+        // Bind the views of a single row.
+        mAdapter.bindStandAloneView(mView, getContext(), mCursor);
+
+        // Cache and call log are up-to-date: no need to request update.
+        assertEquals(0, mAdapter.requests.size());
+    }
+
+    public void testBindView_MismatchBetwenCallLogAndMemoryCache_EnqueueRequest() {
+        mCursor.addRow(createCallLogEntryWithCachedValues());
+
+        // Contact info contains a different name.
+        ContactInfo info = createContactInfo();
+        info.name = "new name";
+        mAdapter.injectContactInfoForTest(TEST_NUMBER, info);
+
+        // Bind the views of a single row.
+        mAdapter.bindStandAloneView(mView, getContext(), mCursor);
+
+        // There is one request for contact details.
+        assertEquals(1, mAdapter.requests.size());
+
+        TestCallLogAdapter.Request request = mAdapter.requests.get(0);
+        // Since there is something in the cache, it is not an immediate request.
+        assertFalse("should not be immediate", request.immediate);
+    }
+
+    /** Returns a contact info with default values. */
+    private ContactInfo createContactInfo() {
+        ContactInfo info = new ContactInfo();
+        info.number = TEST_NUMBER;
+        info.name = TEST_NAME;
+        info.type = TEST_NUMBER_TYPE;
+        info.label = TEST_NUMBER_LABEL;
+        return info;
+    }
+
+    /** Returns a call log entry without cached values. */
+    private Object[] createCallLogEntry() {
+        Object[] values = CallLogQueryTestUtils.createTestExtendedValues();
+        values[CallLogQuery.NUMBER] = TEST_NUMBER;
+        return values;
+    }
+
+    /** Returns a call log entry with a cached values. */
+    private Object[] createCallLogEntryWithCachedValues() {
+        Object[] values = createCallLogEntry();
+        values[CallLogQuery.CACHED_NAME] = TEST_NAME;
+        values[CallLogQuery.CACHED_NUMBER_TYPE] = TEST_NUMBER_TYPE;
+        values[CallLogQuery.CACHED_NUMBER_LABEL] = TEST_NUMBER_LABEL;
+        return values;
+    }
+
+    /**
+     * Subclass of {@link CallLogAdapter} used in tests to intercept certain calls.
+     */
+    // TODO: This would be better done by splitting the contact lookup into a collaborator class
+    // instead.
+    private static final class TestCallLogAdapter extends CallLogAdapter {
+        public static class Request {
+            public final String number;
+            public final ContactInfo callLogInfo;
+            public final boolean immediate;
+
+            public Request(String number, ContactInfo callLogInfo, boolean immediate) {
+                this.number = number;
+                this.callLogInfo = callLogInfo;
+                this.immediate = immediate;
+            }
+        }
+
+        public final List<Request> requests = Lists.newArrayList();
+
+        public TestCallLogAdapter(Context context, CallFetcher callFetcher,
+                String currentCountryIso, String voicemailNumber) {
+            super(context, callFetcher, currentCountryIso, voicemailNumber);
+        }
+
+        @Override
+        void enqueueRequest(String number, ContactInfo callLogInfo, boolean immediate) {
+            requests.add(new Request(number, callLogInfo, immediate));
+        }
+    }
+}
diff --git a/tests/src/com/android/contacts/calllog/CallLogQueryTestUtils.java b/tests/src/com/android/contacts/calllog/CallLogQueryTestUtils.java
index 0e1952a..c273612 100644
--- a/tests/src/com/android/contacts/calllog/CallLogQueryTestUtils.java
+++ b/tests/src/com/android/contacts/calllog/CallLogQueryTestUtils.java
@@ -17,6 +17,9 @@
 package com.android.contacts.calllog;
 
 import static junit.framework.Assert.assertEquals;
+
+import android.provider.CallLog.Calls;
+
 import junit.framework.Assert;
 
 /**
@@ -24,13 +27,18 @@
  */
 public class CallLogQueryTestUtils {
     public static Object[] createTestValues() {
-        Object[] values = new Object[]{ -1L, "", 0L, 0L, 0, "", "", "", null, 0, null };
+        Object[] values = new Object[]{
+                -1L, "", 0L, 0L, Calls.INCOMING_TYPE, "", "", "", null, 0, null,
+        };
         assertEquals(CallLogQuery._PROJECTION.length, values.length);
         return values;
     }
 
     public static Object[] createTestExtendedValues() {
-        Object[] values = new Object[]{ -1L, "", 0L, 0L, 0, "", "", "", null, 0, null, 0 };
+        Object[] values = new Object[]{
+                -1L, "", 0L, 0L, Calls.INCOMING_TYPE, "", "", "", null, 0, null,
+                CallLogQuery.SECTION_OLD_ITEM
+        };
         Assert.assertEquals(CallLogQuery.EXTENDED_PROJECTION.length, values.length);
         return values;
     }
diff --git a/tests/src/com/android/contacts/detail/ContactDetailFragmentTests.java b/tests/src/com/android/contacts/detail/ContactDetailFragmentTests.java
index cfab94a..02faa24 100644
--- a/tests/src/com/android/contacts/detail/ContactDetailFragmentTests.java
+++ b/tests/src/com/android/contacts/detail/ContactDetailFragmentTests.java
@@ -43,7 +43,7 @@
         values.put(Im.DATA, TEST_ADDRESS);
 
         DetailViewEntry entry = new ContactDetailFragment.DetailViewEntry();
-        ContactDetailFragment.buildImActions(entry, values);
+        ContactDetailFragment.buildImActions(mContext, entry, values);
         assertEquals(Intent.ACTION_SENDTO, entry.intent.getAction());
         assertEquals("xmpp:" + TEST_ADDRESS + "?message", entry.intent.getData().toString());
 
@@ -60,7 +60,7 @@
         values.put(Im.CHAT_CAPABILITY, Im.CAPABILITY_HAS_VOICE | Im.CAPABILITY_HAS_VIDEO);
 
         DetailViewEntry entry = new ContactDetailFragment.DetailViewEntry();
-        ContactDetailFragment.buildImActions(entry, values);
+        ContactDetailFragment.buildImActions(mContext, entry, values);
         assertEquals(Intent.ACTION_SENDTO, entry.intent.getAction());
         assertEquals("xmpp:" + TEST_ADDRESS + "?message", entry.intent.getData().toString());
 
@@ -79,7 +79,7 @@
                 Im.CAPABILITY_HAS_VOICE);
 
         DetailViewEntry entry = new ContactDetailFragment.DetailViewEntry();
-        ContactDetailFragment.buildImActions(entry, values);
+        ContactDetailFragment.buildImActions(mContext, entry, values);
         assertEquals(Intent.ACTION_SENDTO, entry.intent.getAction());
         assertEquals("xmpp:" + TEST_ADDRESS + "?message", entry.intent.getData().toString());
 
@@ -98,7 +98,7 @@
         values.put(Im.DATA, TEST_ADDRESS);
 
         DetailViewEntry entry = new ContactDetailFragment.DetailViewEntry();
-        ContactDetailFragment.buildImActions(entry, values);
+        ContactDetailFragment.buildImActions(mContext, entry, values);
         assertEquals(Intent.ACTION_SENDTO, entry.intent.getAction());
 
         final Uri data = entry.intent.getData();
@@ -121,7 +121,7 @@
                 Im.CAPABILITY_HAS_VOICE);
 
         DetailViewEntry entry = new ContactDetailFragment.DetailViewEntry();
-        ContactDetailFragment.buildImActions(entry, values);
+        ContactDetailFragment.buildImActions(mContext, entry, values);
         assertEquals(Intent.ACTION_SENDTO, entry.intent.getAction());
         assertEquals("xmpp:" + TEST_ADDRESS + "?message", entry.intent.getData().toString());
 
diff --git a/tests/src/com/android/contacts/util/FakeAsyncTaskExecutor.java b/tests/src/com/android/contacts/util/FakeAsyncTaskExecutor.java
new file mode 100644
index 0000000..e27c6fb
--- /dev/null
+++ b/tests/src/com/android/contacts/util/FakeAsyncTaskExecutor.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2011 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.contacts.util;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.Executors;
+
+import android.app.Instrumentation;
+import android.os.AsyncTask;
+
+import junit.framework.Assert;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * Test implementation of AsyncTaskExecutor.
+ * <p>
+ * This class is thread-safe. As per the contract of the AsyncTaskExecutor, the submit methods must
+ * be called from the main ui thread, however the other public methods may be called from any thread
+ * (most commonly the test thread).
+ * <p>
+ * Tasks submitted to this executor will not be run immediately. Rather they will be stored in a
+ * list of submitted tasks, where they can be examined. They can also be run on-demand using the run
+ * methods, so that different ordering of AsyncTask execution can be simulated.
+ */
+@ThreadSafe
+public class FakeAsyncTaskExecutor implements AsyncTaskExecutor {
+    private static final long DEFAULT_TIMEOUT_MS = 10000;
+    private static final Executor DEFAULT_EXECUTOR = Executors.sameThreadExecutor();
+
+    /** The maximum length of time in ms to wait for tasks to execute during tests. */
+    private final long mTimeoutMs = DEFAULT_TIMEOUT_MS;
+    /** The executor for the background part of our test tasks. */
+    private final Executor mExecutor = DEFAULT_EXECUTOR;
+
+    private final Object mLock = new Object();
+    @GuardedBy("mLock") private final List<SubmittedTask<?>> mSubmittedTasks = Lists.newArrayList();
+
+    private final Instrumentation mInstrumentation;
+
+    /** Create a fake AsyncTaskExecutor for use in unit tests. */
+    public FakeAsyncTaskExecutor(Instrumentation instrumentation) {
+        mInstrumentation = Preconditions.checkNotNull(instrumentation);
+    }
+
+    /** Encapsulates an async task with the params and identifier it was submitted with. */
+    public interface SubmittedTask<T> {
+        AsyncTask<T, ?, ?> getTask();
+        T[] getParams();
+        Object getIdentifier();
+    }
+
+    private static final class SubmittedTaskImpl<T> implements SubmittedTask<T> {
+        private final Object mIdentifier;
+        private final AsyncTask<T, ?, ?> mTask;
+        private final T[] mParams;
+
+        public SubmittedTaskImpl(Object identifier, AsyncTask<T, ?, ?> task, T[] params) {
+            mIdentifier = identifier;
+            mTask = task;
+            mParams = params;
+        }
+
+        @Override
+        public Object getIdentifier() {
+            return mIdentifier;
+        }
+
+        @Override
+        public AsyncTask<T, ?, ?> getTask() {
+            return mTask;
+        }
+
+        @Override
+        public T[] getParams() {
+            return mParams;
+        }
+
+        @Override
+        public String toString() {
+            return "SubmittedTaskImpl [mIdentifier=" + mIdentifier + "]";
+        }
+    }
+
+    @Override
+    public <T> AsyncTask<T, ?, ?> submit(Object identifier, AsyncTask<T, ?, ?> task, T... params) {
+        AsyncTaskExecutors.checkCalledFromUiThread();
+        synchronized (mLock) {
+            mSubmittedTasks.add(new SubmittedTaskImpl<T>(identifier, task, params));
+            return task;
+        }
+    }
+
+    /**
+     * Runs a single task matching the given identifier.
+     * <p>
+     * Removes the matching task from the list of submitted tasks, then runs it. The executor used
+     * to execute this async task will be a same-thread executor.
+     * <p>
+     * Fails if there was not exactly one task matching the given identifier.
+     * <p>
+     * This method blocks until the AsyncTask has completely finished executing.
+     */
+    public void runTask(Enum<?> identifier) throws InterruptedException {
+        List<SubmittedTask<?>> tasks = getSubmittedTasksByIdentifier(identifier, true);
+        Assert.assertEquals("Expected one task " + identifier + ", got " + tasks, 1, tasks.size());
+        runTask(tasks.get(0));
+    }
+
+    /**
+     * Runs all tasks whose identifier matches the given identifier.
+     * <p>
+     * Removes all matching tasks from the list of submitted tasks, and runs them. The executor used
+     * to execute these async tasks will be a same-thread executor.
+     * <p>
+     * Fails if there were no tasks matching the given identifier.
+     * <p>
+     * This method blocks until the AsyncTask objects have completely finished executing.
+     */
+    public void runAllTasks(Enum<?> identifier) throws InterruptedException {
+        List<SubmittedTask<?>> tasks = getSubmittedTasksByIdentifier(identifier, true);
+        Assert.assertTrue("There were no tasks with identifier " + identifier, tasks.size() > 0);
+        for (SubmittedTask<?> task : tasks) {
+            runTask(task);
+        }
+    }
+
+    /**
+     * Executes a single {@link AsyncTask} using the supplied executors.
+     * <p>
+     * Blocks until the task has completed running.
+     */
+    private <T> void runTask(SubmittedTask<T> submittedTask) throws InterruptedException {
+        final AsyncTask<T, ?, ?> task = submittedTask.getTask();
+        task.executeOnExecutor(mExecutor, submittedTask.getParams());
+        // Block until the task has finished running in the background.
+        try {
+            task.get(mTimeoutMs, TimeUnit.MILLISECONDS);
+        } catch (ExecutionException e) {
+            throw new RuntimeException(e.getCause());
+        } catch (TimeoutException e) {
+            throw new RuntimeException("waited too long");
+        }
+        // Block until the onPostExecute or onCancelled has finished.
+        // Unfortunately we can't be sure when the AsyncTask will have posted its result handling
+        // code to the main ui thread, the best we can do is wait for the Status to be FINISHED.
+        final CountDownLatch latch = new CountDownLatch(1);
+        class AsyncTaskHasFinishedRunnable implements Runnable {
+            @Override
+            public void run() {
+                if (task.getStatus() == AsyncTask.Status.FINISHED) {
+                    latch.countDown();
+                } else {
+                    mInstrumentation.waitForIdle(this);
+                }
+            }
+        }
+        mInstrumentation.waitForIdle(new AsyncTaskHasFinishedRunnable());
+        Assert.assertTrue(latch.await(mTimeoutMs, TimeUnit.MILLISECONDS));
+    }
+
+    private List<SubmittedTask<?>> getSubmittedTasksByIdentifier(
+            Enum<?> identifier, boolean remove) {
+        Preconditions.checkNotNull(identifier, "can't lookup tasks by 'null' identifier");
+        List<SubmittedTask<?>> results = Lists.newArrayList();
+        synchronized (mLock) {
+            Iterator<SubmittedTask<?>> iter = mSubmittedTasks.iterator();
+            while (iter.hasNext()) {
+                SubmittedTask<?> task = iter.next();
+                if (identifier.equals(task.getIdentifier())) {
+                    results.add(task);
+                    iter.remove();
+                }
+            }
+        }
+        return results;
+    }
+
+    /** Get a factory that will return this instance - useful for testing. */
+    public AsyncTaskExecutors.AsyncTaskExecutorFactory getFactory() {
+        return new AsyncTaskExecutors.AsyncTaskExecutorFactory() {
+            @Override
+            public AsyncTaskExecutor createAsyncTaskExeuctor() {
+                return FakeAsyncTaskExecutor.this;
+            }
+        };
+    }
+}
diff --git a/tests/src/com/android/contacts/util/FakeBackgroundTaskService.java b/tests/src/com/android/contacts/util/FakeBackgroundTaskService.java
deleted file mode 100644
index 26be519..0000000
--- a/tests/src/com/android/contacts/util/FakeBackgroundTaskService.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2011 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.contacts.util;
-
-import com.google.common.collect.Lists;
-import com.google.common.util.concurrent.Executors;
-
-import android.app.Instrumentation;
-
-import java.util.List;
-import java.util.concurrent.Executor;
-
-/**
- * Simple test implementation of BackgroundTaskService.
- */
-public class FakeBackgroundTaskService extends BackgroundTaskService {
-    private final List<BackgroundTask> mSubmittedTasks = Lists.newArrayList();
-
-    @Override
-    public void submit(BackgroundTask task) {
-        mSubmittedTasks.add(task);
-    }
-
-    @Override
-    public void submit(BackgroundTask task, Executor executor) {
-        mSubmittedTasks.add(task);
-    }
-
-    public List<BackgroundTask> getSubmittedTasks() {
-        return mSubmittedTasks;
-    }
-
-    public static Executor createMainSyncExecutor(final Instrumentation instrumentation) {
-        return new Executor() {
-            @Override
-            public void execute(Runnable runnable) {
-                instrumentation.runOnMainSync(runnable);
-            }
-        };
-    }
-
-    /**
-     * Executes the background tasks, using the supplied executors.
-     * <p>
-     * This is most commonly used with {@link Executors#sameThreadExecutor()} for the first argument
-     * and {@link #createMainSyncExecutor(Instrumentation)}, so that the test thread can directly
-     * run the tasks in the background, then have the onPostExecute methods happen on the main ui
-     * thread.
-     */
-    public void runAllBackgroundTasks(Executor doInBackgroundExecutor,
-            final Executor onPostExecuteExecutor) {
-        for (final BackgroundTask task : getSubmittedTasks()) {
-            final Object visibilityLock = new Object();
-            doInBackgroundExecutor.execute(new Runnable() {
-                @Override
-                public void run() {
-                    synchronized (visibilityLock) {
-                        task.doInBackground();
-                    }
-                    onPostExecuteExecutor.execute(new Runnable() {
-                        @Override
-                        public void run() {
-                            synchronized (visibilityLock) {
-                                task.onPostExecute();
-                            }
-                        }
-                    });
-                }
-            });
-        }
-    }
-}