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