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