]> nv-tegra.nvidia Code Review - android/platform/packages/providers/DownloadProvider.git/blobdiff - src/com/android/providers/downloads/DownloadProvider.java
DO NOT MERGE Deleting downloads for removed uids on downloadprovider start
[android/platform/packages/providers/DownloadProvider.git] / src / com / android / providers / downloads / DownloadProvider.java
index 5fbe42a5f277e927a27d1ebdc50fb2e615eade24..8590df922e7880635446b58fe61a05e4418d2b56 100644 (file)
 
 package com.android.providers.downloads;
 
+import static android.provider.BaseColumns._ID;
+import static android.provider.Downloads.Impl.COLUMN_DESTINATION;
+import static android.provider.Downloads.Impl.COLUMN_MEDIAPROVIDER_URI;
+import static android.provider.Downloads.Impl.COLUMN_MEDIA_SCANNED;
+import static android.provider.Downloads.Impl.COLUMN_MIME_TYPE;
+import static android.provider.Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD;
+import static android.provider.Downloads.Impl._DATA;
+
+import android.app.AppOpsManager;
 import android.app.DownloadManager;
 import android.app.DownloadManager.Request;
+import android.app.job.JobScheduler;
 import android.content.ContentProvider;
 import android.content.ContentUris;
 import android.content.ContentValues;
@@ -34,19 +44,28 @@ import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteOpenHelper;
 import android.net.Uri;
 import android.os.Binder;
-import android.os.Environment;
 import android.os.ParcelFileDescriptor;
+import android.os.ParcelFileDescriptor.OnCloseListener;
 import android.os.Process;
+import android.provider.BaseColumns;
 import android.provider.Downloads;
 import android.provider.OpenableColumns;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
 import android.util.Log;
 
+import com.android.internal.util.IndentingPrintWriter;
+
+import libcore.io.IoUtils;
+
 import com.google.android.collect.Maps;
 import com.google.common.annotations.VisibleForTesting;
 
 import java.io.File;
+import java.io.FileDescriptor;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -55,7 +74,6 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 
-
 /**
  * Allows application to interact with the download manager.
  */
@@ -63,7 +81,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 = 108;
+    private static final int DB_VERSION = 110;
     /** Name of table in the database */
     private static final String DB_TABLE = "downloads";
 
