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