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