When we leave the voicemail activity, stop playback.
- On pause is propagated through to the VoicemailPlaybackPresenter.
- We use this to stop playback.
- Added a test used when working with this, but suppressed it because
I can't find a simple-enough way of checking that the audio has
stopped.
- Adds the ability to test the CallDetailActivity with a real voicemail.
- Fixes to the FakeAsyncTaskExecutor that Flavio and I have already
discussed - fixes that make the onPreExecute() method work correctly
(i.e. get invoked immediately on the thread that calls submit() on the
executor, rather than on the same thread that calls run... from the
test, which is a poor simulation and also illegal).
Bug: 5206675
Change-Id: I3f96c438dc6bbc1413a40b7bb2306ceb413f14a8
diff --git a/src/com/android/contacts/voicemail/VoicemailPlaybackFragment.java b/src/com/android/contacts/voicemail/VoicemailPlaybackFragment.java
index 5eb0ddf..7d29406 100644
--- a/src/com/android/contacts/voicemail/VoicemailPlaybackFragment.java
+++ b/src/com/android/contacts/voicemail/VoicemailPlaybackFragment.java
@@ -111,6 +111,12 @@
super.onDestroy();
}
+ @Override
+ public void onPause() {
+ mPresenter.onPause();
+ super.onPause();
+ }
+
private PlaybackViewImpl createPlaybackViewImpl() {
return new PlaybackViewImpl(new ActivityReference(), getActivity().getApplicationContext(),
mPlaybackLayout);
diff --git a/src/com/android/contacts/voicemail/VoicemailPlaybackPresenter.java b/src/com/android/contacts/voicemail/VoicemailPlaybackPresenter.java
index d3e4bef..d54cddc 100644
--- a/src/com/android/contacts/voicemail/VoicemailPlaybackPresenter.java
+++ b/src/com/android/contacts/voicemail/VoicemailPlaybackPresenter.java
@@ -555,4 +555,10 @@
}
}
}
+
+ public void onPause() {
+ if (mPlayer.isPlaying()) {
+ stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get());
+ }
+ }
}
diff --git a/tests/assets/README.txt b/tests/assets/README.txt
new file mode 100644
index 0000000..53bb0bd
--- /dev/null
+++ b/tests/assets/README.txt
@@ -0,0 +1,4 @@
+File quick_test_recording.mp3 is copyright 2011 by
+Hugo Hudson and is licensed under a
+Creative Commons Attribution 3.0 Unported License:
+ http://creativecommons.org/licenses/by/3.0/
diff --git a/tests/assets/quick_test_recording.mp3 b/tests/assets/quick_test_recording.mp3
new file mode 100644
index 0000000..ad7cb9c
--- /dev/null
+++ b/tests/assets/quick_test_recording.mp3
Binary files differ
diff --git a/tests/src/com/android/contacts/CallDetailActivityTest.java b/tests/src/com/android/contacts/CallDetailActivityTest.java
index 6a8fcb3..ac02588 100644
--- a/tests/src/com/android/contacts/CallDetailActivityTest.java
+++ b/tests/src/com/android/contacts/CallDetailActivityTest.java
@@ -26,11 +26,13 @@
import com.android.contacts.util.LocaleTestUtils;
import com.android.internal.view.menu.ContextMenuBuilder;
import com.google.common.base.Preconditions;
+import com.google.common.io.Closeables;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Intent;
+import android.content.res.AssetManager;
import android.net.Uri;
import android.provider.CallLog;
import android.provider.VoicemailContract;
@@ -40,6 +42,9 @@
import android.view.Menu;
import android.widget.TextView;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.util.List;
import java.util.Locale;
@@ -48,6 +53,11 @@
*/
@LargeTest
public class CallDetailActivityTest extends ActivityInstrumentationTestCase2<CallDetailActivity> {
+ private static final String TEST_ASSET_NAME = "quick_test_recording.mp3";
+ private static final String MIME_TYPE = "audio/mp3";
+ private static final String CONTACT_NUMBER = "+1412555555";
+ private static final String VOICEMAIL_FILE_LOCATION = "/sdcard/sadlfj893w4j23o9sfu.mp3";
+
private Uri mCallLogUri;
private Uri mVoicemailUri;
private IntegrationTestUtils mTestUtils;
@@ -202,11 +212,25 @@
assertEquals("00:00", mTestUtils.getText(timeDisplay));
}
+ @Suppress
+ public void testClickingCallStopsPlayback() throws Throwable {
+ setActivityIntentForRealFileVoicemailEntry();
+ startActivityUnderTest();
+ mFakeAsyncTaskExecutor.runTask(CHECK_FOR_CONTENT);
+ mFakeAsyncTaskExecutor.runTask(PREPARE_MEDIA_PLAYER);
+ mTestUtils.clickButton(mActivityUnderTest, R.id.playback_speakerphone);
+ mTestUtils.clickButton(mActivityUnderTest, R.id.playback_start_stop);
+ mTestUtils.clickButton(mActivityUnderTest, R.id.call_and_sms_main_action);
+ Thread.sleep(2000);
+ // TODO: Suppressed the test for now, because I'm looking for an easy way to say "the audio
+ // is not playing at this point", and I can't find it without doing dirty things.
+ }
+
private void setActivityIntentForTestCallEntry() {
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.NUMBER, CONTACT_NUMBER);
values.put(CallLog.Calls.TYPE, CallLog.Calls.INCOMING_TYPE);
mCallLogUri = contentResolver.insert(CallLog.Calls.CONTENT_URI, values);
setActivityIntent(new Intent(Intent.ACTION_VIEW, mCallLogUri));
@@ -216,8 +240,9 @@
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.NUMBER, CONTACT_NUMBER);
values.put(VoicemailContract.Voicemails.HAS_CONTENT, 1);
+ values.put(VoicemailContract.Voicemails._DATA, VOICEMAIL_FILE_LOCATION);
mVoicemailUri = contentResolver.insert(VoicemailContract.Voicemails.CONTENT_URI, values);
Uri callLogUri = ContentUris.withAppendedId(CallLog.Calls.CONTENT_URI_WITH_VOICEMAIL,
ContentUris.parseId(mVoicemailUri));
@@ -226,6 +251,44 @@
setActivityIntent(intent);
}
+ private void setActivityIntentForRealFileVoicemailEntry() throws IOException {
+ Preconditions.checkState(mVoicemailUri == null, "mUri should be null");
+ ContentValues values = new ContentValues();
+ values.put(VoicemailContract.Voicemails.DATE, String.valueOf(System.currentTimeMillis()));
+ values.put(VoicemailContract.Voicemails.NUMBER, CONTACT_NUMBER);
+ values.put(VoicemailContract.Voicemails.MIME_TYPE, MIME_TYPE);
+ values.put(VoicemailContract.Voicemails.HAS_CONTENT, 1);
+ String packageName = getInstrumentation().getTargetContext().getPackageName();
+ mVoicemailUri = getContentResolver().insert(
+ VoicemailContract.Voicemails.buildSourceUri(packageName), values);
+ AssetManager assets = getAssets();
+ OutputStream outputStream = null;
+ InputStream inputStream = null;
+ try {
+ inputStream = assets.open(TEST_ASSET_NAME);
+ outputStream = getContentResolver().openOutputStream(mVoicemailUri);
+ copyBetweenStreams(inputStream, outputStream);
+ } finally {
+ Closeables.closeQuietly(outputStream);
+ Closeables.closeQuietly(inputStream);
+ }
+ 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);
+ }
+
+ public void copyBetweenStreams(InputStream in, OutputStream out) throws IOException {
+ byte[] buffer = new byte[1024];
+ int bytesRead;
+ int total = 0;
+ while ((bytesRead = in.read(buffer)) != -1) {
+ total += bytesRead;
+ out.write(buffer, 0, bytesRead);
+ }
+ }
+
private void cleanUpUri() {
if (mVoicemailUri != null) {
getContentResolver().delete(VoicemailContract.Voicemails.CONTENT_URI,
@@ -267,4 +330,8 @@
// of a single unit test.
mFakeAsyncTaskExecutor.runAllTasks(UPDATE_PHONE_CALL_DETAILS);
}
+
+ private AssetManager getAssets() {
+ return getInstrumentation().getContext().getAssets();
+ }
}
diff --git a/tests/src/com/android/contacts/util/FakeAsyncTaskExecutor.java b/tests/src/com/android/contacts/util/FakeAsyncTaskExecutor.java
index e27c6fb..960f0bf 100644
--- a/tests/src/com/android/contacts/util/FakeAsyncTaskExecutor.java
+++ b/tests/src/com/android/contacts/util/FakeAsyncTaskExecutor.java
@@ -16,9 +16,11 @@
package com.android.contacts.util;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
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;
@@ -28,10 +30,8 @@
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;
@@ -46,43 +46,45 @@
* 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.
+ * <p>
+ * The onPreExecute method of the submitted AsyncTask will be called synchronously during the
+ * call to {@link #submit(Object, AsyncTask, Object...)}.
*/
@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();
+ @GuardedBy("mLock") private final List<SubmittedTask> mSubmittedTasks = Lists.newArrayList();
+ private final DelayedExecutor mBlockingExecutor = new DelayedExecutor();
private final Instrumentation mInstrumentation;
/** Create a fake AsyncTaskExecutor for use in unit tests. */
public FakeAsyncTaskExecutor(Instrumentation instrumentation) {
- mInstrumentation = Preconditions.checkNotNull(instrumentation);
+ mInstrumentation = checkNotNull(instrumentation);
}
/** Encapsulates an async task with the params and identifier it was submitted with. */
- public interface SubmittedTask<T> {
- AsyncTask<T, ?, ?> getTask();
- T[] getParams();
+ public interface SubmittedTask {
+ Runnable getRunnable();
Object getIdentifier();
+ AsyncTask<?, ?, ?> getAsyncTask();
}
- private static final class SubmittedTaskImpl<T> implements SubmittedTask<T> {
+ private static final class SubmittedTaskImpl implements SubmittedTask {
private final Object mIdentifier;
- private final AsyncTask<T, ?, ?> mTask;
- private final T[] mParams;
+ private final Runnable mRunnable;
+ private final AsyncTask<?, ?, ?> mAsyncTask;
- public SubmittedTaskImpl(Object identifier, AsyncTask<T, ?, ?> task, T[] params) {
+ public SubmittedTaskImpl(Object identifier, Runnable runnable,
+ AsyncTask<?, ?, ?> asyncTask) {
mIdentifier = identifier;
- mTask = task;
- mParams = params;
+ mRunnable = runnable;
+ mAsyncTask = asyncTask;
}
@Override
@@ -91,13 +93,13 @@
}
@Override
- public AsyncTask<T, ?, ?> getTask() {
- return mTask;
+ public Runnable getRunnable() {
+ return mRunnable;
}
@Override
- public T[] getParams() {
- return mParams;
+ public AsyncTask<?, ?, ?> getAsyncTask() {
+ return mAsyncTask;
}
@Override
@@ -106,13 +108,40 @@
}
}
+ private class DelayedExecutor implements Executor {
+ private final Object mNextLock = new Object();
+ @GuardedBy("mNextLock") private Object mNextIdentifier;
+ @GuardedBy("mNextLock") private AsyncTask<?, ?, ?> mNextTask;
+
+ @Override
+ public void execute(Runnable command) {
+ synchronized (mNextLock) {
+ mSubmittedTasks.add(new SubmittedTaskImpl(mNextIdentifier,
+ command, checkNotNull(mNextTask)));
+ mNextIdentifier = null;
+ mNextTask = null;
+ }
+ }
+
+ public <T> AsyncTask<T, ?, ?> submit(Object identifier,
+ AsyncTask<T, ?, ?> task, T... params) {
+ synchronized (mNextLock) {
+ checkState(mNextIdentifier == null);
+ checkState(mNextTask == null);
+ mNextIdentifier = identifier;
+ mNextTask = checkNotNull(task, "Already had a valid task.\n"
+ + "Are you calling AsyncTaskExecutor.submit(...) from within the "
+ + "onPreExecute() method of another task being submitted?\n"
+ + "Sorry! Not that's not supported.");
+ }
+ return task.executeOnExecutor(this, params);
+ }
+ }
+
@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;
- }
+ return mBlockingExecutor.submit(identifier, task, params);
}
/**
@@ -125,8 +154,8 @@
* <p>
* This method blocks until the AsyncTask has completely finished executing.
*/
- public void runTask(Enum<?> identifier) throws InterruptedException {
- List<SubmittedTask<?>> tasks = getSubmittedTasksByIdentifier(identifier, true);
+ public void runTask(Object identifier) throws InterruptedException {
+ List<SubmittedTask> tasks = getSubmittedTasksByIdentifier(identifier, true);
Assert.assertEquals("Expected one task " + identifier + ", got " + tasks, 1, tasks.size());
runTask(tasks.get(0));
}
@@ -141,30 +170,21 @@
* <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);
+ public void runAllTasks(Object 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) {
+ for (SubmittedTask task : tasks) {
runTask(task);
}
}
/**
- * Executes a single {@link AsyncTask} using the supplied executors.
+ * Executes a single {@link SubmittedTask}.
* <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");
- }
+ private <T> void runTask(final SubmittedTask submittedTask) throws InterruptedException {
+ submittedTask.getRunnable().run();
// 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.
@@ -172,7 +192,7 @@
class AsyncTaskHasFinishedRunnable implements Runnable {
@Override
public void run() {
- if (task.getStatus() == AsyncTask.Status.FINISHED) {
+ if (submittedTask.getAsyncTask().getStatus() == AsyncTask.Status.FINISHED) {
latch.countDown();
} else {
mInstrumentation.waitForIdle(this);
@@ -183,14 +203,14 @@
Assert.assertTrue(latch.await(mTimeoutMs, TimeUnit.MILLISECONDS));
}
- private List<SubmittedTask<?>> getSubmittedTasksByIdentifier(
- Enum<?> identifier, boolean remove) {
+ private List<SubmittedTask> getSubmittedTasksByIdentifier(
+ Object identifier, boolean remove) {
Preconditions.checkNotNull(identifier, "can't lookup tasks by 'null' identifier");
- List<SubmittedTask<?>> results = Lists.newArrayList();
+ List<SubmittedTask> results = Lists.newArrayList();
synchronized (mLock) {
- Iterator<SubmittedTask<?>> iter = mSubmittedTasks.iterator();
+ Iterator<SubmittedTask> iter = mSubmittedTasks.iterator();
while (iter.hasNext()) {
- SubmittedTask<?> task = iter.next();
+ SubmittedTask task = iter.next();
if (identifier.equals(task.getIdentifier())) {
results.add(task);
iter.remove();
diff --git a/tests/src/com/android/contacts/util/IntegrationTestUtils.java b/tests/src/com/android/contacts/util/IntegrationTestUtils.java
index afea349..66dd4ef 100644
--- a/tests/src/com/android/contacts/util/IntegrationTestUtils.java
+++ b/tests/src/com/android/contacts/util/IntegrationTestUtils.java
@@ -90,7 +90,7 @@
* Waits for an idle sync on the main thread (see {@link Instrumentation#waitForIdle(Runnable)})
* before executing this callable.
*/
- private <T> T runOnUiThreadAndGetTheResult(Callable<T> callable) throws Throwable {
+ public <T> T runOnUiThreadAndGetTheResult(Callable<T> callable) throws Throwable {
FutureTask<T> future = new FutureTask<T>(callable);
mInstrumentation.waitForIdle(future);
try {