Support for custom HTTP headers on download requests
Steve Howard [Wed, 14 Jul 2010 18:30:59 +0000 (11:30 -0700)]
Provider changes:
* new many-to-one DB table holding headers for each download.  since
  there was no real migration logic in DownloadProvider, I implemented
  some.
* DownloadProvider.insert() reads request headers out of the
  ContentValues and puts them into the new table
* DownloadProvider.query() supports a new URI form,
  download/#/headers, to fetch the headers associated with a download
* DownloadProvider.delete() removes request headers from this table

Service changes:
* made DownloadInfo store request headers upon initialization.  While
  I was at it, I refactored the initialization logic into DownloadInfo
  to get rid of the massive 24-parameter constructor.  The right next
  step would be to move the update logic into DownloadInfo and merge
  it with the initialization logic; however, I realized that headers
  don't need to be updatable, and in the future, we won't need the
  update logic at all, so i didn't bother touching the update code.
* made DownloadThread read headers from the DownloadInfo and include
  them in the request; merged the custom Cookie and Referer logic into
  this logic

Also added a couple new test cases for this stuff.

Change-Id: I421ce1f0a694e815f2e099795182393650fcb3ff

src/com/android/providers/downloads/DownloadInfo.java
src/com/android/providers/downloads/DownloadProvider.java
src/com/android/providers/downloads/DownloadService.java
src/com/android/providers/downloads/DownloadThread.java
tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java

index 8189543..1ae10ce 100644 (file)
 
 package com.android.providers.downloads;
 
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
+import android.database.Cursor;
 import android.net.Uri;
 import android.provider.Downloads;
 
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
 /**
  * Stores information about an individual download.
  */
@@ -53,39 +59,78 @@ public class DownloadInfo {
     public int mFuzz;
 
     public volatile boolean mHasActiveThread;
+    private Map<String, String> mRequestHeaders = new HashMap<String, String>();
+
+    public DownloadInfo(ContentResolver resolver, Cursor cursor) {
+        int retryRedirect =
+            cursor.getInt(cursor.getColumnIndexOrThrow(Constants.RETRY_AFTER_X_REDIRECT_COUNT));
+        mId = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl._ID));
+        mUri = cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_URI));
+        mNoIntegrity = cursor.getInt(cursor.getColumnIndexOrThrow(
+                                        Downloads.Impl.COLUMN_NO_INTEGRITY)) == 1;
+        mHint = cursor.getString(cursor.getColumnIndexOrThrow(
+                                        Downloads.Impl.COLUMN_FILE_NAME_HINT));
+        mFileName = cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl._DATA));
+        mMimeType = cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_MIME_TYPE));
+        mDestination =
+                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_DESTINATION));
+        mVisibility = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_VISIBILITY));
+        mControl = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_CONTROL));
+        mStatus = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_STATUS));
+        mNumFailed = cursor.getInt(cursor.getColumnIndexOrThrow(Constants.FAILED_CONNECTIONS));
+        mRetryAfter = retryRedirect & 0xfffffff;
+        mRedirectCount = retryRedirect >> 28;
+        mLastMod = cursor.getLong(cursor.getColumnIndexOrThrow(
+                                        Downloads.Impl.COLUMN_LAST_MODIFICATION));
+        mPackage = cursor.getString(cursor.getColumnIndexOrThrow(
+                                        Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE));
+        mClass = cursor.getString(cursor.getColumnIndexOrThrow(
+                                        Downloads.Impl.COLUMN_NOTIFICATION_CLASS));
+        mExtras = cursor.getString(cursor.getColumnIndexOrThrow(
+                                        Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS));
+        mCookies =
+                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_COOKIE_DATA));
+        mUserAgent =
+                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_USER_AGENT));
+        mReferer = cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_REFERER));
+        mTotalBytes =
+                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_TOTAL_BYTES));
+        mCurrentBytes =
+                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_CURRENT_BYTES));
+        mETag = cursor.getString(cursor.getColumnIndexOrThrow(Constants.ETAG));
+        mMediaScanned = cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) == 1;
+        mFuzz = Helpers.sRandom.nextInt(1001);
+
+        readRequestHeaders(resolver, mId);
+    }
+
+    private void readRequestHeaders(ContentResolver resolver, long downloadId) {
+        Uri headerUri = Downloads.Impl.CONTENT_URI.buildUpon()
+                        .appendPath(Long.toString(downloadId))
+                        .appendPath(Downloads.Impl.RequestHeaders.URI_SEGMENT).build();
+        Cursor cursor = resolver.query(headerUri, null, null, null, null);
+        try {
+            int headerIndex =
+                    cursor.getColumnIndexOrThrow(Downloads.Impl.RequestHeaders.COLUMN_HEADER);
+            int valueIndex =
+                    cursor.getColumnIndexOrThrow(Downloads.Impl.RequestHeaders.COLUMN_VALUE);
+            for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
+                mRequestHeaders.put(cursor.getString(headerIndex), cursor.getString(valueIndex));
+            }
+        } finally {
+            cursor.close();
+        }
+
+        if (mCookies != null) {
+            mRequestHeaders.put("Cookie", mCookies);
+        }
+        if (mReferer != null) {
+            mRequestHeaders.put("Referer", mReferer);
+        }
+    }
 
