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