@@ -160,16 +178,15 @@ public final class DownloadProvider extends ContentProvider {
     private static final List<String> downloadManagerColumnsList =
             Arrays.asList(DownloadManager.UNDERLYING_COLUMNS);
 
+    @VisibleForTesting
+    SystemFacade mSystemFacade;
+
     /** The database that lies underneath this content provider */
     private SQLiteOpenHelper mOpenHelper = null;
 
     /** List of uids that can access the downloads */
     private int mSystemUid = -1;
     private int mDefContainerUid = -1;
-    private File mDownloadsDataDir;
-
-    @VisibleForTesting
-    SystemFacade mSystemFacade;
 
     /**
      * This class encapsulates a SQL where clause and its parameters.  It makes it possible for
@@ -313,6 +330,16 @@ public final class DownloadProvider extends ContentProvider {
                             "INTEGER NOT NULL DEFAULT 1");
                     break;
 
+                case 109:
+                    addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ALLOW_WRITE,
+                            "BOOLEAN NOT NULL DEFAULT 0");
+                    break;
+
+                case 110:
+                    addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_FLAGS,
+                            "INTEGER NOT NULL DEFAULT 0");
+                    break;
+
                 default:
                     throw new IllegalStateException("Don't know how to upgrade to " + version);
             }
@@ -384,7 +411,7 @@ public final class DownloadProvider extends ContentProvider {
                         Downloads.Impl.COLUMN_VISIBILITY + " INTEGER, " +
                         Downloads.Impl.COLUMN_CONTROL + " INTEGER, " +
                         Downloads.Impl.COLUMN_STATUS + " INTEGER, " +
-                        Constants.FAILED_CONNECTIONS + " INTEGER, " +
+                        Downloads.Impl.COLUMN_FAILED_CONNECTIONS + " INTEGER, " +
                         Downloads.Impl.COLUMN_LAST_MODIFICATION + " BIGINT, " +
                         Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE + " TEXT, " +
                         Downloads.Impl.COLUMN_NOTIFICATION_CLASS + " TEXT, " +
@@ -399,7 +426,7 @@ public final class DownloadProvider extends ContentProvider {
                         Downloads.Impl.COLUMN_OTHER_UID + " INTEGER, " +
                         Downloads.Impl.COLUMN_TITLE + " TEXT, " +
                         Downloads.Impl.COLUMN_DESCRIPTION + " TEXT, " +
-                        Constants.MEDIA_SCANNED + " BOOLEAN);");
+                        Downloads.Impl.COLUMN_MEDIA_SCANNED + " BOOLEAN);");
             } catch (SQLException ex) {
                 Log.e(Constants.TAG, "couldn't create table in downloads database");
                 throw ex;
@@ -436,20 +463,52 @@ public final class DownloadProvider extends ContentProvider {
             appInfo = getContext().getPackageManager().
                     getApplicationInfo("com.android.defcontainer", 0);
         } catch (NameNotFoundException e) {
-            // TODO Auto-generated catch block
-            e.printStackTrace();
+            Log.wtf(Constants.TAG, "Could not get ApplicationInfo for com.android.defconatiner", e);
         }
         if (appInfo != null) {
             mDefContainerUid = appInfo.uid;
         }
-        // start the DownloadService class. don't wait for the 1st download to be issued.
-        // saves us by getting some initialization code in DownloadService out of the way.
-        Context context = getContext();
-        context.startService(new Intent(context, DownloadService.class));
-        mDownloadsDataDir = StorageManager.getInstance(getContext()).getDownloadDataDirectory();
+
+        // Grant access permissions for all known downloads to the owning apps
+        final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+        final Cursor cursor = db.query(DB_TABLE, new String[] {
+                Downloads.Impl._ID, Constants.UID }, null, null, null, null, null);
+        final ArrayList<Long> idsToDelete = new ArrayList<>();
+        try {
+            while (cursor.moveToNext()) {
+                final long downloadId = cursor.getLong(0);
+                final int uid = cursor.getInt(1);
+                final String ownerPackage = getPackageForUid(uid);
+                if (ownerPackage == null) {
+                    idsToDelete.add(downloadId);
+                } else {
+                    grantAllDownloadsPermission(ownerPackage, downloadId);
+                }
+            }
+        } finally {
+            cursor.close();
+        }
+        if (idsToDelete.size() > 0) {
+            Log.i(Constants.TAG,
+                    "Deleting downloads with ids " + idsToDelete + " as owner package is missing");
+            deleteDownloadsWithIds(idsToDelete);
+        }
         return true;
     }
 
+    private void deleteDownloadsWithIds(ArrayList<Long> downloadIds) {
+        final int N = downloadIds.size();
+        if (N == 0) {
+            return;
+        }
+        final StringBuilder queryBuilder = new StringBuilder(Downloads.Impl._ID + " in (");
+        for (int i = 0; i < N; i++) {
+            queryBuilder.append(downloadIds.get(i));
+            queryBuilder.append((i == N - 1) ? ")" : ",");
+        }
+        delete(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, queryBuilder.toString(), null);
+    }
+
     /**
      * Returns the content-provider-style MIME types of the various
      * types accessible through this content provider.
@@ -463,17 +522,20 @@ public final class DownloadProvider extends ContentProvider {
                 return DOWNLOAD_LIST_TYPE;
             }
             case MY_DOWNLOADS_ID:
-            case ALL_DOWNLOADS_ID: {
-                return DOWNLOAD_TYPE;
-            }
+            case ALL_DOWNLOADS_ID:
             case PUBLIC_DOWNLOAD_ID: {
                 // return the mimetype of this id from the database
                 final String id = getDownloadIdFromUri(uri);
                 final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
-                return DatabaseUtils.stringForQuery(db,
+                final String mimeType = DatabaseUtils.stringForQuery(db,
                         "SELECT " + Downloads.Impl.COLUMN_MIME_TYPE + " FROM " + DB_TABLE +
                         " WHERE " + Downloads.Impl._ID + " = ?",
                         new String[]{id});
+                if (TextUtils.isEmpty(mimeType)) {
+                    return DOWNLOAD_TYPE;
+                } else {
+                    return mimeType;
+                }
             }
             default: {
                 if (Constants.LOGV) {
@@ -514,7 +576,7 @@ public final class DownloadProvider extends ContentProvider {
         // validate the destination column
         Integer dest = values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION);
         if (dest != null) {
-            if (getContext().checkCallingPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED)
+            if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED)
                     != PackageManager.PERMISSION_GRANTED
                     && (dest == Downloads.Impl.DESTINATION_CACHE_PARTITION
                             || dest == Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING
@@ -525,7 +587,7 @@ public final class DownloadProvider extends ContentProvider {
             // for public API behavior, if an app has CACHE_NON_PURGEABLE permission, automatically
             // switch to non-purgeable download
             boolean hasNonPurgeablePermission =
-                    getContext().checkCallingPermission(
+                    getContext().checkCallingOrSelfPermission(
                             Downloads.Impl.PERMISSION_CACHE_NON_PURGEABLE)
                             == PackageManager.PERMISSION_GRANTED;
             if (isPublicApi && dest == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE
@@ -533,11 +595,19 @@ public final class DownloadProvider extends ContentProvider {
                 dest = Downloads.Impl.DESTINATION_CACHE_PARTITION;
             }
             if (dest == Downloads.Impl.DESTINATION_FILE_URI) {
-                getContext().enforcePermission(
-                        android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
-                        Binder.getCallingPid(), Binder.getCallingUid(),
-                        "need WRITE_EXTERNAL_STORAGE permission to use DESTINATION_FILE_URI");
                 checkFileUriDestination(values);
+
+            } else if (dest == Downloads.Impl.DESTINATION_EXTERNAL) {
+                getContext().enforceCallingOrSelfPermission(
+                        android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
+                        "No permission to write");
+
+                final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class);
+                if (appOps.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE,
+                        getCallingPackage()) != AppOpsManager.MODE_ALLOWED) {
+                    throw new SecurityException("No permission to write");
+                }
+
             } else if (dest == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION) {
                 getContext().enforcePermission(
                         android.Manifest.permission.ACCESS_CACHE_FILESYSTEM,
@@ -577,6 +647,7 @@ public final class DownloadProvider extends ContentProvider {
             filteredValues.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0);
             copyInteger(Downloads.Impl.COLUMN_MEDIA_SCANNED, values, filteredValues);
             copyString(Downloads.Impl._DATA, values, filteredValues);
+            copyBoolean(Downloads.Impl.COLUMN_ALLOW_WRITE, values, filteredValues);
         } else {
             filteredValues.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING);
             filteredValues.put(Downloads.Impl.COLUMN_TOTAL_BYTES, -1);
@@ -611,7 +682,7 @@ public final class DownloadProvider extends ContentProvider {
         copyString(Downloads.Impl.COLUMN_REFERER, values, filteredValues);
 
         // UID, PID columns
-        if (getContext().checkCallingPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED)
+        if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED)
                 == PackageManager.PERMISSION_GRANTED) {
             copyInteger(Downloads.Impl.COLUMN_OTHER_UID, values, filteredValues);
         }
@@ -638,6 +709,7 @@ public final class DownloadProvider extends ContentProvider {
             copyInteger(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES, values, filteredValues);
             copyBoolean(Downloads.Impl.COLUMN_ALLOW_ROAMING, values, filteredValues);
             copyBoolean(Downloads.Impl.COLUMN_ALLOW_METERED, values, filteredValues);
+            copyInteger(Downloads.Impl.COLUMN_FLAGS, values, filteredValues);
         }
 
         if (Constants.LOGVV) {
@@ -656,31 +728,40 @@ public final class DownloadProvider extends ContentProvider {
         }
 
         insertRequestHeaders(db, rowID, values);
-        /*
-         * requests coming from
-         * DownloadManager.addCompletedDownload(String, String, String,
-         * boolean, String, String, long) need special treatment
-         */
-        Context context = getContext();
-        if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) ==
-                Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
-            // don't start downloadservice because it has nothing to do in this case.
-            // but does a completion notification need to be sent?
-            if (Downloads.Impl.isNotificationToBeDisplayed(vis)) {
-                DownloadNotification notifier = new DownloadNotification(context, mSystemFacade);
-                notifier.notificationForCompletedDownload(rowID,
-                        values.getAsString(Downloads.Impl.COLUMN_TITLE),
-                        Downloads.Impl.STATUS_SUCCESS,
-                        Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD,
-                        lastMod);
-            }
-        } else {
-            context.startService(new Intent(context, DownloadService.class));
+
+        final String callingPackage = getPackageForUid(Binder.getCallingUid());
+        if (callingPackage == null) {
+            Log.e(Constants.TAG, "Package does not exist for calling uid");
+            return null;
         }
+        grantAllDownloadsPermission(callingPackage, rowID);
         notifyContentChanged(uri, match);
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            Helpers.scheduleJob(getContext(), rowID);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+
+        if (values.getAsInteger(COLUMN_DESTINATION) == DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD
+                && values.getAsInteger(COLUMN_MEDIA_SCANNED) == 0) {
+            DownloadScanner.requestScanBlocking(getContext(), rowID, values.getAsString(_DATA),
+                    values.getAsString(COLUMN_MIME_TYPE));
+        }
+
         return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID);
     }
 
