New URI structure with "my_downloads" and "all_downloads"
Steve Howard [Mon, 13 Sep 2010 01:53:31 +0000 (18:53 -0700)]
This change introduces a second view into the download manager
database via a set of URIs starting with /all_downloads, renaming the
original /download URIs to /my_downloads.  In addition to making
things more clear, this change allows the downloads UI to grant
permissions on individual downloads to viewer apps.

The old semantics were:

* for ordinary callers, /download included only downloads initiated by
  the calling UID
* for intraprocess calls or calls by root, /download included all
  downloads

The new semantics are

* /my_downloads always includes only downloads initiated by the
  calling UID, and requires only INTERNET permission.  It could just
  as well require no permission, but that's not possible in the
  framework, since path-permissions can only broaden access, not
  tighten it.  It doesn't matter, because these URIs are useless
  without INTERNET permission -- if a user can't initiate downloads,
  there's no reason to read this.
* /all_downloads always includes all downloads on the system, and
  requires the new permission ACCESS_ALL_DOWNLOADS.  This permission
  is currently protectionLevel=signature -- this could be relaxed
  later to support third-party download managers.

All download manager code has been changed to use /all_downloads URIs,
except when passing a URI to another app.  In making this change
across the download manager code, I've taken some liberties in
cleaning things up.  Other apps are unchanged and will use
/my_downloads.

Finally, this incorporates changes to DownloadManager to return a
content URI for /cache downloads -- the download UI no longer assumes
it's a file URI, and it grants permissions to the receiver of the VIEW
intent.  The public API test has also been updated.

I've also fixed some null cursor checking in DownloadManager.

Change-Id: I05a501eb4388249fe80c43724405657c950d7238

12 files changed:
AndroidManifest.xml
res/values/strings.xml
src/com/android/providers/downloads/DownloadInfo.java
src/com/android/providers/downloads/DownloadNotification.java
src/com/android/providers/downloads/DownloadProvider.java
src/com/android/providers/downloads/DownloadService.java
src/com/android/providers/downloads/DownloadThread.java
src/com/android/providers/downloads/Helpers.java
tests/src/com/android/providers/downloads/AbstractDownloadManagerFunctionalTest.java
tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
ui/AndroidManifest.xml
ui/src/com/android/providers/downloads/ui/DownloadList.java

index 8431d1e..9da6fc8 100644 (file)
         android:description="@string/permdesc_downloadWithoutNotification"
         android:protectionLevel="signatureOrSystem"/>
 
+    <!-- Allows an app to access all downloads in the system via the /all_downloads/ URIs.  The
+         protection level could be relaxed in the future to support third-party download
+         managers. -->
+    <permission android:name="android.permission.ACCESS_ALL_DOWNLOADS"
+        android:label="@string/permlab_accessAllDownloads"
+        android:description="@string/permdesc_accessAllDownloads"
+        android:protectionLevel="signature"/>
+
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
     <uses-permission android:name="android.permission.ACCESS_DOWNLOAD_MANAGER" />
     <uses-permission android:name="android.permission.ACCESS_DRM" />
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.INSTALL_DRM" />
+    <uses-permission android:name="android.permission.ACCESS_ALL_DOWNLOADS" />
 
     <application android:process="android.process.media"
                  android:label="@string/app_label">
         <provider android:name=".DownloadProvider"
-                android:authorities="downloads" />
+                  android:authorities="downloads"
+                  android:permission="android.permission.ACCESS_ALL_DOWNLOADS">
+          <!-- Anyone can access /my_downloads, the provider internally restricts access by UID for
+               these URIs -->
+          <path-permission android:pathPrefix="/my_downloads"
+                           android:permission="android.permission.INTERNET"/>
+          <!-- Apps with access to /all_downloads/... can grant permissions, allowing them to share
+               downloaded files with other viewers -->
+          <grant-uri-permission android:pathPrefix="/all_downloads/"/>
+        </provider>
         <service android:name=".DownloadService"
                 android:permission="android.permission.ACCESS_DOWNLOAD_MANAGER" />
         <receiver android:name=".DownloadReceiver" android:exported="false">
index b0d95ce..1623fbe 100644 (file)
     to download files through the download manager without any notification
     being shown to the user.</string>
 
+    <!-- The label for the permission to access all downloads in the download
+    manager, not just those owned by the calling user [CHAR LIMIT=50] -->
+    <string name="permlab_accessAllDownloads">Access all system
+    downloads</string>
+
+    <!-- The full sentence description for the permission to access all
+    downloads in the download manager, not just those owned by the calling user
+    [CHAR LIMIT=160] -->
+    <string name="permdesc_accessAllDownloads">Allows the application to view
+    and modify all initiated by any application on the system.</string>
+
 
     <!-- This is the title that is used when displaying the notification
     for a download that doesn't have a title associated with it. -->
