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