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