]> nv-tegra.nvidia Code Review - android/platform/packages/providers/DownloadProvider.git/blobdiff - src/com/android/providers/downloads/DownloadProvider.java
am cc167f03: (-s ours) am bb611587: (-s ours) Import translations. DO NOT MERGE
[android/platform/packages/providers/DownloadProvider.git] / src / com / android / providers / downloads / DownloadProvider.java
index 80e0d22fc8765aabbf40fa1441f34f140a7dcdb4..59e753e531b5e01ecd477a70d8f705a85053c4b8 100644 (file)
@@ -17,6 +17,7 @@
 package com.android.providers.downloads;
 
 import android.app.DownloadManager;
+import android.app.DownloadManager.Request;
 import android.content.ContentProvider;
 import android.content.ContentUris;
 import android.content.ContentValues;
@@ -34,23 +35,36 @@ import android.database.sqlite.SQLiteOpenHelper;
 import android.net.Uri;
 import android.os.Binder;
 import android.os.Environment;
+import android.os.Handler;
 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 com.google.android.collect.Maps;
 import com.google.common.annotations.VisibleForTesting;
 
+import libcore.io.IoUtils;
+
 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;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 
-
 /**
  * Allows application to interact with the download manager.
  */
@@ -58,7 +72,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 = 107;
+    private static final int DB_VERSION = 109;
     /** Name of table in the database */
     private static final String DB_TABLE = "downloads";
 
@@ -133,20 +147,29 @@ public final class DownloadProvider extends ContentProvider {
         Downloads.Impl.COLUMN_FILE_NAME_HINT,
         Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
         Downloads.Impl.COLUMN_DELETED,
+        OpenableColumns.DISPLAY_NAME,
+        OpenableColumns.SIZE,
     };
 
-    private static HashSet<String> sAppReadableColumnsSet;
+    private static final HashSet<String> sAppReadableColumnsSet;
+    private static final HashMap<String, String> sColumnsMap;
+
     static {
         sAppReadableColumnsSet = new HashSet<String>();
         for (int i = 0; i < sAppReadableColumnsArray.length; ++i) {
             sAppReadableColumnsSet.add(sAppReadableColumnsArray[i]);
         }
+
+        sColumnsMap = Maps.newHashMap();
+        sColumnsMap.put(OpenableColumns.DISPLAY_NAME,
+                Downloads.Impl.COLUMN_TITLE + " AS " + OpenableColumns.DISPLAY_NAME);
+        sColumnsMap.put(OpenableColumns.SIZE,
+                Downloads.Impl.COLUMN_TOTAL_BYTES + " AS " + OpenableColumns.SIZE);
     }
     private static final List<String> downloadManagerColumnsList =
             Arrays.asList(DownloadManager.UNDERLYING_COLUMNS);
 
-    // TODO is there a better way to get this package name
-    private static final Object GSF_PACKAGE_NAME = "com.google.android.gsf";
+    private Handler mHandler;
 
     /** The database that lies underneath this content provider */
     private SQLiteOpenHelper mOpenHelper = null;
@@ -154,7 +177,6 @@ public final class DownloadProvider extends ContentProvider {
     /** List of uids that can access the downloads */
     private int mSystemUid = -1;
     private int mDefContainerUid = -1;
-    private File mDownloadsDataDir;
 
     @VisibleForTesting
     SystemFacade mSystemFacade;
@@ -296,6 +318,16 @@ public final class DownloadProvider extends ContentProvider {
                     addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ERROR_MSG, "TEXT");
                     break;
 
+                case 108:
+                    addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ALLOW_METERED,
+                            "INTEGER NOT NULL DEFAULT 1");
+                    break;
+
+                case 109:
+                    addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ALLOW_WRITE,
+                            "BOOLEAN NOT NULL DEFAULT 0");
+                    break;
+
                 default:
                     throw new IllegalStateException("Don't know how to upgrade to " + version);
             }
@@ -367,7 +399,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, " +
@@ -409,6 +441,8 @@ public final class DownloadProvider extends ContentProvider {
             mSystemFacade = new RealSystemFacade(getContext());
         }
 
+        mHandler = new Handler();
+
         mOpenHelper = new DatabaseHelper(getContext());
         // Initialize the system uid
         mSystemUid = Process.SYSTEM_UID;
@@ -419,8 +453,7 @@ 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;
@@ -429,7 +462,6 @@ public final class DownloadProvider extends ContentProvider {
         // 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();
         return true;
     }
 
