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