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