1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.dialer.util;
18
19import android.app.Instrumentation;
20import android.os.AsyncTask;
21
22import com.google.common.collect.Lists;
23
24import junit.framework.Assert;
25
26import java.util.Iterator;
27import java.util.List;
28import java.util.concurrent.CountDownLatch;
29import java.util.concurrent.Executor;
30import java.util.concurrent.TimeUnit;
31
32import javax.annotation.concurrent.GuardedBy;
33import javax.annotation.concurrent.ThreadSafe;
34
35/**
36 * Test implementation of AsyncTaskExecutor.
37 * <p>
38 * This class is thread-safe. As per the contract of the AsyncTaskExecutor, the submit methods must
39 * be called from the main ui thread, however the other public methods may be called from any thread
40 * (most commonly the test thread).
41 * <p>
42 * Tasks submitted to this executor will not be run immediately. Rather they will be stored in a
43 * list of submitted tasks, where they can be examined. They can also be run on-demand using the run
44 * methods, so that different ordering of AsyncTask execution can be simulated.
45 * <p>
46 * The onPreExecute method of the submitted AsyncTask will be called synchronously during the
47 * call to {@link #submit(Object, AsyncTask, Object...)}.
48 */
49@ThreadSafe
50public class FakeAsyncTaskExecutor implements AsyncTaskExecutor {
51    private static final long DEFAULT_TIMEOUT_MS = 10000;
52
53    /** The maximum length of time in ms to wait for tasks to execute during tests. */
54    private final long mTimeoutMs = DEFAULT_TIMEOUT_MS;
55
56    private final Object mLock = new Object();
57    @GuardedBy("mLock") private final List<SubmittedTask> mSubmittedTasks = Lists.newArrayList();
58
59    private final DelayedExecutor mBlockingExecutor = new DelayedExecutor();
60    private final Instrumentation mInstrumentation;
61
62    /** Create a fake AsyncTaskExecutor for use in unit tests. */
63    public FakeAsyncTaskExecutor(Instrumentation instrumentation) {
64        Assert.assertNotNull(instrumentation);
65        mInstrumentation = instrumentation;
66    }
67
68    /** Encapsulates an async task with the params and identifier it was submitted with. */
69    public interface SubmittedTask {
70        Runnable getRunnable();
71        Object getIdentifier();
72        AsyncTask<?, ?, ?> getAsyncTask();
73    }
74
75    private static final class SubmittedTaskImpl implements SubmittedTask {
76        private final Object mIdentifier;
77        private final Runnable mRunnable;
78        private final AsyncTask<?, ?, ?> mAsyncTask;
79
80        public SubmittedTaskImpl(Object identifier, Runnable runnable,
81                AsyncTask<?, ?, ?> asyncTask) {
82            mIdentifier = identifier;
83            mRunnable = runnable;
84            mAsyncTask = asyncTask;
85        }
86
87        @Override
88        public Object getIdentifier() {
89            return mIdentifier;
90        }
91
92        @Override
93        public Runnable getRunnable() {
94            return mRunnable;
95        }
96
97        @Override
98        public AsyncTask<?, ?, ?> getAsyncTask() {
99            return mAsyncTask;
100        }
101
102        @Override
103        public String toString() {
104            return "SubmittedTaskImpl [mIdentifier=" + mIdentifier + "]";
105        }
106    }
107
108    private class DelayedExecutor implements Executor {
109        private final Object mNextLock = new Object();
110        @GuardedBy("mNextLock") private Object mNextIdentifier;
111        @GuardedBy("mNextLock") private AsyncTask<?, ?, ?> mNextTask;
112
113        @Override
114        public void execute(Runnable command) {
115            synchronized (mNextLock) {
116                Assert.assertNotNull(mNextTask);
117                mSubmittedTasks.add(new SubmittedTaskImpl(mNextIdentifier,
118                        command, mNextTask));
119                mNextIdentifier = null;
120                mNextTask = null;
121            }
122        }
123
124        public <T> AsyncTask<T, ?, ?> submit(Object identifier,
125                AsyncTask<T, ?, ?> task, T... params) {
126            synchronized (mNextLock) {
127                Assert.assertNull(mNextIdentifier);
128                Assert.assertNull(mNextTask);
129                mNextIdentifier = identifier;
130                Assert.assertNotNull("Already had a valid task.\n"
131                        + "Are you calling AsyncTaskExecutor.submit(...) from within the "
132                        + "onPreExecute() method of another task being submitted?\n"
133                        + "Sorry!  Not that's not supported.", task);
134                mNextTask = task;
135            }
136            return task.executeOnExecutor(this, params);
137        }
138    }
139
140    @Override
141    public <T> AsyncTask<T, ?, ?> submit(Object identifier, AsyncTask<T, ?, ?> task, T... params) {
142        AsyncTaskExecutors.checkCalledFromUiThread();
143        return mBlockingExecutor.submit(identifier, task, params);
144    }
145
146    /**
147     * Runs a single task matching the given identifier.
148     * <p>
149     * Removes the matching task from the list of submitted tasks, then runs it. The executor used
150     * to execute this async task will be a same-thread executor.
151     * <p>
152     * Fails if there was not exactly one task matching the given identifier.
153     * <p>
154     * This method blocks until the AsyncTask has completely finished executing.
155     */
156    public void runTask(Object identifier) throws InterruptedException {
157        List<SubmittedTask> tasks = getSubmittedTasksByIdentifier(identifier, true);
158        Assert.assertEquals("Expected one task " + identifier + ", got " + tasks, 1, tasks.size());
159        runTask(tasks.get(0));
160    }
161
162    /**
163     * Runs all tasks whose identifier matches the given identifier.
164     * <p>
165     * Removes all matching tasks from the list of submitted tasks, and runs them. The executor used
166     * to execute these async tasks will be a same-thread executor.
167     * <p>
168     * Fails if there were no tasks matching the given identifier.
169     * <p>
170     * This method blocks until the AsyncTask objects have completely finished executing.
171     */
172    public void runAllTasks(Object identifier) throws InterruptedException {
173        List<SubmittedTask> tasks = getSubmittedTasksByIdentifier(identifier, true);
174        Assert.assertTrue("There were no tasks with identifier " + identifier, tasks.size() > 0);
175        for (SubmittedTask task : tasks) {
176            runTask(task);
177        }
178    }
179
180    /**
181     * Executes a single {@link SubmittedTask}.
182     * <p>
183     * Blocks until the task has completed running.
184     */
185    private <T> void runTask(final SubmittedTask submittedTask) throws InterruptedException {
186        submittedTask.getRunnable().run();
187        // Block until the onPostExecute or onCancelled has finished.
188        // Unfortunately we can't be sure when the AsyncTask will have posted its result handling
189        // code to the main ui thread, the best we can do is wait for the Status to be FINISHED.
190        final CountDownLatch latch = new CountDownLatch(1);
191        class AsyncTaskHasFinishedRunnable implements Runnable {
192            @Override
193            public void run() {
194                if (submittedTask.getAsyncTask().getStatus() == AsyncTask.Status.FINISHED) {
195                    latch.countDown();
196                } else {
197                    mInstrumentation.waitForIdle(this);
198                }
199            }
200        }
201        mInstrumentation.waitForIdle(new AsyncTaskHasFinishedRunnable());
202        Assert.assertTrue(latch.await(mTimeoutMs, TimeUnit.MILLISECONDS));
203    }
204
205    private List<SubmittedTask> getSubmittedTasksByIdentifier(
206            Object identifier, boolean remove) {
207        Assert.assertNotNull(identifier);
208        List<SubmittedTask> results = Lists.newArrayList();
209        synchronized (mLock) {
210            Iterator<SubmittedTask> iter = mSubmittedTasks.iterator();
211            while (iter.hasNext()) {
212                SubmittedTask task = iter.next();
213                if (identifier.equals(task.getIdentifier())) {
214                    results.add(task);
215                    iter.remove();
216                }
217            }
218        }
219        return results;
220    }
221
222    /** Get a factory that will return this instance - useful for testing. */
223    public AsyncTaskExecutors.AsyncTaskExecutorFactory getFactory() {
224        return new AsyncTaskExecutors.AsyncTaskExecutorFactory() {
225            @Override
226            public AsyncTaskExecutor createAsyncTaskExeuctor() {
227                return FakeAsyncTaskExecutor.this;
228            }
229        };
230    }
231}
232