Fix up the unit tests for the CallDetailActivity.
- We've recently moved lots of code to use AsyncTask to avoid strict mode
violations.
- Thanks to the new BackgroundTaskService, these weren't being executed,
and the tests were failing. But simply executing them is not a fix,
we want much finer grained control over what executes when, so we
can assert about different states of the ui.
- This cl introduces the concept of an identifier to go with the submitted
task, so that you can uniquely identify tasks from the test.
Additionally, on further reflection, adding a new interface BackgroundTask
wasn't necessarily a great idea. Nor was calling the thing that submits them a
Service - that name is already overloaded to mean something else in Android.
Therefore this cl makes a number of other style changes to the pattern:
- The BackgroundTaskService just becomes an interface AsyncTaskExecutor, with a
single submit() method, in a very similar fashion to the Executor pattern in
java.util.concurrent.
- We introduce the AsyncTaskExecutors class, which may be used to create
AsyncTaskExecutor objects, and also introduces a seam for injecting fake
executors for testing.
- This cl introduces a FakeAsyncTaskExecutor, which can be used to inspect the
tasks that have been submitted, as well as being used to execute them in a
controlled manner between assertions.
- This is now being used to control the flow of voicemail fetching from
the unit tests, and make sure that the recently implemented logic to
read has content, move to buffering state, then move to preparing,
is all working correctly.
- Later this will also be used to exhaustively test all the other
situations we care about.
Change-Id: Ia75df4996f9a5168db8d9f39560b62ccf4b98b46
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/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/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();
- }
- }
- });
- }
- });
- }
- }
-}