Prepare playback immediately, new BackgroundTaskService.

- Immediately we start the CallDetailActivity with a voicemail, do the
  preparing of the media so we can see the duration.
- Use the duration text field to show "buffering..." until media source
  is prepared.
- Do the preparing in the background, this fixes a strict mode
  violation.
- If there's an error preparing, show an error message in that field.
- Add tests for both of the above cases.

BackgroundTaskService:
- Introduces new BackgroundTaskService, lightweight method for
  submitting AsyncTask objects.
- Introduces BackgroundTask interface and simple AbstractBackgroundTask
  abstract implementation.
- Adds BackgroundTaskService to ContactsApplication allowing Activity
  objects to get hold of a regular background task service.
- Adds a MockBackgroundTaskService for use with injecting for test, so
  that we can prevent or control processing of background tasks.

Other:
- Every time we resume the Activity, we were causing a new voicemail
  fragment to be created.  There was no bug tracking this, I just
  noticed it because of occasionally flaky tests.  Added a fix and test
  to catch it again next time.
- Fixes missing tear down method in PeopleActivityTest.

Bug: 5115133
Bug: 5059965
Bug: 5114261
Bug: 5113695
Change-Id: Ia2469229fa756da8b3977231fbf23a9d3fb379ce
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 0c9893b..abe1d01 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1635,7 +1635,10 @@
     <string name="voicemail_initial_time">00:05</string>
 
     <!-- Message to show when there is an error playing back the voicemail. [CHAR LIMIT=40] -->
-    <string name="voicemail_playback_error">Could not play voicemail</string>
+    <string name="voicemail_playback_error">failed to play voicemail</string>
+
+    <!-- Message to display before we have prepared the media player, i.e. before we know duration. [CHAR LIMIT=40] -->
+    <string name="voicemail_buffering">buffering...</string>
 
     <!-- The header in the call log used to identify missed calls and voicemail that have not yet been consumed [CHAR LIMIT=10] -->
     <string name="call_log_new_header">New</string>
diff --git a/src/com/android/contacts/CallDetailActivity.java b/src/com/android/contacts/CallDetailActivity.java
index 163069a..43e6fc7 100644
--- a/src/com/android/contacts/CallDetailActivity.java
+++ b/src/com/android/contacts/CallDetailActivity.java
@@ -19,6 +19,9 @@
 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.voicemail.VoicemailPlaybackFragment;
 import com.android.contacts.voicemail.VoicemailStatusHelper;
 import com.android.contacts.voicemail.VoicemailStatusHelper.StatusMessage;
@@ -34,7 +37,6 @@
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.net.Uri;
-import android.os.AsyncTask;
 import android.os.Bundle;
 import android.provider.CallLog;
 import android.provider.CallLog.Calls;
@@ -87,6 +89,7 @@
     private ImageView mMainActionView;
     private ImageButton mMainActionPushLayerView;
     private ImageView mContactBackgroundView;
+    private BackgroundTaskService mBackgroundTaskService;
 
     private String mNumber = null;
     private String mDefaultCountryIso;
@@ -144,6 +147,8 @@
 
         setContentView(R.layout.call_detail);
 
+        mBackgroundTaskService = (BackgroundTaskService) getApplicationContext().getSystemService(
+                BackgroundTaskService.BACKGROUND_TASK_SERVICE);
         mInflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
         mResources = getResources();
 
@@ -164,13 +169,13 @@
         mContactPhotoManager = ContactPhotoManager.getInstance(this);
         getListView().setOnItemClickListener(this);
         configureActionBar();
