/* * 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.dialer.util; import android.app.Instrumentation; import android.os.AsyncTask; import com.google.common.collect.Lists; import junit.framework.Assert; import java.util.Iterator; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; /** * Test implementation of AsyncTaskExecutor. *

* 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). *

* 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. *

* 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; /** The maximum length of time in ms to wait for tasks to execute during tests. */ private final long mTimeoutMs = DEFAULT_TIMEOUT_MS; private final Object mLock = new Object(); @GuardedBy("mLock") private final List 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) { Assert.assertNotNull(instrumentation); mInstrumentation = instrumentation; } /** Encapsulates an async task with the params and identifier it was submitted with. */ public interface SubmittedTask { Runnable getRunnable(); Object getIdentifier(); AsyncTask getAsyncTask(); } private static final class SubmittedTaskImpl implements SubmittedTask { private final Object mIdentifier; private final Runnable mRunnable; private final AsyncTask mAsyncTask; public SubmittedTaskImpl(Object identifier, Runnable runnable, AsyncTask asyncTask) { mIdentifier = identifier; mRunnable = runnable; mAsyncTask = asyncTask; } @Override public Object getIdentifier() { return mIdentifier; } @Override public Runnable getRunnable() { return mRunnable; } @Override public AsyncTask getAsyncTask() { return mAsyncTask; } @Override public String toString() { return "SubmittedTaskImpl [mIdentifier=" + mIdentifier + "]"; } } 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) { Assert.assertNotNull(mNextTask); mSubmittedTasks.add(new SubmittedTaskImpl(mNextIdentifier, command, mNextTask)); mNextIdentifier = null; mNextTask = null; } } public AsyncTask submit(Object identifier, AsyncTask task, T... params) { synchronized (mNextLock) { Assert.assertNull(mNextIdentifier); Assert.assertNull(mNextTask); mNextIdentifier = identifier; Assert.assertNotNull("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.", task); mNextTask = task; } return task.executeOnExecutor(this, params); } } @Override public AsyncTask submit(Object identifier, AsyncTask task, T... params) { AsyncTaskExecutors.checkCalledFromUiThread(); return mBlockingExecutor.submit(identifier, task, params); } /** * Runs a single task matching the given identifier. *

* 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. *

* Fails if there was not exactly one task matching the given identifier. *

* This method blocks until the AsyncTask has completely finished executing. */ public void runTask(Object identifier) throws InterruptedException { List 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. *

* 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. *

* Fails if there were no tasks matching the given identifier. *

* This method blocks until the AsyncTask objects have completely finished executing. */ public void runAllTasks(Object identifier) throws InterruptedException { List 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 SubmittedTask}. *

* Blocks until the task has completed running. */ private 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. final CountDownLatch latch = new CountDownLatch(1); class AsyncTaskHasFinishedRunnable implements Runnable { @Override public void run() { if (submittedTask.getAsyncTask().getStatus() == AsyncTask.Status.FINISHED) { latch.countDown(); } else { mInstrumentation.waitForIdle(this); } } } mInstrumentation.waitForIdle(new AsyncTaskHasFinishedRunnable()); Assert.assertTrue(latch.await(mTimeoutMs, TimeUnit.MILLISECONDS)); } private List getSubmittedTasksByIdentifier( Object identifier, boolean remove) { Assert.assertNotNull(identifier); List results = Lists.newArrayList(); synchronized (mLock) { Iterator 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; } }; } }