New functional test for download manager public API.
Steve Howard [Fri, 2 Jul 2010 19:59:05 +0000 (12:59 -0700)]
I refactored a lot of the logic from DownloadManagerFunctionalTest
into a new abstract class so that it could be shared with the new
PublicApiFunctionalTest.  There could be even more sharing, but it's
not entirely clear it'd be worthwhile at this point.  The new test
covers most features of the new public API.

Change-Id: I8c6859411f44b6430f8b348cf26a61225f5768cb

tests/src/com/android/providers/downloads/AbstractDownloadManagerFunctionalTest.java [new file with mode: 0644]
tests/src/com/android/providers/downloads/DownloadManagerFunctionalTest.java
tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java [new file with mode: 0644]

diff --git a/tests/src/com/android/providers/downloads/AbstractDownloadManagerFunctionalTest.java b/tests/src/com/android/providers/downloads/AbstractDownloadManagerFunctionalTest.java
new file mode 100644 (file)
index 0000000..1394a17
--- /dev/null
@@ -0,0 +1,281 @@
+/*
+ * 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.providers.downloads;
+
+import android.content.ComponentName;
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.ConnectivityManager;
+import android.provider.Downloads;
+import android.test.MoreAsserts;
+import android.test.RenamingDelegatingContext;
+import android.test.ServiceTestCase;
+import android.test.mock.MockContentResolver;
+import android.util.Log;
+import tests.http.MockResponse;
+import tests.http.MockWebServer;
+import tests.http.RecordedRequest;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.MalformedURLException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+public abstract class AbstractDownloadManagerFunctionalTest extends
+        ServiceTestCase<DownloadService> {
+
+    protected static final String LOG_TAG = "DownloadManagerFunctionalTest";
+    private static final String PROVIDER_AUTHORITY = "downloads";
+    protected static final long REQUEST_TIMEOUT_MILLIS = 10 * 1000;
+    protected static final long RETRY_DELAY_MILLIS = 61 * 1000;
+    protected static final String FILE_CONTENT = "hello world";
+    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 MockWebServer mServer;
+    protected MockContentResolver mResolver;
+    protected TestContext mTestContext;
+    protected FakeSystemFacade mSystemFacade;
+
+    static interface StatusReader {
+        public int getStatus();
+        public boolean isComplete(int status);
+    }
+
+    /**
+     * Context passed to the provider and the service.  Allows most methods to pass through to the
+     * real Context (this is a LargeTest), with a few exceptions, including renaming file operations
+     * to avoid file and DB conflicts (via RenamingDelegatingContext).
+     */
+    static class TestContext extends RenamingDelegatingContext {
+        private static final String FILENAME_PREFIX = "test.";
+
+        private Context mRealContext;
+        private Set<String> mAllowedSystemServices;
+        private ContentResolver mResolver;
+
+        boolean mHasServiceBeenStarted = false;
+        FakeIConnectivityManager mFakeIConnectivityManager;
+
+        public TestContext(Context realContext) {
+            super(realContext, FILENAME_PREFIX);
+            mRealContext = realContext;
+            mAllowedSystemServices = new HashSet<String>(Arrays.asList(new String[] {
+                    Context.NOTIFICATION_SERVICE,
+                    Context.POWER_SERVICE,
+            }));
+            mFakeIConnectivityManager = new FakeIConnectivityManager();
+        }
+
+        public void setResolver(ContentResolver resolver) {
+            mResolver = resolver;
+        }
+
+        /**
+         * Direct DownloadService to our test instance of DownloadProvider.
+         */
+        @Override
+        public ContentResolver getContentResolver() {
+            assert mResolver != null;
+            return mResolver;
+        }
+
+        /**
+         * Stub some system services, allow access to others, and block the rest.
+         */
+        @Override
+        public Object getSystemService(String name) {
+            if (name.equals(Context.CONNECTIVITY_SERVICE)) {
+                return new ConnectivityManager(mFakeIConnectivityManager);
+            }
+            if (mAllowedSystemServices.contains(name)) {
+                return mRealContext.getSystemService(name);
+            }
+            return super.getSystemService(name);
+        }
+
+        /**
+         * Record when DownloadProvider starts DownloadService.
+         */
+        @Override
+        public ComponentName startService(Intent service) {
+            if (service.getComponent().getClassName().equals(DownloadService.class.getName())) {
+                mHasServiceBeenStarted = true;
+                return service.getComponent();
+            }
+            throw new UnsupportedOperationException("Unexpected service: " + service);
+        }
+    }
+
+    public AbstractDownloadManagerFunctionalTest() {
+        super(DownloadService.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        Context realContext = getContext();
+        mTestContext = new TestContext(realContext);
+        setupProviderAndResolver();
+        assert isDatabaseEmpty(); // ensure we're not messing with real data
+
+        mTestContext.setResolver(mResolver);
+        setContext(mTestContext);
+        setupService();
+        mSystemFacade = new FakeSystemFacade();
+        getService().mSystemFacade = mSystemFacade;
+
+        mServer = new MockWebServer();
+        mServer.play();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        cleanUpDownloads();
+        super.tearDown();
+    }
+
+    private boolean isDatabaseEmpty() {
+        Cursor cursor = mResolver.query(Downloads.CONTENT_URI, null, null, null, null);
+        try {
+            return cursor.getCount() == 0;
+        } finally {
+            cursor.close();
+        }
+    }
+
+    void setupProviderAndResolver() {
+        ContentProvider provider = new DownloadProvider();
+        provider.attachInfo(mTestContext, null);
+        mResolver = new MockContentResolver();
+        mResolver.addProvider(PROVIDER_AUTHORITY, provider);
+    }
+
+    /**
+     * Remove any downloaded files and delete any lingering downloads.
+     */
+    void cleanUpDownloads() {
+        if (mResolver == null) {
+            return;
+        }
+        String[] columns = new String[] {Downloads._DATA};
+        Cursor cursor = mResolver.query(Downloads.CONTENT_URI, columns, null, null, null);
+        try {
+            for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
+                String filePath = cursor.getString(0);
+                if (filePath == null) continue;
+                Log.d(LOG_TAG, "Deleting " + filePath);
+                new File(filePath).delete();
+            }
+        } finally {
+            cursor.close();
+        }
+        mResolver.delete(Downloads.CONTENT_URI, null, null);
+    }
+
+    /**
+     * Enqueue a response from the MockWebServer.
+     */
+    MockResponse enqueueResponse(int status, String body) {
+        MockResponse response = new MockResponse()
+                        .setResponseCode(status)
+                        .setBody(body)
+                        .addHeader("Content-type", "text/plain");
+        mServer.enqueue(response);
+        return response;
+    }
+
+    MockResponse enqueueEmptyResponse(int status) {
+        return enqueueResponse(status, "");
+    }
+
+    /**
+     * Wait for a request to come to the MockWebServer and return it.
+     */
+    RecordedRequest takeRequest() throws InterruptedException {
+        RecordedRequest request = mServer.takeRequestWithTimeout(REQUEST_TIMEOUT_MILLIS);
+        assertNotNull("Timed out waiting for request", request);
+        return request;
+    }
+
+    String getServerUri(String path) throws MalformedURLException {
+        return mServer.getUrl(path).toString();
+    }
+
+    /**
+     * Run the service and wait for a request and for the download to reach the given status.
+     * @return the request received
+     */
+    protected RecordedRequest runUntilStatus(StatusReader reader, int status) throws Exception {
+        startService(null);
+        RecordedRequest request = takeRequest();
+        waitForDownloadToStop(reader, status);
+        return request;
+    }
+
+    /**
+     * Wait for a download to given a given status, with a timeout.  Fails if the download reaches
+     * any other final status.
+     */
+    protected void waitForDownloadToStop(StatusReader reader, int expectedStatus)
+            throws Exception {
+        // TODO(showard): find a better way to accomplish this
+        long startTimeMillis = System.currentTimeMillis();
+        int status = reader.getStatus();
+        while (status != expectedStatus) {
+            if (reader.isComplete(status)) {
+                fail("Download completed with unexpected status: " + status);
+            }
+            if (System.currentTimeMillis() > startTimeMillis + REQUEST_TIMEOUT_MILLIS) {
+                fail("Download timed out with status " + status);
+            }
+            Thread.sleep(100);
+            mServer.checkForExceptions();
+            status = reader.getStatus();
+        }
+
+        long delta = System.currentTimeMillis() - startTimeMillis;
+        Log.d(LOG_TAG, "Status " + status + " reached after " + delta + "ms");
+    }
+
+    protected String readStream(InputStream inputStream) throws IOException {
+        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
+        try {
+            char[] buffer = new char[1024];
+            int length = reader.read(buffer);
+            assertTrue("Failed to read anything from input stream", length > -1);
+            return String.valueOf(buffer, 0, length);
+        } finally {
+            reader.close();
+        }
+    }
+
+    protected void assertStartsWith(String expectedPrefix, String actual) {
+        String regex = "^" + expectedPrefix + ".*";
+        MoreAsserts.assertMatchesRegex(regex, actual);
+    }
+}
index 8d4655b..7de90de 100644 (file)
 
 package com.android.providers.downloads;
 