+        optionallyHandleVoicemail();
     }
 
     @Override
     public void onResume() {
         super.onResume();
         updateData(getCallLogEntryUris());
-        optionallyHandleVoicemail();
     }
 
     /**
@@ -210,15 +215,14 @@
     }
 
     private void markVoicemailAsRead(final Uri voicemailUri) {
-        new AsyncTask<Void, Void, Void>() {
+        mBackgroundTaskService.submit(new AbstractBackgroundTask() {
             @Override
-            protected Void doInBackground(Void... params) {
+            public void doInBackground() {
                 ContentValues values = new ContentValues();
                 values.put(Voicemails.IS_READ, true);
                 getContentResolver().update(voicemailUri, values, null, null);
-                return null;
             }
-        }.execute();
+        });
     }
 
     /**
@@ -662,12 +666,16 @@
             }
             callIds.append(ContentUris.parseId(callUri));
         }
-        runInBackgroundThenFinishActivity(new Runnable() {
+        mBackgroundTaskService.submit(new BackgroundTask() {
             @Override
-            public void run() {
+            public void doInBackground() {
                 getContentResolver().delete(Calls.CONTENT_URI_WITH_VOICEMAIL,
                         Calls._ID + " IN (" + callIds + ")", null);
             }
+            @Override
+            public void onPostExecute() {
+                finish();
+            }
         });
     }
     public void onMenuEditNumberBeforeCall(MenuItem menuItem) {
@@ -680,29 +688,16 @@
 
     public void onMenuTrashVoicemail(MenuItem menuItem) {
         final Uri voicemailUri = getVoicemailUri();
-        runInBackgroundThenFinishActivity(new Runnable() {
+        mBackgroundTaskService.submit(new BackgroundTask() {
             @Override
-            public void run() {
+            public void doInBackground() {
                 getContentResolver().delete(voicemailUri, null, null);
             }
-        });
-    }
-
-    /**
-     * Run a task in the background, and then finish this activity when the task is done.
-     */
-    private void runInBackgroundThenFinishActivity(final Runnable runnable) {
-        new AsyncTask<Void, Void, Void>() {
             @Override
-            protected Void doInBackground(Void... params) {
-                runnable.run();
-                return null;
-            }
-            @Override
-            protected void onPostExecute(Void result) {
+            public void onPostExecute() {
                 finish();
             }
-        }.execute();
+        });
     }
 
     private void configureActionBar() {
diff --git a/src/com/android/contacts/ContactsApplication.java b/src/com/android/contacts/ContactsApplication.java
index 1c8c080..1e791b6 100644
--- a/src/com/android/contacts/ContactsApplication.java
+++ b/src/com/android/contacts/ContactsApplication.java
@@ -18,6 +18,7 @@
 
 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;
@@ -33,6 +34,7 @@
     private static InjectedServices sInjectedServices;
     private AccountTypeManager mAccountTypeManager;
     private ContactPhotoManager mContactPhotoManager;
+    private BackgroundTaskService mBackgroundTaskService;
 
     /**
      * Overrides the system services with mocks for testing.
@@ -93,6 +95,13 @@
             return mContactPhotoManager;
         }
 
+        if (BackgroundTaskService.BACKGROUND_TASK_SERVICE.equals(name)) {
+            if (mBackgroundTaskService == null) {
+                mBackgroundTaskService = BackgroundTaskService.createBackgroundTaskService();
+            }
+            return mBackgroundTaskService;
+        }
+
         return super.getSystemService(name);
     }
 
diff --git a/src/com/android/contacts/util/AbstractBackgroundTask.java b/src/com/android/contacts/util/AbstractBackgroundTask.java
new file mode 100644
index 0000000..c492e7c
--- /dev/null
+++ b/src/com/android/contacts/util/AbstractBackgroundTask.java
@@ -0,0 +1,29 @@
+/*
+ * 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/BackgroundTask.java b/src/com/android/contacts/util/BackgroundTask.java
new file mode 100644
index 0000000..ba791fb
--- /dev/null
+++ b/src/com/android/contacts/util/BackgroundTask.java
@@ -0,0 +1,31 @@
+/*
+ * 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
new file mode 100644
index 0000000..00f9e3e
--- /dev/null
+++ b/src/com/android/contacts/util/BackgroundTaskService.java
@@ -0,0 +1,54 @@
+/*
+ * 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;
+
+/**
+ * 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.
+ */
+public abstract class BackgroundTaskService {
+    public static final String BACKGROUND_TASK_SERVICE = BackgroundTaskService.class.getName();
+
+    public abstract void submit(BackgroundTask task);
+
+    public static BackgroundTaskService createBackgroundTaskService() {
+        return new AsyncTaskBackgroundTaskService();
+    }
+
+    private static final class AsyncTaskBackgroundTaskService extends BackgroundTaskService {
+        @Override
+        public void submit(final BackgroundTask task) {
+            new AsyncTask<Void, Void, Void>() {
+                @Override
+                protected Void doInBackground(Void... params) {
+                    task.doInBackground();
+                    return null;
+                }
+
+                @Override
+                protected void onPostExecute(Void result) {
+                    task.onPostExecute();
+                }
+            }.execute();
+        }
+    }
+}
diff --git a/src/com/android/contacts/voicemail/VoicemailPlaybackFragment.java b/src/com/android/contacts/voicemail/VoicemailPlaybackFragment.java
index fcf99b6..0b30cd9 100644
--- a/src/com/android/contacts/voicemail/VoicemailPlaybackFragment.java
+++ b/src/com/android/contacts/voicemail/VoicemailPlaybackFragment.java
@@ -20,6 +20,7 @@
 import static com.android.contacts.CallDetailActivity.EXTRA_VOICEMAIL_URI;
 
 import com.android.contacts.R;
+import com.android.contacts.util.BackgroundTaskService;
 import com.android.ex.variablespeed.MediaPlayerProxy;
 import com.android.ex.variablespeed.VariableSpeed;
 import com.google.common.base.Preconditions;
@@ -36,7 +37,6 @@
 import android.widget.ImageButton;
 import android.widget.SeekBar;
 import android.widget.TextView;
-import android.widget.Toast;
 
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -97,10 +97,15 @@
         boolean startPlayback = arguments.getBoolean(EXTRA_VOICEMAIL_START_PLAYBACK, false);
         mPresenter = new VoicemailPlaybackPresenter(new PlaybackViewImpl(),
                 createMediaPlayer(mScheduledExecutorService), voicemailUri,
-                mScheduledExecutorService, startPlayback);
+                mScheduledExecutorService, startPlayback, getBackgroundTaskService());
         mPresenter.onCreate(savedInstanceState);
     }
 