index 4380059..0cf025b 100644 (file)
@@ -131,9 +131,8 @@ public class DownloadInfo {
     }
 
     private void readRequestHeaders(long downloadId) {
-        Uri headerUri = Downloads.Impl.CONTENT_URI.buildUpon()
-                        .appendPath(Long.toString(downloadId))
-                        .appendPath(Downloads.Impl.RequestHeaders.URI_SEGMENT).build();
+        Uri headerUri = Uri.withAppendedPath(
+                getAllDownloadsUri(), Downloads.Impl.RequestHeaders.URI_SEGMENT);
         Cursor cursor = mContext.getContentResolver().query(headerUri, null, null, null, null);
         try {
             int headerIndex =
@@ -159,7 +158,7 @@ public class DownloadInfo {
         return Collections.unmodifiableMap(mRequestHeaders);
     }
 
-    public void sendIntentIfRequested(Uri contentUri) {
+    public void sendIntentIfRequested() {
         if (mPackage == null) {
             return;
         }
@@ -181,7 +180,7 @@ public class DownloadInfo {
             // We only send the content: URI, for security reasons. Otherwise, malicious
             //     applications would have an easier time spoofing download results by
             //     sending spoofed intents.
-            intent.setData(contentUri);
+            intent.setData(getMyDownloadsUri());
         }
         mSystemFacade.sendBroadcast(intent);
     }
@@ -374,9 +373,7 @@ public class DownloadInfo {
             mStatus = Impl.STATUS_RUNNING;
             ContentValues values = new ContentValues();
             values.put(Impl.COLUMN_STATUS, mStatus);
-            mContext.getContentResolver().update(
-                    ContentUris.withAppendedId(Impl.CONTENT_URI, mId),
-                    values, null, null);
+            mContext.getContentResolver().update(getAllDownloadsUri(), values, null, null);
         }
         DownloadThread downloader = new DownloadThread(mContext, mSystemFacade, this);
         mHasActiveThread = true;
@@ -388,4 +385,12 @@ public class DownloadInfo {
                 || mDestination == Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING
                 || mDestination == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE);
     }
+
+    public Uri getMyDownloadsUri() {
+        return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, mId);
+    }
+
+    public Uri getAllDownloadsUri() {
+        return ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, mId);
+    }
 }
index 472a5f3..38def59 100644 (file)
@@ -18,6 +18,7 @@ package com.android.providers.downloads;
 
 import android.app.Notification;
 import android.app.PendingIntent;
+import android.content.ContentUris;
 import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
@@ -194,7 +195,7 @@ class DownloadNotification {
             Intent intent = new Intent(Constants.ACTION_LIST);
             intent.setClassName("com.android.providers.downloads",
                     DownloadReceiver.class.getName());
-            intent.setData(Uri.parse(Downloads.Impl.CONTENT_URI + "/" + item.mId));
+            intent.setData(ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, item.mId));
             intent.putExtra("multiple", item.mTitleCount > 1);
 
             n.contentIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
@@ -223,7 +224,7 @@ class DownloadNotification {
                 title = mContext.getResources().getString(
                         R.string.download_unknown_title);
             }
