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