-import android.content.ComponentName;
-import android.content.ContentProvider;
-import android.content.ContentResolver;
 import android.content.ContentValues;
-import android.content.Context;
-import android.content.Intent;
 import android.database.Cursor;
-import android.net.ConnectivityManager;
 import android.net.NetworkInfo;
 import android.net.Uri;
 import android.os.Environment;
 import android.provider.Downloads;
-import android.test.MoreAsserts;
-import android.test.RenamingDelegatingContext;
-import android.test.ServiceTestCase;
-import android.test.mock.MockContentResolver;
 import android.test.suitebuilder.annotation.LargeTest;
-import android.util.Log;
-import tests.http.MockResponse;
 import tests.http.MockWebServer;
 import tests.http.RecordedRequest;
 
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.IOException;
 import java.io.InputStream;
-import java.io.InputStreamReader;
 import java.net.MalformedURLException;
-import java.util.Arrays;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
 
 /**
  * This test exercises the entire download manager working together -- it requests downloads through
@@ -56,157 +37,7 @@ import java.util.Set;
  * device to serve downloads.
  */
 @LargeTest
-public class DownloadManagerFunctionalTest extends ServiceTestCase<DownloadService> {
-    private static final String LOG_TAG = "DownloadManagerFunctionalTest";
-
-    private static final String PROVIDER_AUTHORITY = "downloads";
-    private static final int RETRY_DELAY_MILLIS = 61 * 1000;
-    private static final long REQUEST_TIMEOUT_MILLIS = 10 * 1000;
-    private static final String FILE_CONTENT = "hello world";
-
-    private static final int HTTP_OK = 200;
-    private static final int HTTP_PARTIAL_CONTENT = 206;
-    private static final int HTTP_NOT_FOUND = 404;
-    private static final int HTTP_SERVICE_UNAVAILABLE = 503;
-
-    private MockWebServer mServer;
-    // resolves requests to the DownloadProvider we set up
-    private MockContentResolver mResolver;
-    private TestContext mTestContext;
-    private FakeSystemFacade mSystemFacade;
-
-    /**
-     * Context passed to the provider and the service.  Allows most methods to pass through to the
-     * real Context (this is a LargeTest), with a few exceptions, including renaming file operations
-     * to avoid file and DB conflicts (via RenamingDelegatingContext).
-     */
-    private static class TestContext extends RenamingDelegatingContext {
-        private static final String FILENAME_PREFIX = "test.";
-
-        private Context mRealContext;
-        private Set<String> mAllowedSystemServices;
-        private ContentResolver mResolver;
-
-        boolean mHasServiceBeenStarted = false;
-        FakeIConnectivityManager mFakeIConnectivityManager;
-
-        public TestContext(Context realContext) {
-            super(realContext, FILENAME_PREFIX);
-            mRealContext = realContext;
-            mAllowedSystemServices = new HashSet<String>(Arrays.asList(new String[] {
-                    Context.NOTIFICATION_SERVICE,
-                    Context.POWER_SERVICE,
-            }));
-            mFakeIConnectivityManager = new FakeIConnectivityManager();
-        }
-
-        public void setResolver(ContentResolver resolver) {
-            mResolver = resolver;
-        }
-
-        /**
-         * Direct DownloadService to our test instance of DownloadProvider.
-         */
-        @Override
-        public ContentResolver getContentResolver() {
-            assert mResolver != null;
-            return mResolver;
-        }
-
-        /**
-         * Stub some system services, allow access to others, and block the rest.
-         */
-        @Override
-        public Object getSystemService(String name) {
-            if (name.equals(Context.CONNECTIVITY_SERVICE)) {
-                return new ConnectivityManager(mFakeIConnectivityManager);
-            }
-            if (mAllowedSystemServices.contains(name)) {
-                return mRealContext.getSystemService(name);
-            }
-            return super.getSystemService(name);
-        }
-
-        /**
-         * Record when DownloadProvider starts DownloadService.
-         */
-        @Override
-        public ComponentName startService(Intent service) {
-            if (service.getComponent().getClassName().equals(DownloadService.class.getName())) {
-                mHasServiceBeenStarted = true;
-                return service.getComponent();
-            }
-            throw new UnsupportedOperationException("Unexpected service: " + service);
-        }
-    }
-
-    public DownloadManagerFunctionalTest() {
-        super(DownloadService.class);
-    }
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-
-        Context realContext = getContext();
-        mTestContext = new TestContext(realContext);
-        setupProviderAndResolver();
-        assert isDatabaseEmpty(); // ensure we're not messing with real data
-
-        mTestContext.setResolver(mResolver);
-        setContext(mTestContext);
-        setupService();
-        mSystemFacade = new FakeSystemFacade();
-        getService().mSystemFacade = mSystemFacade;
-
-        mServer = new MockWebServer();
-        mServer.play();
-    }
-
-    private void setupProviderAndResolver() {
-        ContentProvider provider = new DownloadProvider();
-        provider.attachInfo(mTestContext, null);
-        mResolver = new MockContentResolver();
-        mResolver.addProvider(PROVIDER_AUTHORITY, provider);
-    }
-
-    private boolean isDatabaseEmpty() {
-        Cursor cursor = mResolver.query(Downloads.CONTENT_URI, null, null, null, null);
-        try {
-            return cursor.getCount() == 0;
-        } finally {
-            cursor.close();
-        }
-    }
-
-    @Override
-    protected void tearDown() throws Exception {
-        cleanUpDownloads();
-        super.tearDown();
-    }
-
-    /**
-     * Remove any downloaded files and delete any lingering downloads.
-     */
-    private void cleanUpDownloads() {
-        if (mResolver == null) {
-            return;
-        }
-        String[] columns = new String[] {Downloads._DATA};
-        Cursor cursor = mResolver.query(Downloads.CONTENT_URI, columns, null, null, null);
-        try {
-            for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
-                String filePath = cursor.getString(0);
-                if (filePath == null) continue;
-                Log.d(LOG_TAG, "Deleting " + filePath);
-                new File(filePath).delete();
-            }
-        } finally {
-            cursor.close();
-        }
-        mResolver.delete(Downloads.CONTENT_URI, null, null);
-    }
-
+public class DownloadManagerFunctionalTest extends AbstractDownloadManagerFunctionalTest {
     public void testBasicRequest() throws Exception {
         enqueueResponse(HTTP_OK, FILE_CONTENT);
 
@@ -273,7 +104,7 @@ public class DownloadManagerFunctionalTest extends ServiceTestCase<DownloadServi
         // without connectivity, download immediately pauses
         mTestContext.mFakeIConnectivityManager.setNetworkState(NetworkInfo.State.DISCONNECTED);
         startService(null);
-        waitForDownloadToStop(downloadUri, Downloads.STATUS_RUNNING_PAUSED);
+        super.waitForDownloadToStop(getStatusReader(downloadUri), Downloads.STATUS_RUNNING_PAUSED);
 
         // connecting should start the download
         mTestContext.mFakeIConnectivityManager.setNetworkState(NetworkInfo.State.CONNECTED);
@@ -310,47 +141,6 @@ public class DownloadManagerFunctionalTest extends ServiceTestCase<DownloadServi
         assertEquals(FILE_CONTENT, getDownloadContents(downloadUri));
     }
 
-    private void assertStartsWith(String expectedPrefix, String actual) {
-        String regex = "^" + expectedPrefix + ".*";
-        MoreAsserts.assertMatchesRegex(regex, actual);
-    }
-
-    /**
-     * Enqueue a response from the MockWebServer.
-     */
-    private MockResponse enqueueResponse(int status, String body) {
-        MockResponse response = new MockResponse()
-                        .setResponseCode(status)
-                        .setBody(body)
-                        .addHeader("Content-type", "text/plain");
-        mServer.enqueue(response);
-        return response;
-    }
-
-    private MockResponse enqueueEmptyResponse(int status) {
-        return enqueueResponse(status, "");
-    }
-
-    /**
-     * Run the service and wait for a request and for the download to reach the given status.
-     * @return the request received
-     */
-    private RecordedRequest runUntilStatus(Uri downloadUri, int status) throws Exception {
-        startService(null);
-        RecordedRequest request = takeRequest();
-        waitForDownloadToStop(downloadUri, status);
-        return request;
-    }
-
-    /**
-     * Wait for a request to come to the MockWebServer and return it.
-     */
-    private RecordedRequest takeRequest() throws InterruptedException {
-        RecordedRequest request = mServer.takeRequestWithTimeout(REQUEST_TIMEOUT_MILLIS);
-        assertNotNull("Timed out waiting for request", request);
-        return request;
-    }
-
     /**
      * Read a downloaded file from disk.
      */
@@ -363,31 +153,23 @@ public class DownloadManagerFunctionalTest extends ServiceTestCase<DownloadServi
         }
     }
 
