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