DownloadManagerBaseTest.java revision 6f35c0ecba6cb1ce5d7563a9962acf9557dbaced
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 android.app;
18
19import coretestutils.http.MockResponse;
20import coretestutils.http.MockWebServer;
21
22import android.app.DownloadManager.Query;
23import android.app.DownloadManager.Request;
24import android.app.DownloadManagerBaseTest.DataType;
25import android.content.BroadcastReceiver;
26import android.content.Context;
27import android.content.Intent;
28import android.content.IntentFilter;
29import android.database.Cursor;
30import android.net.ConnectivityManager;
31import android.net.NetworkInfo;
32import android.net.Uri;
33import android.net.wifi.WifiManager;
34import android.os.Environment;
35import android.os.ParcelFileDescriptor;
36import android.os.ParcelFileDescriptor.AutoCloseInputStream;
37import android.os.SystemClock;
38import android.provider.Settings;
39import android.test.InstrumentationTestCase;
40import android.util.Log;
41
42import java.io.DataInputStream;
43import java.io.DataOutputStream;
44import java.io.File;
45import java.io.FileInputStream;
46import java.io.FileOutputStream;
47import java.io.IOException;
48import java.net.URL;
49import java.util.ArrayList;
50import java.util.Collections;
51import java.util.HashSet;
52import java.util.Random;
53import java.util.Set;
54import java.util.concurrent.TimeoutException;
55
56/**
57 * Base class for Instrumented tests for the Download Manager.
58 */
59public class DownloadManagerBaseTest extends InstrumentationTestCase {
60    private static final String TAG = "DownloadManagerBaseTest";
61    protected DownloadManager mDownloadManager = null;
62    protected MockWebServer mServer = null;
63    protected String mFileType = "text/plain";
64    protected Context mContext = null;
65    protected MultipleDownloadsCompletedReceiver mReceiver = null;
66    protected static final int DEFAULT_FILE_SIZE = 10 * 1024;  // 10kb
67    protected static final int FILE_BLOCK_READ_SIZE = 1024 * 1024;
68
69    protected static final String LOG_TAG = "android.net.DownloadManagerBaseTest";
70    protected static final int HTTP_OK = 200;
71    protected static final int HTTP_REDIRECT = 307;
72    protected static final int HTTP_PARTIAL_CONTENT = 206;
73    protected static final int HTTP_NOT_FOUND = 404;
74    protected static final int HTTP_SERVICE_UNAVAILABLE = 503;
75    protected String DEFAULT_FILENAME = "somefile.txt";
76
77    protected static final int DEFAULT_MAX_WAIT_TIME = 2 * 60 * 1000;  // 2 minutes
78    protected static final int DEFAULT_WAIT_POLL_TIME = 5 * 1000;  // 5 seconds
79
80    protected static final int WAIT_FOR_DOWNLOAD_POLL_TIME = 1 * 1000;  // 1 second
81    protected static final int MAX_WAIT_FOR_DOWNLOAD_TIME = 5 * 60 * 1000; // 5 minutes
82    protected static final int MAX_WAIT_FOR_LARGE_DOWNLOAD_TIME = 15 * 60 * 1000; // 15 minutes
83
84    protected static final int DOWNLOAD_TO_SYSTEM_CACHE = 1;
85    protected static final int DOWNLOAD_TO_DOWNLOAD_CACHE_DIR = 2;
86
87    // Just a few popular file types used to return from a download
88    protected enum DownloadFileType {
89        PLAINTEXT,
90        APK,
91        GIF,
92        GARBAGE,
93        UNRECOGNIZED,
94        ZIP
95    }
96
97    protected enum DataType {
98        TEXT,
99        BINARY
100    }
101
102    public static class LoggingRng extends Random {
103
104        /**
105         * Constructor
106         *
107         * Creates RNG with self-generated seed value.
108         */
109        public LoggingRng() {
110            this(SystemClock.uptimeMillis());
111        }
112
113        /**
114         * Constructor
115         *
116         * Creats RNG with given initial seed value
117
118         * @param seed The initial seed value
119         */
120        public LoggingRng(long seed) {
121            super(seed);
122            Log.i(LOG_TAG, "Seeding RNG with value: " + seed);
123        }
124    }
125
126    public static class MultipleDownloadsCompletedReceiver extends BroadcastReceiver {
127        private volatile int mNumDownloadsCompleted = 0;
128        private Set<Long> downloadIds = Collections.synchronizedSet(new HashSet<Long>());
129
130        /**
131         * {@inheritDoc}
132         */
133        @Override
134        public void onReceive(Context context, Intent intent) {
135            if (intent.getAction().equalsIgnoreCase(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) {
136                synchronized(this) {
137                    long id = intent.getExtras().getLong(DownloadManager.EXTRA_DOWNLOAD_ID);
138                    Log.i(LOG_TAG, "Received Notification for download: " + id);
139                    if (!downloadIds.contains(id)) {
140                        ++mNumDownloadsCompleted;
141                        Log.i(LOG_TAG, "MultipleDownloadsCompletedReceiver got intent: " +
142                                intent.getAction() + " --> total count: " + mNumDownloadsCompleted);
143                        downloadIds.add(id);
144
145                        DownloadManager dm = (DownloadManager)context.getSystemService(
146                                Context.DOWNLOAD_SERVICE);
147
148                        Cursor cursor = dm.query(new Query().setFilterById(id));
149                        try {
150                            if (cursor.moveToFirst()) {
151                                int status = cursor.getInt(cursor.getColumnIndex(
152                                        DownloadManager.COLUMN_STATUS));
153                                Log.i(LOG_TAG, "Download status is: " + status);
154                            } else {
155                                fail("No status found for completed download!");
156                            }
157                        } finally {
158                            cursor.close();
159                        }
160                    } else {
161                        Log.i(LOG_TAG, "Notification for id: " + id + " has already been made.");
162                    }
163                }
164            }
165        }
166
167        /**
168         * Gets the number of times the {@link #onReceive} callback has been called for the
169         * {@link DownloadManager.ACTION_DOWNLOAD_COMPLETED} action, indicating the number of
170         * downloads completed thus far.
171         *
172         * @return the number of downloads completed so far.
173         */
174        public int numDownloadsCompleted() {
175            return mNumDownloadsCompleted;
176        }
177
178        /**
179         * Gets the list of download IDs.
180         * @return A Set<Long> with the ids of the completed downloads.
181         */
182        public Set<Long> getDownloadIds() {
183            synchronized(this) {
184                Set<Long> returnIds = new HashSet<Long>(downloadIds);
185                return returnIds;
186            }
187        }
188
189    }
190
191    public static class WiFiChangedReceiver extends BroadcastReceiver {
192        private Context mContext = null;
193
194        /**
195         * Constructor
196         *
197         * Sets the current state of WiFi.
198         *
199         * @param context The current app {@link Context}.
200         */
201        public WiFiChangedReceiver(Context context) {
202            mContext = context;
203        }
204
205        /**
206         * {@inheritDoc}
207         */
208        @Override
209        public void onReceive(Context context, Intent intent) {
210            if (intent.getAction().equalsIgnoreCase(ConnectivityManager.CONNECTIVITY_ACTION)) {
211                Log.i(LOG_TAG, "ConnectivityManager state change: " + intent.getAction());
212                synchronized (this) {
213                    this.notify();
214                }
215            }
216        }
217
218        /**
219         * Gets the current state of WiFi.
220         *
221         * @return Returns true if WiFi is on, false otherwise.
222         */
223        public boolean getWiFiIsOn() {
224            ConnectivityManager connManager = (ConnectivityManager)mContext.getSystemService(
225                    Context.CONNECTIVITY_SERVICE);
226            NetworkInfo info = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
227            Log.i(LOG_TAG, "WiFi Connection state is currently: " + info.isConnected());
228            return info.isConnected();
229        }
230    }
231
232    /**
233     * {@inheritDoc}
234     */
235    @Override
236    public void setUp() throws Exception {
237        mContext = getInstrumentation().getContext();
238        mDownloadManager = (DownloadManager)mContext.getSystemService(Context.DOWNLOAD_SERVICE);
239        mServer = new MockWebServer();
240        mReceiver = registerNewMultipleDownloadsReceiver();
241        // Note: callers overriding this should call mServer.play() with the desired port #
242    }
243
244    /**
245     * Helper to enqueue a response from the MockWebServer with no body.
246     *
247     * @param status The HTTP status code to return for this response
248     * @return Returns the mock web server response that was queued (which can be modified)
249     */
250    protected MockResponse enqueueResponse(int status) {
251        return doEnqueueResponse(status);
252
253    }
254
255    /**
256     * Helper to enqueue a response from the MockWebServer.
257     *
258     * @param status The HTTP status code to return for this response
259     * @param body The body to return in this response
260     * @return Returns the mock web server response that was queued (which can be modified)
261     */
262    protected MockResponse enqueueResponse(int status, byte[] body) {
263        return doEnqueueResponse(status).setBody(body);
264
265    }
266
267    /**
268     * Helper to enqueue a response from the MockWebServer.
269     *
270     * @param status The HTTP status code to return for this response
271     * @param bodyFile The body to return in this response
272     * @return Returns the mock web server response that was queued (which can be modified)
273     */
274    protected MockResponse enqueueResponse(int status, File bodyFile) {
275        return doEnqueueResponse(status).setBody(bodyFile);
276    }
277
278    /**
279     * Helper for enqueue'ing a response from the MockWebServer.
280     *
281     * @param status The HTTP status code to return for this response
282     * @return Returns the mock web server response that was queued (which can be modified)
283     */
284    protected MockResponse doEnqueueResponse(int status) {
285        MockResponse response = new MockResponse().setResponseCode(status);
286        response.addHeader("Content-type", mFileType);
287        mServer.enqueue(response);
288        return response;
289    }
290
291    /**
292     * Helper to generate a random blob of bytes.
293     *
294     * @param size The size of the data to generate
295     * @param type The type of data to generate: currently, one of {@link DataType.TEXT} or
296     *         {@link DataType.BINARY}.
297     * @return The random data that is generated.
298     */
299    protected byte[] generateData(int size, DataType type) {
300        return generateData(size, type, null);
301    }
302
303    /**
304     * Helper to generate a random blob of bytes using a given RNG.
305     *
306     * @param size The size of the data to generate
307     * @param type The type of data to generate: currently, one of {@link DataType.TEXT} or
308     *         {@link DataType.BINARY}.
309     * @param rng (optional) The RNG to use; pass null to use
310     * @return The random data that is generated.
311     */
312    protected byte[] generateData(int size, DataType type, Random rng) {
313        int min = Byte.MIN_VALUE;
314        int max = Byte.MAX_VALUE;
315
316        // Only use chars in the HTTP ASCII printable character range for Text
317        if (type == DataType.TEXT) {
318            min = 32;
319            max = 126;
320        }
321        byte[] result = new byte[size];
322        Log.i(LOG_TAG, "Generating data of size: " + size);
323
324        if (rng == null) {
325            rng = new LoggingRng();
326        }
327
328        for (int i = 0; i < size; ++i) {
329            result[i] = (byte) (min + rng.nextInt(max - min + 1));
330        }
331        return result;
332    }
333
334    /**
335     * Helper to verify the size of a file.
336     *
337     * @param pfd The input file to compare the size of
338     * @param size The expected size of the file
339     */
340    protected void verifyFileSize(ParcelFileDescriptor pfd, long size) {
341        assertEquals(pfd.getStatSize(), size);
342    }
343
344    /**
345     * Helper to verify the contents of a downloaded file versus a byte[].
346     *
347     * @param actual The file of whose contents to verify
348     * @param expected The data we expect to find in the aforementioned file
349     * @throws IOException if there was a problem reading from the file
350     */
351    protected void verifyFileContents(ParcelFileDescriptor actual, byte[] expected)
352            throws IOException {
353        AutoCloseInputStream input = new ParcelFileDescriptor.AutoCloseInputStream(actual);
354        long fileSize = actual.getStatSize();
355
356        assertTrue(fileSize <= Integer.MAX_VALUE);
357        assertEquals(expected.length, fileSize);
358
359        byte[] actualData = new byte[expected.length];
360        assertEquals(input.read(actualData), fileSize);
361        compareByteArrays(actualData, expected);
362    }
363
364    /**
365     * Helper to compare 2 byte arrays.
366     *
367     * @param actual The array whose data we want to verify
368     * @param expected The array of data we expect to see
369     */
370    protected void compareByteArrays(byte[] actual, byte[] expected) {
371        assertEquals(actual.length, expected.length);
372        int length = actual.length;
373        for (int i = 0; i < length; ++i) {
374            // assert has a bit of overhead, so only do the assert when the values are not the same
375            if (actual[i] != expected[i]) {
376                fail("Byte arrays are not equal.");
377            }
378        }
379    }
380
381    /**
382     * Verifies the contents of a downloaded file versus the contents of a File.
383     *
384     * @param pfd The file whose data we want to verify
385     * @param file The file containing the data we expect to see in the aforementioned file
386     * @throws IOException If there was a problem reading either of the two files
387     */
388    protected void verifyFileContents(ParcelFileDescriptor pfd, File file) throws IOException {
389        byte[] actual = new byte[FILE_BLOCK_READ_SIZE];
390        byte[] expected = new byte[FILE_BLOCK_READ_SIZE];
391
392        AutoCloseInputStream input = new ParcelFileDescriptor.AutoCloseInputStream(pfd);
393
394        assertEquals(file.length(), pfd.getStatSize());
395
396        DataInputStream inFile = new DataInputStream(new FileInputStream(file));
397        int actualRead = 0;
398        int expectedRead = 0;
399
400        while (((actualRead = input.read(actual)) != -1) &&
401                ((expectedRead = inFile.read(expected)) != -1)) {
402            assertEquals(actualRead, expectedRead);
403            compareByteArrays(actual, expected);
404        }
405    }
406
407    /**
408     * Sets the MIME type of file that will be served from the mock server
409     *
410     * @param type The MIME type to return from the server
411     */
412    protected void setServerMimeType(DownloadFileType type) {
413        mFileType = getMimeMapping(type);
414    }
415
416    /**
417     * Gets the MIME content string for a given type
418     *
419     * @param type The MIME type to return
420     * @return the String representation of that MIME content type
421     */
422    protected String getMimeMapping(DownloadFileType type) {
423        switch (type) {
424            case APK:
425                return "application/vnd.android.package-archive";
426            case GIF:
427                return "image/gif";
428            case ZIP:
429                return "application/x-zip-compressed";
430            case GARBAGE:
431                return "zip\\pidy/doo/da";
432            case UNRECOGNIZED:
433                return "application/new.undefined.type.of.app";
434        }
435        return "text/plain";
436    }
437
438    /**
439     * Gets the Uri that should be used to access the mock server
440     *
441     * @param filename The name of the file to try to retrieve from the mock server
442     * @return the Uri to use for access the file on the mock server
443     */
444    protected Uri getServerUri(String filename) throws Exception {
445        URL url = mServer.getUrl("/" + filename);
446        return Uri.parse(url.toString());
447    }
448
449   /**
450    * Gets the Uri that should be used to access the mock server
451    *
452    * @param filename The name of the file to try to retrieve from the mock server
453    * @return the Uri to use for access the file on the mock server
454    */
455    protected void logDBColumnData(Cursor cursor, String column) {
456        int index = cursor.getColumnIndex(column);
457        Log.i(LOG_TAG, "columnName: " + column);
458        Log.i(LOG_TAG, "columnValue: " + cursor.getString(index));
459    }
460
461    /**
462     * Helper to create and register a new MultipleDownloadCompletedReciever
463     *
464     * This is used to track many simultaneous downloads by keeping count of all the downloads
465     * that have completed.
466     *
467     * @return A new receiver that records and can be queried on how many downloads have completed.
468     */
469    protected MultipleDownloadsCompletedReceiver registerNewMultipleDownloadsReceiver() {
470        MultipleDownloadsCompletedReceiver receiver = new MultipleDownloadsCompletedReceiver();
471        mContext.registerReceiver(receiver, new IntentFilter(
472                DownloadManager.ACTION_DOWNLOAD_COMPLETE));
473        return receiver;
474    }
475
476    /**
477     * Helper to verify a standard single-file download from the mock server, and clean up after
478     * verification
479     *
480     * Note that this also calls the Download manager's remove, which cleans up the file from cache.
481     *
482     * @param requestId The id of the download to remove
483     * @param fileData The data to verify the file contains
484     */
485    protected void verifyAndCleanupSingleFileDownload(long requestId, byte[] fileData)
486            throws Exception {
487        int fileSize = fileData.length;
488        ParcelFileDescriptor pfd = mDownloadManager.openDownloadedFile(requestId);
489        Cursor cursor = mDownloadManager.query(new Query().setFilterById(requestId));
490
491        try {
492            assertEquals(1, cursor.getCount());
493            assertTrue(cursor.moveToFirst());
494
495            mServer.checkForExceptions();
496
497            verifyFileSize(pfd, fileSize);
498            verifyFileContents(pfd, fileData);
499        } finally {
500            pfd.close();
501            cursor.close();
502            mDownloadManager.remove(requestId);
503        }
504    }
505
506    /**
507     * Enables or disables WiFi.
508     *
509     * Note: Needs the following permissions:
510     *  android.permission.ACCESS_WIFI_STATE
511     *  android.permission.CHANGE_WIFI_STATE
512     * @param enable true if it should be enabled, false if it should be disabled
513     */
514    protected void setWiFiStateOn(boolean enable) throws Exception {
515        Log.i(LOG_TAG, "Setting WiFi State to: " + enable);
516        WifiManager manager = (WifiManager)mContext.getSystemService(Context.WIFI_SERVICE);
517
518        manager.setWifiEnabled(enable);
519
520        String timeoutMessage = "Timed out waiting for Wifi to be "
521            + (enable ? "enabled!" : "disabled!");
522
523        WiFiChangedReceiver receiver = new WiFiChangedReceiver(mContext);
524        mContext.registerReceiver(receiver, new IntentFilter(
525                ConnectivityManager.CONNECTIVITY_ACTION));
526
527        synchronized (receiver) {
528            long timeoutTime = SystemClock.elapsedRealtime() + DEFAULT_MAX_WAIT_TIME;
529            boolean timedOut = false;
530
531            while (receiver.getWiFiIsOn() != enable && !timedOut) {
532                try {
533                    receiver.wait(DEFAULT_WAIT_POLL_TIME);
534
535                    if (SystemClock.elapsedRealtime() > timeoutTime) {
536                        timedOut = true;
537                    }
538                }
539                catch (InterruptedException e) {
540                    // ignore InterruptedExceptions
541                }
542            }
543            if (timedOut) {
544                fail(timeoutMessage);
545            }
546        }
547        assertEquals(enable, receiver.getWiFiIsOn());
548    }
549
550    /**
551     * Helper to enables or disables airplane mode. If successful, it also broadcasts an intent
552     * indicating that the mode has changed.
553     *
554     * Note: Needs the following permission:
555     *  android.permission.WRITE_SETTINGS
556     * @param enable true if airplane mode should be ON, false if it should be OFF
557     */
558    protected void setAirplaneModeOn(boolean enable) throws Exception {
559        int state = enable ? 1 : 0;
560
561        // Change the system setting
562        Settings.System.putInt(mContext.getContentResolver(), Settings.System.AIRPLANE_MODE_ON,
563                state);
564
565        String timeoutMessage = "Timed out waiting for airplane mode to be " +
566                (enable ? "enabled!" : "disabled!");
567
568        // wait for airplane mode to change state
569        int currentWaitTime = 0;
570        while (Settings.System.getInt(mContext.getContentResolver(),
571                Settings.System.AIRPLANE_MODE_ON, -1) != state) {
572            timeoutWait(currentWaitTime, DEFAULT_WAIT_POLL_TIME, DEFAULT_MAX_WAIT_TIME,
573                    timeoutMessage);
574        }
575
576        // Post the intent
577        Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
578        intent.putExtra("state", true);
579        mContext.sendBroadcast(intent);
580    }
581
582    /**
583     * Helper to create a large file of random data on the SD card.
584     *
585     * @param filename (optional) The name of the file to create on the SD card; pass in null to
586     *          use a default temp filename.
587     * @param type The type of file to create
588     * @param subdirectory If not null, the subdirectory under the SD card where the file should go
589     * @return The File that was created
590     * @throws IOException if there was an error while creating the file.
591     */
592    protected File createFileOnSD(String filename, long fileSize, DataType type,
593            String subdirectory) throws IOException {
594
595        // Build up the file path and name
596        String sdPath = Environment.getExternalStorageDirectory().getPath();
597        StringBuilder fullPath = new StringBuilder(sdPath);
598        if (subdirectory != null) {
599            fullPath.append(File.separatorChar).append(subdirectory);
600        }
601
602        File file = null;
603        if (filename == null) {
604            file = File.createTempFile("DMTEST_", null, new File(fullPath.toString()));
605        }
606        else {
607            fullPath.append(File.separatorChar).append(filename);
608            file = new File(fullPath.toString());
609            file.createNewFile();
610        }
611
612        // Fill the file with random data
613        DataOutputStream output = new DataOutputStream(new FileOutputStream(file));
614        final int CHUNK_SIZE = 1000000;  // copy random data in 1000000-char chunks
615        long remaining = fileSize;
616        int nextChunkSize = CHUNK_SIZE;
617        byte[] randomData = null;
618        Random rng = new LoggingRng();
619        byte[] chunkSizeData = generateData(nextChunkSize, type, rng);
620
621        try {
622            while (remaining > 0) {
623                if (remaining < CHUNK_SIZE) {
624                    nextChunkSize = (int)remaining;
625                    remaining = 0;
626                    randomData = generateData(nextChunkSize, type, rng);
627                }
628                else {
629                    remaining -= CHUNK_SIZE;
630                    randomData = chunkSizeData;
631                }
632                output.write(randomData);
633                Log.i(TAG, "while creating " + fileSize + " file, " +
634                        "remaining bytes to be written: " + remaining);
635            }
636        } catch (IOException e) {
637            Log.e(LOG_TAG, "Error writing to file " + file.getAbsolutePath());
638            file.delete();
639            throw e;
640        } finally {
641            output.close();
642        }
643        return file;
644    }
645
646    /**
647     * Helper to wait for a particular download to finish, or else a timeout to occur
648     *
649     * Does not wait for a receiver notification of the download.
650     *
651     * @param id The download id to query on (wait for)
652     */
653    protected void waitForDownloadOrTimeout_skipNotification(long id) throws TimeoutException,
654            InterruptedException {
655        waitForDownloadOrTimeout(id, WAIT_FOR_DOWNLOAD_POLL_TIME, MAX_WAIT_FOR_DOWNLOAD_TIME);
656    }
657
658    /**
659     * Helper to wait for a particular download to finish, or else a timeout to occur
660     *
661     * Also guarantees a notification has been posted for the download.
662     *
663     * @param id The download id to query on (wait for)
664     */
665    protected void waitForDownloadOrTimeout(long id) throws TimeoutException,
666            InterruptedException {
667        waitForDownloadOrTimeout_skipNotification(id);
668        waitForReceiverNotifications(1);
669    }
670
671    /**
672     * Helper to wait for a particular download to finish, or else a timeout to occur
673     *
674     * Also guarantees a notification has been posted for the download.
675     *
676     * @param id The download id to query on (wait for)
677     * @param poll The amount of time to wait
678     * @param timeoutMillis The max time (in ms) to wait for the download(s) to complete
679     */
680    protected void waitForDownloadOrTimeout(long id, long poll, long timeoutMillis)
681            throws TimeoutException, InterruptedException {
682        doWaitForDownloadsOrTimeout(new Query().setFilterById(id), poll, timeoutMillis);
683        waitForReceiverNotifications(1);
684    }
685
686    /**
687     * Helper to wait for all downloads to finish, or else a specified timeout to occur
688     *
689     * Makes no guaranee that notifications have been posted for all downloads.
690     *
691     * @param poll The amount of time to wait
692     * @param timeoutMillis The max time (in ms) to wait for the download(s) to complete
693     */
694    protected void waitForDownloadsOrTimeout(long poll, long timeoutMillis) throws TimeoutException,
695            InterruptedException {
696        doWaitForDownloadsOrTimeout(new Query(), poll, timeoutMillis);
697    }
698
699    /**
700     * Helper to wait for all downloads to finish, or else a timeout to occur, but does not throw
701     *
702     * Also guarantees a notification has been posted for the download.
703     *
704     * @param id The id of the download to query against
705     * @param poll The amount of time to wait
706     * @param timeoutMillis The max time (in ms) to wait for the download(s) to complete
707     * @return true if download completed successfully (didn't timeout), false otherwise
708     */
709    protected boolean waitForDownloadOrTimeoutNoThrow(long id, long poll, long timeoutMillis) {
710        try {
711            doWaitForDownloadsOrTimeout(new Query().setFilterById(id), poll, timeoutMillis);
712            waitForReceiverNotifications(1);
713        } catch (TimeoutException e) {
714            return false;
715        }
716        return true;
717    }
718
719    /**
720     * Helper function to synchronously wait, or timeout if the maximum threshold has been exceeded.
721     *
722     * @param currentTotalWaitTime The total time waited so far
723     * @param poll The amount of time to wait
724     * @param maxTimeoutMillis The total wait time threshold; if we've waited more than this long,
725     *          we timeout and fail
726     * @param timedOutMessage The message to display in the failure message if we timeout
727     * @return The new total amount of time we've waited so far
728     * @throws TimeoutException if timed out waiting for SD card to mount
729     */
730    protected int timeoutWait(int currentTotalWaitTime, long poll, long maxTimeoutMillis,
731            String timedOutMessage) throws TimeoutException {
732        long now = SystemClock.elapsedRealtime();
733        long end = now + poll;
734
735        // if we get InterruptedException's, ignore them and just keep sleeping
736        while (now < end) {
737            try {
738                Thread.sleep(end - now);
739            } catch (InterruptedException e) {
740                // ignore interrupted exceptions
741            }
742            now = SystemClock.elapsedRealtime();
743        }
744
745        currentTotalWaitTime += poll;
746        if (currentTotalWaitTime > maxTimeoutMillis) {
747            throw new TimeoutException(timedOutMessage);
748        }
749        return currentTotalWaitTime;
750    }
751
752    /**
753     * Helper to wait for all downloads to finish, or else a timeout to occur
754     *
755     * @param query The query to pass to the download manager
756     * @param poll The poll time to wait between checks
757     * @param timeoutMillis The max amount of time (in ms) to wait for the download(s) to complete
758     */
759    protected void doWaitForDownloadsOrTimeout(Query query, long poll, long timeoutMillis)
760            throws TimeoutException {
761        int currentWaitTime = 0;
762        while (true) {
763            query.setFilterByStatus(DownloadManager.STATUS_PENDING | DownloadManager.STATUS_PAUSED
764                    | DownloadManager.STATUS_RUNNING);
765            Cursor cursor = mDownloadManager.query(query);
766
767            try {
768                if (cursor.getCount() == 0) {
769                    Log.i(LOG_TAG, "All downloads should be done...");
770                    break;
771                }
772                currentWaitTime = timeoutWait(currentWaitTime, poll, timeoutMillis,
773                        "Timed out waiting for all downloads to finish");
774            } finally {
775                cursor.close();
776            }
777        }
778    }
779
780    /**
781     * Synchronously waits for external store to be mounted (eg: SD Card).
782     *
783     * @throws InterruptedException if interrupted
784     * @throws Exception if timed out waiting for SD card to mount
785     */
786    protected void waitForExternalStoreMount() throws Exception {
787        String extStorageState = Environment.getExternalStorageState();
788        int currentWaitTime = 0;
789        while (!extStorageState.equals(Environment.MEDIA_MOUNTED)) {
790            Log.i(LOG_TAG, "Waiting for SD card...");
791            currentWaitTime = timeoutWait(currentWaitTime, DEFAULT_WAIT_POLL_TIME,
792                    DEFAULT_MAX_WAIT_TIME, "Timed out waiting for SD Card to be ready!");
793            extStorageState = Environment.getExternalStorageState();
794        }
795    }
796
797    /**
798     * Synchronously waits for a download to start.
799     *
800     * @param dlRequest the download request id used by Download Manager to track the download.
801     * @throws Exception if timed out while waiting for SD card to mount
802     */
803    protected void waitForDownloadToStart(long dlRequest) throws Exception {
804        Cursor cursor = getCursor(dlRequest);
805        try {
806            int columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS);
807            int value = cursor.getInt(columnIndex);
808            int currentWaitTime = 0;
809
810            while (value != DownloadManager.STATUS_RUNNING &&
811                    (value != DownloadManager.STATUS_FAILED) &&
812                    (value != DownloadManager.STATUS_SUCCESSFUL)) {
813                Log.i(LOG_TAG, "Waiting for download to start...");
814                currentWaitTime = timeoutWait(currentWaitTime, WAIT_FOR_DOWNLOAD_POLL_TIME,
815                        MAX_WAIT_FOR_DOWNLOAD_TIME, "Timed out waiting for download to start!");
816                cursor.requery();
817                assertTrue(cursor.moveToFirst());
818                columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS);
819                value = cursor.getInt(columnIndex);
820            }
821            assertFalse("Download failed immediately after start",
822                    value == DownloadManager.STATUS_FAILED);
823        } finally {
824            cursor.close();
825        }
826    }
827
828    /**
829     * Convenience function to wait for just 1 notification of a download.
830     *
831     * @throws Exception if timed out while waiting
832     */
833    protected void waitForReceiverNotification() throws Exception {
834        waitForReceiverNotifications(1);
835    }
836
837    /**
838     * Synchronously waits for our receiver to receive notification for a given number of
839     * downloads.
840     *
841     * @param targetNumber The number of notifications for unique downloads to wait for; pass in
842     *         -1 to not wait for notification.
843     * @throws Exception if timed out while waiting
844     */
845    protected void waitForReceiverNotifications(int targetNumber) throws TimeoutException {
846        int count = mReceiver.numDownloadsCompleted();
847        int currentWaitTime = 0;
848
849        while (count < targetNumber) {
850            Log.i(LOG_TAG, "Waiting for notification of downloads...");
851            currentWaitTime = timeoutWait(currentWaitTime, WAIT_FOR_DOWNLOAD_POLL_TIME,
852                    MAX_WAIT_FOR_DOWNLOAD_TIME, "Timed out waiting for download notifications!"
853                    + " Received " + count + "notifications.");
854            count = mReceiver.numDownloadsCompleted();
855        }
856    }
857
858    /**
859     * Synchronously waits for a file to increase in size (such as to monitor that a download is
860     * progressing).
861     *
862     * @param file The file whose size to track.
863     * @throws Exception if timed out while waiting for the file to grow in size.
864     */
865    protected void waitForFileToGrow(File file) throws Exception {
866        int currentWaitTime = 0;
867
868        // File may not even exist yet, so wait until it does (or we timeout)
869        while (!file.exists()) {
870            Log.i(LOG_TAG, "Waiting for file to exist...");
871            currentWaitTime = timeoutWait(currentWaitTime, WAIT_FOR_DOWNLOAD_POLL_TIME,
872                    MAX_WAIT_FOR_DOWNLOAD_TIME, "Timed out waiting for file to be created.");
873        }
874
875        // Get original file size...
876        long originalSize = file.length();
877
878        while (file.length() <= originalSize) {
879            Log.i(LOG_TAG, "Waiting for file to be written to...");
880            currentWaitTime = timeoutWait(currentWaitTime, WAIT_FOR_DOWNLOAD_POLL_TIME,
881                    MAX_WAIT_FOR_DOWNLOAD_TIME, "Timed out waiting for file to be written to.");
882        }
883    }
884
885    /**
886     * Helper to remove all downloads that are registered with the DL Manager.
887     *
888     * Note: This gives us a clean slate b/c it includes downloads that are pending, running,
889     * paused, or have completed.
890     */
891    protected void removeAllCurrentDownloads() {
892        Log.i(LOG_TAG, "Removing all current registered downloads...");
893        ArrayList<Long> ids = new ArrayList<Long>();
894        Cursor cursor = mDownloadManager.query(new Query());
895        try {
896            if (cursor.moveToFirst()) {
897                do {
898                    int index = cursor.getColumnIndex(DownloadManager.COLUMN_ID);
899                    long downloadId = cursor.getLong(index);
900                    ids.add(downloadId);
901                } while (cursor.moveToNext());
902            }
903        } finally {
904            cursor.close();
905        }
906        // delete all ids
907        for (long id : ids) {
908            mDownloadManager.remove(id);
909        }
910        // make sure the database is empty
911        cursor = mDownloadManager.query(new Query());
912        try {
913            assertEquals(0, cursor.getCount());
914        } finally {
915            cursor.close();
916        }
917    }
918
919    /**
920     * Helper to perform a standard enqueue of data to the mock server.
921     * download is performed to the downloads cache dir (NOT systemcache dir)
922     *
923     * @param body The body to return in the response from the server
924     */
925    protected long doStandardEnqueue(byte[] body) throws Exception {
926        return enqueueDownloadRequest(body, DOWNLOAD_TO_DOWNLOAD_CACHE_DIR);
927    }
928
929    protected long enqueueDownloadRequest(byte[] body, int location) throws Exception {
930        // Prepare the mock server with a standard response
931        enqueueResponse(HTTP_OK, body);
932        return doEnqueue(location);
933    }
934
935    /**
936     * Helper to perform a standard enqueue of data to the mock server.
937     *
938     * @param body The body to return in the response from the server, contained in the file
939     */
940    protected long doStandardEnqueue(File body) throws Exception {
941        return enqueueDownloadRequest(body, DOWNLOAD_TO_DOWNLOAD_CACHE_DIR);
942    }
943
944    protected long enqueueDownloadRequest(File body, int location) throws Exception {
945        // Prepare the mock server with a standard response
946        enqueueResponse(HTTP_OK, body);
947        return doEnqueue(location);
948    }
949
950    /**
951     * Helper to do the additional steps (setting title and Uri of default filename) when
952     * doing a standard enqueue request to the server.
953     */
954    protected long doCommonStandardEnqueue() throws Exception {
955        return doEnqueue(DOWNLOAD_TO_DOWNLOAD_CACHE_DIR);
956    }
957
958    private long doEnqueue(int location) throws Exception {
959        Uri uri = getServerUri(DEFAULT_FILENAME);
960        Request request = new Request(uri).setTitle(DEFAULT_FILENAME);
961        if (location == DOWNLOAD_TO_SYSTEM_CACHE) {
962            request.setDestinationToSystemCache();
963        }
964
965        return mDownloadManager.enqueue(request);
966    }
967
968    /**
969     * Helper to verify an int value in a Cursor
970     *
971     * @param cursor The cursor containing the query results
972     * @param columnName The name of the column to query
973     * @param expected The expected int value
974     */
975    protected void verifyInt(Cursor cursor, String columnName, int expected) {
976        int index = cursor.getColumnIndex(columnName);
977        int actual = cursor.getInt(index);
978        assertEquals(expected, actual);
979    }
980
981    /**
982     * Helper to verify a String value in a Cursor
983     *
984     * @param cursor The cursor containing the query results
985     * @param columnName The name of the column to query
986     * @param expected The expected String value
987     */
988    protected void verifyString(Cursor cursor, String columnName, String expected) {
989        int index = cursor.getColumnIndex(columnName);
990        String actual = cursor.getString(index);
991        Log.i(LOG_TAG, ": " + actual);
992        assertEquals(expected, actual);
993    }
994
995    /**
996     * Performs a query based on ID and returns a Cursor for the query.
997     *
998     * @param id The id of the download in DL Manager; pass -1 to query all downloads
999     * @return A cursor for the query results
1000     */
1001    protected Cursor getCursor(long id) throws Exception {
1002        Query query = new Query();
1003        if (id != -1) {
1004            query.setFilterById(id);
1005        }
1006
1007        Cursor cursor = mDownloadManager.query(query);
1008        int currentWaitTime = 0;
1009
1010        try {
1011            while (!cursor.moveToFirst()) {
1012                Thread.sleep(DEFAULT_WAIT_POLL_TIME);
1013                currentWaitTime += DEFAULT_WAIT_POLL_TIME;
1014                if (currentWaitTime > DEFAULT_MAX_WAIT_TIME) {
1015                    fail("timed out waiting for a non-null query result");
1016                }
1017                cursor.requery();
1018            }
1019        } catch (Exception e) {
1020            cursor.close();
1021            throw e;
1022        }
1023        return cursor;
1024    }
1025
1026    /**
1027     * Helper that does the actual basic download verification.
1028     */
1029    protected long doBasicDownload(byte[] blobData, int location) throws Exception {
1030        long dlRequest = enqueueDownloadRequest(blobData, location);
1031
1032        // wait for the download to complete
1033        waitForDownloadOrTimeout(dlRequest);
1034
1035        assertEquals(1, mReceiver.numDownloadsCompleted());
1036        return dlRequest;
1037    }
1038}