@@ -441,20 +473,25 @@ public final class DownloadProvider extends ContentProvider {
     public String getType(final Uri uri) {
         int match = sURIMatcher.match(uri);
         switch (match) {
-            case MY_DOWNLOADS: {
+            case MY_DOWNLOADS:
+            case ALL_DOWNLOADS: {
                 return DOWNLOAD_LIST_TYPE;
             }
-            case MY_DOWNLOADS_ID: {
-                return DOWNLOAD_TYPE;
-            }
+            case MY_DOWNLOADS_ID:
+            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) {
@@ -495,7 +532,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
@@ -506,7 +543,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
@@ -546,8 +583,8 @@ public final class DownloadProvider extends ContentProvider {
 
         /*
          * requests coming from
-         * DownloadManager.completedDownload(String, String, String, boolean, String,
-         * String, long) need special treatment
+         * DownloadManager.addCompletedDownload(String, String, String,
+         * boolean, String, String, long) need special treatment
          */
         if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) ==
                 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
@@ -558,6 +595,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);
@@ -565,8 +603,8 @@ public final class DownloadProvider extends ContentProvider {
         }
 
         // set lastupdate to current time
-        filteredValues.put(Downloads.Impl.COLUMN_LAST_MODIFICATION,
-                           mSystemFacade.currentTimeMillis());
+        long lastMod = mSystemFacade.currentTimeMillis();
+        filteredValues.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, lastMod);
 
         // use packagename of the caller to set the notification columns
         String pckg = values.getAsString(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE);
@@ -592,7 +630,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);
         }
@@ -618,13 +656,7 @@ public final class DownloadProvider extends ContentProvider {
         if (isPublicApi) {
             copyInteger(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES, values, filteredValues);
             copyBoolean(Downloads.Impl.COLUMN_ALLOW_ROAMING, values, filteredValues);
-        }
-
-        // TODO: replace this hack with something cleaner
-        if (pckg.equals(GSF_PACKAGE_NAME) &&
-                (getContext().checkCallingPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED)
-                        == PackageManager.PERMISSION_GRANTED)) {
-            filteredValues.put(Constants.OTA_UPDATE, Boolean.TRUE);
+            copyBoolean(Downloads.Impl.COLUMN_ALLOW_METERED, values, filteredValues);
         }
 
         if (Constants.LOGVV) {
@@ -636,9 +668,6 @@ public final class DownloadProvider extends ContentProvider {
             }
         }
 
-        Context context = getContext();
-        context.startService(new Intent(context, DownloadService.class));
-
         long rowID = db.insert(DB_TABLE, null, filteredValues);
         if (rowID == -1) {
             Log.d(Constants.TAG, "couldn't insert into downloads database");
@@ -646,8 +675,12 @@ public final class DownloadProvider extends ContentProvider {
         }
 
         insertRequestHeaders(db, rowID, values);
-        context.startService(new Intent(context, DownloadService.class));
         notifyContentChanged(uri, match);
+
+        // Always start service to handle notifications and/or scanning
+        final Context context = getContext();
+        context.startService(new Intent(context, DownloadService.class));
+
         return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID);
     }
 
@@ -665,13 +698,18 @@ public final class DownloadProvider extends ContentProvider {
         if (scheme == null || !scheme.equals("file")) {
             throw new IllegalArgumentException("Not a file URI: " + uri);
         }
-        String path = uri.getPath();
+        final String path = uri.getPath();
         if (path == null) {
             throw new IllegalArgumentException("Invalid file URI: " + uri);
         }
-        String externalPath = Environment.getExternalStorageDirectory().getAbsolutePath();
-        if (!path.startsWith(externalPath)) {
-            throw new SecurityException("Destination must be on external storage: " + uri);
+        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);
+            }
+        } catch (IOException e) {
+            throw new SecurityException("Problem resolving path: " + uri);
         }
     }
 
@@ -703,8 +741,8 @@ public final class DownloadProvider extends ContentProvider {
         if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) ==
                 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
             /* this row is inserted by
-             * DownloadManager.completedDownload(String, String, String, boolean, String,
-             * String, long)
+             * DownloadManager.addCompletedDownload(String, String, String,
+             * boolean, String, String, long)
              */
             values.remove(Downloads.Impl.COLUMN_TOTAL_BYTES);
             values.remove(Downloads.Impl._DATA);