-    public DownloadInfo(int id, String uri, boolean noIntegrity,
-            String hint, String fileName,
-            String mimeType, int destination, int visibility, int control,
-            int status, int numFailed, int retryAfter, int redirectCount, long lastMod,
-            String pckg, String clazz, String extras, String cookies,
-            String userAgent, String referer, int totalBytes, int currentBytes, String eTag,
-            boolean mediaScanned) {
-        mId = id;
-        mUri = uri;
-        mNoIntegrity = noIntegrity;
-        mHint = hint;
-        mFileName = fileName;
-        mMimeType = mimeType;
-        mDestination = destination;
-        mVisibility = visibility;
-        mControl = control;
-        mStatus = status;
-        mNumFailed = numFailed;
-        mRetryAfter = retryAfter;
-        mRedirectCount = redirectCount;
-        mLastMod = lastMod;
-        mPackage = pckg;
-        mClass = clazz;
-        mExtras = extras;
-        mCookies = cookies;
-        mUserAgent = userAgent;
-        mReferer = referer;
-        mTotalBytes = totalBytes;
-        mCurrentBytes = currentBytes;
-        mETag = eTag;
-        mMediaScanned = mediaScanned;
-        mFuzz = Helpers.sRandom.nextInt(1001); 
+    public Map<String, String> getHeaders() {
+        return Collections.unmodifiableMap(mRequestHeaders);
     }
 
     public void sendIntentIfRequested(Uri contentUri, Context context) {
index 7070f31..186b01f 100644 (file)
@@ -16,8 +16,6 @@
 
 package com.android.providers.downloads;
 
-import com.google.common.annotations.VisibleForTesting;
-
 import android.content.ContentProvider;
 import android.content.ContentValues;
 import android.content.Context;
@@ -42,9 +40,12 @@ import android.provider.Downloads;
 import android.util.Config;
 import android.util.Log;
 
+import com.google.common.annotations.VisibleForTesting;
+
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.util.HashSet;
+import java.util.Map;
 
 
 /**
@@ -55,11 +56,7 @@ public final class DownloadProvider extends ContentProvider {
     /** Database filename */
     private static final String DB_NAME = "downloads.db";
     /** Current database version */
-    private static final int DB_VERSION = 100;
-    /** Database version from which upgrading is a nop */
-    private static final int DB_VERSION_NOP_UPGRADE_FROM = 31;
-    /** Database version to which upgrading is a nop */
-    private static final int DB_VERSION_NOP_UPGRADE_TO = 100;
+    private static final int DB_VERSION = 101;
     /** Name of table in the database */
     private static final String DB_TABLE = "downloads";
 
@@ -74,9 +71,13 @@ public final class DownloadProvider extends ContentProvider {
     private static final int DOWNLOADS = 1;
     /** URI matcher constant for the URI of an individual download */
     private static final int DOWNLOADS_ID = 2;
+    /** URI matcher constant for the URI of a download's request headers */
+    private static final int REQUEST_HEADERS_URI = 3;
     static {
         sURIMatcher.addURI("downloads", "download", DOWNLOADS);
         sURIMatcher.addURI("downloads", "download/#", DOWNLOADS_ID);
+        sURIMatcher.addURI("downloads", "download/#/" + Downloads.Impl.RequestHeaders.URI_SEGMENT,
+                           REQUEST_HEADERS_URI);
     }
 
     private static final String[] sAppReadableColumnsArray = new String[] {
@@ -122,7 +123,6 @@ public final class DownloadProvider extends ContentProvider {
      * an updated version of the database.
      */
     private final class DatabaseHelper extends SQLiteOpenHelper {
-
         public DatabaseHelper(final Context context) {
             super(context, DB_NAME, null, DB_VERSION);
         }
@@ -135,40 +135,109 @@ public final class DownloadProvider extends ContentProvider {
             if (Constants.LOGVV) {
                 Log.v(Constants.TAG, "populating new database");
             }
-            createTable(db);
+            onUpgrade(db, 0, DB_VERSION);
         }
 
-        /* (not a javadoc comment)
-         * Checks data integrity when opening the database.
-         */
-        /*
-         * @Override
-         * public void onOpen(final SQLiteDatabase db) {
-         *     super.onOpen(db);
-         * }
-         */
-
         /**
          * Updates the database format when a content provider is used
          * with a database that was created with a different format.
+         *
+         * Note: to support downgrades, creating a table should always drop it first if it already
+         * exists.
          */
-        // Note: technically, this could also be a downgrade, so if we want
-        //       to gracefully handle upgrades we should be careful about
-        //       what to do on downgrades.
         @Override
         public void onUpgrade(final SQLiteDatabase db, int oldV, final int newV) {
-            if (oldV == DB_VERSION_NOP_UPGRADE_FROM) {
-                if (newV == DB_VERSION_NOP_UPGRADE_TO) { // that's a no-op upgrade.
-                    return;
-                }
-                // NOP_FROM and NOP_TO are identical, just in different codelines. Upgrading
-                //     from NOP_FROM is the same as upgrading from NOP_TO.
-                oldV = DB_VERSION_NOP_UPGRADE_TO;
+            if (oldV == 31) {
+                // 31 and 100 are identical, just in different codelines. Upgrading from 31 is the
+                // same as upgrading from 100.
+                oldV = 100;
+            } else if (oldV < 100) {
+                // no logic to upgrade from these older version, just recreate the DB
+                Log.i(Constants.TAG, "Upgrading downloads database from version " + oldV
+                      + " to version " + newV + ", which will destroy all old data");
+                oldV = 99;
+            } else if (oldV > newV) {
+                // user must have downgraded software; we have no way to know how to downgrade the
+                // DB, so just recreate it
+                Log.i(Constants.TAG, "Downgrading downloads database from version " + oldV
+                      + " (current version is " + newV + "), destroying all old data");
+                oldV = 99;
+            }
+
+            for (int version = oldV + 1; version <= newV; version++) {
+                upgradeTo(db, version);
             }
-            Log.i(Constants.TAG, "Upgrading downloads database from version " + oldV + " to " + newV
-                    + ", which will destroy all old data");
-            dropTable(db);
-            createTable(db);
+        }
+
+        /**
+         * Upgrade database from (version - 1) to version.
+         */
+        private void upgradeTo(SQLiteDatabase db, int version) {
+            switch (version) {
+                case 100:
+                    createDownloadsTable(db);
+                    break;
+
+                case 101:
+                    createHeadersTable(db);
+                    break;
+
+                default:
+                    throw new IllegalStateException("Don't know how to upgrade to " + version);
+            }
+        }
+
+        /**
+         * Creates the table that'll hold the download information.
+         */
+        private void createDownloadsTable(SQLiteDatabase db) {
+            try {
+                db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);
+                db.execSQL("CREATE TABLE " + DB_TABLE + "(" +
+                        Downloads.Impl._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+                        Downloads.Impl.COLUMN_URI + " TEXT, " +
+                        Constants.RETRY_AFTER_X_REDIRECT_COUNT + " INTEGER, " +
+                        Downloads.Impl.COLUMN_APP_DATA + " TEXT, " +
+                        Downloads.Impl.COLUMN_NO_INTEGRITY + " BOOLEAN, " +
+                        Downloads.Impl.COLUMN_FILE_NAME_HINT + " TEXT, " +
+                        Constants.OTA_UPDATE + " BOOLEAN, " +
+                        Downloads.Impl._DATA + " TEXT, " +
+                        Downloads.Impl.COLUMN_MIME_TYPE + " TEXT, " +
+                        Downloads.Impl.COLUMN_DESTINATION + " INTEGER, " +
+                        Constants.NO_SYSTEM_FILES + " BOOLEAN, " +
+                        Downloads.Impl.COLUMN_VISIBILITY + " INTEGER, " +
+                        Downloads.Impl.COLUMN_CONTROL + " INTEGER, " +
+                        Downloads.Impl.COLUMN_STATUS + " INTEGER, " +
+                        Constants.FAILED_CONNECTIONS + " INTEGER, " +
+                        Downloads.Impl.COLUMN_LAST_MODIFICATION + " BIGINT, " +
+                        Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE + " TEXT, " +
+                        Downloads.Impl.COLUMN_NOTIFICATION_CLASS + " TEXT, " +
+                        Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS + " TEXT, " +
+                        Downloads.Impl.COLUMN_COOKIE_DATA + " TEXT, " +
+                        Downloads.Impl.COLUMN_USER_AGENT + " TEXT, " +
+                        Downloads.Impl.COLUMN_REFERER + " TEXT, " +
+                        Downloads.Impl.COLUMN_TOTAL_BYTES + " INTEGER, " +
+                        Downloads.Impl.COLUMN_CURRENT_BYTES + " INTEGER, " +
+                        Constants.ETAG + " TEXT, " +
+                        Constants.UID + " INTEGER, " +
+                        Downloads.Impl.COLUMN_OTHER_UID + " INTEGER, " +
+                        Downloads.Impl.COLUMN_TITLE + " TEXT, " +
+                        Downloads.Impl.COLUMN_DESCRIPTION + " TEXT, " +
+                        Constants.MEDIA_SCANNED + " BOOLEAN);");
+            } catch (SQLException ex) {
+                Log.e(Constants.TAG, "couldn't create table in downloads database");
+                throw ex;
+            }
+        }
+
+        private void createHeadersTable(SQLiteDatabase db) {
+            db.execSQL("DROP TABLE IF EXISTS " + Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE);
+            db.execSQL("CREATE TABLE " + Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE + "(" +
+                       "id INTEGER PRIMARY KEY AUTOINCREMENT," +
+                       Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + " INTEGER NOT NULL," +
+                       Downloads.Impl.RequestHeaders.COLUMN_HEADER + " TEXT NOT NULL," +
+                       Downloads.Impl.RequestHeaders.COLUMN_VALUE + " TEXT NOT NULL" +
+                       ");");
         }
     }
 
@@ -224,60 +293,6 @@ public final class DownloadProvider extends ContentProvider {
     }
 
     /**
-     * Creates the table that'll hold the download information.
-     */
-    private void createTable(SQLiteDatabase db) {
-        try {
-            db.execSQL("CREATE TABLE " + DB_TABLE + "(" +
-                    Downloads.Impl._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
-                    Downloads.Impl.COLUMN_URI + " TEXT, " +
-                    Constants.RETRY_AFTER_X_REDIRECT_COUNT + " INTEGER, " +
-                    Downloads.Impl.COLUMN_APP_DATA + " TEXT, " +
-                    Downloads.Impl.COLUMN_NO_INTEGRITY + " BOOLEAN, " +
-                    Downloads.Impl.COLUMN_FILE_NAME_HINT + " TEXT, " +
-                    Constants.OTA_UPDATE + " BOOLEAN, " +
-                    Downloads.Impl._DATA + " TEXT, " +
-                    Downloads.Impl.COLUMN_MIME_TYPE + " TEXT, " +
-                    Downloads.Impl.COLUMN_DESTINATION + " INTEGER, " +
-                    Constants.NO_SYSTEM_FILES + " BOOLEAN, " +
-                    Downloads.Impl.COLUMN_VISIBILITY + " INTEGER, " +
-                    Downloads.Impl.COLUMN_CONTROL + " INTEGER, " +
-                    Downloads.Impl.COLUMN_STATUS + " INTEGER, " +
-                    Constants.FAILED_CONNECTIONS + " INTEGER, " +
-                    Downloads.Impl.COLUMN_LAST_MODIFICATION + " BIGINT, " +
-                    Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE + " TEXT, " +
-                    Downloads.Impl.COLUMN_NOTIFICATION_CLASS + " TEXT, " +
-                    Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS + " TEXT, " +
-                    Downloads.Impl.COLUMN_COOKIE_DATA + " TEXT, " +
-                    Downloads.Impl.COLUMN_USER_AGENT + " TEXT, " +
-                    Downloads.Impl.COLUMN_REFERER + " TEXT, " +
-                    Downloads.Impl.COLUMN_TOTAL_BYTES + " INTEGER, " +
-                    Downloads.Impl.COLUMN_CURRENT_BYTES + " INTEGER, " +
-                    Constants.ETAG + " TEXT, " +
-                    Constants.UID + " INTEGER, " +
-                    Downloads.Impl.COLUMN_OTHER_UID + " INTEGER, " +
-                    Downloads.Impl.COLUMN_TITLE + " TEXT, " +
-                    Downloads.Impl.COLUMN_DESCRIPTION + " TEXT, " +
-                    Constants.MEDIA_SCANNED + " BOOLEAN);");
-        } catch (SQLException ex) {
-            Log.e(Constants.TAG, "couldn't create table in downloads database");
-            throw ex;
-        }
-    }
-
-    /**
-     * Deletes the table that holds the download information.
-     */
-    private void dropTable(SQLiteDatabase db) {
-        try {
-            db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);
-        } catch (SQLException ex) {
-            Log.e(Constants.TAG, "couldn't drop table in downloads database");
-            throw ex;
-        }
-    }
-
-    /**
      * Inserts a row in the database
      */
     @Override
@@ -373,6 +388,7 @@ public final class DownloadProvider extends ContentProvider {
         context.startService(new Intent(context, DownloadService.class));
 
         long rowID = db.insert(DB_TABLE, null, filteredValues);
+        insertRequestHeaders(db, rowID, values);
 
         Uri ret = null;
 
@@ -413,10 +429,16 @@ public final class DownloadProvider extends ContentProvider {
             case DOWNLOADS_ID: {
                 qb.setTables(DB_TABLE);
                 qb.appendWhere(Downloads.Impl._ID + "=");
-                qb.appendWhere(uri.getPathSegments().get(1));
+                qb.appendWhere(getDownloadIdFromUri(uri));
                 emptyWhere = false;
                 break;
             }
+            case REQUEST_HEADERS_URI:
+                if (projection != null || selection != null || sort != null) {
+                    throw new UnsupportedOperationException("Request header queries do not support "
+                                                            + "projections, selections or sorting");
+                }
+                return queryRequestHeaders(db, uri);
             default: {
                 if (Constants.LOGV) {
                     Log.v(Constants.TAG, "querying unknown URI: " + uri);
@@ -425,11 +447,7 @@ public final class DownloadProvider extends ContentProvider {
             }
         }
 
-        int callingUid = Binder.getCallingUid();
-        if (Binder.getCallingPid() != Process.myPid() &&
-                callingUid != mSystemUid &&
-                callingUid != mDefContainerUid &&
-                Process.supportsProcesses()) {
+        if (shouldRestrictVisibility()) {
             boolean canSeeAllExternal;
             if (projection == null) {
                 projection = sAppReadableColumnsArray;
@@ -530,6 +548,72 @@ public final class DownloadProvider extends ContentProvider {
         return ret;
     }
 
+    private String getDownloadIdFromUri(final Uri uri) {
+        return uri.getPathSegments().get(1);
+    }
+
+    /**
+     * Insert request headers for a download into the DB.
+     */
+    private void insertRequestHeaders(SQLiteDatabase db, long downloadId, ContentValues values) {
+        ContentValues rowValues = new ContentValues();
+        rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID, downloadId);
+        for (Map.Entry<String, Object> entry : values.valueSet()) {
+            String key = entry.getKey();
+            if (key.startsWith(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX)) {
+                String headerLine = entry.getValue().toString();
+                if (!headerLine.contains(":")) {
+                    throw new IllegalArgumentException("Invalid HTTP header line: " + headerLine);
+                }
+                String[] parts = headerLine.split(":", 2);
+                rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_HEADER, parts[0].trim());
+                rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_VALUE, parts[1].trim());
+                db.insert(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, null, rowValues);
+            }
+        }
+    }
+
+    /**
+     * Handle a query for the custom request headers registered for a download.
+     */
+    private Cursor queryRequestHeaders(SQLiteDatabase db, Uri uri) {
+        String where = Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "="
+                       + getDownloadIdFromUri(uri);
+        String[] projection = new String[] {Downloads.Impl.RequestHeaders.COLUMN_HEADER,
+                                            Downloads.Impl.RequestHeaders.COLUMN_VALUE};
+        Cursor cursor = db.query(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, projection, where,
+                                 null, null, null, null);
+        return new ReadOnlyCursorWrapper(cursor);
+    }
+
+    /**
+     * Delete request headers for downloads matching the given query.
+     */
+    private void deleteRequestHeaders(SQLiteDatabase db, String where, String[] whereArgs) {
+        String[] projection = new String[] {Downloads.Impl._ID};
+        Cursor cursor = db.query(DB_TABLE, projection , where, whereArgs, null, null, null, null);
+        try {
+            for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
+                long id = cursor.getLong(0);
+                String idWhere = Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "=" + id;
+                db.delete(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, idWhere, null);
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+    /**
+     * @return true if we should restrict this call to viewing only its own downloads
+     */
+    private boolean shouldRestrictVisibility() {
+        int callingUid = Binder.getCallingUid();
+        return Binder.getCallingPid() != Process.myPid() &&
+                callingUid != mSystemUid &&
+                callingUid != mDefContainerUid &&
+                Process.supportsProcesses();
+    }
+
     /**
      * Updates a row in the database
      */
@@ -586,7 +670,7 @@ public final class DownloadProvider extends ContentProvider {
                     myWhere = "";
                 }
                 if (match == DOWNLOADS_ID) {
-                    String segment = uri.getPathSegments().get(1);
+                    String segment = getDownloadIdFromUri(uri);
                     rowId = Long.parseLong(segment);
                     myWhere += " ( " + Downloads.Impl._ID + " = " + rowId + " ) ";
                 }
@@ -645,7 +729,7 @@ public final class DownloadProvider extends ContentProvider {
                     myWhere = "";
                 }
                 if (match == DOWNLOADS_ID) {
-                    String segment = uri.getPathSegments().get(1);
+                    String segment = getDownloadIdFromUri(uri);
                     long rowId = Long.parseLong(segment);
                     myWhere += " ( " + Downloads.Impl._ID + " = " + rowId + " ) ";
                 }
@@ -657,6 +741,7 @@ public final class DownloadProvider extends ContentProvider {
                             + Downloads.Impl.COLUMN_OTHER_UID + "="
                             +  Binder.getCallingUid() + " )";
                 }
+                deleteRequestHeaders(db, where, whereArgs);
                 count = db.delete(DB_TABLE, myWhere, whereArgs);
                 break;
             }
index f3bc958..2e713fb 100644 (file)
@@ -555,41 +555,7 @@ public class DownloadService extends Service {
     private void insertDownload(
             Cursor cursor, int arrayPos,
             boolean networkAvailable, boolean networkRoaming, long now) {
-        int statusColumn = cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_STATUS);
-        int failedColumn = cursor.getColumnIndexOrThrow(Constants.FAILED_CONNECTIONS);
-        int retryRedirect =
-                cursor.getInt(cursor.getColumnIndexOrThrow(Constants.RETRY_AFTER_X_REDIRECT_COUNT));
-        DownloadInfo info = new DownloadInfo(
-                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl._ID)),
-                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_URI)),
-                cursor.getInt(cursor.getColumnIndexOrThrow(
-                        Downloads.Impl.COLUMN_NO_INTEGRITY)) == 1,
-                cursor.getString(cursor.getColumnIndexOrThrow(
-                        Downloads.Impl.COLUMN_FILE_NAME_HINT)),
-                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl._DATA)),
-                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_MIME_TYPE)),
-                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_DESTINATION)),
-                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_VISIBILITY)),
-                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_CONTROL)),
-                cursor.getInt(statusColumn),
-                cursor.getInt(failedColumn),
-                retryRedirect & 0xfffffff,
-                retryRedirect >> 28,
-                cursor.getLong(cursor.getColumnIndexOrThrow(
-                        Downloads.Impl.COLUMN_LAST_MODIFICATION)),
-                cursor.getString(cursor.getColumnIndexOrThrow(
-                        Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE)),
-                cursor.getString(cursor.getColumnIndexOrThrow(
-                        Downloads.Impl.COLUMN_NOTIFICATION_CLASS)),
-                cursor.getString(cursor.getColumnIndexOrThrow(
-                        Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS)),
-                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_COOKIE_DATA)),
-                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_USER_AGENT)),
-                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_REFERER)),
-                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_TOTAL_BYTES)),
-                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_CURRENT_BYTES)),
-                cursor.getString(cursor.getColumnIndexOrThrow(Constants.ETAG)),
-                cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) == 1);
+        DownloadInfo info = new DownloadInfo(getContentResolver(), cursor);
 
         if (Constants.LOGVV) {
             Log.v(Constants.TAG, "Service adding new entry");
index f1296b8..8188eaa 100644 (file)
 
 package com.android.providers.downloads;
 
-import org.apache.http.Header;
-import org.apache.http.HttpResponse;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.client.HttpClient;
-import org.apache.http.entity.StringEntity;
-
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
@@ -37,9 +31,11 @@ import android.provider.DrmStore;
 import android.util.Config;
 import android.util.Log;
 
+import org.apache.http.Header;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
 
 import java.io.File;
-import java.io.FileDescriptor;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
@@ -48,6 +44,7 @@ import java.io.SyncFailedException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.Locale;
+import java.util.Map;
 
 /**
  * Runs an actual download
@@ -189,12 +186,8 @@ http_request_loop:
                     Log.v(Constants.TAG, "initiating download for " + mInfo.mUri);
                 }
 
-                if (mInfo.mCookies != null) {
-                    request.addHeader("Cookie", mInfo.mCookies);
-                }
-                if (mInfo.mReferer != null) {
-                    request.addHeader("Referer", mInfo.mReferer);
-                }
+                addRequestHeaders(request);
+
                 if (continuingDownload) {
                     if (headerETag != null) {
                         request.addHeader("If-Match", headerETag);
@@ -728,6 +721,15 @@ http_request_loop:
     }
 
     /**
+     * Add custom headers for this download to the HTTP request.
+     */
+    private void addRequestHeaders(HttpGet request) {
+        for (Map.Entry<String, String> header : mInfo.getHeaders().entrySet()) {
+            request.addHeader(header.getKey(), header.getValue());
+        }
+    }
+
+    /**
      * Stores information about the completed download, and notifies the initiating application.
      */
     private void notifyDownloadCompleted(
index e3b278b..e919560 100644 (file)
@@ -20,6 +20,7 @@ import android.database.Cursor;
 import android.net.DownloadManager;
 import android.net.Uri;
 import android.os.Environment;
+import android.util.Log;
 import tests.http.RecordedRequest;
 
 import java.io.File;
@@ -271,6 +272,27 @@ public class PublicApiFunctionalTest extends AbstractDownloadManagerFunctionalTe
         }
     }
 
+    public void testRequestHeaders() throws Exception {
+        enqueueEmptyResponse(HTTP_OK);
+        Download download = enqueueRequest(getRequest().setRequestHeader("Header1", "value1")
+                                           .setRequestHeader("Header2", "value2"));
+        RecordedRequest request = download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL);
+
+        assertTrue(request.getHeaders().contains("Header1: value1"));
+        assertTrue(request.getHeaders().contains("Header2: value2"));
+    }
+
+    public void testDelete() throws Exception {
+        Download download = enqueueRequest(getRequest().setRequestHeader("header", "value"));
+        mManager.remove(download.mId);
+        Cursor cursor = mManager.query(new DownloadManager.Query());
+        try {
+            assertEquals(0, cursor.getCount());
+        } finally {
+            cursor.close();
+        }
+    }
+
     private DownloadManager.Request getRequest() throws MalformedURLException {
         return getRequest(getServerUri(REQUEST_PATH));
     }