+    private String getPackageForUid(int uid) {
+        String[] packages = getContext().getPackageManager().getPackagesForUid(uid);
+        if (packages == null || packages.length == 0) {
+            return null;
+        }
+        // For permission related purposes, any package belonging to the given uid should work.
+        return packages[0];
+    }
+
     /**
      * Check that the file URI provided for DESTINATION_FILE_URI is valid.
      */
@@ -699,14 +780,31 @@ public final class DownloadProvider extends ContentProvider {
         if (path == null) {
             throw new IllegalArgumentException("Invalid file URI: " + uri);
         }
+
+        final File file;
         try {
-            final String canonicalPath = new File(path).getCanonicalPath();
-            final String externalPath = Environment.getExternalStorageDirectory().getAbsolutePath();
-            if (!canonicalPath.startsWith(externalPath)) {
-                throw new SecurityException("Destination must be on external storage: " + uri);
-            }
+            file = new File(path).getCanonicalFile();
         } catch (IOException e) {
-            throw new SecurityException("Problem resolving path: " + uri);
+            throw new SecurityException(e);
+        }
+
+        if (Helpers.isFilenameValidInExternalPackage(getContext(), file, getCallingPackage())) {
+            // No permissions required for paths belonging to calling package
+            return;
+        } else if (Helpers.isFilenameValidInExternal(getContext(), file)) {
+            // Otherwise we require write permission
+            getContext().enforceCallingOrSelfPermission(
+                    android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
+                    "No permission to write to " + file);
+
+            final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class);
+            if (appOps.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE,
+                    getCallingPackage()) != AppOpsManager.MODE_ALLOWED) {
+                throw new SecurityException("No permission to write to " + file);
+            }
+
+        } else {
+            throw new SecurityException("Unsupported path " + file);
         }
     }
 
