auto import from //branches/cupcake/...@130745
[android/platform/packages/providers/DownloadProvider.git] / src / com / android / providers / downloads / DownloadProvider.java
1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.android.providers.downloads;
18
19 import android.content.ContentProvider;
20 import android.content.ContentValues;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.UriMatcher;
24 import android.content.pm.PackageManager;
25 import android.database.CrossProcessCursor;
26 import android.database.Cursor;
27 import android.database.CursorWindow;
28 import android.database.CursorWrapper;
29 import android.database.sqlite.SQLiteDatabase;
30 import android.database.sqlite.SQLiteOpenHelper;
31 import android.database.sqlite.SQLiteQueryBuilder;
32 import android.database.SQLException;
33 import android.net.Uri;
34 import android.os.Binder;
35 import android.os.ParcelFileDescriptor;
36 import android.os.Process;
37 import android.provider.Downloads;
38 import android.util.Config;
39 import android.util.Log;
40
41 import java.io.File;
42 import java.io.FileNotFoundException;
43 import java.io.IOException;
44 import java.util.HashSet;
45
46
47 /**
48  * Allows application to interact with the download manager.
49  */
50 public final class DownloadProvider extends ContentProvider {
51
52     /** Database filename */
53     private static final String DB_NAME = "downloads.db";
54     /** Current database version */
55     private static final int DB_VERSION = 100;
56     /** Database version from which upgrading is a nop */
57     private static final int DB_VERSION_NOP_UPGRADE_FROM = 31;
58     /** Database version to which upgrading is a nop */
59     private static final int DB_VERSION_NOP_UPGRADE_TO = 100;
60     /** Name of table in the database */
61     private static final String DB_TABLE = "downloads";
62
63     /** MIME type for the entire download list */
64     private static final String DOWNLOAD_LIST_TYPE = "vnd.android.cursor.dir/download";
65     /** MIME type for an individual download */
66     private static final String DOWNLOAD_TYPE = "vnd.android.cursor.item/download";
67
68     /** URI matcher used to recognize URIs sent by applications */
69     private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
70     /** URI matcher constant for the URI of the entire download list */
71     private static final int DOWNLOADS = 1;
72     /** URI matcher constant for the URI of an individual download */
73     private static final int DOWNLOADS_ID = 2;
74     static {
75         sURIMatcher.addURI("downloads", "download", DOWNLOADS);
76         sURIMatcher.addURI("downloads", "download/#", DOWNLOADS_ID);
77     }
78
79     private static final String[] sAppReadableColumnsArray = new String[] {
80         Downloads._ID,
81         Downloads.APP_DATA,
82         Downloads._DATA,
83         Downloads.MIMETYPE,
84         Downloads.VISIBILITY,
85         Downloads.CONTROL,
86         Downloads.STATUS,
87         Downloads.LAST_MODIFICATION,
88         Downloads.NOTIFICATION_PACKAGE,
89         Downloads.NOTIFICATION_CLASS,
90         Downloads.TOTAL_BYTES,
91         Downloads.CURRENT_BYTES,
92         Downloads.TITLE,
93         Downloads.DESCRIPTION
94     };
95
96     private static HashSet<String> sAppReadableColumnsSet;
97     static {
98         sAppReadableColumnsSet = new HashSet<String>();
99         for (int i = 0; i < sAppReadableColumnsArray.length; ++i) {
100             sAppReadableColumnsSet.add(sAppReadableColumnsArray[i]);
101         }
102     }
103
104     /** The database that lies underneath this content provider */
105     private SQLiteOpenHelper mOpenHelper = null;
106
107     /**
108      * Creates and updated database on demand when opening it.
109      * Helper class to create database the first time the provider is
110      * initialized and upgrade it when a new version of the provider needs
111      * an updated version of the database.
112      */
113     private final class DatabaseHelper extends SQLiteOpenHelper {
114
115         public DatabaseHelper(final Context context) {
116             super(context, DB_NAME, null, DB_VERSION);
117         }
118
119         /**
120          * Creates database the first time we try to open it.
121          */
122         @Override
123         public void onCreate(final SQLiteDatabase db) {
124             if (Constants.LOGVV) {
125                 Log.v(Constants.TAG, "populating new database");
126             }
127             createTable(db);
128         }
129
130         /* (not a javadoc comment)
131          * Checks data integrity when opening the database.
132          */
133         /*
134          * @Override
135          * public void onOpen(final SQLiteDatabase db) {
136          *     super.onOpen(db);
137          * }
138          */
139
140         /**
141          * Updates the database format when a content provider is used
142          * with a database that was created with a different format.
143          */
144         // Note: technically, this could also be a downgrade, so if we want
145         //       to gracefully handle upgrades we should be careful about
146         //       what to do on downgrades.
147         @Override
148         public void onUpgrade(final SQLiteDatabase db, int oldV, final int newV) {
149             if (oldV == DB_VERSION_NOP_UPGRADE_FROM) {
150                 if (newV == DB_VERSION_NOP_UPGRADE_TO) { // that's a no-op upgrade.
151                     return;
152                 }
153                 // NOP_FROM and NOP_TO are identical, just in different codelines. Upgrading
154                 //     from NOP_FROM is the same as upgrading from NOP_TO.
155                 oldV = DB_VERSION_NOP_UPGRADE_TO;
156             }
157             Log.i(Constants.TAG, "Upgrading downloads database from version " + oldV + " to " + newV
158                     + ", which will destroy all old data");
159             dropTable(db);
160             createTable(db);
161         }
162     }
163
164     /**
165      * Initializes the content provider when it is created.
166      */
167     @Override
168     public boolean onCreate() {
169         mOpenHelper = new DatabaseHelper(getContext());
170         return true;
171     }
172
173     /**
174      * Returns the content-provider-style MIME types of the various
175      * types accessible through this content provider.
176      */
177     @Override
178     public String getType(final Uri uri) {
179         int match = sURIMatcher.match(uri);
180         switch (match) {
181             case DOWNLOADS: {
182                 return DOWNLOAD_LIST_TYPE;
183             }
184             case DOWNLOADS_ID: {
185                 return DOWNLOAD_TYPE;
186             }
187             default: {
188                 if (Constants.LOGV) {
189                     Log.v(Constants.TAG, "calling getType on an unknown URI: " + uri);
190                 }
191                 throw new IllegalArgumentException("Unknown URI: " + uri);
192             }
193         }
194     }
195
196     /**
197      * Creates the table that'll hold the download information.
198      */
199     private void createTable(SQLiteDatabase db) {
200         try {
201             db.execSQL("CREATE TABLE " + DB_TABLE + "(" +
202                     Downloads._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
203                     Downloads.URI + " TEXT, " +
204                     Constants.RETRY_AFTER___REDIRECT_COUNT + " INTEGER, " +
205                     Downloads.APP_DATA + " TEXT, " +
206                     Downloads.NO_INTEGRITY + " BOOLEAN, " +
207                     Downloads.FILENAME_HINT + " TEXT, " +
208                     Constants.OTA_UPDATE + " BOOLEAN, " +
209                     Downloads._DATA + " TEXT, " +
210                     Downloads.MIMETYPE + " TEXT, " +
211                     Downloads.DESTINATION + " INTEGER, " +
212                     Constants.NO_SYSTEM_FILES + " BOOLEAN, " +
213                     Downloads.VISIBILITY + " INTEGER, " +
214                     Downloads.CONTROL + " INTEGER, " +
215                     Downloads.STATUS + " INTEGER, " +
216                     Constants.FAILED_CONNECTIONS + " INTEGER, " +
217                     Downloads.LAST_MODIFICATION + " BIGINT, " +
218                     Downloads.NOTIFICATION_PACKAGE + " TEXT, " +
219                     Downloads.NOTIFICATION_CLASS + " TEXT, " +
220                     Downloads.NOTIFICATION_EXTRAS + " TEXT, " +
221                     Downloads.COOKIE_DATA + " TEXT, " +
222                     Downloads.USER_AGENT + " TEXT, " +
223                     Downloads.REFERER + " TEXT, " +
224                     Downloads.TOTAL_BYTES + " INTEGER, " +
225                     Downloads.CURRENT_BYTES + " INTEGER, " +
226                     Constants.ETAG + " TEXT, " +
227                     Constants.UID + " INTEGER, " +
228                     Downloads.OTHER_UID + " INTEGER, " +
229                     Downloads.TITLE + " TEXT, " +
230                     Downloads.DESCRIPTION + " TEXT, " +
231                     Constants.MEDIA_SCANNED + " BOOLEAN);");
232         } catch (SQLException ex) {
233             Log.e(Constants.TAG, "couldn't create table in downloads database");
234             throw ex;
235         }
236     }
237
238     /**
239      * Deletes the table that holds the download information.
240      */
241     private void dropTable(SQLiteDatabase db) {
242         try {
243             db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);
244         } catch (SQLException ex) {
245             Log.e(Constants.TAG, "couldn't drop table in downloads database");
246             throw ex;
247         }
248     }
249
250     /**
251      * Inserts a row in the database
252      */
253     @Override
254     public Uri insert(final Uri uri, final ContentValues values) {
255         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
256
257         if (sURIMatcher.match(uri) != DOWNLOADS) {
258             if (Config.LOGD) {
259                 Log.d(Constants.TAG, "calling insert on an unknown/invalid URI: " + uri);
260             }
261             throw new IllegalArgumentException("Unknown/Invalid URI " + uri);
262         }
263
264         ContentValues filteredValues = new ContentValues();
265
266         copyString(Downloads.URI, values, filteredValues);
267         copyString(Downloads.APP_DATA, values, filteredValues);
268         copyBoolean(Downloads.NO_INTEGRITY, values, filteredValues);
269         copyString(Downloads.FILENAME_HINT, values, filteredValues);
270         copyString(Downloads.MIMETYPE, values, filteredValues);
271         Integer dest = values.getAsInteger(Downloads.DESTINATION);
272         if (dest != null) {
273             if (getContext().checkCallingPermission(Downloads.PERMISSION_ACCESS_ADVANCED)
274                     != PackageManager.PERMISSION_GRANTED
275                     && dest != Downloads.DESTINATION_EXTERNAL
276                     && dest != Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE) {
277                 throw new SecurityException("unauthorized destination code");
278             }
279             filteredValues.put(Downloads.DESTINATION, dest);
280         }
281         Integer vis = values.getAsInteger(Downloads.VISIBILITY);
282         if (vis == null) {
283             if (dest == Downloads.DESTINATION_EXTERNAL) {
284                 filteredValues.put(Downloads.VISIBILITY,
285                         Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
286             } else {
287                 filteredValues.put(Downloads.VISIBILITY, Downloads.VISIBILITY_HIDDEN);
288             }
289         } else {
290             filteredValues.put(Downloads.VISIBILITY, vis);
291         }
292         copyInteger(Downloads.CONTROL, values, filteredValues);
293         filteredValues.put(Downloads.STATUS, Downloads.STATUS_PENDING);
294         filteredValues.put(Downloads.LAST_MODIFICATION, System.currentTimeMillis());
295         String pckg = values.getAsString(Downloads.NOTIFICATION_PACKAGE);
296         String clazz = values.getAsString(Downloads.NOTIFICATION_CLASS);
297         if (pckg != null && clazz != null) {
298             int uid = Binder.getCallingUid();
299             try {
300                 if (uid == 0 ||
301                         getContext().getPackageManager().getApplicationInfo(pckg, 0).uid == uid) {
302                     filteredValues.put(Downloads.NOTIFICATION_PACKAGE, pckg);
303                     filteredValues.put(Downloads.NOTIFICATION_CLASS, clazz);
304                 }
305             } catch (PackageManager.NameNotFoundException ex) {
306                 /* ignored for now */
307             }
308         }
309         copyString(Downloads.NOTIFICATION_EXTRAS, values, filteredValues);
310         copyString(Downloads.COOKIE_DATA, values, filteredValues);
311         copyString(Downloads.USER_AGENT, values, filteredValues);
312         copyString(Downloads.REFERER, values, filteredValues);
313         if (getContext().checkCallingPermission(Downloads.PERMISSION_ACCESS_ADVANCED)
314                 == PackageManager.PERMISSION_GRANTED) {
315             copyInteger(Downloads.OTHER_UID, values, filteredValues);
316         }
317         filteredValues.put(Constants.UID, Binder.getCallingUid());
318         if (Binder.getCallingUid() == 0) {
319             copyInteger(Constants.UID, values, filteredValues);
320         }
321         copyString(Downloads.TITLE, values, filteredValues);
322         copyString(Downloads.DESCRIPTION, values, filteredValues);
323
324         if (Constants.LOGVV) {
325             Log.v(Constants.TAG, "initiating download with UID "
326                     + filteredValues.getAsInteger(Constants.UID));
327             if (filteredValues.containsKey(Downloads.OTHER_UID)) {
328                 Log.v(Constants.TAG, "other UID " +
329                         filteredValues.getAsInteger(Downloads.OTHER_UID));
330             }
331         }
332
333         Context context = getContext();
334         context.startService(new Intent(context, DownloadService.class));
335
336         long rowID = db.insert(DB_TABLE, null, filteredValues);
337
338         Uri ret = null;
339
340         if (rowID != -1) {
341             context.startService(new Intent(context, DownloadService.class));
342             ret = Uri.parse(Downloads.CONTENT_URI + "/" + rowID);
343             context.getContentResolver().notifyChange(uri, null);
344         } else {
345             if (Config.LOGD) {
346                 Log.d(Constants.TAG, "couldn't insert into downloads database");
347             }
348         }
349
350         return ret;
351     }
352
353     /**
354      * Starts a database query
355      */
356     @Override
357     public Cursor query(final Uri uri, String[] projection,
358              final String selection, final String[] selectionArgs,
359              final String sort) {
360
361         Helpers.validateSelection(selection, sAppReadableColumnsSet);
362
363         SQLiteDatabase db = mOpenHelper.getReadableDatabase();
364
365         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
366
367         int match = sURIMatcher.match(uri);
368         boolean emptyWhere = true;
369         switch (match) {
370             case DOWNLOADS: {
371                 qb.setTables(DB_TABLE);
372                 break;
373             }
374             case DOWNLOADS_ID: {
375                 qb.setTables(DB_TABLE);
376                 qb.appendWhere(Downloads._ID + "=");
377                 qb.appendWhere(uri.getPathSegments().get(1));
378                 emptyWhere = false;
379                 break;
380             }
381             default: {
382                 if (Constants.LOGV) {
383                     Log.v(Constants.TAG, "querying unknown URI: " + uri);
384                 }
385                 throw new IllegalArgumentException("Unknown URI: " + uri);
386             }
387         }
388
389         if (Binder.getCallingPid() != Process.myPid() && Binder.getCallingUid() != 0) {
390             if (!emptyWhere) {
391                 qb.appendWhere(" AND ");
392             }
393             qb.appendWhere("( " + Constants.UID + "=" +  Binder.getCallingUid() + " OR "
394                     + Downloads.OTHER_UID + "=" +  Binder.getCallingUid() + " )");
395             emptyWhere = false;
396
397             if (projection == null) {
398                 projection = sAppReadableColumnsArray;
399             } else {
400                 for (int i = 0; i < projection.length; ++i) {
401                     if (!sAppReadableColumnsSet.contains(projection[i])) {
402                         throw new IllegalArgumentException(
403                                 "column " + projection[i] + " is not allowed in queries");
404                     }
405                 }
406             }
407         }
408
409         if (Constants.LOGVV) {
410             java.lang.StringBuilder sb = new java.lang.StringBuilder();
411             sb.append("starting query, database is ");
412             if (db != null) {
413                 sb.append("not ");
414             }
415             sb.append("null; ");
416             if (projection == null) {
417                 sb.append("projection is null; ");
418             } else if (projection.length == 0) {
419                 sb.append("projection is empty; ");
420             } else {
421                 for (int i = 0; i < projection.length; ++i) {
422                     sb.append("projection[");
423                     sb.append(i);
424                     sb.append("] is ");
425                     sb.append(projection[i]);
426                     sb.append("; ");
427                 }
428             }
429             sb.append("selection is ");
430             sb.append(selection);
431             sb.append("; ");
432             if (selectionArgs == null) {
433                 sb.append("selectionArgs is null; ");
434             } else if (selectionArgs.length == 0) {
435                 sb.append("selectionArgs is empty; ");
436             } else {
437                 for (int i = 0; i < selectionArgs.length; ++i) {
438                     sb.append("selectionArgs[");
439                     sb.append(i);
440                     sb.append("] is ");
441                     sb.append(selectionArgs[i]);
442                     sb.append("; ");
443                 }
444             }
445             sb.append("sort is ");
446             sb.append(sort);
447             sb.append(".");
448             Log.v(Constants.TAG, sb.toString());
449         }
450
451         Cursor ret = qb.query(db, projection, selection, selectionArgs,
452                               null, null, sort);
453
454         if (ret != null) {
455            ret = new ReadOnlyCursorWrapper(ret);
456         }
457
458         if (ret != null) {
459             ret.setNotificationUri(getContext().getContentResolver(), uri);
460             if (Constants.LOGVV) {
461                 Log.v(Constants.TAG,
462                         "created cursor " + ret + " on behalf of " + Binder.getCallingPid());
463             }
464         } else {
465             if (Constants.LOGV) {
466                 Log.v(Constants.TAG, "query failed in downloads database");
467             }
468         }
469
470         return ret;
471     }
472
473     /**
474      * Updates a row in the database
475      */
476     @Override
477     public int update(final Uri uri, final ContentValues values,
478             final String where, final String[] whereArgs) {
479
480         Helpers.validateSelection(where, sAppReadableColumnsSet);
481
482         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
483
484         int count;
485         long rowId = 0;
486         boolean startService = false;
487
488         ContentValues filteredValues;
489         if (Binder.getCallingPid() != Process.myPid()) {
490             filteredValues = new ContentValues();
491             copyString(Downloads.APP_DATA, values, filteredValues);
492             copyInteger(Downloads.VISIBILITY, values, filteredValues);
493             Integer i = values.getAsInteger(Downloads.CONTROL);
494             if (i != null) {
495                 filteredValues.put(Downloads.CONTROL, i);
496                 startService = true;
497             }
498             copyInteger(Downloads.CONTROL, values, filteredValues);
499             copyString(Downloads.TITLE, values, filteredValues);
500             copyString(Downloads.DESCRIPTION, values, filteredValues);
501         } else {
502             filteredValues = values;
503         }
504         int match = sURIMatcher.match(uri);
505         switch (match) {
506             case DOWNLOADS:
507             case DOWNLOADS_ID: {
508                 String myWhere;
509                 if (where != null) {
510                     if (match == DOWNLOADS) {
511                         myWhere = "( " + where + " )";
512                     } else {
513                         myWhere = "( " + where + " ) AND ";
514                     }
515                 } else {
516                     myWhere = "";
517                 }
518                 if (match == DOWNLOADS_ID) {
519                     String segment = uri.getPathSegments().get(1);
520                     rowId = Long.parseLong(segment);
521                     myWhere += " ( " + Downloads._ID + " = " + rowId + " ) ";
522                 }
523                 if (Binder.getCallingPid() != Process.myPid() && Binder.getCallingUid() != 0) {
524                     myWhere += " AND ( " + Constants.UID + "=" +  Binder.getCallingUid() + " OR "
525                             + Downloads.OTHER_UID + "=" +  Binder.getCallingUid() + " )";
526                 }
527                 if (filteredValues.size() > 0) {
528                     count = db.update(DB_TABLE, filteredValues, myWhere, whereArgs);
529                 } else {
530                     count = 0;
531                 }
532                 break;
533             }
534             default: {
535                 if (Config.LOGD) {
536                     Log.d(Constants.TAG, "updating unknown/invalid URI: " + uri);
537                 }
538                 throw new UnsupportedOperationException("Cannot update URI: " + uri);
539             }
540         }
541         getContext().getContentResolver().notifyChange(uri, null);
542         if (startService) {
543             Context context = getContext();
544             context.startService(new Intent(context, DownloadService.class));
545         }
546         return count;
547     }
548
549     /**
550      * Deletes a row in the database
551      */
552     @Override
553     public int delete(final Uri uri, final String where,
554             final String[] whereArgs) {
555
556         Helpers.validateSelection(where, sAppReadableColumnsSet);
557
558         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
559         int count;
560         int match = sURIMatcher.match(uri);
561         switch (match) {
562             case DOWNLOADS:
563             case DOWNLOADS_ID: {
564                 String myWhere;
565                 if (where != null) {
566                     if (match == DOWNLOADS) {
567                         myWhere = "( " + where + " )";
568                     } else {
569                         myWhere = "( " + where + " ) AND ";
570                     }
571                 } else {
572                     myWhere = "";
573                 }
574                 if (match == DOWNLOADS_ID) {
575                     String segment = uri.getPathSegments().get(1);
576                     long rowId = Long.parseLong(segment);
577                     myWhere += " ( " + Downloads._ID + " = " + rowId + " ) ";
578                 }
579                 if (Binder.getCallingPid() != Process.myPid() && Binder.getCallingUid() != 0) {
580                     myWhere += " AND ( " + Constants.UID + "=" +  Binder.getCallingUid() + " OR "
581                             + Downloads.OTHER_UID + "=" +  Binder.getCallingUid() + " )";
582                 }
583                 count = db.delete(DB_TABLE, myWhere, whereArgs);
584                 break;
585             }
586             default: {
587                 if (Config.LOGD) {
588                     Log.d(Constants.TAG, "deleting unknown/invalid URI: " + uri);
589                 }
590                 throw new UnsupportedOperationException("Cannot delete URI: " + uri);
591             }
592         }
593         getContext().getContentResolver().notifyChange(uri, null);
594         return count;
595     }
596
597     /**
598      * Remotely opens a file
599      */
600     @Override
601     public ParcelFileDescriptor openFile(Uri uri, String mode)
602             throws FileNotFoundException {
603         if (Constants.LOGVV) {
604             Log.v(Constants.TAG, "openFile uri: " + uri + ", mode: " + mode
605                     + ", uid: " + Binder.getCallingUid());
606             Cursor cursor = query(Downloads.CONTENT_URI, new String[] { "_id" }, null, null, "_id");
607             if (cursor == null) {
608                 Log.v(Constants.TAG, "null cursor in openFile");
609             } else {
610                 if (!cursor.moveToFirst()) {
611                     Log.v(Constants.TAG, "empty cursor in openFile");
612                 } else {
613                     do {
614                         Log.v(Constants.TAG, "row " + cursor.getInt(0) + " available");
615                     } while(cursor.moveToNext());
616                 }
617                 cursor.close();
618             }
619             cursor = query(uri, new String[] { "_data" }, null, null, null);
620             if (cursor == null) {
621                 Log.v(Constants.TAG, "null cursor in openFile");
622             } else {
623                 if (!cursor.moveToFirst()) {
624                     Log.v(Constants.TAG, "empty cursor in openFile");
625                 } else {
626                     String filename = cursor.getString(0);
627                     Log.v(Constants.TAG, "filename in openFile: " + filename);
628                     if (new java.io.File(filename).isFile()) {
629                         Log.v(Constants.TAG, "file exists in openFile");
630                     }
631                 }
632                cursor.close();
633             }
634         }
635
636         // This logic is mostly copied form openFileHelper. If openFileHelper eventually
637         //     gets split into small bits (to extract the filename and the modebits),
638         //     this code could use the separate bits and be deeply simplified.
639         Cursor c = query(uri, new String[]{"_data"}, null, null, null);
640         int count = (c != null) ? c.getCount() : 0;
641         if (count != 1) {
642             // If there is not exactly one result, throw an appropriate exception.
643             if (c != null) {
644                 c.close();
645             }
646             if (count == 0) {
647                 throw new FileNotFoundException("No entry for " + uri);
648             }
649             throw new FileNotFoundException("Multiple items at " + uri);
650         }
651
652         c.moveToFirst();
653         String path = c.getString(0);
654         c.close();
655         if (path == null) {
656             throw new FileNotFoundException("No filename found.");
657         }
658         if (!Helpers.isFilenameValid(path)) {
659             throw new FileNotFoundException("Invalid filename.");
660         }
661
662         if (!"r".equals(mode)) {
663             throw new FileNotFoundException("Bad mode for " + uri + ": " + mode);
664         }
665         ParcelFileDescriptor ret = ParcelFileDescriptor.open(new File(path),
666                 ParcelFileDescriptor.MODE_READ_ONLY);
667
668         if (ret == null) {
669             if (Constants.LOGV) {
670                 Log.v(Constants.TAG, "couldn't open file");
671             }
672             throw new FileNotFoundException("couldn't open file");
673         } else {
674             ContentValues values = new ContentValues();
675             values.put(Downloads.LAST_MODIFICATION, System.currentTimeMillis());
676             update(uri, values, null, null);
677         }
678         return ret;
679     }
680
681     private static final void copyInteger(String key, ContentValues from, ContentValues to) {
682         Integer i = from.getAsInteger(key);
683         if (i != null) {
684             to.put(key, i);
685         }
686     }
687
688     private static final void copyBoolean(String key, ContentValues from, ContentValues to) {
689         Boolean b = from.getAsBoolean(key);
690         if (b != null) {
691             to.put(key, b);
692         }
693     }
694
695     private static final void copyString(String key, ContentValues from, ContentValues to) {
696         String s = from.getAsString(key);
697         if (s != null) {
698             to.put(key, s);
699         }
700     }
701
702     private class ReadOnlyCursorWrapper extends CursorWrapper implements CrossProcessCursor {
703         public ReadOnlyCursorWrapper(Cursor cursor) {
704             super(cursor);
705             mCursor = (CrossProcessCursor) cursor;
706         }
707
708         public boolean deleteRow() {
709             throw new SecurityException("Download manager cursors are read-only");
710         }
711
712         public boolean commitUpdates() {
713             throw new SecurityException("Download manager cursors are read-only");
714         }
715
716         public void fillWindow(int pos, CursorWindow window) {
717             mCursor.fillWindow(pos, window);
718         }
719
720         public CursorWindow getWindow() {
721             return mCursor.getWindow();
722         }
723
724         public boolean onMove(int oldPosition, int newPosition) {
725             return mCursor.onMove(oldPosition, newPosition);
726         }
727
728         private CrossProcessCursor mCursor;
729     }
730
731 }