diff options
author | Neal Nguyen <tommyn@google.com> | 2010-08-09 14:08:26 -0700 |
---|---|---|
committer | Neal Nguyen <tommyn@google.com> | 2010-09-08 17:02:53 -0700 |
commit | 5f53bca55b2c9e217dee12bff8ce55e168829783 (patch) | |
tree | 14f9203ef32283086d3fe6f1ac06d30804c1e2c9 | |
parent | 3fa7d8af6560de07ef673f73308f7e51de64e4ec (diff) | |
download | frameworks_base-5f53bca55b2c9e217dee12bff8ce55e168829783.zip frameworks_base-5f53bca55b2c9e217dee12bff8ce55e168829783.tar.gz frameworks_base-5f53bca55b2c9e217dee12bff8ce55e168829783.tar.bz2 |
Adding Download Manager Integration, stress, and hosts-based tests.
Change-Id: I97008f6cfd95ea9950db0b4e093da02528849b63
15 files changed, 3046 insertions, 9 deletions
diff --git a/core/tests/coretests/Android.mk b/core/tests/coretests/Android.mk index 693ef18..b496805 100644 --- a/core/tests/coretests/Android.mk +++ b/core/tests/coretests/Android.mk @@ -12,7 +12,7 @@ LOCAL_SRC_FILES := \ $(call all-java-files-under, EnabledTestApp/src) LOCAL_DX_FLAGS := --core-library -LOCAL_STATIC_JAVA_LIBRARIES := core-tests-supportlib android-common +LOCAL_STATIC_JAVA_LIBRARIES := core-tests-supportlib android-common frameworks-core-util-lib LOCAL_JAVA_LIBRARIES := android.test.runner LOCAL_PACKAGE_NAME := FrameworksCoreTests diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml index ce73ae1..f09421b 100644 --- a/core/tests/coretests/AndroidManifest.xml +++ b/core/tests/coretests/AndroidManifest.xml @@ -36,12 +36,16 @@ android:description="@string/permdesc_testDenied" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <uses-permission android:name="android.permission.BROADCAST_STICKY" /> + <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" /> <uses-permission android:name="android.permission.CLEAR_APP_CACHE" /> <uses-permission android:name="android.permission.CLEAR_APP_USER_DATA" /> <uses-permission android:name="android.permission.DELETE_CACHE_FILES" /> + <uses-permission android:name="android.permission.DOWNLOAD_CACHE_NON_PURGEABLE" /> <uses-permission android:name="android.permission.GET_PACKAGE_SIZE" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.READ_CONTACTS" /> diff --git a/core/tests/coretests/src/android/net/DownloadManagerBaseTest.java b/core/tests/coretests/src/android/net/DownloadManagerBaseTest.java new file mode 100644 index 0000000..ee0f5f1 --- /dev/null +++ b/core/tests/coretests/src/android/net/DownloadManagerBaseTest.java @@ -0,0 +1,888 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.database.Cursor; +import android.net.ConnectivityManager; +import android.net.DownloadManager; +import android.net.NetworkInfo; +import android.net.DownloadManager.Query; +import android.net.DownloadManager.Request; +import android.net.wifi.WifiManager; +import android.os.Environment; +import android.os.ParcelFileDescriptor; +import android.os.SystemClock; +import android.os.ParcelFileDescriptor.AutoCloseInputStream; +import android.provider.Settings; +import android.test.InstrumentationTestCase; +import android.util.Log; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URL; +import java.util.concurrent.TimeoutException; +import java.util.Iterator; +import java.util.List; +import java.util.Random; +import java.util.Vector; + +import junit.framework.AssertionFailedError; + +import coretestutils.http.MockResponse; +import coretestutils.http.MockWebServer; + +/** + * Base class for Instrumented tests for the Download Manager. + */ +public class DownloadManagerBaseTest extends InstrumentationTestCase { + + protected DownloadManager mDownloadManager = null; + protected MockWebServer mServer = null; + protected String mFileType = "text/plain"; + protected Context mContext = null; + protected static final int DEFAULT_FILE_SIZE = 130 * 1024; // 130kb + protected static final int FILE_BLOCK_READ_SIZE = 1024 * 1024; + + protected static final String LOG_TAG = "android.net.DownloadManagerBaseTest"; + protected static final int HTTP_OK = 200; + protected static final int HTTP_PARTIAL_CONTENT = 206; + protected static final int HTTP_NOT_FOUND = 404; + protected static final int HTTP_SERVICE_UNAVAILABLE = 503; + protected String DEFAULT_FILENAME = "somefile.txt"; + + protected static final int DEFAULT_MAX_WAIT_TIME = 2 * 60 * 1000; // 2 minutes + protected static final int DEFAULT_WAIT_POLL_TIME = 5 * 1000; // 5 seconds + + protected static final int WAIT_FOR_DOWNLOAD_POLL_TIME = 1 * 1000; // 1 second + protected static final int MAX_WAIT_FOR_DOWNLOAD_TIME = 5 * 60 * 1000; // 5 minutes + + // Just a few popular file types used to return from a download + protected enum DownloadFileType { + PLAINTEXT, + APK, + GIF, + GARBAGE, + UNRECOGNIZED, + ZIP + } + + protected enum DataType { + TEXT, + BINARY + } + + public static class LoggingRng extends Random { + + /** + * Constructor + * + * Creates RNG with self-generated seed value. + */ + public LoggingRng() { + this(SystemClock.uptimeMillis()); + } + + /** + * Constructor + * + * Creats RNG with given initial seed value + + * @param seed The initial seed value + */ + public LoggingRng(long seed) { + super(seed); + Log.i(LOG_TAG, "Seeding RNG with value: " + seed); + } + } + + public static class MultipleDownloadsCompletedReceiver extends BroadcastReceiver { + private volatile int mNumDownloadsCompleted = 0; + + /** + * {@inheritDoc} + */ + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equalsIgnoreCase(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) { + ++mNumDownloadsCompleted; + Log.i(LOG_TAG, "MultipleDownloadsCompletedReceiver got intent: " + + intent.getAction() + " --> total count: " + mNumDownloadsCompleted); + } + } + + /** + * Gets the number of times the {@link #onReceive} callback has been called for the + * {@link DownloadManager.ACTION_DOWNLOAD_COMPLETED} action, indicating the number of + * downloads completed thus far. + * + * @return the number of downloads completed so far. + */ + public int numDownloadsCompleted() { + return mNumDownloadsCompleted; + } + } + + public static class WiFiChangedReceiver extends BroadcastReceiver { + private Context mContext = null; + + /** + * Constructor + * + * Sets the current state of WiFi. + * + * @param context The current app {@link Context}. + */ + public WiFiChangedReceiver(Context context) { + mContext = context; + } + + /** + * {@inheritDoc} + */ + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equalsIgnoreCase(ConnectivityManager.CONNECTIVITY_ACTION)) { + Log.i(LOG_TAG, "ConnectivityManager state change: " + intent.getAction()); + synchronized (this) { + this.notify(); + } + } + } + + /** + * Gets the current state of WiFi. + * + * @return Returns true if WiFi is on, false otherwise. + */ + public boolean getWiFiIsOn() { + ConnectivityManager connManager = (ConnectivityManager)mContext.getSystemService( + Context.CONNECTIVITY_SERVICE); + NetworkInfo info = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); + return info.isConnected(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void setUp() throws Exception { + mContext = getInstrumentation().getContext(); + mDownloadManager = (DownloadManager)mContext.getSystemService(Context.DOWNLOAD_SERVICE); + mServer = new MockWebServer(); + // Note: callers overriding this should call mServer.play() with the desired port # + } + + /** + * Helper to enqueue a response from the MockWebServer. + * + * @param status The HTTP status code to return for this response + * @param body The body to return in this response + * @return Returns the mock web server response that was queued (which can be modified) + */ + protected MockResponse enqueueResponse(int status, byte[] body) { + return doEnqueueResponse(status).setBody(body); + + } + + /** + * Helper to enqueue a response from the MockWebServer. + * + * @param status The HTTP status code to return for this response + * @param bodyFile The body to return in this response + * @return Returns the mock web server response that was queued (which can be modified) + */ + protected MockResponse enqueueResponse(int status, File bodyFile) { + return doEnqueueResponse(status).setBody(bodyFile); + } + + /** + * Helper for enqueue'ing a response from the MockWebServer. + * + * @param status The HTTP status code to return for this response + * @return Returns the mock web server response that was queued (which can be modified) + */ + protected MockResponse doEnqueueResponse(int status) { + MockResponse response = new MockResponse().setResponseCode(status); + response.addHeader("Content-type", mFileType); + mServer.enqueue(response); + return response; + } + + /** + * Helper to generate a random blob of bytes. + * + * @param size The size of the data to generate + * @param type The type of data to generate: currently, one of {@link DataType.TEXT} or + * {@link DataType.BINARY}. + * @return The random data that is generated. + */ + protected byte[] generateData(int size, DataType type) { + return generateData(size, type, null); + } + + /** + * Helper to generate a random blob of bytes using a given RNG. + * + * @param size The size of the data to generate + * @param type The type of data to generate: currently, one of {@link DataType.TEXT} or + * {@link DataType.BINARY}. + * @param rng (optional) The RNG to use; pass null to use + * @return The random data that is generated. + */ + protected byte[] generateData(int size, DataType type, Random rng) { + int min = Byte.MIN_VALUE; + int max = Byte.MAX_VALUE; + + // Only use chars in the HTTP ASCII printable character range for Text + if (type == DataType.TEXT) { + min = 32; + max = 126; + } + byte[] result = new byte[size]; + Log.i(LOG_TAG, "Generating data of size: " + size); + + if (rng == null) { + rng = new LoggingRng(); + } + + for (int i = 0; i < size; ++i) { + result[i] = (byte) (min + rng.nextInt(max - min + 1)); + } + return result; + } + + /** + * Helper to verify the size of a file. + * + * @param pfd The input file to compare the size of + * @param size The expected size of the file + */ + protected void verifyFileSize(ParcelFileDescriptor pfd, long size) { + assertEquals(pfd.getStatSize(), size); + } + + /** + * Helper to verify the contents of a downloaded file versus a byte[]. + * + * @param actual The file of whose contents to verify + * @param expected The data we expect to find in the aforementioned file + * @throws IOException if there was a problem reading from the file + */ + protected void verifyFileContents(ParcelFileDescriptor actual, byte[] expected) + throws IOException { + AutoCloseInputStream input = new ParcelFileDescriptor.AutoCloseInputStream(actual); + long fileSize = actual.getStatSize(); + + assertTrue(fileSize <= Integer.MAX_VALUE); + assertEquals(expected.length, fileSize); + + byte[] actualData = new byte[expected.length]; + assertEquals(input.read(actualData), fileSize); + compareByteArrays(actualData, expected); + } + + /** + * Helper to compare 2 byte arrays. + * + * @param actual The array whose data we want to verify + * @param expected The array of data we expect to see + */ + protected void compareByteArrays(byte[] actual, byte[] expected) { + assertEquals(actual.length, expected.length); + int length = actual.length; + for (int i = 0; i < length; ++i) { + // assert has a bit of overhead, so only do the assert when the values are not the same + if (actual[i] != expected[i]) { + fail("Byte arrays are not equal."); + } + } + } + + /** + * Verifies the contents of a downloaded file versus the contents of a File. + * + * @param pfd The file whose data we want to verify + * @param file The file containing the data we expect to see in the aforementioned file + * @throws IOException If there was a problem reading either of the two files + */ + protected void verifyFileContents(ParcelFileDescriptor pfd, File file) throws IOException { + byte[] actual = new byte[FILE_BLOCK_READ_SIZE]; + byte[] expected = new byte[FILE_BLOCK_READ_SIZE]; + + AutoCloseInputStream input = new ParcelFileDescriptor.AutoCloseInputStream(pfd); + + assertEquals(file.length(), pfd.getStatSize()); + + DataInputStream inFile = new DataInputStream(new FileInputStream(file)); + int actualRead = 0; + int expectedRead = 0; + + while (((actualRead = input.read(actual)) != -1) && + ((expectedRead = inFile.read(expected)) != -1)) { + assertEquals(actualRead, expectedRead); + compareByteArrays(actual, expected); + } + } + + /** + * Sets the MIME type of file that will be served from the mock server + * + * @param type The MIME type to return from the server + */ + protected void setServerMimeType(DownloadFileType type) { + mFileType = getMimeMapping(type); + } + + /** + * Gets the MIME content string for a given type + * + * @param type The MIME type to return + * @return the String representation of that MIME content type + */ + protected String getMimeMapping(DownloadFileType type) { + switch (type) { + case APK: + return "application/vnd.android.package-archive"; + case GIF: + return "image/gif"; + case ZIP: + return "application/x-zip-compressed"; + case GARBAGE: + return "zip\\pidy/doo/da"; + case UNRECOGNIZED: + return "application/new.undefined.type.of.app"; + } + return "text/plain"; + } + + /** + * Gets the Uri that should be used to access the mock server + * + * @param filename The name of the file to try to retrieve from the mock server + * @return the Uri to use for access the file on the mock server + */ + protected Uri getServerUri(String filename) throws Exception { + URL url = mServer.getUrl("/" + filename); + return Uri.parse(url.toString()); + } + + /** + * Gets the Uri that should be used to access the mock server + * + * @param filename The name of the file to try to retrieve from the mock server + * @return the Uri to use for access the file on the mock server + */ + protected void logDBColumnData(Cursor cursor, String column) { + int index = cursor.getColumnIndex(column); + Log.i(LOG_TAG, "columnName: " + column); + Log.i(LOG_TAG, "columnValue: " + cursor.getString(index)); + } + + /** + * Helper to create and register a new MultipleDownloadCompletedReciever + * + * This is used to track many simultaneous downloads by keeping count of all the downloads + * that have completed. + * + * @return A new receiver that records and can be queried on how many downloads have completed. + */ + protected MultipleDownloadsCompletedReceiver registerNewMultipleDownloadsReceiver() { + MultipleDownloadsCompletedReceiver receiver = new MultipleDownloadsCompletedReceiver(); + mContext.registerReceiver(receiver, new IntentFilter( + DownloadManager.ACTION_DOWNLOAD_COMPLETE)); + return receiver; + } + + /** + * Helper to verify a standard single-file download from the mock server, and clean up after + * verification + * + * Note that this also calls the Download manager's remove, which cleans up the file from cache. + * + * @param requestId The id of the download to remove + * @param fileData The data to verify the file contains + */ + protected void verifyAndCleanupSingleFileDownload(long requestId, byte[] fileData) + throws Exception { + int fileSize = fileData.length; + ParcelFileDescriptor pfd = mDownloadManager.openDownloadedFile(requestId); + Cursor cursor = mDownloadManager.query(new Query().setFilterById(requestId)); + + try { + assertEquals(1, cursor.getCount()); + assertTrue(cursor.moveToFirst()); + + mServer.checkForExceptions(); + + verifyFileSize(pfd, fileSize); + verifyFileContents(pfd, fileData); + } finally { + pfd.close(); + cursor.close(); + mDownloadManager.remove(requestId); + } + } + + /** + * Enables or disables WiFi. + * + * Note: Needs the following permissions: + * android.permission.ACCESS_WIFI_STATE + * android.permission.CHANGE_WIFI_STATE + * @param enable true if it should be enabled, false if it should be disabled + */ + protected void setWiFiStateOn(boolean enable) throws Exception { + WifiManager manager = (WifiManager)mContext.getSystemService(Context.WIFI_SERVICE); + + manager.setWifiEnabled(enable); + + String timeoutMessage = "Timed out waiting for Wifi to be " + + (enable ? "enabled!" : "disabled!"); + + WiFiChangedReceiver receiver = new WiFiChangedReceiver(mContext); + mContext.registerReceiver(receiver, new IntentFilter( + ConnectivityManager.CONNECTIVITY_ACTION)); + + synchronized (receiver) { + long timeoutTime = SystemClock.elapsedRealtime() + DEFAULT_MAX_WAIT_TIME; + boolean timedOut = false; + + while (receiver.getWiFiIsOn() != enable && !timedOut) { + try { + receiver.wait(DEFAULT_MAX_WAIT_TIME); + + if (SystemClock.elapsedRealtime() > timeoutTime) { + timedOut = true; + } + } + catch (InterruptedException e) { + // ignore InterruptedExceptions + } + } + if (timedOut) { + fail(timeoutMessage); + } + } + assertEquals(enable, receiver.getWiFiIsOn()); + } + + /** + * Helper to enables or disables airplane mode. If successful, it also broadcasts an intent + * indicating that the mode has changed. + * + * Note: Needs the following permission: + * android.permission.WRITE_SETTINGS + * @param enable true if airplane mode should be ON, false if it should be OFF + */ + protected void setAirplaneModeOn(boolean enable) throws Exception { + int state = enable ? 1 : 0; + + // Change the system setting + Settings.System.putInt(mContext.getContentResolver(), Settings.System.AIRPLANE_MODE_ON, + state); + + String timeoutMessage = "Timed out waiting for airplane mode to be " + + (enable ? "enabled!" : "disabled!"); + + // wait for airplane mode to change state + int currentWaitTime = 0; + while (Settings.System.getInt(mContext.getContentResolver(), + Settings.System.AIRPLANE_MODE_ON, -1) != state) { + timeoutWait(currentWaitTime, DEFAULT_WAIT_POLL_TIME, DEFAULT_MAX_WAIT_TIME, + timeoutMessage); + } + + // Post the intent + Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); + intent.putExtra("state", true); + mContext.sendBroadcast(intent); + } + + /** + * Helper to create a large file of random data on the SD card. + * + * @param filename (optional) The name of the file to create on the SD card; pass in null to + * use a default temp filename. + * @param type The type of file to create + * @param subdirectory If not null, the subdirectory under the SD card where the file should go + * @return The File that was created + * @throws IOException if there was an error while creating the file. + */ + protected File createFileOnSD(String filename, long fileSize, DataType type, + String subdirectory) throws IOException { + + // Build up the file path and name + String sdPath = Environment.getExternalStorageDirectory().getPath(); + StringBuilder fullPath = new StringBuilder(sdPath); + if (subdirectory != null) { + fullPath.append(File.separatorChar).append(subdirectory); + } + + File file = null; + if (filename == null) { + file = File.createTempFile("DMTEST_", null, new File(fullPath.toString())); + } + else { + fullPath.append(File.separatorChar).append(filename); + file = new File(fullPath.toString()); + file.createNewFile(); + } + + // Fill the file with random data + DataOutputStream output = new DataOutputStream(new FileOutputStream(file)); + final int CHUNK_SIZE = 1000000; // copy random data in 1000000-char chunks + long remaining = fileSize; + int nextChunkSize = CHUNK_SIZE; + byte[] randomData = null; + Random rng = new LoggingRng(); + + try { + while (remaining > 0) { + if (remaining < CHUNK_SIZE) { + nextChunkSize = (int)remaining; + remaining = 0; + } + else { + remaining -= CHUNK_SIZE; + } + + randomData = generateData(nextChunkSize, type, rng); + output.write(randomData); + } + } catch (IOException e) { + Log.e(LOG_TAG, "Error writing to file " + file.getAbsolutePath()); + file.delete(); + throw e; + } finally { + output.close(); + } + return file; + } + + /** + * Helper to wait for a particular download to finish, or else a timeout to occur + * + * @param id The download id to query on (wait for) + */ + protected void waitForDownloadOrTimeout(long id) throws TimeoutException, + InterruptedException { + waitForDownloadOrTimeout(id, WAIT_FOR_DOWNLOAD_POLL_TIME, MAX_WAIT_FOR_DOWNLOAD_TIME); + } + + /** + * Helper to wait for a particular download to finish, or else a timeout to occur + * + * @param id The download id to query on (wait for) + * @param poll The amount of time to wait + * @param timeoutMillis The max time (in ms) to wait for the download(s) to complete + */ + protected void waitForDownloadOrTimeout(long id, long poll, long timeoutMillis) + throws TimeoutException, InterruptedException { + doWaitForDownloadsOrTimeout(new Query().setFilterById(id), poll, timeoutMillis); + } + + /** + * Helper to wait for all downloads to finish, or else a specified timeout to occur + * + * @param poll The amount of time to wait + * @param timeoutMillis The max time (in ms) to wait for the download(s) to complete + */ + protected void waitForDownloadsOrTimeout(long poll, long timeoutMillis) throws TimeoutException, + InterruptedException { + doWaitForDownloadsOrTimeout(new Query(), poll, timeoutMillis); + } + + /** + * Helper to wait for all downloads to finish, or else a timeout to occur, but does not throw + * + * @param id The id of the download to query against + * @param poll The amount of time to wait + * @param timeoutMillis The max time (in ms) to wait for the download(s) to complete + * @return true if download completed successfully (didn't timeout), false otherwise + */ + protected boolean waitForDownloadOrTimeoutNoThrow(long id, long poll, long timeoutMillis) { + try { + doWaitForDownloadsOrTimeout(new Query().setFilterById(id), poll, timeoutMillis); + } catch (TimeoutException e) { + return false; + } + return true; + } + + /** + * Helper function to synchronously wait, or timeout if the maximum threshold has been exceeded. + * + * @param currentTotalWaitTime The total time waited so far + * @param poll The amount of time to wait + * @param maxTimeoutMillis The total wait time threshold; if we've waited more than this long, + * we timeout and fail + * @param timedOutMessage The message to display in the failure message if we timeout + * @return The new total amount of time we've waited so far + * @throws TimeoutException if timed out waiting for SD card to mount + */ + protected int timeoutWait(int currentTotalWaitTime, long poll, long maxTimeoutMillis, + String timedOutMessage) throws TimeoutException { + long now = SystemClock.elapsedRealtime(); + long end = now + poll; + + // if we get InterruptedException's, ignore them and just keep sleeping + while (now < end) { + try { + Thread.sleep(end - now); + } catch (InterruptedException e) { + // ignore interrupted exceptions + } + now = SystemClock.elapsedRealtime(); + } + + currentTotalWaitTime += poll; + if (currentTotalWaitTime > maxTimeoutMillis) { + throw new TimeoutException(timedOutMessage); + } + return currentTotalWaitTime; + } + + /** + * Helper to wait for all downloads to finish, or else a timeout to occur + * + * @param query The query to pass to the download manager + * @param poll The poll time to wait between checks + * @param timeoutMillis The max amount of time (in ms) to wait for the download(s) to complete + */ + protected void doWaitForDownloadsOrTimeout(Query query, long poll, long timeoutMillis) + throws TimeoutException { + int currentWaitTime = 0; + while (true) { + query.setFilterByStatus(DownloadManager.STATUS_PENDING | DownloadManager.STATUS_PAUSED + | DownloadManager.STATUS_RUNNING); + Cursor cursor = mDownloadManager.query(query); + + try { + // If we've finished the downloads then we're done + if (cursor.getCount() == 0) { + break; + } + currentWaitTime = timeoutWait(currentWaitTime, poll, timeoutMillis, + "Timed out waiting for all downloads to finish"); + } finally { + cursor.close(); + } + } + } + + /** + * Synchronously waits for external store to be mounted (eg: SD Card). + * + * @throws InterruptedException if interrupted + * @throws Exception if timed out waiting for SD card to mount + */ + protected void waitForExternalStoreMount() throws Exception { + String extStorageState = Environment.getExternalStorageState(); + int currentWaitTime = 0; + while (!extStorageState.equals(Environment.MEDIA_MOUNTED)) { + Log.i(LOG_TAG, "Waiting for SD card..."); + currentWaitTime = timeoutWait(currentWaitTime, DEFAULT_WAIT_POLL_TIME, + DEFAULT_MAX_WAIT_TIME, "Timed out waiting for SD Card to be ready!"); + extStorageState = Environment.getExternalStorageState(); + } + } + + /** + * Synchronously waits for a download to start. + * + * @param dlRequest the download request id used by Download Manager to track the download. + * @throws Exception if timed out while waiting for SD card to mount + */ + protected void waitForDownloadToStart(long dlRequest) throws Exception { + Cursor cursor = getCursor(dlRequest); + try { + int columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS); + int value = cursor.getInt(columnIndex); + int currentWaitTime = 0; + + while (value != DownloadManager.STATUS_RUNNING && + (value != DownloadManager.STATUS_FAILED) && + (value != DownloadManager.STATUS_SUCCESSFUL)) { + Log.i(LOG_TAG, "Waiting for download to start..."); + currentWaitTime = timeoutWait(currentWaitTime, WAIT_FOR_DOWNLOAD_POLL_TIME, + MAX_WAIT_FOR_DOWNLOAD_TIME, "Timed out waiting for download to start!"); + cursor.requery(); + assertTrue(cursor.moveToFirst()); + columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS); + value = cursor.getInt(columnIndex); + } + assertFalse("Download failed immediately after start", + value == DownloadManager.STATUS_FAILED); + } finally { + cursor.close(); + } + } + + /** + * Synchronously waits for a file to increase in size (such as to monitor that a download is + * progressing). + * + * @param file The file whose size to track. + * @throws Exception if timed out while waiting for the file to grow in size. + */ + protected void waitForFileToGrow(File file) throws Exception { + int currentWaitTime = 0; + + // File may not even exist yet, so wait until it does (or we timeout) + while (!file.exists()) { + Log.i(LOG_TAG, "Waiting for file to exist..."); + currentWaitTime = timeoutWait(currentWaitTime, WAIT_FOR_DOWNLOAD_POLL_TIME, + MAX_WAIT_FOR_DOWNLOAD_TIME, "Timed out waiting for file to be created."); + } + + // Get original file size... + long originalSize = file.length(); + + while (file.length() <= originalSize) { + Log.i(LOG_TAG, "Waiting for file to be written to..."); + currentWaitTime = timeoutWait(currentWaitTime, WAIT_FOR_DOWNLOAD_POLL_TIME, + MAX_WAIT_FOR_DOWNLOAD_TIME, "Timed out waiting for file to be written to."); + } + } + + /** + * Helper to remove all downloads that are registered with the DL Manager. + * + * Note: This gives us a clean slate b/c it includes downloads that are pending, running, + * paused, or have completed. + */ + protected void removeAllCurrentDownloads() { + Log.i(LOG_TAG, "Removing all current registered downloads..."); + Cursor cursor = mDownloadManager.query(new Query()); + try { + if (cursor.moveToFirst()) { + do { + int index = cursor.getColumnIndex(DownloadManager.COLUMN_ID); + long downloadId = cursor.getLong(index); + + mDownloadManager.remove(downloadId); + } while (cursor.moveToNext()); + } + } finally { + cursor.close(); + } + } + + /** + * Helper to perform a standard enqueue of data to the mock server. + * + * @param body The body to return in the response from the server + */ + protected long doStandardEnqueue(byte[] body) throws Exception { + // Prepare the mock server with a standard response + enqueueResponse(HTTP_OK, body); + return doCommonStandardEnqueue(); + } + + /** + * Helper to perform a standard enqueue of data to the mock server. + * + * @param body The body to return in the response from the server, contained in the file + */ + protected long doStandardEnqueue(File body) throws Exception { + // Prepare the mock server with a standard response + enqueueResponse(HTTP_OK, body); + return doCommonStandardEnqueue(); + } + + /** + * Helper to do the additional steps (setting title and Uri of default filename) when + * doing a standard enqueue request to the server. + */ + protected long doCommonStandardEnqueue() throws Exception { + Uri uri = getServerUri(DEFAULT_FILENAME); + Request request = new Request(uri); + request.setTitle(DEFAULT_FILENAME); + + long dlRequest = mDownloadManager.enqueue(request); + Log.i(LOG_TAG, "request ID: " + dlRequest); + return dlRequest; + } + + /** + * Helper to verify an int value in a Cursor + * + * @param cursor The cursor containing the query results + * @param columnName The name of the column to query + * @param expected The expected int value + */ + protected void verifyInt(Cursor cursor, String columnName, int expected) { + int index = cursor.getColumnIndex(columnName); + int actual = cursor.getInt(index); + assertEquals(expected, actual); + } + + /** + * Helper to verify a String value in a Cursor + * + * @param cursor The cursor containing the query results + * @param columnName The name of the column to query + * @param expected The expected String value + */ + protected void verifyString(Cursor cursor, String columnName, String expected) { + int index = cursor.getColumnIndex(columnName); + String actual = cursor.getString(index); + Log.i(LOG_TAG, ": " + actual); + assertEquals(expected, actual); + } + + /** + * Performs a query based on ID and returns a Cursor for the query. + * + * @param id The id of the download in DL Manager; pass -1 to query all downloads + * @return A cursor for the query results + */ + protected Cursor getCursor(long id) throws Exception { + Query query = new Query(); + if (id != -1) { + query.setFilterById(id); + } + + Cursor cursor = mDownloadManager.query(query); + int currentWaitTime = 0; + + try { + while (!cursor.moveToFirst()) { + Thread.sleep(DEFAULT_WAIT_POLL_TIME); + currentWaitTime += DEFAULT_WAIT_POLL_TIME; + if (currentWaitTime > DEFAULT_MAX_WAIT_TIME) { + fail("timed out waiting for a non-null query result"); + } + cursor.requery(); + } + } catch (Exception e) { + cursor.close(); + throw e; + } + return cursor; + } + +}
\ No newline at end of file diff --git a/core/tests/coretests/src/android/net/DownloadManagerIntegrationTest.java b/core/tests/coretests/src/android/net/DownloadManagerIntegrationTest.java new file mode 100644 index 0000000..be3cbf7 --- /dev/null +++ b/core/tests/coretests/src/android/net/DownloadManagerIntegrationTest.java @@ -0,0 +1,381 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.database.Cursor; +import android.net.DownloadManager.Query; +import android.net.DownloadManager.Request; +import android.net.DownloadManagerBaseTest.DataType; +import android.net.DownloadManagerBaseTest.MultipleDownloadsCompletedReceiver; +import android.net.wifi.WifiManager; +import android.os.Environment; +import android.os.ParcelFileDescriptor; +import android.os.SystemClock; +import android.test.InstrumentationTestCase; +import android.test.suitebuilder.annotation.LargeTest; +import android.test.suitebuilder.annotation.MediumTest; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.Log; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.net.URL; +import java.util.Random; + +import junit.framework.AssertionFailedError; + +import coretestutils.http.MockResponse; +import coretestutils.http.MockWebServer; + +/** + * Integration tests of the DownloadManager API. + */ +public class DownloadManagerIntegrationTest extends DownloadManagerBaseTest { + + private static String LOG_TAG = "android.net.DownloadManagerIntegrationTest"; + private static String PROHIBITED_DIRECTORY = "/system"; + protected MultipleDownloadsCompletedReceiver mReceiver = null; + + /** + * {@inheritDoc} + */ + @Override + public void setUp() throws Exception { + super.setUp(); + setWiFiStateOn(true); + mServer.play(); + removeAllCurrentDownloads(); + mReceiver = registerNewMultipleDownloadsReceiver(); + } + + /** + * {@inheritDoc} + */ + @Override + public void tearDown() throws Exception { + super.tearDown(); + setWiFiStateOn(true); + + if (mReceiver != null) { + mContext.unregisterReceiver(mReceiver); + mReceiver = null; + removeAllCurrentDownloads(); + } + } + + /** + * Helper that does the actual basic download verification. + */ + protected void doBasicDownload(byte[] blobData) throws Exception { + long dlRequest = doStandardEnqueue(blobData); + + // wait for the download to complete + waitForDownloadOrTimeout(dlRequest); + + verifyAndCleanupSingleFileDownload(dlRequest, blobData); + assertEquals(1, mReceiver.numDownloadsCompleted()); + } + + /** + * Test a basic download of a binary file 500k in size. + */ + @LargeTest + public void testBasicBinaryDownload() throws Exception { + int fileSize = 500 * 1024; // 500k + byte[] blobData = generateData(fileSize, DataType.BINARY); + + doBasicDownload(blobData); + } + + /** + * Tests the basic downloading of a text file 300000 bytes in size. + */ + @LargeTest + public void testBasicTextDownload() throws Exception { + int fileSize = 300000; + byte[] blobData = generateData(fileSize, DataType.TEXT); + + doBasicDownload(blobData); + } + + /** + * Tests when the server drops the connection after all headers (but before any data send). + */ + @LargeTest + public void testDropConnection_headers() throws Exception { + byte[] blobData = generateData(DEFAULT_FILE_SIZE, DataType.TEXT); + + MockResponse response = enqueueResponse(HTTP_OK, blobData); + response.setCloseConnectionAfterHeader("content-length"); + long dlRequest = doCommonStandardEnqueue(); + + // Download will never complete when header is dropped + boolean success = waitForDownloadOrTimeoutNoThrow(dlRequest, DEFAULT_WAIT_POLL_TIME, + DEFAULT_MAX_WAIT_TIME); + + assertFalse(success); + } + + /** + * Tests that we get an error code when the server drops the connection during a download. + */ + @LargeTest + public void testServerDropConnection_body() throws Exception { + byte[] blobData = generateData(25000, DataType.TEXT); // file size = 25000 bytes + + MockResponse response = enqueueResponse(HTTP_OK, blobData); + response.setCloseConnectionAfterXBytes(15382); + long dlRequest = doCommonStandardEnqueue(); + waitForDownloadOrTimeout(dlRequest); + + Cursor cursor = getCursor(dlRequest); + try { + verifyInt(cursor, DownloadManager.COLUMN_STATUS, DownloadManager.STATUS_FAILED); + verifyInt(cursor, DownloadManager.COLUMN_ERROR_CODE, + DownloadManager.ERROR_CANNOT_RESUME); + } finally { + cursor.close(); + } + // Even tho the server drops the connection, we should still get a completed notification + assertEquals(1, mReceiver.numDownloadsCompleted()); + } + + /** + * Attempts to download several files simultaneously + */ + @LargeTest + public void testMultipleDownloads() throws Exception { + // need to be sure all current downloads have stopped first + removeAllCurrentDownloads(); + int NUM_FILES = 50; + int MAX_FILE_SIZE = 500 * 1024; // 500 kb + + Random r = new LoggingRng(); + for (int i=0; i<NUM_FILES; ++i) { + int size = r.nextInt(MAX_FILE_SIZE); + byte[] blobData = generateData(size, DataType.TEXT); + + Uri uri = getServerUri(DEFAULT_FILENAME); + Request request = new Request(uri); + request.setTitle(String.format("%s--%d", DEFAULT_FILENAME, i)); + + // Prepare the mock server with a standard response + enqueueResponse(HTTP_OK, blobData); + + Log.i(LOG_TAG, "request: " + i); + mDownloadManager.enqueue(request); + } + + waitForDownloadsOrTimeout(WAIT_FOR_DOWNLOAD_POLL_TIME, MAX_WAIT_FOR_DOWNLOAD_TIME); + Cursor cursor = mDownloadManager.query(new Query()); + try { + assertEquals(NUM_FILES, cursor.getCount()); + + if (cursor.moveToFirst()) { + do { + int status = cursor.getInt(cursor.getColumnIndex( + DownloadManager.COLUMN_STATUS)); + String filename = cursor.getString(cursor.getColumnIndex( + DownloadManager.COLUMN_URI)); + String errorString = String.format( + "File %s failed to download successfully. Status code: %d", + filename, status); + assertEquals(errorString, DownloadManager.STATUS_SUCCESSFUL, status); + } while (cursor.moveToNext()); + } + + assertEquals(NUM_FILES, mReceiver.numDownloadsCompleted()); + } finally { + cursor.close(); + } + } + + /** + * Tests trying to download to SD card when the file with same name already exists. + */ + @LargeTest + public void testDownloadToExternal_fileExists() throws Exception { + File existentFile = createFileOnSD(null, 1, DataType.TEXT, null); + byte[] blobData = generateData(DEFAULT_FILE_SIZE, DataType.TEXT); + + // Prepare the mock server with a standard response + enqueueResponse(HTTP_OK, blobData); + + try { + Uri uri = getServerUri(DEFAULT_FILENAME); + Request request = new Request(uri); + + Uri localUri = Uri.fromFile(existentFile); + Log.i(LOG_TAG, "setting localUri to: " + localUri.getPath()); + request.setDestinationUri(localUri); + + long dlRequest = mDownloadManager.enqueue(request); + + // wait for the download to complete + waitForDownloadOrTimeout(dlRequest); + Cursor cursor = getCursor(dlRequest); + + try { + verifyInt(cursor, DownloadManager.COLUMN_STATUS, DownloadManager.STATUS_FAILED); + verifyInt(cursor, DownloadManager.COLUMN_ERROR_CODE, + DownloadManager.ERROR_FILE_ERROR); + } finally { + cursor.close(); + } + } finally { + existentFile.delete(); + } + } + + /** + * Tests trying to download a file to SD card. + */ + @LargeTest + public void testDownloadToExternal() throws Exception { + String localDownloadDirectory = Environment.getExternalStorageDirectory().getPath(); + File downloadedFile = new File(localDownloadDirectory, DEFAULT_FILENAME); + // make sure the file doesn't already exist in the directory + downloadedFile.delete(); + + try { + byte[] blobData = generateData(DEFAULT_FILE_SIZE, DataType.TEXT); + + // Prepare the mock server with a standard response + enqueueResponse(HTTP_OK, blobData); + + Uri uri = getServerUri(DEFAULT_FILENAME); + Request request = new Request(uri); + + Uri localUri = Uri.fromFile(downloadedFile); + Log.i(LOG_TAG, "setting localUri to: " + localUri.getPath()); + request.setDestinationUri(localUri); + + long dlRequest = mDownloadManager.enqueue(request); + + // wait for the download to complete + waitForDownloadOrTimeout(dlRequest); + + verifyAndCleanupSingleFileDownload(dlRequest, blobData); + + assertEquals(1, mReceiver.numDownloadsCompleted()); + } finally { + downloadedFile.delete(); + } + } + + /** + * Tests trying to download a file to the system partition. + */ + @LargeTest + public void testDownloadToProhibitedDirectory() throws Exception { + File downloadedFile = new File(PROHIBITED_DIRECTORY, DEFAULT_FILENAME); + try { + byte[] blobData = generateData(DEFAULT_FILE_SIZE, DataType.TEXT); + + // Prepare the mock server with a standard response + enqueueResponse(HTTP_OK, blobData); + + Uri uri = getServerUri(DEFAULT_FILENAME); + Request request = new Request(uri); + + Uri localUri = Uri.fromFile(downloadedFile); + Log.i(LOG_TAG, "setting localUri to: " + localUri.getPath()); + request.setDestinationUri(localUri); + + try { + mDownloadManager.enqueue(request); + fail("Failed to throw SecurityException when trying to write to /system."); + } catch (SecurityException s) { + assertFalse(downloadedFile.exists()); + } + } finally { + // Just in case file somehow got created, make sure to delete it + downloadedFile.delete(); + } + } + + /** + * Tests that a download set for Wifi does not progress while Wifi is disabled, but resumes + * once Wifi is re-enabled. + */ + @LargeTest + public void testDownloadNoWifi() throws Exception { + long timeout = 60 * 1000; // wait only 60 seconds before giving up + int fileSize = 140 * 1024; // 140k + byte[] blobData = generateData(fileSize, DataType.TEXT); + + setWiFiStateOn(false); + enqueueResponse(HTTP_OK, blobData); + + try { + Uri uri = getServerUri(DEFAULT_FILENAME); + Request request = new Request(uri); + request.setAllowedNetworkTypes(Request.NETWORK_WIFI); + + long dlRequest = mDownloadManager.enqueue(request); + + // wait for the download to complete + boolean success = waitForDownloadOrTimeoutNoThrow(dlRequest, + WAIT_FOR_DOWNLOAD_POLL_TIME, timeout); + assertFalse("Download proceeded without Wifi connection!", success); + + setWiFiStateOn(true); + waitForDownloadOrTimeout(dlRequest); + + assertEquals(1, mReceiver.numDownloadsCompleted()); + } finally { + setWiFiStateOn(true); + } + } + + /** + * Tests trying to download two large files (50M bytes, followed by 60M bytes) + */ + @LargeTest + public void testInsufficientSpaceSingleFiles() throws Exception { + long fileSize1 = 50000000L; + long fileSize2 = 60000000L; + File largeFile1 = createFileOnSD(null, fileSize1, DataType.TEXT, null); + File largeFile2 = createFileOnSD(null, fileSize2, DataType.TEXT, null); + + try { + long dlRequest = doStandardEnqueue(largeFile1); + waitForDownloadOrTimeout(dlRequest); + ParcelFileDescriptor pfd = mDownloadManager.openDownloadedFile(dlRequest); + verifyFileContents(pfd, largeFile1); + verifyFileSize(pfd, largeFile1.length()); + + dlRequest = doStandardEnqueue(largeFile2); + waitForDownloadOrTimeout(dlRequest); + Cursor cursor = getCursor(dlRequest); + try { + verifyInt(cursor, DownloadManager.COLUMN_ERROR_CODE, + DownloadManager.ERROR_INSUFFICIENT_SPACE); + } finally { + cursor.close(); + } + } finally { + largeFile1.delete(); + largeFile2.delete(); + } + } +} diff --git a/core/tests/coretests/src/android/net/DownloadManagerStressTest.java b/core/tests/coretests/src/android/net/DownloadManagerStressTest.java new file mode 100644 index 0000000..9fa8620 --- /dev/null +++ b/core/tests/coretests/src/android/net/DownloadManagerStressTest.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import java.io.File; +import java.util.Random; + +import android.database.Cursor; +import android.net.DownloadManager.Query; +import android.net.DownloadManager.Request; +import android.os.ParcelFileDescriptor; +import android.test.suitebuilder.annotation.LargeTest; +import android.util.Log; + + +public class DownloadManagerStressTest extends DownloadManagerBaseTest { + private static String LOG_TAG = "android.net.DownloadManagerStressTest"; + + /** + * {@inheritDoc} + */ + @Override + public void setUp() throws Exception { + super.setUp(); + mServer.play(0); + removeAllCurrentDownloads(); + } + + /** + * Attempts to downloading thousands of files simultaneously + */ + public void testDownloadThousands() throws Exception { + int NUM_FILES = 1500; + int MAX_FILE_SIZE = 3000; + long[] reqs = new long[NUM_FILES]; + + // need to be sure all current downloads have stopped first + MultipleDownloadsCompletedReceiver receiver = registerNewMultipleDownloadsReceiver(); + Cursor cursor = null; + try { + Random r = new LoggingRng(); + for (int i = 0; i < NUM_FILES; ++i) { + int size = r.nextInt(MAX_FILE_SIZE); + byte[] blobData = generateData(size, DataType.TEXT); + + Uri uri = getServerUri(DEFAULT_FILENAME); + Request request = new Request(uri); + request.setTitle(String.format("%s--%d", DEFAULT_FILENAME, i)); + + // Prepare the mock server with a standard response + enqueueResponse(HTTP_OK, blobData); + + Log.i(LOG_TAG, "issuing request: " + i); + long reqId = mDownloadManager.enqueue(request); + reqs[i] = reqId; + } + + // wait for the download to complete or timeout + waitForDownloadsOrTimeout(WAIT_FOR_DOWNLOAD_POLL_TIME, MAX_WAIT_FOR_DOWNLOAD_TIME); + cursor = mDownloadManager.query(new Query()); + assertEquals(NUM_FILES, cursor.getCount()); + Log.i(LOG_TAG, "Verified number of downloads in download manager is what we expect."); + while (cursor.moveToNext()) { + int status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)); + String filename = cursor.getString(cursor.getColumnIndex( + DownloadManager.COLUMN_URI)); + String errorString = String.format("File %s failed to download successfully. " + + "Status code: %d", filename, status); + assertEquals(errorString, DownloadManager.STATUS_SUCCESSFUL, status); + } + Log.i(LOG_TAG, "Verified each download was successful."); + assertEquals(NUM_FILES, receiver.numDownloadsCompleted()); + Log.i(LOG_TAG, "Verified number of completed downloads in our receiver."); + + // Verify that for each request, we can open the downloaded file + for (int i = 0; i < NUM_FILES; ++i) { + ParcelFileDescriptor pfd = mDownloadManager.openDownloadedFile(reqs[i]); + pfd.close(); + } + Log.i(LOG_TAG, "Verified we can open each file."); + } finally { + if (cursor != null) { + cursor.close(); + } + mContext.unregisterReceiver(receiver); + removeAllCurrentDownloads(); + } + } + + /** + * Tests trying to download a large file (50M bytes). + */ + public void testDownloadLargeFile() throws Exception { + long fileSize = 50000000L; // note: kept relatively small to not exceed /cache dir size + File largeFile = createFileOnSD(null, fileSize, DataType.TEXT, null); + MultipleDownloadsCompletedReceiver receiver = registerNewMultipleDownloadsReceiver(); + + try { + long dlRequest = doStandardEnqueue(largeFile); + + // wait for the download to complete + waitForDownloadOrTimeout(dlRequest); + + ParcelFileDescriptor pfd = mDownloadManager.openDownloadedFile(dlRequest); + verifyFileContents(pfd, largeFile); + verifyFileSize(pfd, largeFile.length()); + + assertEquals(1, receiver.numDownloadsCompleted()); + mContext.unregisterReceiver(receiver); + } catch (Exception e) { + throw e; + } finally { + largeFile.delete(); + } + } + + /** + * Tests trying to download a large file (~300M bytes) when there's not enough space in cache + */ + public void testInsufficientSpace() throws Exception { + long fileSize = 300000000L; + File largeFile = createFileOnSD(null, fileSize, DataType.TEXT, null); + + Cursor cursor = null; + try { + long dlRequest = doStandardEnqueue(largeFile); + + // wait for the download to complete + waitForDownloadOrTimeout(dlRequest); + + cursor = getCursor(dlRequest); + verifyInt(cursor, DownloadManager.COLUMN_STATUS, DownloadManager.STATUS_FAILED); + verifyInt(cursor, DownloadManager.COLUMN_ERROR_CODE, + DownloadManager.ERROR_INSUFFICIENT_SPACE); + } finally { + if (cursor != null) { + cursor.close(); + } + largeFile.delete(); + } + } +} diff --git a/core/tests/hosttests/src/android/content/pm/PackageManagerHostTestUtils.java b/core/tests/hosttests/src/android/content/pm/PackageManagerHostTestUtils.java index b225c37..1593bb5 100644 --- a/core/tests/hosttests/src/android/content/pm/PackageManagerHostTestUtils.java +++ b/core/tests/hosttests/src/android/content/pm/PackageManagerHostTestUtils.java @@ -37,7 +37,9 @@ import java.io.InputStreamReader; import java.io.StringReader; import java.lang.Runtime; import java.lang.Process; +import java.util.Hashtable; import java.util.Map; +import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -117,18 +119,38 @@ public class PackageManagerHostTestUtils extends Assert { /** * Helper method to run tests and return the listener that collected the results. + * + * For the optional params, pass null to use the default values. + * @param pkgName Android application package for tests - * @return the {@link CollectingTestRunListener} + * @param className (optional) The class containing the method to test + * @param methodName (optional) The method in the class of which to test + * @param runnerName (optional) The name of the TestRunner of the test on the device to be run + * @param params (optional) Any additional parameters to pass into the Test Runner * @throws TimeoutException in case of a timeout on the connection. * @throws AdbCommandRejectedException if adb rejects the command * @throws ShellCommandUnresponsiveException if the device did not output anything for * a period longer than the max time to output. * @throws IOException if connection to device was lost. + * @return the {@link CollectingTestRunListener} */ - private CollectingTestRunListener doRunTests(String pkgName) throws IOException, - TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException { - RemoteAndroidTestRunner testRunner = new RemoteAndroidTestRunner( - pkgName, mDevice); + private CollectingTestRunListener doRunTests(String pkgName, String className, + String methodName, String runnerName, Map<String, String> params) throws IOException, + TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException { + RemoteAndroidTestRunner testRunner = new RemoteAndroidTestRunner(pkgName, runnerName, + mDevice); + + if (className != null && methodName != null) { + testRunner.setMethodName(className, methodName); + } + + // Add in any additional args to pass into the test + if (params != null) { + for (Entry<String, String> argPair : params.entrySet()) { + testRunner.addInstrumentationArg(argPair.getKey(), argPair.getValue()); + } + } + CollectingTestRunListener listener = new CollectingTestRunListener(); testRunner.run(listener); return listener; @@ -138,16 +160,34 @@ public class PackageManagerHostTestUtils extends Assert { * Runs the specified packages tests, and returns whether all tests passed or not. * * @param pkgName Android application package for tests - * @return true if every test passed, false otherwise. + * @param className The class containing the method to test + * @param methodName The method in the class of which to test + * @param runnerName The name of the TestRunner of the test on the device to be run + * @param params Any additional parameters to pass into the Test Runner + * @return true if test passed, false otherwise. + */ + public boolean runDeviceTestsDidAllTestsPass(String pkgName, String className, + String methodName, String runnerName, Map<String, String> params) throws IOException, + TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException { + CollectingTestRunListener listener = doRunTests(pkgName, className, methodName, + runnerName, params); + return listener.didAllTestsPass(); + } + + /** + * Runs the specified packages tests, and returns whether all tests passed or not. + * + * @param pkgName Android application package for tests * @throws TimeoutException in case of a timeout on the connection. * @throws AdbCommandRejectedException if adb rejects the command * @throws ShellCommandUnresponsiveException if the device did not output anything for * a period longer than the max time to output. * @throws IOException if connection to device was lost. + * @return true if every test passed, false otherwise. */ public boolean runDeviceTestsDidAllTestsPass(String pkgName) throws IOException, TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException { - CollectingTestRunListener listener = doRunTests(pkgName); + CollectingTestRunListener listener = doRunTests(pkgName, null, null, null, null); return listener.didAllTestsPass(); } @@ -531,7 +571,7 @@ public class PackageManagerHostTestUtils extends Assert { } // For collecting results from running device tests - private static class CollectingTestRunListener implements ITestRunListener { + public static class CollectingTestRunListener implements ITestRunListener { private boolean mAllTestsPassed = true; private String mTestRunErrorMessage = null; diff --git a/core/tests/hosttests/src/android/net/DownloadManagerHostTests.java b/core/tests/hosttests/src/android/net/DownloadManagerHostTests.java new file mode 100644 index 0000000..cfabb6c --- /dev/null +++ b/core/tests/hosttests/src/android/net/DownloadManagerHostTests.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.content.pm.PackageManagerHostTestUtils; + +import com.android.ddmlib.Log; +import com.android.hosttest.DeviceTestCase; +import com.android.hosttest.DeviceTestSuite; + +import java.io.File; +import java.util.Hashtable; + +import junit.framework.Test; + +/** + * Host-based tests of the DownloadManager API. (Uses a device-based app to actually invoke the + * various tests.) + */ +public class DownloadManagerHostTests extends DeviceTestCase { + protected PackageManagerHostTestUtils mPMUtils = null; + + private static final String LOG_TAG = "android.net.DownloadManagerHostTests"; + private static final String FILE_DOWNLOAD_APK = "DownloadManagerTestApp.apk"; + private static final String FILE_DOWNLOAD_PKG = "com.android.frameworks.downloadmanagertests"; + private static final String FILE_DOWNLOAD_CLASS = + "com.android.frameworks.downloadmanagertests.DownloadManagerTestApp"; + private static final String DOWNLOAD_TEST_RUNNER_NAME = + "com.android.frameworks.downloadmanagertests.DownloadManagerTestRunner"; + + // Extra parameters to pass to the TestRunner + private static final String EXTERNAL_DOWNLOAD_URI_KEY = "external_download_uri"; + // Note: External environment variable ANDROID_TEST_EXTERNAL_URI must be set to point to the + // external URI under which the files downloaded by the tests can be found. Note that the Uri + // must be accessible by the device during a test run. + private static String EXTERNAL_DOWNLOAD_URI_VALUE = null; + + Hashtable<String, String> mExtraParams = null; + + public static Test suite() { + return new DeviceTestSuite(DownloadManagerHostTests.class); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + // ensure apk path has been set before test is run + assertNotNull(getTestAppPath()); + mPMUtils = new PackageManagerHostTestUtils(getDevice()); + EXTERNAL_DOWNLOAD_URI_VALUE = System.getenv("ANDROID_TEST_EXTERNAL_URI"); + assertNotNull(EXTERNAL_DOWNLOAD_URI_VALUE); + mExtraParams = getExtraParams(); + } + + /** + * Helper function to get extra params that can be used to pass into the helper app. + */ + protected Hashtable<String, String> getExtraParams() { + Hashtable<String, String> extraParams = new Hashtable<String, String>(); + extraParams.put(EXTERNAL_DOWNLOAD_URI_KEY, EXTERNAL_DOWNLOAD_URI_VALUE); + return extraParams; + } + + /** + * Tests that a large download over WiFi + * @throws Exception if the test failed at any point + */ + public void testLargeDownloadOverWiFi() throws Exception { + mPMUtils.installAppAndVerifyExistsOnDevice(String.format("%s%s%s", getTestAppPath(), + File.separator, FILE_DOWNLOAD_APK), FILE_DOWNLOAD_PKG, true); + + boolean testPassed = mPMUtils.runDeviceTestsDidAllTestsPass(FILE_DOWNLOAD_PKG, + FILE_DOWNLOAD_CLASS, "runLargeDownloadOverWiFi", DOWNLOAD_TEST_RUNNER_NAME, + mExtraParams); + + assertTrue("Failed to install large file over WiFi in < 10 minutes!", testPassed); + } + + /** + * Spawns a device-based function to initiate a download on the device, reboots the device, + * then waits and verifies the download succeeded. + * + * @throws Exception if the test failed at any point + */ + public void testDownloadManagerSingleReboot() throws Exception { + mPMUtils.installAppAndVerifyExistsOnDevice(String.format("%s%s%s", getTestAppPath(), + File.separator, FILE_DOWNLOAD_APK), FILE_DOWNLOAD_PKG, true); + + boolean testPassed = mPMUtils.runDeviceTestsDidAllTestsPass(FILE_DOWNLOAD_PKG, + FILE_DOWNLOAD_CLASS, "initiateDownload", DOWNLOAD_TEST_RUNNER_NAME, + mExtraParams); + + assertTrue("Failed to initiate download properly!", testPassed); + mPMUtils.rebootDevice(); + testPassed = mPMUtils.runDeviceTestsDidAllTestsPass(FILE_DOWNLOAD_PKG, + FILE_DOWNLOAD_CLASS, "verifyFileDownloadSucceeded", DOWNLOAD_TEST_RUNNER_NAME, + mExtraParams); + assertTrue("Failed to verify initiated download completed properyly!", testPassed); + } + + /** + * Spawns a device-based function to initiate a download on the device, reboots the device three + * times (using different intervals), then waits and verifies the download succeeded. + * + * @throws Exception if the test failed at any point + */ + public void testDownloadManagerMultipleReboots() throws Exception { + mPMUtils.installAppAndVerifyExistsOnDevice(String.format("%s%s%s", getTestAppPath(), + File.separator, FILE_DOWNLOAD_APK), FILE_DOWNLOAD_PKG, true); + + boolean testPassed = mPMUtils.runDeviceTestsDidAllTestsPass(FILE_DOWNLOAD_PKG, + FILE_DOWNLOAD_CLASS, "initiateDownload", DOWNLOAD_TEST_RUNNER_NAME, + mExtraParams); + + assertTrue("Failed to initiate download properly!", testPassed); + Thread.sleep(5000); + + // Do 3 random reboots - after 13, 9, and 19 seconds + Log.i(LOG_TAG, "First reboot..."); + mPMUtils.rebootDevice(); + Thread.sleep(13000); + Log.i(LOG_TAG, "Second reboot..."); + mPMUtils.rebootDevice(); + Thread.sleep(9000); + Log.i(LOG_TAG, "Third reboot..."); + mPMUtils.rebootDevice(); + Thread.sleep(19000); + testPassed = mPMUtils.runDeviceTestsDidAllTestsPass(FILE_DOWNLOAD_PKG, + FILE_DOWNLOAD_CLASS, "verifyFileDownloadSucceeded", DOWNLOAD_TEST_RUNNER_NAME, + mExtraParams); + assertTrue("Failed to verify initiated download completed properyly!", testPassed); + } + + /** + * Spawns a device-based function to test download while WiFi is enabled/disabled multiple times + * during the download. + * + * @throws Exception if the test failed at any point + */ + public void testDownloadMultipleWiFiEnableDisable() throws Exception { + mPMUtils.installAppAndVerifyExistsOnDevice(String.format("%s%s%s", getTestAppPath(), + File.separator, FILE_DOWNLOAD_APK), FILE_DOWNLOAD_PKG, true); + + boolean testPassed = mPMUtils.runDeviceTestsDidAllTestsPass(FILE_DOWNLOAD_PKG, + FILE_DOWNLOAD_CLASS, "runDownloadMultipleWiFiEnableDisable", + DOWNLOAD_TEST_RUNNER_NAME, mExtraParams); + assertTrue(testPassed); + } + + /** + * Spawns a device-based function to test switching on/off both airplane mode and WiFi + * + * @throws Exception if the test failed at any point + */ + public void testDownloadMultipleSwitching() throws Exception { + mPMUtils.installAppAndVerifyExistsOnDevice(String.format("%s%s%s", getTestAppPath(), + File.separator, FILE_DOWNLOAD_APK), FILE_DOWNLOAD_PKG, true); + + boolean testPassed = mPMUtils.runDeviceTestsDidAllTestsPass(FILE_DOWNLOAD_PKG, + FILE_DOWNLOAD_CLASS, "runDownloadMultipleSwitching", + DOWNLOAD_TEST_RUNNER_NAME, mExtraParams); + assertTrue(testPassed); + } + + /** + * Spawns a device-based function to test switching on/off airplane mode multiple times + * + * @throws Exception if the test failed at any point + */ + public void testDownloadMultipleAirplaneModeEnableDisable() throws Exception { + mPMUtils.installAppAndVerifyExistsOnDevice(String.format("%s%s%s", getTestAppPath(), + File.separator, FILE_DOWNLOAD_APK), FILE_DOWNLOAD_PKG, true); + + boolean testPassed = mPMUtils.runDeviceTestsDidAllTestsPass(FILE_DOWNLOAD_PKG, + FILE_DOWNLOAD_CLASS, "runDownloadMultipleAirplaneModeEnableDisable", + DOWNLOAD_TEST_RUNNER_NAME, mExtraParams); + assertTrue(testPassed); + } +} diff --git a/core/tests/hosttests/test-apps/DownloadManagerTestApp/Android.mk b/core/tests/hosttests/test-apps/DownloadManagerTestApp/Android.mk new file mode 100644 index 0000000..576765c --- /dev/null +++ b/core/tests/hosttests/test-apps/DownloadManagerTestApp/Android.mk @@ -0,0 +1,29 @@ +# Copyright (C) 2010 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := tests + +LOCAL_SRC_FILES := $(call all-java-files-under, src) \ + ../../../coretests/src/android/net/DownloadManagerBaseTest.java + +LOCAL_STATIC_JAVA_LIBRARIES := android-common frameworks-core-util-lib +LOCAL_SDK_VERSION := current + +LOCAL_PACKAGE_NAME := DownloadManagerTestApp + +include $(BUILD_PACKAGE) diff --git a/core/tests/hosttests/test-apps/DownloadManagerTestApp/AndroidManifest.xml b/core/tests/hosttests/test-apps/DownloadManagerTestApp/AndroidManifest.xml new file mode 100644 index 0000000..3f2be3c --- /dev/null +++ b/core/tests/hosttests/test-apps/DownloadManagerTestApp/AndroidManifest.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2010 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.frameworks.downloadmanagertests"> + + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> + <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" /> + <uses-permission android:name="android.permission.INTERNET" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.WRITE_SETTINGS" /> + + <application android:label="DownloadManagerTestApp"> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation + android:name=".DownloadManagerTestRunner" + android:targetPackage="com.android.frameworks.downloadmanagertests" + android:label="Frameworks Download Manager Test App" /> + +</manifest> diff --git a/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerTestApp.java b/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerTestApp.java new file mode 100644 index 0000000..ef81353 --- /dev/null +++ b/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerTestApp.java @@ -0,0 +1,463 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.frameworks.downloadmanagertests; + +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.DownloadManager; +import android.net.DownloadManager.Query; +import android.net.DownloadManager.Request; +import android.net.DownloadManagerBaseTest; +import android.net.Uri; +import android.os.Environment; +import android.os.ParcelFileDescriptor; +import android.provider.Settings; +import android.test.suitebuilder.annotation.LargeTest; +import android.util.Log; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileWriter; + +import coretestutils.http.MockResponse; +import coretestutils.http.MockWebServer; +import coretestutils.http.RecordedRequest; + +/** + * Class to test downloading files from a real (not mock) external server. + */ +public class DownloadManagerTestApp extends DownloadManagerBaseTest { + protected static String DOWNLOAD_STARTED_FLAG = "DMTEST_DOWNLOAD_STARTED"; + protected static String LOG_TAG = + "com.android.frameworks.downloadmanagertests.DownloadManagerTestApp"; + + protected static String DOWNLOAD_500K_FILENAME = "External541kb.apk"; + protected static long DOWNLOAD_500K_FILESIZE = 570927; + protected static String DOWNLOAD_1MB_FILENAME = "External1mb.apk"; + protected static long DOWNLOAD_1MB_FILESIZE = 1041262; + protected static String DOWNLOAD_10MB_FILENAME = "External10mb.apk"; + protected static long DOWNLOAD_10MB_FILESIZE = 10258741; + + // Values to be obtained from TestRunner + private String externalDownloadUriValue = null; + + /** + * {@inheritDoc } + */ + @Override + public void setUp() throws Exception { + super.setUp(); + DownloadManagerTestRunner mRunner = (DownloadManagerTestRunner)getInstrumentation(); + externalDownloadUriValue = mRunner.externalDownloadUriValue; + assertNotNull(externalDownloadUriValue); + + if (!externalDownloadUriValue.endsWith("/")) { + externalDownloadUriValue += "/"; + } + } + + /** + * Gets the external URL of the file to download + * + * @return the Uri of the external file to download + */ + private Uri getExternalFileUri(String file) { + return Uri.parse(externalDownloadUriValue + file); + } + + /** + * Gets the path to the file that flags that a download has started. The file contains the + * DownloadManager id of the download being trackted between reboot sessions. + * + * @return The path of the file tracking that a download has started + * @throws InterruptedException if interrupted + * @throws Exception if timed out while waiting for SD card to mount + */ + protected String getDownloadStartedFilePath() { + String path = Environment.getExternalStorageDirectory().getPath(); + return path + File.separatorChar + DOWNLOAD_STARTED_FLAG; + } + + /** + * Common setup steps for downloads. + * + * Note that these are not included in setUp, so that individual tests can control their own + * state between reboots, etc. + */ + protected void doCommonDownloadSetup() throws Exception { + setWiFiStateOn(true); + setAirplaneModeOn(false); + waitForExternalStoreMount(); + removeAllCurrentDownloads(); + } + + /** + * Initiates a download. + * + * Queues up a download to the download manager, and saves the DownloadManager's assigned + * download ID for this download to a file. + * + * @throws Exception if unsuccessful + */ + public void initiateDownload() throws Exception { + String filename = DOWNLOAD_1MB_FILENAME; + mContext.deleteFile(DOWNLOAD_STARTED_FLAG); + FileOutputStream fileOutput = mContext.openFileOutput(DOWNLOAD_STARTED_FLAG, 0); + DataOutputStream outputFile = null; + doCommonDownloadSetup(); + + try { + long dlRequest = -1; + + // Make sure there are no pending downloads currently going on + removeAllCurrentDownloads(); + + Uri remoteUri = getExternalFileUri(filename); + Request request = new Request(remoteUri); + + dlRequest = mDownloadManager.enqueue(request); + waitForDownloadToStart(dlRequest); + assertTrue(dlRequest != -1); + + // Store ID of download for later retrieval + outputFile = new DataOutputStream(fileOutput); + outputFile.writeLong(dlRequest); + } finally { + if (outputFile != null) { + outputFile.flush(); + outputFile.close(); + } + } + } + + /** + * Waits for a previously-initiated download and verifies it has completed successfully. + * + * @throws Exception if unsuccessful + */ + public void verifyFileDownloadSucceeded() throws Exception { + String filename = DOWNLOAD_1MB_FILENAME; + long filesize = DOWNLOAD_1MB_FILESIZE; + long dlRequest = -1; + boolean rebootMarkerValid = false; + DataInputStream dataInputFile = null; + + setWiFiStateOn(true); + setAirplaneModeOn(false); + + try { + FileInputStream inFile = mContext.openFileInput(DOWNLOAD_STARTED_FLAG); + dataInputFile = new DataInputStream(inFile); + dlRequest = dataInputFile.readLong(); + } catch (Exception e) { + // The file was't valid so we just leave the flag false + Log.i(LOG_TAG, "Unable to determine initial download id."); + throw e; + } finally { + if (dataInputFile != null) { + dataInputFile.close(); + } + mContext.deleteFile(DOWNLOAD_STARTED_FLAG); + } + + assertTrue(dlRequest != -1); + Cursor cursor = getCursor(dlRequest); + ParcelFileDescriptor pfd = null; + try { + assertTrue("Unable to query last initiated download!", cursor.moveToFirst()); + + int columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS); + int status = cursor.getInt(columnIndex); + int currentWaitTime = 0; + + // Wait until the download finishes + waitForDownloadOrTimeout(dlRequest); + + Log.i(LOG_TAG, "Verifying download information..."); + // Verify specific info about the file (size, name, etc)... + pfd = mDownloadManager.openDownloadedFile(dlRequest); + verifyFileSize(pfd, filesize); + } catch (Exception e) { + Log.i(LOG_TAG, "error: " + e.toString()); + throw e; + } finally { + // Clean up... + cursor.close(); + mDownloadManager.remove(dlRequest); + if (pfd != null) { + pfd.close(); + } + } + } + + /** + * Tests downloading a large file over WiFi (~10 Mb). + * + * @throws Exception if unsuccessful + */ + public void runLargeDownloadOverWiFi() throws Exception { + String filename = DOWNLOAD_10MB_FILENAME; + long filesize = DOWNLOAD_10MB_FILESIZE; + long dlRequest = -1; + doCommonDownloadSetup(); + + // Make sure there are no pending downloads currently going on + removeAllCurrentDownloads(); + + Uri remoteUri = getExternalFileUri(filename); + Request request = new Request(remoteUri); + request.setMediaType(getMimeMapping(DownloadFileType.APK)); + + dlRequest = mDownloadManager.enqueue(request); + + // Rather large file, so wait up to 15 mins... + waitForDownloadOrTimeout(dlRequest, WAIT_FOR_DOWNLOAD_POLL_TIME, 15 * 60 * 1000); + + Cursor cursor = getCursor(dlRequest); + ParcelFileDescriptor pfd = null; + try { + Log.i(LOG_TAG, "Verifying download information..."); + // Verify specific info about the file (size, name, etc)... + pfd = mDownloadManager.openDownloadedFile(dlRequest); + verifyFileSize(pfd, filesize); + } finally { + if (pfd != null) { + pfd.close(); + } + mDownloadManager.remove(dlRequest); + cursor.close(); + } + } + + /** + * Tests that downloads resume when switching back and forth from having connectivity to + * having no connectivity using both WiFi and airplane mode. + * + * Note: Device has no mobile access when running this test. + * + * @throws Exception if unsuccessful + */ + public void runDownloadMultipleSwitching() throws Exception { + String filename = DOWNLOAD_500K_FILENAME; + long filesize = DOWNLOAD_500K_FILESIZE; + doCommonDownloadSetup(); + + String localDownloadDirectory = Environment.getExternalStorageDirectory().getPath(); + File downloadedFile = new File(localDownloadDirectory, filename); + + long dlRequest = -1; + try { + downloadedFile.delete(); + + // Make sure there are no pending downloads currently going on + removeAllCurrentDownloads(); + + Uri remoteUri = getExternalFileUri(filename); + Request request = new Request(remoteUri); + + // Local destination of downloaded file + Uri localUri = Uri.fromFile(downloadedFile); + Log.i(LOG_TAG, "setting localUri to: " + localUri.getPath()); + request.setDestinationUri(localUri); + + request.setAllowedNetworkTypes(Request.NETWORK_MOBILE | Request.NETWORK_WIFI); + + dlRequest = mDownloadManager.enqueue(request); + waitForDownloadToStart(dlRequest); + // make sure we're starting to download some data... + waitForFileToGrow(downloadedFile); + + // download disable + setWiFiStateOn(false); + + // download disable + Log.i(LOG_TAG, "Turning on airplane mode..."); + setAirplaneModeOn(true); + Thread.sleep(30 * 1000); // wait 30 secs + + // download disable + setWiFiStateOn(true); + Thread.sleep(30 * 1000); // wait 30 secs + + // download enable + Log.i(LOG_TAG, "Turning off airplane mode..."); + setAirplaneModeOn(false); + Thread.sleep(5 * 1000); // wait 5 seconds + + // download disable + Log.i(LOG_TAG, "Turning off WiFi..."); + setWiFiStateOn(false); + Thread.sleep(30 * 1000); // wait 30 secs + + // finally, turn WiFi back on and finish up the download + Log.i(LOG_TAG, "Turning on WiFi..."); + setWiFiStateOn(true); + Log.i(LOG_TAG, "Waiting up to 3 minutes for download to complete..."); + waitForDownloadsOrTimeout(dlRequest, 3 * 60 * 1000); + ParcelFileDescriptor pfd = mDownloadManager.openDownloadedFile(dlRequest); + verifyFileSize(pfd, filesize); + } finally { + Log.i(LOG_TAG, "Cleaning up files..."); + if (dlRequest != -1) { + mDownloadManager.remove(dlRequest); + } + downloadedFile.delete(); + } + } + + /** + * Tests that downloads resume when switching on/off WiFi at various intervals. + * + * Note: Device has no mobile access when running this test. + * + * @throws Exception if unsuccessful + */ + public void runDownloadMultipleWiFiEnableDisable() throws Exception { + String filename = DOWNLOAD_500K_FILENAME; + long filesize = DOWNLOAD_500K_FILESIZE; + doCommonDownloadSetup(); + + String localDownloadDirectory = Environment.getExternalStorageDirectory().getPath(); + File downloadedFile = new File(localDownloadDirectory, filename); + long dlRequest = -1; + try { + downloadedFile.delete(); + + // Make sure there are no pending downloads currently going on + removeAllCurrentDownloads(); + + Uri remoteUri = getExternalFileUri(filename); + Request request = new Request(remoteUri); + + // Local destination of downloaded file + Uri localUri = Uri.fromFile(downloadedFile); + Log.i(LOG_TAG, "setting localUri to: " + localUri.getPath()); + request.setDestinationUri(localUri); + + request.setAllowedNetworkTypes(Request.NETWORK_WIFI); + + dlRequest = mDownloadManager.enqueue(request); + waitForDownloadToStart(dlRequest); + // are we making any progress? + waitForFileToGrow(downloadedFile); + + // download disable + Log.i(LOG_TAG, "Turning off WiFi..."); + setWiFiStateOn(false); + Thread.sleep(40 * 1000); // wait 40 seconds + + // enable download... + Log.i(LOG_TAG, "Turning on WiFi again..."); + setWiFiStateOn(true); + waitForFileToGrow(downloadedFile); + + // download disable + Log.i(LOG_TAG, "Turning off WiFi..."); + setWiFiStateOn(false); + Thread.sleep(20 * 1000); // wait 20 seconds + + // enable download... + Log.i(LOG_TAG, "Turning on WiFi again..."); + setWiFiStateOn(true); + + Log.i(LOG_TAG, "Waiting up to 3 minutes for download to complete..."); + waitForDownloadsOrTimeout(dlRequest, 3 * 60 * 1000); + ParcelFileDescriptor pfd = mDownloadManager.openDownloadedFile(dlRequest); + verifyFileSize(pfd, filesize); + } finally { + Log.i(LOG_TAG, "Cleaning up files..."); + if (dlRequest != -1) { + mDownloadManager.remove(dlRequest); + } + downloadedFile.delete(); + } + } + + /** + * Tests that downloads resume when switching on/off Airplane mode numerous times at + * various intervals. + * + * Note: Device has no mobile access when running this test. + * + * @throws Exception if unsuccessful + */ + public void runDownloadMultipleAirplaneModeEnableDisable() throws Exception { + String filename = DOWNLOAD_500K_FILENAME; + long filesize = DOWNLOAD_500K_FILESIZE; + // make sure WiFi is enabled, and airplane mode is not on + doCommonDownloadSetup(); + + String localDownloadDirectory = Environment.getExternalStorageDirectory().getPath(); + File downloadedFile = new File(localDownloadDirectory, filename); + long dlRequest = -1; + try { + downloadedFile.delete(); + + // Make sure there are no pending downloads currently going on + removeAllCurrentDownloads(); + + Uri remoteUri = getExternalFileUri(filename); + Request request = new Request(remoteUri); + + // Local destination of downloaded file + Uri localUri = Uri.fromFile(downloadedFile); + Log.i(LOG_TAG, "setting localUri to: " + localUri.getPath()); + request.setDestinationUri(localUri); + + request.setAllowedNetworkTypes(Request.NETWORK_WIFI); + + dlRequest = mDownloadManager.enqueue(request); + waitForDownloadToStart(dlRequest); + // are we making any progress? + waitForFileToGrow(downloadedFile); + + // download disable + Log.i(LOG_TAG, "Turning on Airplane mode..."); + setAirplaneModeOn(true); + Thread.sleep(60 * 1000); // wait 1 minute + + // download enable + Log.i(LOG_TAG, "Turning off Airplane mode..."); + setAirplaneModeOn(false); + // make sure we're starting to download some data... + waitForFileToGrow(downloadedFile); + + // reenable the connection to start up the download again + Log.i(LOG_TAG, "Turning on Airplane mode again..."); + setAirplaneModeOn(true); + Thread.sleep(20 * 1000); // wait 20 seconds + + // Finish up the download... + Log.i(LOG_TAG, "Turning off Airplane mode again..."); + setAirplaneModeOn(false); + + Log.i(LOG_TAG, "Waiting up to 3 minutes for donwload to complete..."); + waitForDownloadsOrTimeout(dlRequest, 180 * 1000); // wait up to 3 mins before timeout + ParcelFileDescriptor pfd = mDownloadManager.openDownloadedFile(dlRequest); + verifyFileSize(pfd, filesize); + } finally { + Log.i(LOG_TAG, "Cleaning up files..."); + if (dlRequest != -1) { + mDownloadManager.remove(dlRequest); + } + downloadedFile.delete(); + } + } +} diff --git a/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerTestRunner.java b/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerTestRunner.java new file mode 100644 index 0000000..0f16619 --- /dev/null +++ b/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerTestRunner.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2010, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.frameworks.downloadmanagertests; + +import android.os.Bundle; +import android.test.InstrumentationTestRunner; +import android.test.InstrumentationTestSuite; +import android.util.Log; + +import com.android.frameworks.downloadmanagertests.DownloadManagerTestApp; + +import junit.framework.TestSuite; + +/** + * Instrumentation Test Runner for all download manager tests. + * + * To run the download manager tests: + * + * adb shell am instrument -e external_download_1mb_uri <uri> external_download_500k_uri <uri> \ + * -w com.android.frameworks.downloadmanagertests/.DownloadManagerTestRunner + */ + +public class DownloadManagerTestRunner extends InstrumentationTestRunner { + private static final String EXTERNAL_DOWNLOAD_URI_KEY = "external_download_uri"; + public String externalDownloadUriValue = null; + + @Override + public TestSuite getAllTests() { + TestSuite suite = new InstrumentationTestSuite(this); + suite.addTestSuite(DownloadManagerTestApp.class); + return suite; + } + + @Override + public ClassLoader getLoader() { + return DownloadManagerTestRunner.class.getClassLoader(); + } + + @Override + public void onCreate(Bundle icicle) { + // Extract the extra params passed in from the bundle... + String externalDownloadUri = (String) icicle.get(EXTERNAL_DOWNLOAD_URI_KEY); + if (externalDownloadUri != null) { + externalDownloadUriValue = externalDownloadUri; + } + super.onCreate(icicle); + } + +} diff --git a/core/tests/utillib/Android.mk b/core/tests/utillib/Android.mk new file mode 100644 index 0000000..299ea5a --- /dev/null +++ b/core/tests/utillib/Android.mk @@ -0,0 +1,27 @@ +# Copyright (C) 2010 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_MODULE := frameworks-core-util-lib + +include $(BUILD_STATIC_JAVA_LIBRARY) + +# Build the test APKs using their own makefiles +include $(call all-makefiles-under,$(LOCAL_PATH)) + diff --git a/core/tests/utillib/src/coretestutils/http/MockResponse.java b/core/tests/utillib/src/coretestutils/http/MockResponse.java new file mode 100644 index 0000000..5b03e5f --- /dev/null +++ b/core/tests/utillib/src/coretestutils/http/MockResponse.java @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package coretestutils.http; + +import static coretestutils.http.MockWebServer.ASCII; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import android.util.Log; + +/** + * A scripted response to be replayed by the mock web server. + */ +public class MockResponse { + private static final byte[] EMPTY_BODY = new byte[0]; + static final String LOG_TAG = "coretestutils.http.MockResponse"; + + private String status = "HTTP/1.1 200 OK"; + private Map<String, String> headers = new HashMap<String, String>(); + private byte[] body = EMPTY_BODY; + private boolean closeConnectionAfter = false; + private String closeConnectionAfterHeader = null; + private int closeConnectionAfterXBytes = -1; + private int pauseConnectionAfterXBytes = -1; + private File bodyExternalFile = null; + + public MockResponse() { + addHeader("Content-Length", 0); + } + + /** + * Returns the HTTP response line, such as "HTTP/1.1 200 OK". + */ + public String getStatus() { + return status; + } + + public MockResponse setResponseCode(int code) { + this.status = "HTTP/1.1 " + code + " OK"; + return this; + } + + /** + * Returns the HTTP headers, such as "Content-Length: 0". + */ + public List<String> getHeaders() { + List<String> headerStrings = new ArrayList<String>(); + for (String header : headers.keySet()) { + headerStrings.add(header + ": " + headers.get(header)); + } + return headerStrings; + } + + public MockResponse addHeader(String header, String value) { + headers.put(header.toLowerCase(), value); + return this; + } + + public MockResponse addHeader(String header, long value) { + return addHeader(header, Long.toString(value)); + } + + public MockResponse removeHeader(String header) { + headers.remove(header.toLowerCase()); + return this; + } + + /** + * Returns true if the body should come from an external file, false otherwise. + */ + private boolean bodyIsExternal() { + return bodyExternalFile != null; + } + + /** + * Returns an input stream containing the raw HTTP payload. + */ + public InputStream getBody() { + if (bodyIsExternal()) { + try { + return new FileInputStream(bodyExternalFile); + } catch (FileNotFoundException e) { + Log.e(LOG_TAG, "File not found: " + bodyExternalFile.getAbsolutePath()); + } + } + return new ByteArrayInputStream(this.body); + } + + public MockResponse setBody(File body) { + addHeader("Content-Length", body.length()); + this.bodyExternalFile = body; + return this; + } + + public MockResponse setBody(byte[] body) { + addHeader("Content-Length", body.length); + this.body = body; + return this; + } + + public MockResponse setBody(String body) { + try { + return setBody(body.getBytes(ASCII)); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(); + } + } + + /** + * Sets the body as chunked. + * + * Currently chunked body is not supported for external files as bodies. + */ + public MockResponse setChunkedBody(byte[] body, int maxChunkSize) throws IOException { + addHeader("Transfer-encoding", "chunked"); + + ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); + int pos = 0; + while (pos < body.length) { + int chunkSize = Math.min(body.length - pos, maxChunkSize); + bytesOut.write(Integer.toHexString(chunkSize).getBytes(ASCII)); + bytesOut.write("\r\n".getBytes(ASCII)); + bytesOut.write(body, pos, chunkSize); + bytesOut.write("\r\n".getBytes(ASCII)); + pos += chunkSize; + } + bytesOut.write("0\r\n".getBytes(ASCII)); + this.body = bytesOut.toByteArray(); + return this; + } + + public MockResponse setChunkedBody(String body, int maxChunkSize) throws IOException { + return setChunkedBody(body.getBytes(ASCII), maxChunkSize); + } + + @Override public String toString() { + return status; + } + + public boolean shouldCloseConnectionAfter() { + return closeConnectionAfter; + } + + public MockResponse setCloseConnectionAfter(boolean closeConnectionAfter) { + this.closeConnectionAfter = closeConnectionAfter; + return this; + } + + /** + * Sets the header after which sending the server should close the connection. + */ + public MockResponse setCloseConnectionAfterHeader(String header) { + closeConnectionAfterHeader = header; + setCloseConnectionAfter(true); + return this; + } + + /** + * Returns the header after which sending the server should close the connection. + */ + public String getCloseConnectionAfterHeader() { + return closeConnectionAfterHeader; + } + + /** + * Sets the number of bytes in the body to send before which the server should close the + * connection. Set to -1 to unset and send the entire body (default). + */ + public MockResponse setCloseConnectionAfterXBytes(int position) { + closeConnectionAfterXBytes = position; + setCloseConnectionAfter(true); + return this; + } + + /** + * Returns the number of bytes in the body to send before which the server should close the + * connection. Returns -1 if the entire body should be sent (default). + */ + public int getCloseConnectionAfterXBytes() { + return closeConnectionAfterXBytes; + } + + /** + * Sets the number of bytes in the body to send before which the server should pause the + * connection (stalls in sending data). Only one pause per response is supported. + * Set to -1 to unset pausing (default). + */ + public MockResponse setPauseConnectionAfterXBytes(int position) { + pauseConnectionAfterXBytes = position; + return this; + } + + /** + * Returns the number of bytes in the body to send before which the server should pause the + * connection (stalls in sending data). (Returns -1 if it should not pause). + */ + public int getPauseConnectionAfterXBytes() { + return pauseConnectionAfterXBytes; + } + + /** + * Returns true if this response is flagged to pause the connection mid-stream, false otherwise + */ + public boolean getShouldPause() { + return (pauseConnectionAfterXBytes != -1); + } + + /** + * Returns true if this response is flagged to close the connection mid-stream, false otherwise + */ + public boolean getShouldClose() { + return (closeConnectionAfterXBytes != -1); + } +} diff --git a/core/tests/utillib/src/coretestutils/http/MockWebServer.java b/core/tests/utillib/src/coretestutils/http/MockWebServer.java new file mode 100644 index 0000000..c329ffa --- /dev/null +++ b/core/tests/utillib/src/coretestutils/http/MockWebServer.java @@ -0,0 +1,426 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package coretestutils.http; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.URL; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import android.util.Log; + +/** + * A scriptable web server. Callers supply canned responses and the server + * replays them upon request in sequence. + * + * TODO: merge with the version from libcore/support/src/tests/java once it's in. + */ +public final class MockWebServer { + static final String ASCII = "US-ASCII"; + static final String LOG_TAG = "coretestutils.http.MockWebServer"; + + private final BlockingQueue<RecordedRequest> requestQueue + = new LinkedBlockingQueue<RecordedRequest>(); + private final BlockingQueue<MockResponse> responseQueue + = new LinkedBlockingQueue<MockResponse>(); + private int bodyLimit = Integer.MAX_VALUE; + private final ExecutorService executor = Executors.newCachedThreadPool(); + // keep Futures around so we can rethrow any exceptions thrown by Callables + private final Queue<Future<?>> futures = new LinkedList<Future<?>>(); + private final Object downloadPauseLock = new Object(); + // global flag to signal when downloads should resume on the server + private volatile boolean downloadResume = false; + + private int port = -1; + + public int getPort() { + if (port == -1) { + throw new IllegalStateException("Cannot retrieve port before calling play()"); + } + return port; + } + + /** + * Returns a URL for connecting to this server. + * + * @param path the request path, such as "/". + */ + public URL getUrl(String path) throws MalformedURLException { + return new URL("http://localhost:" + getPort() + path); + } + + /** + * Sets the number of bytes of the POST body to keep in memory to the given + * limit. + */ + public void setBodyLimit(int maxBodyLength) { + this.bodyLimit = maxBodyLength; + } + + public void enqueue(MockResponse response) { + responseQueue.add(response); + } + + /** + * Awaits the next HTTP request, removes it, and returns it. Callers should + * use this to verify the request sent was as intended. + */ + public RecordedRequest takeRequest() throws InterruptedException { + return requestQueue.take(); + } + + public RecordedRequest takeRequestWithTimeout(long timeoutMillis) throws InterruptedException { + return requestQueue.poll(timeoutMillis, TimeUnit.MILLISECONDS); + } + + public List<RecordedRequest> drainRequests() { + List<RecordedRequest> requests = new ArrayList<RecordedRequest>(); + requestQueue.drainTo(requests); + return requests; + } + + /** + * Starts the server, serves all enqueued requests, and shuts the server + * down using the default (server-assigned) port. + */ + public void play() throws IOException { + play(0); + } + + /** + * Starts the server, serves all enqueued requests, and shuts the server + * down. + * + * @param port The port number to use to listen to connections on; pass in 0 to have the + * server automatically assign a free port + */ + public void play(int portNumber) throws IOException { + final ServerSocket ss = new ServerSocket(portNumber); + ss.setReuseAddress(true); + port = ss.getLocalPort(); + submitCallable(new Callable<Void>() { + public Void call() throws Exception { + int count = 0; + while (true) { + if (count > 0 && responseQueue.isEmpty()) { + ss.close(); + executor.shutdown(); + return null; + } + + serveConnection(ss.accept()); + count++; + } + } + }); + } + + private void serveConnection(final Socket s) { + submitCallable(new Callable<Void>() { + public Void call() throws Exception { + InputStream in = new BufferedInputStream(s.getInputStream()); + OutputStream out = new BufferedOutputStream(s.getOutputStream()); + + int sequenceNumber = 0; + while (true) { + RecordedRequest request = readRequest(in, sequenceNumber); + if (request == null) { + if (sequenceNumber == 0) { + throw new IllegalStateException("Connection without any request!"); + } else { + break; + } + } + requestQueue.add(request); + MockResponse response = computeResponse(request); + writeResponse(out, response); + if (response.shouldCloseConnectionAfter()) { + break; + } + sequenceNumber++; + } + + in.close(); + out.close(); + return null; + } + }); + } + + private void submitCallable(Callable<?> callable) { + Future<?> future = executor.submit(callable); + futures.add(future); + } + + /** + * Check for and raise any exceptions that have been thrown by child threads. Will not block on + * children still running. + * @throws ExecutionException for the first child thread that threw an exception + */ + public void checkForExceptions() throws ExecutionException, InterruptedException { + final int originalSize = futures.size(); + for (int i = 0; i < originalSize; i++) { + Future<?> future = futures.remove(); + try { + future.get(0, TimeUnit.SECONDS); + } catch (TimeoutException e) { + futures.add(future); // still running + } + } + } + + /** + * @param sequenceNumber the index of this request on this connection. + */ + private RecordedRequest readRequest(InputStream in, int sequenceNumber) throws IOException { + String request = readAsciiUntilCrlf(in); + if (request.equals("")) { + return null; // end of data; no more requests + } + + List<String> headers = new ArrayList<String>(); + int contentLength = -1; + boolean chunked = false; + String header; + while (!(header = readAsciiUntilCrlf(in)).equals("")) { + headers.add(header); + String lowercaseHeader = header.toLowerCase(); + if (contentLength == -1 && lowercaseHeader.startsWith("content-length:")) { + contentLength = Integer.parseInt(header.substring(15).trim()); + } + if (lowercaseHeader.startsWith("transfer-encoding:") && + lowercaseHeader.substring(18).trim().equals("chunked")) { + chunked = true; + } + } + + boolean hasBody = false; + TruncatingOutputStream requestBody = new TruncatingOutputStream(); + List<Integer> chunkSizes = new ArrayList<Integer>(); + if (contentLength != -1) { + hasBody = true; + transfer(contentLength, in, requestBody); + } else if (chunked) { + hasBody = true; + while (true) { + int chunkSize = Integer.parseInt(readAsciiUntilCrlf(in).trim(), 16); + if (chunkSize == 0) { + readEmptyLine(in); + break; + } + chunkSizes.add(chunkSize); + transfer(chunkSize, in, requestBody); + readEmptyLine(in); + } + } + + if (request.startsWith("GET ")) { + if (hasBody) { + throw new IllegalArgumentException("GET requests should not have a body!"); + } + } else if (request.startsWith("POST ")) { + if (!hasBody) { + throw new IllegalArgumentException("POST requests must have a body!"); + } + } else { + throw new UnsupportedOperationException("Unexpected method: " + request); + } + + return new RecordedRequest(request, headers, chunkSizes, + requestBody.numBytesReceived, requestBody.toByteArray(), sequenceNumber); + } + + /** + * Returns a response to satisfy {@code request}. + */ + private MockResponse computeResponse(RecordedRequest request) throws InterruptedException { + if (responseQueue.isEmpty()) { + throw new IllegalStateException("Unexpected request: " + request); + } + return responseQueue.take(); + } + + private void writeResponse(OutputStream out, MockResponse response) throws IOException { + out.write((response.getStatus() + "\r\n").getBytes(ASCII)); + boolean doCloseConnectionAfterHeader = (response.getCloseConnectionAfterHeader() != null); + + // Send headers + String closeConnectionAfterHeader = response.getCloseConnectionAfterHeader(); + for (String header : response.getHeaders()) { + out.write((header + "\r\n").getBytes(ASCII)); + + if (doCloseConnectionAfterHeader && header.startsWith(closeConnectionAfterHeader)) { + Log.i(LOG_TAG, "Closing connection after header" + header); + break; + } + } + + // Send actual body data + if (!doCloseConnectionAfterHeader) { + out.write(("\r\n").getBytes(ASCII)); + + InputStream body = response.getBody(); + final int READ_BLOCK_SIZE = 10000; // process blocks this size + byte[] currentBlock = new byte[READ_BLOCK_SIZE]; + int currentBlockSize = 0; + int writtenSoFar = 0; + + boolean shouldPause = response.getShouldPause(); + boolean shouldClose = response.getShouldClose(); + int pause = response.getPauseConnectionAfterXBytes(); + int close = response.getCloseConnectionAfterXBytes(); + + // Don't bother pausing if it's set to pause -after- the connection should be dropped + if (shouldPause && shouldClose && (pause > close)) { + shouldPause = false; + } + + // Process each block we read in... + while ((currentBlockSize = body.read(currentBlock)) != -1) { + int startIndex = 0; + int writeLength = currentBlockSize; + + // handle the case of pausing + if (shouldPause && (writtenSoFar + currentBlockSize >= pause)) { + writeLength = pause - writtenSoFar; + out.write(currentBlock, 0, writeLength); + out.flush(); + writtenSoFar += writeLength; + + // now pause... + try { + Log.i(LOG_TAG, "Pausing connection after " + pause + " bytes"); + // Wait until someone tells us to resume sending... + synchronized(downloadPauseLock) { + while (!downloadResume) { + downloadPauseLock.wait(); + } + // reset resume back to false + downloadResume = false; + } + } catch (InterruptedException e) { + Log.e(LOG_TAG, "Server was interrupted during pause in download."); + } + + startIndex = writeLength; + writeLength = currentBlockSize - writeLength; + } + + // handle the case of closing the connection + if (shouldClose && (writtenSoFar + writeLength > close)) { + writeLength = close - writtenSoFar; + out.write(currentBlock, startIndex, writeLength); + writtenSoFar += writeLength; + Log.i(LOG_TAG, "Closing connection after " + close + " bytes"); + break; + } + out.write(currentBlock, startIndex, writeLength); + writtenSoFar += writeLength; + } + } + out.flush(); + } + + /** + * Transfer bytes from {@code in} to {@code out} until either {@code length} + * bytes have been transferred or {@code in} is exhausted. + */ + private void transfer(int length, InputStream in, OutputStream out) throws IOException { + byte[] buffer = new byte[1024]; + while (length > 0) { + int count = in.read(buffer, 0, Math.min(buffer.length, length)); + if (count == -1) { + return; + } + out.write(buffer, 0, count); + length -= count; + } + } + + /** + * Returns the text from {@code in} until the next "\r\n", or null if + * {@code in} is exhausted. + */ + private String readAsciiUntilCrlf(InputStream in) throws IOException { + StringBuilder builder = new StringBuilder(); + while (true) { + int c = in.read(); + if (c == '\n' && builder.length() > 0 && builder.charAt(builder.length() - 1) == '\r') { + builder.deleteCharAt(builder.length() - 1); + return builder.toString(); + } else if (c == -1) { + return builder.toString(); + } else { + builder.append((char) c); + } + } + } + + private void readEmptyLine(InputStream in) throws IOException { + String line = readAsciiUntilCrlf(in); + if (!line.equals("")) { + throw new IllegalStateException("Expected empty but was: " + line); + } + } + + /** + * An output stream that drops data after bodyLimit bytes. + */ + private class TruncatingOutputStream extends ByteArrayOutputStream { + private int numBytesReceived = 0; + @Override public void write(byte[] buffer, int offset, int len) { + numBytesReceived += len; + super.write(buffer, offset, Math.min(len, bodyLimit - count)); + } + @Override public void write(int oneByte) { + numBytesReceived++; + if (count < bodyLimit) { + super.write(oneByte); + } + } + } + + /** + * Trigger the server to resume sending the download + */ + public void doResumeDownload() { + synchronized (downloadPauseLock) { + downloadResume = true; + downloadPauseLock.notifyAll(); + } + } +} diff --git a/core/tests/utillib/src/coretestutils/http/RecordedRequest.java b/core/tests/utillib/src/coretestutils/http/RecordedRequest.java new file mode 100644 index 0000000..293ff80 --- /dev/null +++ b/core/tests/utillib/src/coretestutils/http/RecordedRequest.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package coretestutils.http; + +import java.util.List; + +/** + * An HTTP request that came into the mock web server. + */ +public final class RecordedRequest { + private final String requestLine; + private final List<String> headers; + private final List<Integer> chunkSizes; + private final int bodySize; + private final byte[] body; + private final int sequenceNumber; + + RecordedRequest(String requestLine, List<String> headers, List<Integer> chunkSizes, + int bodySize, byte[] body, int sequenceNumber) { + this.requestLine = requestLine; + this.headers = headers; + this.chunkSizes = chunkSizes; + this.bodySize = bodySize; + this.body = body; + this.sequenceNumber = sequenceNumber; + } + + public String getRequestLine() { + return requestLine; + } + + public List<String> getHeaders() { + return headers; + } + + /** + * Returns the sizes of the chunks of this request's body, or an empty list + * if the request's body was empty or unchunked. + */ + public List<Integer> getChunkSizes() { + return chunkSizes; + } + + /** + * Returns the total size of the body of this POST request (before + * truncation). + */ + public int getBodySize() { + return bodySize; + } + + /** + * Returns the body of this POST request. This may be truncated. + */ + public byte[] getBody() { + return body; + } + + /** + * Returns the index of this request on its HTTP connection. Since a single + * HTTP connection may serve multiple requests, each request is assigned its + * own sequence number. + */ + public int getSequenceNumber() { + return sequenceNumber; + } + + @Override public String toString() { + return requestLine; + } + + public String getMethod() { + return getRequestLine().split(" ")[0]; + } + + public String getPath() { + return getRequestLine().split(" ")[1]; + } +} |