+    private BackgroundTaskService getBackgroundTaskService() {
+        return (BackgroundTaskService) getActivity().getApplicationContext().getSystemService(
+                BackgroundTaskService.BACKGROUND_TASK_SERVICE);
+    }
+
     @Override
     public void onSaveInstanceState(Bundle outState) {
         mPresenter.onSaveInstanceState(outState);
@@ -213,6 +218,11 @@
         }
 
         @Override
+        public void setIsBuffering() {
+          mTextController.setPermanentText(getString(R.string.voicemail_buffering));
+        }
+
+        @Override
         public int getDesiredClipPosition() {
             return mPlaybackSeek.getProgress();
         }
@@ -224,7 +234,7 @@
             mStartStopButton.setEnabled(false);
             mPlaybackSeek.setProgress(0);
             mPlaybackSeek.setEnabled(false);
-            Toast.makeText(getActivity(), R.string.voicemail_playback_error, Toast.LENGTH_SHORT);
+            mTextController.setPermanentText(getString(R.string.voicemail_playback_error));
             Log.e(TAG, "Could not play voicemail", e);
         }
 
diff --git a/src/com/android/contacts/voicemail/VoicemailPlaybackPresenter.java b/src/com/android/contacts/voicemail/VoicemailPlaybackPresenter.java
index 6f3a625..e901fab 100644
--- a/src/com/android/contacts/voicemail/VoicemailPlaybackPresenter.java
+++ b/src/com/android/contacts/voicemail/VoicemailPlaybackPresenter.java
@@ -19,6 +19,8 @@
 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.ex.variablespeed.MediaPlayerProxy;
 import com.android.ex.variablespeed.SingleThreadedMediaPlayerProxy;
 
@@ -58,6 +60,7 @@
         void setStartStopListener(View.OnClickListener listener);
         void setPositionSeekListener(SeekBar.OnSeekBarChangeListener listener);
         void setSpeakerphoneListener(View.OnClickListener listener);
+        void setIsBuffering();
         void setClipPosition(int clipPositionInMillis, int clipLengthInMillis);
         int getDesiredClipPosition();
         void playbackStarted();