-    /**
-     * Wait for a download to given a given status, with a timeout.  Fails if the download reaches
-     * any other final status.
-     */
-    private void waitForDownloadToStop(Uri downloadUri, int expectedStatus) throws Exception {
-        // TODO(showard): find a better way to accomplish this
-        long startTimeMillis = System.currentTimeMillis();
-        int status = getDownloadStatus(downloadUri);
-        while (status != expectedStatus) {
-            if (!Downloads.isStatusInformational(status)) {
-                fail("Download completed with unexpected status: " + status);
-            }
-            if (System.currentTimeMillis() > startTimeMillis + REQUEST_TIMEOUT_MILLIS) {
-                fail("Download timed out with status " + status);
+    private RecordedRequest runUntilStatus(Uri downloadUri, int status) throws Exception {
+        return super.runUntilStatus(getStatusReader(downloadUri), status);
+    }
+
+    private StatusReader getStatusReader(final Uri downloadUri) {
+        return new StatusReader() {
+            public int getStatus() {
+                return getDownloadStatus(downloadUri);
             }
-            Thread.sleep(100);
-            mServer.checkForExceptions();
-            status = getDownloadStatus(downloadUri);
-        }
 
-        long delta = System.currentTimeMillis() - startTimeMillis;
-        Log.d(LOG_TAG, "Status " + status + " reached after " + delta + "ms");
+            public boolean isComplete(int status) {
+                return !Downloads.isStatusInformational(status);
+            }
+        };
     }
 
-    private int getDownloadStatus(Uri downloadUri) {
+    protected int getDownloadStatus(Uri downloadUri) {
         return Integer.valueOf(getDownloadField(downloadUri, Downloads.COLUMN_STATUS));
     }
 
@@ -412,7 +194,7 @@ public class DownloadManagerFunctionalTest extends ServiceTestCase<DownloadServi
      */
     private Uri requestDownload(String path) throws MalformedURLException {
         ContentValues values = new ContentValues();
-        values.put(Downloads.COLUMN_URI, mServer.getUrl(path).toString());
+        values.put(Downloads.COLUMN_URI, getServerUri(path));
         values.put(Downloads.COLUMN_DESTINATION, Downloads.DESTINATION_EXTERNAL);
         return mResolver.insert(Downloads.CONTENT_URI, values);
     }
@@ -426,16 +208,4 @@ public class DownloadManagerFunctionalTest extends ServiceTestCase<DownloadServi
         int numChanged = mResolver.update(downloadUri, values, null, null);
         assertEquals(1, numChanged);
     }
-
-    private String readStream(InputStream inputStream) throws IOException {
-        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
-        try {
-            char[] buffer = new char[1024];
-            int length = reader.read(buffer);
-            assertTrue("Failed to read anything from input stream", length > -1);
-            return String.valueOf(buffer, 0, length);
-        } finally {
-            reader.close();
-        }
-    }
 }
diff --git a/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java b/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
new file mode 100644 (file)
index 0000000..1927545
--- /dev/null
@@ -0,0 +1,238 @@
+/*
+ * 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.providers.downloads;
+
+import android.database.Cursor;
+import android.net.DownloadManager;
+import android.net.Uri;
+import android.os.Environment;
+import tests.http.RecordedRequest;
+
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+
+public class PublicApiFunctionalTest extends AbstractDownloadManagerFunctionalTest {
+    /**
+     *
+     */
+    private static final String REQUEST_PATH = "/path";
+
+    class Download implements StatusReader {
+        final long mId;
+
+        private Download(long downloadId) {
+            this.mId = downloadId;
+        }
+
+        public int getStatus() {
+            return (int) getLongField(DownloadManager.COLUMN_STATUS);
+        }
+
+        public boolean isComplete(int status) {
+            return status != DownloadManager.STATUS_PENDING
+                    && status != DownloadManager.STATUS_RUNNING
+                    && status != DownloadManager.STATUS_PAUSED;
+        }
+
+        String getStringField(String field) {
+            Cursor cursor = mManager.query(new DownloadManager.Query().setFilterById(mId));
+            try {
+                assertEquals(1, cursor.getCount());
+                cursor.moveToFirst();
+                return cursor.getString(cursor.getColumnIndexOrThrow(field));
+            } finally {
+                cursor.close();
+            }
+        }
+
+        long getLongField(String field) {
+            Cursor cursor = mManager.query(new DownloadManager.Query().setFilterById(mId));
+            try {
+                assertEquals(1, cursor.getCount());
+                cursor.moveToFirst();
+                return cursor.getLong(cursor.getColumnIndexOrThrow(field));
+            } finally {
+                cursor.close();
+            }
+        }
+
+        String getContents() throws Exception {
+            InputStream stream = new FileInputStream(
+                    mManager.openDownloadedFile(mId).getFileDescriptor());
+            try {
+                return readStream(stream);
+            } finally {
+                stream.close();
+            }
+        }
+
+        RecordedRequest runUntilStatus(int status) throws Exception {
+            return PublicApiFunctionalTest.this.runUntilStatus(this, status);
+        }
+    }
+
+    private DownloadManager mManager;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mManager = new DownloadManager(mResolver);
+    }
+
+    public void testBasicRequest() throws Exception {
+        enqueueResponse(HTTP_OK, FILE_CONTENT);
+
+        Download download = enqueueRequest(getRequest());
+        assertEquals(DownloadManager.STATUS_PENDING,
+                     download.getLongField(DownloadManager.COLUMN_STATUS));
+        assertEquals(getServerUri(REQUEST_PATH),
+                     download.getStringField(DownloadManager.COLUMN_URI));
+        assertEquals(download.mId, download.getLongField(DownloadManager.COLUMN_ID));
+
+        RecordedRequest request = download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL);
+        assertEquals("GET", request.getMethod());
+        assertEquals(REQUEST_PATH, request.getPath());
+
+        Uri localUri = Uri.parse(download.getStringField(DownloadManager.COLUMN_LOCAL_URI));
+        assertEquals("file", localUri.getScheme());
+        assertStartsWith(Environment.getDownloadCacheDirectory().getPath(),
+                         localUri.getSchemeSpecificPart());
+        assertEquals("text/plain", download.getStringField(DownloadManager.COLUMN_MEDIA_TYPE));
+
+        int size = FILE_CONTENT.length();
+        assertEquals(size, download.getLongField(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
+        assertEquals(size, download.getLongField(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
+
+        assertEquals(FILE_CONTENT, download.getContents());
+    }
+
+    public void testTitleAndDescription() throws Exception {
+        Download download = enqueueRequest(getRequest()
+                                           .setTitle("my title")
+                                           .setDescription("my description"));
+        assertEquals("my title", download.getStringField(DownloadManager.COLUMN_TITLE));
+        assertEquals("my description",
+                     download.getStringField(DownloadManager.COLUMN_DESCRIPTION));
+    }
+
+    public void testDownloadError() throws Exception {
+        enqueueEmptyResponse(HTTP_NOT_FOUND);
+        Download download = enqueueRequest(getRequest());
+        download.runUntilStatus(DownloadManager.STATUS_FAILED);
+        assertEquals(HTTP_NOT_FOUND, download.getLongField(DownloadManager.COLUMN_ERROR_CODE));
+    }
+
+    public void testUnhandledHttpStatus() throws Exception {
+        enqueueEmptyResponse(1234); // some invalid HTTP status
+        Download download = enqueueRequest(getRequest());
+        download.runUntilStatus(DownloadManager.STATUS_FAILED);
+        assertEquals(DownloadManager.ERROR_UNHANDLED_HTTP_CODE,
+                     download.getLongField(DownloadManager.COLUMN_ERROR_CODE));
+    }
+
+    public void testInterruptedDownload() throws Exception {
+        int initialLength = 5;
+        String etag = "my_etag";
+        int totalLength = FILE_CONTENT.length();
+        // the first response has normal headers but unexpectedly closes after initialLength bytes
+        enqueueResponse(HTTP_OK, FILE_CONTENT.substring(0, initialLength))
+                .addHeader("Content-length", totalLength)
+                .addHeader("Etag", etag)
+                .setCloseConnectionAfter(true);
+        Download download = enqueueRequest(getRequest());
+
+        download.runUntilStatus(DownloadManager.STATUS_PAUSED);
+        assertEquals(initialLength,
+                     download.getLongField(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
+
+        mSystemFacade.incrementTimeMillis(RETRY_DELAY_MILLIS);
+        // the second response returns partial content for the rest of the data
+        enqueueResponse(HTTP_PARTIAL_CONTENT, FILE_CONTENT.substring(initialLength))
+                .addHeader("Content-range",
+                           "bytes " + initialLength + "-" + totalLength + "/" + totalLength)
+                .addHeader("Etag", etag);
+        download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL);
+        assertEquals(totalLength,
+                     download.getLongField(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
+    }
+
+    public void testFiltering() throws Exception {
+        enqueueEmptyResponse(HTTP_OK);
+        Download download1 = enqueueRequest(getRequest());
+        download1.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL);
+        enqueueEmptyResponse(HTTP_NOT_FOUND);
+        Download download2 = enqueueRequest(getRequest());
+        download2.runUntilStatus(DownloadManager.STATUS_FAILED);
+        Download download3 = enqueueRequest(getRequest());
+
+        Cursor cursor = mManager.query(new DownloadManager.Query());
+        checkAndCloseCursor(cursor, download1, download2, download3);
+
+        cursor = mManager.query(new DownloadManager.Query().setFilterById(download2.mId));
+        checkAndCloseCursor(cursor, download2);
+
+        cursor = mManager.query(new DownloadManager.Query()
+                                .setFilterByStatus(DownloadManager.STATUS_PENDING));
+        checkAndCloseCursor(cursor, download3);
+
+        cursor = mManager.query(new DownloadManager.Query()
+                                .setFilterByStatus(DownloadManager.STATUS_FAILED
+                                              | DownloadManager.STATUS_SUCCESSFUL));
+        checkAndCloseCursor(cursor, download1, download2);
+
+        cursor = mManager.query(new DownloadManager.Query()
+                                .setFilterByStatus(DownloadManager.STATUS_RUNNING));
+        checkAndCloseCursor(cursor);
+    }
+
+    private void checkAndCloseCursor(Cursor cursor, Download... downloads) {
+        try {
+            int idIndex = cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID);
+            assertEquals(downloads.length, cursor.getCount());
+            cursor.moveToFirst();
+            for (Download download : downloads) {
+                assertEquals(download.mId, cursor.getLong(idIndex));
+                cursor.moveToNext();
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+    public void testInvalidUri() throws Exception {
+        try {
+            enqueueRequest(getRequest("/no_host"));
+        } catch (IllegalArgumentException exc) { // expected
+            return;
+        }
+
+        fail("No exception thrown for invalid URI");
+    }
+
+    private DownloadManager.Request getRequest() throws MalformedURLException {
+        return getRequest(getServerUri(REQUEST_PATH));
+    }
+
+    private DownloadManager.Request getRequest(String path) {
+        return new DownloadManager.Request(Uri.parse(path));
+    }
+
+    private Download enqueueRequest(DownloadManager.Request request) {
+        return new Download(mManager.enqueue(request));
+    }
+}