@@ -774,8 +872,10 @@ public final class DownloadProvider extends ContentProvider {
         values.remove(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES);
         values.remove(Downloads.Impl.COLUMN_ALLOW_ROAMING);
         values.remove(Downloads.Impl.COLUMN_ALLOW_METERED);
+        values.remove(Downloads.Impl.COLUMN_FLAGS);
         values.remove(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI);
         values.remove(Downloads.Impl.COLUMN_MEDIA_SCANNED);
+        values.remove(Downloads.Impl.COLUMN_ALLOW_WRITE);
         Iterator<Map.Entry<String, Object>> iterator = values.valueSet().iterator();
         while (iterator.hasNext()) {
             String key = iterator.next().getKey();
@@ -817,6 +917,16 @@ public final class DownloadProvider extends ContentProvider {
         throw new SecurityException("Invalid value for " + column + ": " + value);
     }
 
+    private Cursor queryCleared(Uri uri, String[] projection, String selection,
+            String[] selectionArgs, String sort) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            return query(uri, projection, selection, selectionArgs, sort);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
     /**
      * Starts a database query
      */
@@ -1010,14 +1120,7 @@ public final class DownloadProvider extends ContentProvider {
         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
 
         int count;
-        boolean startService = false;
-
-        if (values.containsKey(Downloads.Impl.COLUMN_DELETED)) {
-            if (values.getAsInteger(Downloads.Impl.COLUMN_DELETED) == 1) {
-                // some rows are to be 'deleted'. need to start DownloadService.
-                startService = true;
-            }
-        }
+        boolean updateSchedule = false;
 
         ContentValues filteredValues;
         if (Binder.getCallingPid() != Process.myPid()) {
@@ -1027,7 +1130,7 @@ public final class DownloadProvider extends ContentProvider {
             Integer i = values.getAsInteger(Downloads.Impl.COLUMN_CONTROL);
             if (i != null) {
                 filteredValues.put(Downloads.Impl.COLUMN_CONTROL, i);
-                startService = true;
+                updateSchedule = true;
             }
 
             copyInteger(Downloads.Impl.COLUMN_CONTROL, values, filteredValues);
@@ -1039,12 +1142,16 @@ public final class DownloadProvider extends ContentProvider {
             filteredValues = values;
             String filename = values.getAsString(Downloads.Impl._DATA);
             if (filename != null) {
-                Cursor c = query(uri, new String[]
-                        { Downloads.Impl.COLUMN_TITLE }, null, null, null);
-                if (!c.moveToFirst() || c.getString(0).isEmpty()) {
-                    values.put(Downloads.Impl.COLUMN_TITLE, new File(filename).getName());
+                Cursor c = null;
+                try {
+                    c = query(uri, new String[]
+                            { Downloads.Impl.COLUMN_TITLE }, null, null, null);
+                    if (!c.moveToFirst() || c.getString(0).isEmpty()) {
+                        values.put(Downloads.Impl.COLUMN_TITLE, new File(filename).getName());
+                    }
+                } finally {
+                    IoUtils.closeQuietly(c);
                 }
-                c.close();
             }
 
             Integer status = values.getAsInteger(Downloads.Impl.COLUMN_STATUS);
@@ -1052,7 +1159,7 @@ public final class DownloadProvider extends ContentProvider {
             boolean isUserBypassingSizeLimit =
                 values.containsKey(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT);
             if (isRestart || isUserBypassingSizeLimit) {
-                startService = true;
+                updateSchedule = true;
             }
         }
 
@@ -1062,12 +1169,27 @@ public final class DownloadProvider extends ContentProvider {
             case MY_DOWNLOADS_ID:
             case ALL_DOWNLOADS:
             case ALL_DOWNLOADS_ID:
-                SqlSelection selection = getWhereClause(uri, where, whereArgs, match);
-                if (filteredValues.size() > 0) {
-                    count = db.update(DB_TABLE, filteredValues, selection.getSelection(),
-                            selection.getParameters());
-                } else {
+                if (filteredValues.size() == 0) {
                     count = 0;
+                    break;
+                }
+
+                final SqlSelection selection = getWhereClause(uri, where, whereArgs, match);
+                count = db.update(DB_TABLE, filteredValues, selection.getSelection(),
+                        selection.getParameters());
+                if (updateSchedule) {
+                    final long token = Binder.clearCallingIdentity();
+                    try {
+                        try (Cursor cursor = db.query(DB_TABLE, new String[] { _ID },
+                                selection.getSelection(), selection.getParameters(),
+                                null, null, null)) {
+                            while (cursor.moveToNext()) {
+                                Helpers.scheduleJob(getContext(), cursor.getInt(0));
+                            }
+                        }
+                    } finally {
+                        Binder.restoreCallingIdentity(token);
+                    }
                 }
                 break;
 
@@ -1077,10 +1199,6 @@ public final class DownloadProvider extends ContentProvider {
         }
 
         notifyContentChanged(uri, match);
-        if (startService) {
-            Context context = getContext();
-            context.startService(new Intent(context, DownloadService.class));
-        }
         return count;
     }
 
@@ -1111,7 +1229,7 @@ public final class DownloadProvider extends ContentProvider {
             selection.appendClause(Downloads.Impl._ID + " = ?", getDownloadIdFromUri(uri));
         }
         if ((uriMatch == MY_DOWNLOADS || uriMatch == MY_DOWNLOADS_ID)
-                && getContext().checkCallingPermission(Downloads.Impl.PERMISSION_ACCESS_ALL)
+                && getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS_ALL)
                 != PackageManager.PERMISSION_GRANTED) {
             selection.appendClause(
                     Constants.UID + "= ? OR " + Downloads.Impl.COLUMN_OTHER_UID + "= ?",
@@ -1124,12 +1242,13 @@ public final class DownloadProvider extends ContentProvider {
      * Deletes a row in the database
      */
     @Override
-    public int delete(final Uri uri, final String where,
-            final String[] whereArgs) {
-
-        Helpers.validateSelection(where, sAppReadableColumnsSet);
+    public int delete(final Uri uri, final String where, final String[] whereArgs) {
+        if (shouldRestrictVisibility()) {
+            Helpers.validateSelection(where, sAppReadableColumnsSet);
+        }
 
-        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        final JobScheduler scheduler = getContext().getSystemService(JobScheduler.class);
+        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
         int count;
         int match = sURIMatcher.match(uri);
         switch (match) {
@@ -1137,8 +1256,45 @@ public final class DownloadProvider extends ContentProvider {
             case MY_DOWNLOADS_ID:
             case ALL_DOWNLOADS:
             case ALL_DOWNLOADS_ID:
-                SqlSelection selection = getWhereClause(uri, where, whereArgs, match);
+                final SqlSelection selection = getWhereClause(uri, where, whereArgs, match);
                 deleteRequestHeaders(db, selection.getSelection(), selection.getParameters());
+
+                try (Cursor cursor = db.query(DB_TABLE, new String[] {
+                        _ID, _DATA, COLUMN_MEDIAPROVIDER_URI
+                }, selection.getSelection(), selection.getParameters(), null, null, null)) {
+                    while (cursor.moveToNext()) {
+                        final long id = cursor.getLong(0);
+                        scheduler.cancel((int) id);
+
+                        revokeAllDownloadsPermission(id);
+                        DownloadStorageProvider.onDownloadProviderDelete(getContext(), id);
+
+                        final String path = cursor.getString(1);
+                        if (!TextUtils.isEmpty(path)) {
+                            try {
+                                final File file = new File(path).getCanonicalFile();
+                                if (Helpers.isFilenameValid(getContext(), file)) {
+                                    Log.v(Constants.TAG,
+                                            "Deleting " + file + " via provider delete");
+                                    file.delete();
+                                }
+                            } catch (IOException ignored) {
+                            }
+                        }
+
+                        final String mediaUri = cursor.getString(2);
+                        if (!TextUtils.isEmpty(mediaUri)) {
+                            final long token = Binder.clearCallingIdentity();
+                            try {
+                                getContext().getContentResolver().delete(Uri.parse(mediaUri), null,
+                                        null);
+                            } finally {
+                                Binder.restoreCallingIdentity(token);
+                            }
+                        }
+                    }
+                }
+
                 count = db.delete(DB_TABLE, selection.getSelection(), selection.getParameters());
                 break;
 
@@ -1154,13 +1310,30 @@ public final class DownloadProvider extends ContentProvider {
      * Remotely opens a file
      */
     @Override
-    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
+    public ParcelFileDescriptor openFile(final Uri uri, String mode) throws FileNotFoundException {
         if (Constants.LOGVV) {
             logVerboseOpenFileInfo(uri, mode);
         }
 
-        Cursor cursor = query(uri, new String[] {"_data"}, null, null, null);
-        String path;
+        // Perform normal query to enforce caller identity access before
+        // clearing it to reach internal-only columns
+        final Cursor probeCursor = query(uri, new String[] {
+                Downloads.Impl._DATA }, null, null, null);
+        try {
+            if ((probeCursor == null) || (probeCursor.getCount() == 0)) {
+                throw new FileNotFoundException(
+                        "No file found for " + uri + " as UID " + Binder.getCallingUid());
+            }
+        } finally {
+            IoUtils.closeQuietly(probeCursor);
+        }
+
+        final Cursor cursor = queryCleared(uri, new String[] {
+                Downloads.Impl._DATA, Downloads.Impl.COLUMN_STATUS,
+                Downloads.Impl.COLUMN_DESTINATION, Downloads.Impl.COLUMN_MEDIA_SCANNED }, null,
+                null, null);
+        final String path;
+        final boolean shouldScan;
         try {
             int count = (cursor != null) ? cursor.getCount() : 0;
             if (count != 1) {
@@ -1171,34 +1344,102 @@ public final class DownloadProvider extends ContentProvider {
                 throw new FileNotFoundException("Multiple items at " + uri);
             }
 
-            cursor.moveToFirst();
-            path = cursor.getString(0);
-        } finally {
-            if (cursor != null) {
-                cursor.close();
+            if (cursor.moveToFirst()) {
+                final int status = cursor.getInt(1);
+                final int destination = cursor.getInt(2);
+                final int mediaScanned = cursor.getInt(3);
+
+                path = cursor.getString(0);
+                shouldScan = Downloads.Impl.isStatusSuccess(status) && (
+                        destination == Downloads.Impl.DESTINATION_EXTERNAL
+                        || destination == Downloads.Impl.DESTINATION_FILE_URI
+                        || destination == Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD)
+                        && mediaScanned != 2;
+            } else {
+                throw new FileNotFoundException("Failed moveToFirst");
             }
+        } finally {
+            IoUtils.closeQuietly(cursor);
         }
 
         if (path == null) {
             throw new FileNotFoundException("No filename found.");
         }
-        if (!Helpers.isFilenameValid(path, mDownloadsDataDir)) {
-            throw new FileNotFoundException("Invalid filename: " + path);
+
+        final File file;
+        try {
+            file = new File(path).getCanonicalFile();
+        } catch (IOException e) {
+            throw new FileNotFoundException(e.getMessage());
+        }
+
+        if (!Helpers.isFilenameValid(getContext(), file)) {
+            throw new FileNotFoundException("Invalid file: " + file);
         }
-        if (!"r".equals(mode)) {
-            throw new FileNotFoundException("Bad mode for " + uri + ": " + mode);
+
+        final int pfdMode = ParcelFileDescriptor.parseMode(mode);
+        if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY) {
+            return ParcelFileDescriptor.open(file, pfdMode);
+        } else {
+            try {
+                // When finished writing, update size and timestamp
+                return ParcelFileDescriptor.open(file, pfdMode, Helpers.getAsyncHandler(),
+                        new OnCloseListener() {
+                    @Override
+                    public void onClose(IOException e) {
+                        final ContentValues values = new ContentValues();
+                        values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, file.length());
+                        values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION,
+                                System.currentTimeMillis());
+                        update(uri, values, null, null);
+
+                        if (shouldScan) {
+                            final Intent intent = new Intent(
+                                    Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+                            intent.setData(Uri.fromFile(file));
+                            getContext().sendBroadcast(intent);
+                        }
+                    }
+                });
+            } catch (IOException e) {
+                throw new FileNotFoundException("Failed to open for writing: " + e);
+            }
         }
+    }
 
-        ParcelFileDescriptor ret = ParcelFileDescriptor.open(new File(path),
-                ParcelFileDescriptor.MODE_READ_ONLY);
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+        final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ", 120);
 
-        if (ret == null) {
-            if (Constants.LOGV) {
-                Log.v(Constants.TAG, "couldn't open file");
+        pw.println("Downloads updated in last hour:");
+        pw.increaseIndent();
+
+        final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+        final long modifiedAfter = mSystemFacade.currentTimeMillis() - DateUtils.HOUR_IN_MILLIS;
+        final Cursor cursor = db.query(DB_TABLE, null,
+                Downloads.Impl.COLUMN_LAST_MODIFICATION + ">" + modifiedAfter, null, null, null,
+                Downloads.Impl._ID + " ASC");
+        try {
+            final String[] cols = cursor.getColumnNames();
+            final int idCol = cursor.getColumnIndex(BaseColumns._ID);
+            while (cursor.moveToNext()) {
+                pw.println("Download #" + cursor.getInt(idCol) + ":");
+                pw.increaseIndent();
+                for (int i = 0; i < cols.length; i++) {
+                    // Omit sensitive data when dumping
+                    if (Downloads.Impl.COLUMN_COOKIE_DATA.equals(cols[i])) {
+                        continue;
+                    }
+                    pw.printPair(cols[i], cursor.getString(i));
+                }
+                pw.println();
+                pw.decreaseIndent();
             }
-            throw new FileNotFoundException("couldn't open file");
+        } finally {
+            cursor.close();
         }
-        return ret;
+
+        pw.decreaseIndent();
     }
 
     private void logVerboseOpenFileInfo(Uri uri, String mode) {
@@ -1209,29 +1450,35 @@ public final class DownloadProvider extends ContentProvider {
         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());
+            try {
+                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());
+                }
+            } finally {
+                cursor.close();
             }
-            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");
+            try {
+                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");
+                    }
                 }
+            } finally {
+                cursor.close();
             }
-           cursor.close();
         }
     }
 
@@ -1263,4 +1510,15 @@ public final class DownloadProvider extends ContentProvider {
             to.put(key, defaultValue);
         }
     }
+
+    private void grantAllDownloadsPermission(String toPackage, long id) {
+        final Uri uri = ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id);
+        getContext().grantUriPermission(toPackage, uri,
+                Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+    }
+
+    private void revokeAllDownloadsPermission(long id) {
+        final Uri uri = ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id);
+        getContext().revokeUriPermission(uri, ~0);
+    }
 }