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