1/*
2 * Copyright (C) 2010 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.contacts.interactions;
18
19import com.google.common.annotations.VisibleForTesting;
20
21import android.app.Activity;
22import android.app.LoaderManager;
23import android.content.AsyncTaskLoader;
24import android.content.Loader;
25import android.os.Bundle;
26import android.util.Log;
27
28import java.io.FileDescriptor;
29import java.io.PrintWriter;
30import java.util.ArrayList;
31import java.util.HashSet;
32import java.util.List;
33
34import junit.framework.Assert;
35
36/**
37 * A {@link LoaderManager} that records which loaders have been completed.
38 * <p>
39 * You should wrap the existing LoaderManager with an instance of this class, which will then
40 * delegate to the original object.
41 * <p>
42 * Typically, one would override {@link Activity#getLoaderManager()} to return the
43 * TestLoaderManager and ensuring it wraps the {@link LoaderManager} for this object, e.g.:
44 * <pre>
45 *   private TestLoaderManager mTestLoaderManager;
46 *
47 *   public LoaderManager getLoaderManager() {
48 *     LoaderManager loaderManager = super.getLoaderManager();
49 *     if (mTestLoaderManager != null) {
50 *       mTestLoaderManager.setDelegate(loaderManager);
51 *       return mTestLoaderManager;
52 *     } else {
53 *       return loaderManager;
54 *     }
55 *   }
56 *
57 *   void setTestLoaderManager(TestLoaderManager testLoaderManager) {
58 *     mTestLoaderManager = testLoaderManager;
59 *   }
60 * </pre>
61 * In the tests, one would set the TestLoaderManager upon creating the activity, and then wait for
62 * the loader to complete.
63 * <pre>
64 *   public void testLoadedCorrect() {
65 *     TestLoaderManager testLoaderManager = new TestLoaderManager();
66 *     getActivity().setTestLoaderManager(testLoaderManager);
67 *     runOnUiThread(new Runnable() { public void run() { getActivity().startLoading(); } });
68 *     testLoaderManager.waitForLoader(R.id.test_loader_id);
69 *   }
70 * </pre>
71 * If the loader completes before the call to {@link #waitForLoaders(int...)}, the TestLoaderManager
72 * will have stored the fact that the loader has completed and correctly terminate immediately.
73 * <p>
74 * It one needs to wait for the same loader multiple times, call {@link #reset()} between the them
75 * as in:
76 * <pre>
77 *   public void testLoadedCorrect() {
78 *     TestLoaderManager testLoaderManager = new TestLoaderManager();
79 *     getActivity().setTestLoaderManager(testLoaderManager);
80 *     runOnUiThread(new Runnable() { public void run() { getActivity().startLoading(); } });
81 *     testLoaderManager.waitForLoader(R.id.test_loader_id);
82 *     testLoaderManager.reset();
83 *     // Load and wait again.
84 *     runOnUiThread(new Runnable() { public void run() { getActivity().startLoading(); } });
85 *     testLoaderManager.waitForLoader(R.id.test_loader_id);
86 *   }
87 * </pre>
88 */
89public class TestLoaderManager extends LoaderManager {
90    private static final String TAG = "TestLoaderManager";
91
92    private final HashSet<Integer> mFinishedLoaders;
93
94    private LoaderManager mDelegate;
95
96    public TestLoaderManager() {
97        mFinishedLoaders = new HashSet<Integer>();
98    }
99
100    /**
101     * Sets the object to which we delegate the actual work.
102     * <p>
103     * It can not be set to null. Once set, it cannot be changed (but it allows setting it to the
104     * same value again).
105     */
106    public void setDelegate(LoaderManager delegate) {
107        if (delegate == null || (mDelegate != null && mDelegate != delegate)) {
108            throw new IllegalArgumentException("TestLoaderManager cannot be shared");
109        }
110
111        mDelegate = delegate;
112    }
113
114    public LoaderManager getDelegate() {
115        return mDelegate;
116    }
117
118    public void reset() {
119        mFinishedLoaders.clear();
120    }
121
122    /**
123     * Waits for the specified loaders to complete loading.
124     * <p>
125     * If one of the loaders has already completed since the last call to {@link #reset()}, it will
126     * not wait for it to complete again.
127     */
128    @VisibleForTesting
129    /*package*/ synchronized void waitForLoaders(int... loaderIds) {
130        List<Loader<?>> loaders = new ArrayList<Loader<?>>(loaderIds.length);
131        for (int loaderId : loaderIds) {
132            if (mFinishedLoaders.contains(loaderId)) {
133                // This loader has already completed since the last reset, do not wait for it.
134                continue;
135            }
136
137            final AsyncTaskLoader<?> loader =
138                    (AsyncTaskLoader<?>) mDelegate.getLoader(loaderId);
139            if (loader == null) {
140                Assert.fail("Loader does not exist: " + loaderId);
141                return;
142            }
143
144            loaders.add(loader);
145        }
146
147        waitForLoaders(loaders.toArray(new Loader<?>[0]));
148    }
149
150    /**
151     * Waits for the specified loaders to complete loading.
152     */
153    public static void waitForLoaders(Loader<?>... loaders) {
154        // We want to wait for each loader using a separate thread, so that we can
155        // simulate race conditions.
156        Thread[] waitThreads = new Thread[loaders.length];
157        for (int i = 0; i < loaders.length; i++) {
158            final AsyncTaskLoader<?> loader = (AsyncTaskLoader<?>) loaders[i];
159            waitThreads[i] = new Thread("LoaderWaitingThread" + i) {
160                @Override
161                public void run() {
162                    try {
163                        loader.waitForLoader();
164                    } catch (Throwable e) {
165                        Log.e(TAG, "Exception while waiting for loader: " + loader.getId(), e);
166                        Assert.fail("Exception while waiting for loader: " + loader.getId());
167                    }
168                }
169            };
170            waitThreads[i].start();
171        }
172
173        // Now we wait for all these threads to finish
174        for (Thread thread : waitThreads) {
175            try {
176                thread.join();
177            } catch (InterruptedException e) {
178                // Ignore
179            }
180        }
181    }
182
183    @Override
184    public <D> Loader<D> initLoader(final int id, Bundle args, final LoaderCallbacks<D> callback) {
185        return mDelegate.initLoader(id, args, new LoaderManager.LoaderCallbacks<D>() {
186            @Override
187            public Loader<D> onCreateLoader(int id, Bundle args) {
188                return callback.onCreateLoader(id, args);
189            }
190
191            @Override
192            public void onLoadFinished(Loader<D> loader, D data) {
193                callback.onLoadFinished(loader, data);
194                synchronized (this) {
195                    mFinishedLoaders.add(id);
196                }
197            }
198
199            @Override
200            public void onLoaderReset(Loader<D> loader) {
201                callback.onLoaderReset(loader);
202            }
203        });
204    }
205
206    @Override
207    public <D> Loader<D> restartLoader(int id, Bundle args, LoaderCallbacks<D> callback) {
208        return mDelegate.restartLoader(id, args, callback);
209    }
210
211    @Override
212    public void destroyLoader(int id) {
213        mDelegate.destroyLoader(id);
214    }
215
216    @Override
217    public <D> Loader<D> getLoader(int id) {
218        return mDelegate.getLoader(id);
219    }
220
221    @Override
222    public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
223        mDelegate.dump(prefix, fd, writer, args);
224    }
225}
226