@@ -718,10 +756,15 @@ public final class DownloadProvider extends ContentProvider {
         if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_NO_NOTIFICATION)
                 == PackageManager.PERMISSION_GRANTED) {
             enforceAllowedValues(values, Downloads.Impl.COLUMN_VISIBILITY,
-                    Downloads.Impl.VISIBILITY_HIDDEN, Downloads.Impl.VISIBILITY_VISIBLE);
+                    Request.VISIBILITY_HIDDEN,
+                    Request.VISIBILITY_VISIBLE,
+                    Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED,
+                    Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION);
         } else {
             enforceAllowedValues(values, Downloads.Impl.COLUMN_VISIBILITY,
-                    Downloads.Impl.VISIBILITY_VISIBLE);
+                    Request.VISIBILITY_VISIBLE,
+                    Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED,
+                    Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION);
         }
 
         // remove the rest of the columns that are allowed (with any value)
@@ -733,9 +776,10 @@ public final class DownloadProvider extends ContentProvider {
         values.remove(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE); // checked later in insert()
         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_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();
@@ -809,7 +853,7 @@ public final class DownloadProvider extends ContentProvider {
 
         if (shouldRestrictVisibility()) {
             if (projection == null) {
-                projection = sAppReadableColumnsArray;
+                projection = sAppReadableColumnsArray.clone();
             } else {
                 // check the validity of the columns in projection 
                 for (int i = 0; i < projection.length; ++i) {
@@ -820,6 +864,13 @@ public final class DownloadProvider extends ContentProvider {
                     }
                 }
             }
+
+            for (int i = 0; i < projection.length; i++) {
+                final String newColumn = sColumnsMap.get(projection[i]);
+                if (newColumn != null) {
+                    projection[i] = newColumn;
+                }
+            }
         }
 
         if (Constants.LOGVV) {
@@ -948,8 +999,7 @@ public final class DownloadProvider extends ContentProvider {
         int callingUid = Binder.getCallingUid();
         return Binder.getCallingPid() != Process.myPid() &&
                 callingUid != mSystemUid &&
-                callingUid != mDefContainerUid &&
-                Process.supportsProcesses();
+                callingUid != mDefContainerUid;
     }
 
     /**
@@ -993,12 +1043,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);
@@ -1065,11 +1119,11 @@ 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 + "= ?",
-                    Binder.getCallingUid(), Binder.getCallingPid());
+                    Binder.getCallingUid(), Binder.getCallingUid());
         }
         return selection;
     }
@@ -1081,7 +1135,9 @@ public final class DownloadProvider extends ContentProvider {
     public int delete(final Uri uri, final String where,
             final String[] whereArgs) {
 
-        Helpers.validateSelection(where, sAppReadableColumnsSet);
+        if (shouldRestrictVisibility()) {
+            Helpers.validateSelection(where, sAppReadableColumnsSet);
+        }
 
         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
         int count;
@@ -1093,6 +1149,19 @@ public final class DownloadProvider extends ContentProvider {
             case ALL_DOWNLOADS_ID:
                 SqlSelection selection = getWhereClause(uri, where, whereArgs, match);
                 deleteRequestHeaders(db, selection.getSelection(), selection.getParameters());
+
+                final Cursor cursor = db.query(DB_TABLE, new String[] {
+                        Downloads.Impl._ID }, selection.getSelection(), selection.getParameters(),
+                        null, null, null);
+                try {
+                    while (cursor.moveToNext()) {
+                        final long id = cursor.getLong(0);
+                        DownloadStorageProvider.onDownloadProviderDelete(getContext(), id);
+                    }
+                } finally {
+                    IoUtils.closeQuietly(cursor);
+                }
+
                 count = db.delete(DB_TABLE, selection.getSelection(), selection.getParameters());
                 break;
 
@@ -1108,12 +1177,12 @@ 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);
+        final Cursor cursor = query(uri, new String[] { Downloads.Impl._DATA }, null, null, null);
         String path;
         try {
             int count = (cursor != null) ? cursor.getCount() : 0;
@@ -1128,31 +1197,73 @@ public final class DownloadProvider extends ContentProvider {
             cursor.moveToFirst();
             path = cursor.getString(0);
         } finally {
-            if (cursor != null) {
-                cursor.close();
-            }
+            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 = new File(path);
+        if (!Helpers.isFilenameValid(getContext(), file)) {
+            throw new FileNotFoundException("Invalid file: " + file);
         }
-        if (!"r".equals(mode)) {
-            throw new FileNotFoundException("Bad mode for " + uri + ": " + mode);
+
+        if ("r".equals(mode)) {
+            return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
+        } else {
+            try {
+                // When finished writing, update size and timestamp
+                return ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode(mode),
+                        mHandler, 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);
+                            }
+                        });
+            } 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) {
@@ -1163,29 +1274,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();
         }
     }