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