-            Uri contentUri = Uri.parse(Downloads.Impl.CONTENT_URI + "/" + id);
+            Uri contentUri = ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, id);
             String caption;
             Intent intent;
             if (Downloads.Impl.isStatusError(download.mStatus)) {
index d957989..17f3d81 100644 (file)
@@ -17,6 +17,7 @@
 package com.android.providers.downloads;
 
 import android.content.ContentProvider;
+import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
@@ -54,7 +55,6 @@ import java.util.Map;
  * Allows application to interact with the download manager.
  */
 public final class DownloadProvider extends ContentProvider {
-
     /** Database filename */
     private static final String DB_NAME = "downloads.db";
     /** Current database version */
@@ -69,19 +69,35 @@ public final class DownloadProvider extends ContentProvider {
 
     /** URI matcher used to recognize URIs sent by applications */
     private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
-    /** URI matcher constant for the URI of the entire download list */
-    private static final int DOWNLOADS = 1;
+    /** URI matcher constant for the URI of all downloads belonging to the calling UID */
+    private static final int MY_DOWNLOADS = 1;
+    /** URI matcher constant for the URI of an individual download belonging to the calling UID */
+    private static final int MY_DOWNLOADS_ID = 2;
+    /** URI matcher constant for the URI of all downloads in the system */
+    private static final int ALL_DOWNLOADS = 3;
     /** URI matcher constant for the URI of an individual download */
-    private static final int DOWNLOADS_ID = 2;
+    private static final int ALL_DOWNLOADS_ID = 4;
     /** URI matcher constant for the URI of a download's request headers */
-    private static final int REQUEST_HEADERS_URI = 3;
+    private static final int REQUEST_HEADERS_URI = 5;
     static {
-        sURIMatcher.addURI("downloads", "download", DOWNLOADS);
-        sURIMatcher.addURI("downloads", "download/#", DOWNLOADS_ID);
-        sURIMatcher.addURI("downloads", "download/#/" + Downloads.Impl.RequestHeaders.URI_SEGMENT,
-                           REQUEST_HEADERS_URI);
+        sURIMatcher.addURI("downloads", "my_downloads", MY_DOWNLOADS);
+        sURIMatcher.addURI("downloads", "my_downloads/#", MY_DOWNLOADS_ID);
+        sURIMatcher.addURI("downloads", "all_downloads", ALL_DOWNLOADS);
+        sURIMatcher.addURI("downloads", "all_downloads/#", ALL_DOWNLOADS_ID);
+        sURIMatcher.addURI("downloads",
+                "my_downloads/#/" + Downloads.Impl.RequestHeaders.URI_SEGMENT,
+                REQUEST_HEADERS_URI);
+        sURIMatcher.addURI("downloads",
+                "all_downloads/#/" + Downloads.Impl.RequestHeaders.URI_SEGMENT,
+                REQUEST_HEADERS_URI);
     }
 
+    /** Different base URIs that could be used to access an individual download */
+    private static final Uri[] BASE_URIS = new Uri[] {
+            Downloads.Impl.CONTENT_URI,
+            Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
+    };
+
     private static final String[] sAppReadableColumnsArray = new String[] {
         Downloads.Impl._ID,
         Downloads.Impl.COLUMN_APP_DATA,
@@ -319,10 +335,10 @@ public final class DownloadProvider extends ContentProvider {
     public String getType(final Uri uri) {
         int match = sURIMatcher.match(uri);
         switch (match) {
-            case DOWNLOADS: {
+            case MY_DOWNLOADS: {
                 return DOWNLOAD_LIST_TYPE;
             }
-            case DOWNLOADS_ID: {
+            case MY_DOWNLOADS_ID: {
                 return DOWNLOAD_TYPE;
             }
             default: {
@@ -342,10 +358,10 @@ public final class DownloadProvider extends ContentProvider {
         checkInsertPermissions(values);
         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
 
-        if (sURIMatcher.match(uri) != DOWNLOADS) {
-            if (Config.LOGD) {
-                Log.d(Constants.TAG, "calling insert on an unknown/invalid URI: " + uri);
-            }
+        // note we disallow inserting into ALL_DOWNLOADS
+        int match = sURIMatcher.match(uri);
+        if (match != MY_DOWNLOADS) {
+            Log.d(Constants.TAG, "calling insert on an unknown/invalid URI: " + uri);
             throw new IllegalArgumentException("Unknown/Invalid URI " + uri);
         }
 
@@ -463,21 +479,15 @@ 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;
-
-        if (rowID != -1) {
-            context.startService(new Intent(context, DownloadService.class));
-            ret = Uri.parse(Downloads.Impl.CONTENT_URI + "/" + rowID);
-            context.getContentResolver().notifyChange(uri, null);
-        } else {
-            if (Config.LOGD) {
-                Log.d(Constants.TAG, "couldn't insert into downloads database");
-            }
+        if (rowID == -1) {
+            Log.d(Constants.TAG, "couldn't insert into downloads database");
+            return null;
         }
 
-        return ret;
+        insertRequestHeaders(db, rowID, values);
+        context.startService(new Intent(context, DownloadService.class));
+        notifyContentChanged(uri, match);
+        return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID);
     }
 
     /**
@@ -600,33 +610,27 @@ public final class DownloadProvider extends ContentProvider {
         SQLiteDatabase db = mOpenHelper.getReadableDatabase();
 
         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+        qb.setTables(DB_TABLE);
 
         int match = sURIMatcher.match(uri);
-        boolean emptyWhere = true;
-        switch (match) {
-            case DOWNLOADS: {
-                qb.setTables(DB_TABLE);
-                break;
-            }
-            case DOWNLOADS_ID: {
-                qb.setTables(DB_TABLE);
-                qb.appendWhere(Downloads.Impl._ID + "=");
-                qb.appendWhere(getDownloadIdFromUri(uri));
-                emptyWhere = false;
-                break;
+        if (match == -1) {
+            if (Constants.LOGV) {
+                Log.v(Constants.TAG, "querying unknown URI: " + uri);
             }
-            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);
-                }
-                throw new IllegalArgumentException("Unknown URI: " + uri);
+            throw new IllegalArgumentException("Unknown URI: " + uri);
+        }
+
+        if (match == 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);
+        }
+
+        String where = getWhereClause(uri, null, match);
+        if (!where.isEmpty()) {
+            qb.appendWhere(where);
         }
 
         if (shouldRestrictVisibility()) {
@@ -640,53 +644,10 @@ public final class DownloadProvider extends ContentProvider {
                     }
                 }
             }
-            if (!emptyWhere) {
-                qb.appendWhere(" AND ");
-                emptyWhere = false;
-            }
-            qb.appendWhere(getRestrictedUidClause());
         }
 
         if (Constants.LOGVV) {
-            java.lang.StringBuilder sb = new java.lang.StringBuilder();
-            sb.append("starting query, database is ");
-            if (db != null) {
-                sb.append("not ");
-            }
-            sb.append("null; ");
-            if (projection == null) {
-                sb.append("projection is null; ");
-            } else if (projection.length == 0) {
-                sb.append("projection is empty; ");
-            } else {
-                for (int i = 0; i < projection.length; ++i) {
-                    sb.append("projection[");
-                    sb.append(i);
-                    sb.append("] is ");
-                    sb.append(projection[i]);
-                    sb.append("; ");
-                }
-            }
-            sb.append("selection is ");
-            sb.append(selection);
-            sb.append("; ");
-            if (selectionArgs == null) {
-                sb.append("selectionArgs is null; ");
-            } else if (selectionArgs.length == 0) {
-                sb.append("selectionArgs is empty; ");
-            } else {
-                for (int i = 0; i < selectionArgs.length; ++i) {
-                    sb.append("selectionArgs[");
-                    sb.append(i);
-                    sb.append("] is ");
-                    sb.append(selectionArgs[i]);
-                    sb.append("; ");
-                }
-            }
-            sb.append("sort is ");
-            sb.append(sort);
-            sb.append(".");
-            Log.v(Constants.TAG, sb.toString());
+            logVerboseQueryInfo(projection, selection, selectionArgs, sort, db);
         }
 
         Cursor ret = qb.query(db, projection, selection, selectionArgs,
@@ -711,6 +672,49 @@ public final class DownloadProvider extends ContentProvider {
         return ret;
     }
 
+    private void logVerboseQueryInfo(String[] projection, final String selection,
+            final String[] selectionArgs, final String sort, SQLiteDatabase db) {
+        java.lang.StringBuilder sb = new java.lang.StringBuilder();
+        sb.append("starting query, database is ");
+        if (db != null) {
+            sb.append("not ");
+        }
+        sb.append("null; ");
+        if (projection == null) {
+            sb.append("projection is null; ");
+        } else if (projection.length == 0) {
+            sb.append("projection is empty; ");
+        } else {
+            for (int i = 0; i < projection.length; ++i) {
+                sb.append("projection[");
+                sb.append(i);
+                sb.append("] is ");
+                sb.append(projection[i]);
+                sb.append("; ");
+            }
+        }
+        sb.append("selection is ");
+        sb.append(selection);
+        sb.append("; ");
+        if (selectionArgs == null) {
+            sb.append("selectionArgs is null; ");
+        } else if (selectionArgs.length == 0) {
+            sb.append("selectionArgs is empty; ");
+        } else {
+            for (int i = 0; i < selectionArgs.length; ++i) {
+                sb.append("selectionArgs[");
+                sb.append(i);
+                sb.append("] is ");
+                sb.append(selectionArgs[i]);
+                sb.append("; ");
+            }
+        }
+        sb.append("sort is ");
+        sb.append(sort);
+        sb.append(".");
+        Log.v(Constants.TAG, sb.toString());
+    }
+
     private String getDownloadIdFromUri(final Uri uri) {
         return uri.getPathSegments().get(1);
     }
@@ -767,7 +771,7 @@ public final class DownloadProvider extends ContentProvider {
     }
 
     /**
-     * @return true if we should restrict this caller to viewing only its own downloads
+     * @return true if we should restrict the columns readable by this caller
      */
     private boolean shouldRestrictVisibility() {
         int callingUid = Binder.getCallingUid();
@@ -831,26 +835,27 @@ public final class DownloadProvider extends ContentProvider {
                 startService = true;
             }
         }
+
         int match = sURIMatcher.match(uri);
         switch (match) {
-            case DOWNLOADS:
-            case DOWNLOADS_ID: {
-                String fullWhere = getWhereClause(uri, where);
+            case MY_DOWNLOADS:
+            case MY_DOWNLOADS_ID:
+            case ALL_DOWNLOADS:
+            case ALL_DOWNLOADS_ID:
+                String fullWhere = getWhereClause(uri, where, match);
                 if (filteredValues.size() > 0) {
                     count = db.update(DB_TABLE, filteredValues, fullWhere, whereArgs);
                 } else {
                     count = 0;
                 }
                 break;
-            }
-            default: {
-                if (Config.LOGD) {
-                    Log.d(Constants.TAG, "updating unknown/invalid URI: " + uri);
-                }
+
+            default:
+                Log.d(Constants.TAG, "updating unknown/invalid URI: " + uri);
                 throw new UnsupportedOperationException("Cannot update URI: " + uri);
-            }
         }
-        getContext().getContentResolver().notifyChange(uri, null);
+
+        notifyContentChanged(uri, match);
         if (startService) {
             Context context = getContext();
             context.startService(new Intent(context, DownloadService.class));
@@ -858,17 +863,34 @@ public final class DownloadProvider extends ContentProvider {
         return count;
     }
 
-    private String getWhereClause(final Uri uri, final String where) {
+    /**
+     * Notify of a change through both URIs (/my_downloads and /all_downloads)
+     * @param uri either URI for the changed download(s)
+     * @param uriMatch the match ID from {@link #sURIMatcher}
+     */
+    private void notifyContentChanged(final Uri uri, int uriMatch) {
+        Long downloadId = null;
+        if (uriMatch == MY_DOWNLOADS_ID || uriMatch == ALL_DOWNLOADS_ID) {
+            downloadId = Long.parseLong(getDownloadIdFromUri(uri));
+        }
+        for (Uri uriToNotify : BASE_URIS) {
+            if (downloadId != null) {
+                uriToNotify = ContentUris.withAppendedId(uriToNotify, downloadId);
+            }
+            getContext().getContentResolver().notifyChange(uriToNotify, null);
+        }
+    }
+
+    private String getWhereClause(final Uri uri, final String where, int uriMatch) {
         StringBuilder myWhere = new StringBuilder();
         if (where != null) {
             myWhere.append("( " + where + " )");
         }
-        if (sURIMatcher.match(uri) == DOWNLOADS_ID) {
-            String segment = getDownloadIdFromUri(uri);
-            long rowId = Long.parseLong(segment);
-            appendClause(myWhere, " ( " + Downloads.Impl._ID + " = " + rowId + " ) ");
+        if (uriMatch == MY_DOWNLOADS_ID || uriMatch == ALL_DOWNLOADS_ID) {
+            appendClause(myWhere,
+                    " ( " + Downloads.Impl._ID + " = " + getDownloadIdFromUri(uri) + " ) ");
         }
-        if (shouldRestrictVisibility()) {
+        if (uriMatch == MY_DOWNLOADS || uriMatch == MY_DOWNLOADS_ID) {
             appendClause(myWhere, getRestrictedUidClause());
         }
         return myWhere.toString();
@@ -887,21 +909,20 @@ public final class DownloadProvider extends ContentProvider {
         int count;
         int match = sURIMatcher.match(uri);
         switch (match) {
-            case DOWNLOADS:
-            case DOWNLOADS_ID: {
-                String fullWhere = getWhereClause(uri, where);
+            case MY_DOWNLOADS:
+            case MY_DOWNLOADS_ID:
+            case ALL_DOWNLOADS:
+            case ALL_DOWNLOADS_ID:
+                String fullWhere = getWhereClause(uri, where, match);
                 deleteRequestHeaders(db, fullWhere, whereArgs);
                 count = db.delete(DB_TABLE, fullWhere, whereArgs);
                 break;
-            }
-            default: {
-                if (Config.LOGD) {
-                    Log.d(Constants.TAG, "deleting unknown/invalid URI: " + uri);
-                }
+
+            default:
+                Log.d(Constants.TAG, "deleting unknown/invalid URI: " + uri);
                 throw new UnsupportedOperationException("Cannot delete URI: " + uri);
-            }
         }
-        getContext().getContentResolver().notifyChange(uri, null);
+        notifyContentChanged(uri, match);
         return count;
     }
 
@@ -916,71 +937,41 @@ public final class DownloadProvider extends ContentProvider {
      * Remotely opens a file
      */
     @Override
-    public ParcelFileDescriptor openFile(Uri uri, String mode)
-            throws FileNotFoundException {
+    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
         if (Constants.LOGVV) {
-            Log.v(Constants.TAG, "openFile uri: " + uri + ", mode: " + mode
-                    + ", uid: " + Binder.getCallingUid());
-            Cursor cursor = query(Downloads.Impl.CONTENT_URI,
-                    new String[] { "_id" }, null, null, "_id");
-            if (cursor == null) {
-                Log.v(Constants.TAG, "null cursor in openFile");
-            } else {
-                if (!cursor.moveToFirst()) {
-                    Log.v(Constants.TAG, "empty cursor in openFile");
-                } else {
-                    do {
-                        Log.v(Constants.TAG, "row " + cursor.getInt(0) + " available");
-                    } while(cursor.moveToNext());
-                }
-                cursor.close();
-            }
-            cursor = query(uri, new String[] { "_data" }, null, null, null);
-            if (cursor == null) {
-                Log.v(Constants.TAG, "null cursor in openFile");
-            } else {
-                if (!cursor.moveToFirst()) {
-                    Log.v(Constants.TAG, "empty cursor in openFile");
-                } else {
-                    String filename = cursor.getString(0);
-                    Log.v(Constants.TAG, "filename in openFile: " + filename);
-                    if (new java.io.File(filename).isFile()) {
-                        Log.v(Constants.TAG, "file exists in openFile");
-                    }
-                }
-               cursor.close();
-            }
+            logVerboseOpenFileInfo(uri, mode);
         }
 
-        // This logic is mostly copied form openFileHelper. If openFileHelper eventually
-        //     gets split into small bits (to extract the filename and the modebits),
-        //     this code could use the separate bits and be deeply simplified.
-        Cursor c = query(uri, new String[]{"_data"}, null, null, null);
-        int count = (c != null) ? c.getCount() : 0;
-        if (count != 1) {
-            // If there is not exactly one result, throw an appropriate exception.
-            if (c != null) {
-                c.close();
+        Cursor cursor = query(uri, new String[] {"_data"}, null, null, null);
+        String path;
+        try {
+            int count = (cursor != null) ? cursor.getCount() : 0;
+            if (count != 1) {
+                // If there is not exactly one result, throw an appropriate exception.
+                if (count == 0) {
+                    throw new FileNotFoundException("No entry for " + uri);
+                }
+                throw new FileNotFoundException("Multiple items at " + uri);
             }
-            if (count == 0) {
-                throw new FileNotFoundException("No entry for " + uri);
+
+            cursor.moveToFirst();
+            path = cursor.getString(0);
+        } finally {
+            if (cursor != null) {
+                cursor.close();
             }
-            throw new FileNotFoundException("Multiple items at " + uri);
         }
 
-        c.moveToFirst();
-        String path = c.getString(0);
-        c.close();
         if (path == null) {
             throw new FileNotFoundException("No filename found.");
         }
         if (!Helpers.isFilenameValid(path)) {
             throw new FileNotFoundException("Invalid filename.");
         }
-
         if (!"r".equals(mode)) {
             throw new FileNotFoundException("Bad mode for " + uri + ": " + mode);
         }
+
         ParcelFileDescriptor ret = ParcelFileDescriptor.open(new File(path),
                 ParcelFileDescriptor.MODE_READ_ONLY);
 
@@ -989,14 +980,44 @@ public final class DownloadProvider extends ContentProvider {
                 Log.v(Constants.TAG, "couldn't open file");
             }
             throw new FileNotFoundException("couldn't open file");
-        } else {
-            ContentValues values = new ContentValues();
-            values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
-            update(uri, values, null, null);
         }
         return ret;
     }
 
+    private void logVerboseOpenFileInfo(Uri uri, String mode) {
+        Log.v(Constants.TAG, "openFile uri: " + uri + ", mode: " + mode
+                + ", uid: " + Binder.getCallingUid());
+        Cursor cursor = query(Downloads.Impl.CONTENT_URI,
+                new String[] { "_id" }, null, null, "_id");
+        if (cursor == null) {
+            Log.v(Constants.TAG, "null cursor in openFile");
+        } else {
+            if (!cursor.moveToFirst()) {
+                Log.v(Constants.TAG, "empty cursor in openFile");
+            } else {
+                do {
+                    Log.v(Constants.TAG, "row " + cursor.getInt(0) + " available");
+                } while(cursor.moveToNext());
+            }
+            cursor.close();
+        }
+        cursor = query(uri, new String[] { "_data" }, null, null, null);
+        if (cursor == null) {
+            Log.v(Constants.TAG, "null cursor in openFile");
+        } else {
+            if (!cursor.moveToFirst()) {
+                Log.v(Constants.TAG, "empty cursor in openFile");
+            } else {
+                String filename = cursor.getString(0);
+                Log.v(Constants.TAG, "filename in openFile: " + filename);
+                if (new java.io.File(filename).isFile()) {
+                    Log.v(Constants.TAG, "file exists in openFile");
+                }
+            }
+           cursor.close();
+        }
+    }
+
     private static final void copyInteger(String key, ContentValues from, ContentValues to) {
         Integer i = from.getAsInteger(key);
         if (i != null) {
index 6d9ee22..b85fb90 100644 (file)
@@ -211,7 +211,7 @@ public class DownloadService extends Service {
         mDownloads = Lists.newArrayList();
 
         mObserver = new DownloadManagerContentObserver();
-        getContentResolver().registerContentObserver(Downloads.Impl.CONTENT_URI,
+        getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
                 true, mObserver);
 
         mMediaScannerService = null;
@@ -313,7 +313,7 @@ public class DownloadService extends Service {
                 }
                 long now = mSystemFacade.currentTimeMillis();
 
-                Cursor cursor = getContentResolver().query(Downloads.Impl.CONTENT_URI,
+                Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
                         null, null, null, Downloads.Impl._ID);
 
                 if (cursor == null) {
@@ -482,7 +482,7 @@ public class DownloadService extends Service {
             // when running the simulator).
             return;
         }
-        HashSet<String> fileSet = new HashSet();
+        HashSet<String> fileSet = new HashSet<String>();
         for (int i = 0; i < files.length; i++) {
             if (files[i].getName().equals(Constants.KNOWN_SPURIOUS_FILENAME)) {
                 continue;
@@ -493,7 +493,7 @@ public class DownloadService extends Service {
             fileSet.add(files[i].getPath());
         }
 
-        Cursor cursor = getContentResolver().query(Downloads.Impl.CONTENT_URI,
+        Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
                 new String[] { Downloads.Impl._DATA }, null, null, null);
         if (cursor != null) {
             if (cursor.moveToFirst()) {
@@ -517,7 +517,7 @@ public class DownloadService extends Service {
      * Drops old rows from the database to prevent it from growing too large
      */
     private void trimDatabase() {
-        Cursor cursor = getContentResolver().query(Downloads.Impl.CONTENT_URI,
+        Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
                 new String[] { Downloads.Impl._ID },
                 Downloads.Impl.COLUMN_STATUS + " >= '200'", null,
                 Downloads.Impl.COLUMN_LAST_MODIFICATION);
@@ -530,9 +530,9 @@ public class DownloadService extends Service {
             int numDelete = cursor.getCount() - Constants.MAX_DOWNLOADS;
             int columnId = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
             while (numDelete > 0) {
-                getContentResolver().delete(
-                        ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI,
-                        cursor.getLong(columnId)), null, null);
+                Uri downloadUri = ContentUris.withAppendedId(
+                        Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, cursor.getLong(columnId));
+                getContentResolver().delete(downloadUri, null, null);
                 if (!cursor.moveToNext()) {
                     break;
                 }
@@ -601,16 +601,13 @@ public class DownloadService extends Service {
             //Log.i(Constants.TAG, "*** QUERY " + mimetypeIntent + ": " + list);
 
             if (ri == null) {
-                if (Config.LOGD) {
-                    Log.d(Constants.TAG, "no application to handle MIME type " + info.mMimeType);
-                }
+                Log.d(Constants.TAG, "no application to handle MIME type " + info.mMimeType);
                 info.mStatus = Downloads.Impl.STATUS_NOT_ACCEPTABLE;
 
-                Uri uri = ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, info.mId);
                 ContentValues values = new ContentValues();
                 values.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_NOT_ACCEPTABLE);
-                getContentResolver().update(uri, values, null, null);
-                info.sendIntentIfRequested(uri);
+                getContentResolver().update(info.getAllDownloadsUri(), values, null, null);
+                info.sendIntentIfRequested();
                 return;
             }
         }
@@ -624,7 +621,7 @@ public class DownloadService extends Service {
      * Updates the local copy of the info about a download.
      */
     private void updateDownload(Cursor cursor, int arrayPos, long now) {
-        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
+        DownloadInfo info = mDownloads.get(arrayPos);
         int statusColumn = cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_STATUS);
         int failedColumn = cursor.getColumnIndexOrThrow(Constants.FAILED_CONNECTIONS);
         info.mId = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl._ID));
@@ -785,7 +782,7 @@ public class DownloadService extends Service {
      * Returns true if the file has been properly scanned.
      */
     private boolean scanFile(Cursor cursor, int arrayPos) {
-        DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
+        DownloadInfo info = mDownloads.get(arrayPos);
         synchronized (this) {
             if (mMediaScannerService != null) {
                 try {
@@ -796,16 +793,11 @@ public class DownloadService extends Service {
                     if (cursor != null) {
                         ContentValues values = new ContentValues();
                         values.put(Constants.MEDIA_SCANNED, 1);
-                        getContentResolver().update(ContentUris.withAppendedId(
-                                       Downloads.Impl.CONTENT_URI, cursor.getLong(
-                                               cursor.getColumnIndexOrThrow(Downloads.Impl._ID))),
-                                values, null, null);
+                        getContentResolver().update(info.getAllDownloadsUri(), values, null, null);
                     }
                     return true;
                 } catch (RemoteException e) {
-                    if (Config.LOGD) {
-                        Log.d(Constants.TAG, "Failed to scan file " + info.mFileName);
-                    }
+                    Log.d(Constants.TAG, "Failed to scan file " + info.mFileName);
                 }
             }
         }
index 8a8a0da..b2353e1 100644 (file)
 
 package com.android.providers.downloads;
 
-import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.drm.mobile1.DrmRawContent;
-import android.net.Uri;
 import android.net.http.AndroidHttpClient;
 import android.os.FileUtils;
 import android.os.PowerManager;
@@ -85,14 +83,12 @@ public class DownloadThread extends Thread {
         public int mRetryAfter = 0;
         public int mRedirectCount = 0;
         public String mNewUri;
-        public Uri mContentUri;
         public boolean mGotData = false;
         public String mRequestUri;
 
         public State(DownloadInfo info) {
             mMimeType = sanitizeMimeType(info.mMimeType);
             mRedirectCount = info.mRedirectCount;
-            mContentUri = Uri.parse(Downloads.Impl.CONTENT_URI + "/" + info.mId);
             mRequestUri = info.mUri;
             mFilename = info.mFileName;
         }
@@ -405,8 +401,7 @@ public class DownloadThread extends Thread {
                         > Constants.MIN_PROGRESS_TIME) {
             ContentValues values = new ContentValues();
             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
-            mContext.getContentResolver().update(
-                    state.mContentUri, values, null, null);
+            mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
             innerState.mBytesNotified = innerState.mBytesSoFar;
             innerState.mTimeLastNotification = now;
         }
@@ -450,7 +445,7 @@ public class DownloadThread extends Thread {
         if (innerState.mHeaderContentLength == null) {
             values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, innerState.mBytesSoFar);
         }
-        mContext.getContentResolver().update(state.mContentUri, values, null, null);
+        mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
 
         boolean lengthMismatched = (innerState.mHeaderContentLength != null)
                 && (innerState.mBytesSoFar != Integer.parseInt(innerState.mHeaderContentLength));
@@ -488,7 +483,7 @@ public class DownloadThread extends Thread {
             logNetworkState();
             ContentValues values = new ContentValues();
             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
-            mContext.getContentResolver().update(state.mContentUri, values, null, null);
+            mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
             if (cannotResume(innerState)) {
                 Log.d(Constants.TAG, "download IOException for download " + mInfo.mId, ex);
                 Log.d(Constants.TAG, "can't resume interrupted download with no ETag");
@@ -572,7 +567,7 @@ public class DownloadThread extends Thread {
             values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType);
         }
         values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mInfo.mTotalBytes);
-        mContext.getContentResolver().update(state.mContentUri, values, null, null);
+        mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
     }
 
     /**
@@ -868,7 +863,7 @@ public class DownloadThread extends Thread {
         notifyThroughDatabase(
                 status, countRetry, retryAfter, redirectCount, gotData, filename, uri, mimeType);
         if (Downloads.Impl.isStatusCompleted(status)) {
-            notifyThroughIntent();
+            mInfo.sendIntentIfRequested();
         }
     }
 
@@ -892,17 +887,7 @@ public class DownloadThread extends Thread {
             values.put(Constants.FAILED_CONNECTIONS, mInfo.mNumFailed + 1);
         }
 
-        mContext.getContentResolver().update(ContentUris.withAppendedId(
-                Downloads.Impl.CONTENT_URI, mInfo.mId), values, null, null);
-    }
-
-    /**
-     * Notifies the initiating app if it requested it. That way, it can know that the
-     * download completed even if it's not actively watching the cursor.
-     */
-    private void notifyThroughIntent() {
-        Uri uri = Uri.parse(Downloads.Impl.CONTENT_URI + "/" + mInfo.mId);
-        mInfo.sendIntentIfRequested(uri);
+        mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
     }
 
     /**
index 42a49f1..f8900d9 100644 (file)
@@ -469,7 +469,7 @@ public class Helpers {
      */
     public static final boolean discardPurgeableFiles(Context context, long targetBytes) {
         Cursor cursor = context.getContentResolver().query(
-                Downloads.Impl.CONTENT_URI,
+                Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
                 null,
                 "( " +
                 Downloads.Impl.COLUMN_STATUS + " = '" + Downloads.Impl.STATUS_SUCCESS + "' AND " +
@@ -493,7 +493,8 @@ public class Helpers {
                 file.delete();
                 long id = cursor.getLong(cursor.getColumnIndex(Downloads.Impl._ID));
                 context.getContentResolver().delete(
-                        ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, id), null, null);
+                        ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id),
+                        null, null);
                 cursor.moveToNext();
             }
         } finally {
index 3e4bccc..d04fd2d 100644 (file)
@@ -165,7 +165,8 @@ public abstract class AbstractDownloadManagerFunctionalTest extends
     }
 
     private boolean isDatabaseEmpty() {
-        Cursor cursor = mResolver.query(Downloads.CONTENT_URI, null, null, null, null);
+        Cursor cursor = mResolver.query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
+                null, null, null, null);
         try {
             return cursor.getCount() == 0;
         } finally {
index e48ce22..d577e2c 100644 (file)
@@ -29,6 +29,8 @@ import tests.http.RecordedRequest;
 
 import java.io.File;
 import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
 import java.io.InputStream;
 import java.net.MalformedURLException;
 import java.util.List;
@@ -89,9 +91,8 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
         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("content", localUri.getScheme());
+        checkUriContent(localUri);
         assertEquals("text/plain", download.getStringField(DownloadManager.COLUMN_MEDIA_TYPE));
 
         int size = FILE_CONTENT.length();
@@ -103,6 +104,15 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
         checkCompleteDownload(download);
     }
 
+    private void checkUriContent(Uri uri) throws FileNotFoundException, IOException {
+        InputStream inputStream = mResolver.openInputStream(uri);
+        try {
+            assertEquals(FILE_CONTENT, readStream(inputStream));
+        } finally {
+            inputStream.close();
+        }
+    }
+
     public void testTitleAndDescription() throws Exception {
         Download download = enqueueRequest(getRequest()
                                            .setTitle("my title")
index 71fad40..31f1483 100644 (file)
@@ -3,8 +3,8 @@
         package="com.android.providers.downloads.ui"
         android:sharedUserId="android.media">
 
-    <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.SEND_DOWNLOAD_COMPLETED_INTENTS" />
+    <uses-permission android:name="android.permission.ACCESS_ALL_DOWNLOADS" />
 
     <application android:process="android.process.media"
                  android:label="@string/app_label">
index dd9a608..fce2f16 100644 (file)
@@ -30,6 +30,7 @@ import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
 import android.provider.Downloads;
+import android.util.Log;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
@@ -47,7 +48,8 @@ import android.widget.Toast;
 
 import com.android.providers.downloads.ui.DownloadItem.DownloadSelectListener;
 
-import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Set;
@@ -58,6 +60,8 @@ import java.util.Set;
 public class DownloadList extends Activity
         implements OnChildClickListener, OnItemClickListener, DownloadSelectListener,
         OnClickListener, OnCancelListener {
+    private static final String LOG_TAG = "DownloadList";
+
     private ExpandableListView mDateOrderedListView;
     private ListView mSizeOrderedListView;
     private View mEmptyView;
@@ -103,6 +107,7 @@ public class DownloadList extends Activity
         setupViews();
 
         mDownloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
+        mDownloadManager.setAccessAllDownloads(true);
         DownloadManager.Query baseQuery = new DownloadManager.Query()
                 .setOnlyIncludeVisibleInDownloadsUi(true);
         mDateSortedCursor = mDownloadManager.query(baseQuery);
@@ -112,7 +117,7 @@ public class DownloadList extends Activity
 
         // only attach everything to the listbox if we can access the download database. Otherwise,
         // just show it empty
-        if (mDateSortedCursor != null && mSizeSortedCursor != null) {
+        if (haveCursors()) {
             startManagingCursor(mDateSortedCursor);
             startManagingCursor(mSizeSortedCursor);
 
@@ -160,19 +165,23 @@ public class DownloadList extends Activity
         ((Button) findViewById(R.id.deselect_all)).setOnClickListener(this);
     }
 
+    private boolean haveCursors() {
+        return mDateSortedCursor != null && mSizeSortedCursor != null;
+    }
+
     @Override
     protected void onResume() {
         super.onResume();
-        if (mDateSortedCursor != null) {
+        if (haveCursors()) {
             mDateSortedCursor.registerContentObserver(mContentObserver);
+            refresh();
         }
-        refresh();
     }
 
     @Override
     protected void onPause() {
         super.onPause();
-        if (mDateSortedCursor != null) {
+        if (haveCursors()) {
             mDateSortedCursor.unregisterContentObserver(mContentObserver);
         }
     }
@@ -207,7 +216,7 @@ public class DownloadList extends Activity
 
     @Override
     public boolean onCreateOptionsMenu(Menu menu) {
-        if (mDateSortedCursor != null) {
+        if (haveCursors()) {
             MenuInflater inflater = getMenuInflater();
             inflater.inflate(R.menu.download_menu, menu);
         }
@@ -243,7 +252,7 @@ public class DownloadList extends Activity
         mDateOrderedListView.setVisibility(View.GONE);
         mSizeOrderedListView.setVisibility(View.GONE);
 
-        if (mDateSortedCursor.getCount() == 0) {
+        if (mDateSortedCursor == null || mDateSortedCursor.getCount() == 0) {
             mEmptyView.setVisibility(View.VISIBLE);
         } else {
             mEmptyView.setVisibility(View.GONE);
@@ -290,15 +299,20 @@ public class DownloadList extends Activity
      * Send an Intent to open the download currently pointed to by the given cursor.
      */
     private void openCurrentDownload(Cursor cursor) {
-        Uri fileUri = Uri.parse(cursor.getString(mLocalUriColumnId));
-        if (!new File(fileUri.getPath()).exists()) {
+        Uri localUri = Uri.parse(cursor.getString(mLocalUriColumnId));
+        try {
+            getContentResolver().openFileDescriptor(localUri, "r").close();
+        } catch (FileNotFoundException exc) {
+            Log.d(LOG_TAG, "Failed to open download " + cursor.getLong(mIdColumnId), exc);
             showFailedDialog(cursor.getLong(mIdColumnId), R.string.dialog_file_missing_body);
             return;
+        } catch (IOException exc) {
+            // close() failed, not a problem
         }
 
         Intent intent = new Intent(Intent.ACTION_VIEW);
-        intent.setDataAndType(fileUri, cursor.getString(mMediaTypeColumnId));
-        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.setDataAndType(localUri, cursor.getString(mMediaTypeColumnId));
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION);
         try {
             startActivity(intent);
         } catch (ActivityNotFoundException ex) {