@@ -124,18 +127,48 @@
     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;
 
     public VoicemailPlaybackPresenter(PlaybackView view, MediaPlayerProxy player,
             Uri voicemailUri, ScheduledExecutorService executorService,
-            boolean startPlayingImmediately) {
+            boolean startPlayingImmediately, BackgroundTaskService backgroundTaskService) {
         mView = view;
         mPlayer = player;
         mVoicemailUri = voicemailUri;
         mStartPlayingImmediately = startPlayingImmediately;
+        mBackgroundTaskService = backgroundTaskService;
         mPositionUpdater = new PositionUpdater(executorService, SLIDER_UPDATE_PERIOD_MILLIS);
     }
 
     public void onCreate(Bundle bundle) {
+        mView.setIsBuffering();
+        mBackgroundTaskService.submit(new BackgroundTask() {
+            private Exception mException;
+
+            @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);
+                }
+            }
+        });
+    }
+
+    private void postSuccessfulPrepareActions() {
         mView.setPositionSeekListener(new PlaybackPositionListener());
         mView.setStartStopListener(new StartStopButtonListener());
         mView.setSpeakerphoneListener(new SpeakerphoneListener());
@@ -144,7 +177,7 @@
         mView.setSpeakerPhoneOn(mView.isSpeakerPhoneOn());
         mView.setRateDecreaseButtonListener(createRateDecreaseListener());
         mView.setRateIncreaseButtonListener(createRateIncreaseListener());
-        mView.setClipPosition(0, 0);
+        mView.setClipPosition(0, mPlayer.getDuration());
         mView.playbackStopped();
         if (mStartPlayingImmediately) {
             resetPrepareStartPlaying(0);
diff --git a/tests/src/com/android/contacts/CallDetailActivityTest.java b/tests/src/com/android/contacts/CallDetailActivityTest.java
index 7e225c4..33d5cc9 100644
--- a/tests/src/com/android/contacts/CallDetailActivityTest.java
+++ b/tests/src/com/android/contacts/CallDetailActivityTest.java
@@ -16,10 +16,14 @@
 
 package com.android.contacts;
 
+import com.android.contacts.test.InjectedServices;
+import com.android.contacts.util.BackgroundTaskService;
 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;
@@ -46,6 +50,7 @@
     private Uri mUri;
     private IntegrationTestUtils mTestUtils;
     private LocaleTestUtils mLocaleTestUtils;
+    private FakeBackgroundTaskService mMockBackgroundTaskService;
 
     public CallDetailActivityTest() {
         super(CallDetailActivity.class);
@@ -54,6 +59,11 @@
     @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);
         // I don't like the default of focus-mode for tests, the green focus border makes the
         // screenshots look weak.
         setActivityInitialTouchMode(true);
@@ -70,9 +80,47 @@
         mLocaleTestUtils = null;
         cleanUpUri();
         mTestUtils = null;
+        ContactsApplication.injectServices(null);
         super.tearDown();
     }
 
+    public void testInitialActivityStartsWithBuffering() throws Throwable {
+        setActivityIntentForTestVoicemailEntry();
+        CallDetailActivity activity = getActivity();
+        // When the activity first starts, we will show "buffering..." on the screen.
+        // The duration should not be visible.
+        assertHasOneTextViewContaining(activity, "buffering...");
+        assertZeroTextViewsContaining(activity, "00:00");
+    }
+
+    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.
+        // 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...");
+    }
+
+    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.
+        setActivityIntentForTestVoicemailEntry();
+        final CallDetailActivity activity = getActivity();
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                getInstrumentation().callActivityOnPause(activity);
+                getInstrumentation().callActivityOnResume(activity);
+                getInstrumentation().callActivityOnPause(activity);
+                getInstrumentation().callActivityOnResume(activity);
+            }
+        });
+        assertHasOneTextViewContaining(activity, "buffering...");
+    }
+
     /**
      * Test for bug where increase rate button with invalid voicemail causes a crash.
      * <p>
@@ -184,4 +232,24 @@
     private ContentResolver getContentResolver() {
         return getInstrumentation().getTargetContext().getContentResolver();
     }
+
+    private TextView assertHasOneTextViewContaining(Activity activity, String text)
+            throws Throwable {
+        List<TextView> views = mTestUtils.getTextViewsWithString(activity, 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);
+        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()));
+    }
 }
diff --git a/tests/src/com/android/contacts/activities/PeopleActivityTest.java b/tests/src/com/android/contacts/activities/PeopleActivityTest.java
index 8cef001..fe6d60a 100644
--- a/tests/src/com/android/contacts/activities/PeopleActivityTest.java
+++ b/tests/src/com/android/contacts/activities/PeopleActivityTest.java
@@ -102,6 +102,12 @@
         ContactsApplication.injectServices(services);
     }
 
+    @Override
+    protected void tearDown() throws Exception {
+        ContactsApplication.injectServices(null);
+        super.tearDown();
+    }
+
     public void testSingleAccountNoGroups() {
         // This two-pane UI test only makes sense if we run with two panes.
         // Let's ignore this in the single pane case
diff --git a/tests/src/com/android/contacts/util/FakeBackgroundTaskService.java b/tests/src/com/android/contacts/util/FakeBackgroundTaskService.java
new file mode 100644
index 0000000..a9d80b4
--- /dev/null
+++ b/tests/src/com/android/contacts/util/FakeBackgroundTaskService.java
@@ -0,0 +1,81 @@
+/*
+ * 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);
+    }
+
+    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();
+                            }
+                        }
+                    });
+                }
+            });
+        }
+    }
+}