4007e7617efa6595c3a917bcc9c5281abab77272
[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.ApplicationInfo;
25 import android.content.pm.PackageManager;
26 import android.content.pm.PackageManager.NameNotFoundException;
27 import android.database.CrossProcessCursor;
28 import android.database.Cursor;
29 import android.database.CursorWindow;
30 import android.database.CursorWrapper;
31 import android.database.SQLException;
32 import android.database.sqlite.SQLiteDatabase;
33 import android.database.sqlite.SQLiteOpenHelper;
34 import android.database.sqlite.SQLiteQueryBuilder;
35 import android.net.Uri;
36 import android.os.Binder;
37 import android.os.ParcelFileDescriptor;
38 import android.os.Process;
39 import android.provider.Downloads;
40 import android.util.Config;
41 import android.util.Log;
42
43 import com.google.common.annotations.VisibleForTesting;
44
45 import java.io.File;
46 import java.io.FileNotFoundException;
47 import java.util.HashSet;
48 import java.util.Map;
49
50
51 /**
52  * Allows application to interact with the download manager.
53  */
54 public final class DownloadProvider extends ContentProvider {
55
56     /** Database filename */
57     private static final String DB_NAME = "downloads.db";
58     /** Current database version */
59     private static final int DB_VERSION = 101;
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     /** URI matcher constant for the URI of a download's request headers */
75     private static final int REQUEST_HEADERS_URI = 3;
76     static {
77         sURIMatcher.addURI("downloads", "download", DOWNLOADS);
78         sURIMatcher.addURI("downloads", "download/#", DOWNLOADS_ID);
79         sURIMatcher.addURI("downloads", "download/#/" + Downloads.Impl.RequestHeaders.URI_SEGMENT,
80                            REQUEST_HEADERS_URI);
81     }
82
83     private static final String[] sAppReadableColumnsArray = new String[] {
84         Downloads.Impl._ID,
85         Downloads.Impl.COLUMN_APP_DATA,
86         Downloads.Impl._DATA,
87         Downloads.Impl.COLUMN_MIME_TYPE,
88         Downloads.Impl.COLUMN_VISIBILITY,
89         Downloads.Impl.COLUMN_DESTINATION,
90         Downloads.Impl.COLUMN_CONTROL,
91         Downloads.Impl.COLUMN_STATUS,
92         Downloads.Impl.COLUMN_LAST_MODIFICATION,
93         Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE,
94         Downloads.Impl.COLUMN_NOTIFICATION_CLASS,
95         Downloads.Impl.COLUMN_TOTAL_BYTES,
96         Downloads.Impl.COLUMN_CURRENT_BYTES,
97         Downloads.Impl.COLUMN_TITLE,
98         Downloads.Impl.COLUMN_DESCRIPTION
99     };
100
101     private static HashSet<String> sAppReadableColumnsSet;
102     static {
103         sAppReadableColumnsSet = new HashSet<String>();
104         for (int i = 0; i < sAppReadableColumnsArray.length; ++i) {
105             sAppReadableColumnsSet.add(sAppReadableColumnsArray[i]);
106         }
107     }
108
109     /** The database that lies underneath this content provider */
110     private SQLiteOpenHelper mOpenHelper = null;
111
112     /** List of uids that can access the downloads */
113     private int mSystemUid = -1;
114     private int mDefContainerUid = -1;
115
116     @VisibleForTesting
117     SystemFacade mSystemFacade;
118
119     /**
120      * Creates and updated database on demand when opening it.
121      * Helper class to create database the first time the provider is
122      * initialized and upgrade it when a new version of the provider needs
123      * an updated version of the database.
124      */
125     private final class DatabaseHelper extends SQLiteOpenHelper {
126         public DatabaseHelper(final Context context) {
127             super(context, DB_NAME, null, DB_VERSION);
128         }
129
130         /**
131          * Creates database the first time we try to open it.
132          */
133         @Override
134         public void onCreate(final SQLiteDatabase db) {
135             if (Constants.LOGVV) {
136                 Log.v(Constants.TAG, "populating new database");
137             }
138             onUpgrade(db, 0, DB_VERSION);
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: to support downgrades, creating a table should always drop it first if it already
146          * exists.
147          */
148         @Override
149         public void onUpgrade(final SQLiteDatabase db, int oldV, final int newV) {
150             if (oldV == 31) {
151                 // 31 and 100 are identical, just in different codelines. Upgrading from 31 is the
152                 // same as upgrading from 100.
153                 oldV = 100;
154             } else if (oldV < 100) {
155                 // no logic to upgrade from these older version, just recreate the DB
156                 Log.i(Constants.TAG, "Upgrading downloads database from version " + oldV
157                       + " to version " + newV + ", which will destroy all old data");
158                 oldV = 99;
159             } else if (oldV > newV) {
160                 // user must have downgraded software; we have no way to know how to downgrade the
161                 // DB, so just recreate it
162                 Log.i(Constants.TAG, "Downgrading downloads database from version " + oldV
163                       + " (current version is " + newV + "), destroying all old data");
164                 oldV = 99;
165             }
166
167             for (int version = oldV + 1; version <= newV; version++) {
168                 upgradeTo(db, version);
169             }
170         }
171
172         /**
173          * Upgrade database from (version - 1) to version.
174          */
175         private void upgradeTo(SQLiteDatabase db, int version) {
176             switch (version) {
177                 case 100:
178                     createDownloadsTable(db);
179                     break;
180
181                 case 101:
182                     createHeadersTable(db);
183                     break;
184
185                 default:
186                     throw new IllegalStateException("Don't know how to upgrade to " + version);
187             }
188         }
189
190         /**
191          * Creates the table that'll hold the download information.
192          */
193         private void createDownloadsTable(SQLiteDatabase db) {
194             try {
195                 db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);
196                 db.execSQL("CREATE TABLE " + DB_TABLE + "(" +
197                         Downloads.Impl._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
198                         Downloads.Impl.COLUMN_URI + " TEXT, " +
199                         Constants.RETRY_AFTER_X_REDIRECT_COUNT + " INTEGER, " +
200                         Downloads.Impl.COLUMN_APP_DATA + " TEXT, " +
201                         Downloads.Impl.COLUMN_NO_INTEGRITY + " BOOLEAN, " +
202                         Downloads.Impl.COLUMN_FILE_NAME_HINT + " TEXT, " +
203                         Constants.OTA_UPDATE + " BOOLEAN, " +
204                         Downloads.Impl._DATA + " TEXT, " +
205                         Downloads.Impl.COLUMN_MIME_TYPE + " TEXT, " +
206                         Downloads.Impl.COLUMN_DESTINATION + " INTEGER, " +
207                         Constants.NO_SYSTEM_FILES + " BOOLEAN, " +
208                         Downloads.Impl.COLUMN_VISIBILITY + " INTEGER, " +
209                         Downloads.Impl.COLUMN_CONTROL + " INTEGER, " +
210                         Downloads.Impl.COLUMN_STATUS + " INTEGER, " +
211                         Constants.FAILED_CONNECTIONS + " INTEGER, " +
212                         Downloads.Impl.COLUMN_LAST_MODIFICATION + " BIGINT, " +
213                         Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE + " TEXT, " +
214                         Downloads.Impl.COLUMN_NOTIFICATION_CLASS + " TEXT, " +
215                         Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS + " TEXT, " +
216                         Downloads.Impl.COLUMN_COOKIE_DATA + " TEXT, " +
217                         Downloads.Impl.COLUMN_USER_AGENT + " TEXT, " +
218                         Downloads.Impl.COLUMN_REFERER + " TEXT, " +
219                         Downloads.Impl.COLUMN_TOTAL_BYTES + " INTEGER, " +
220                         Downloads.Impl.COLUMN_CURRENT_BYTES + " INTEGER, " +
221                         Constants.ETAG + " TEXT, " +
222                         Constants.UID + " INTEGER, " +
223                         Downloads.Impl.COLUMN_OTHER_UID + " INTEGER, " +
224                         Downloads.Impl.COLUMN_TITLE + " TEXT, " +
225                         Downloads.Impl.COLUMN_DESCRIPTION + " TEXT, " +
226                         Constants.MEDIA_SCANNED + " BOOLEAN);");
227             } catch (SQLException ex) {
228                 Log.e(Constants.TAG, "couldn't create table in downloads database");
229                 throw ex;
230             }
231         }
232
233         private void createHeadersTable(SQLiteDatabase db) {
234             db.execSQL("DROP TABLE IF EXISTS " + Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE);
235             db.execSQL("CREATE TABLE " + Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE + "(" +
236                        "id INTEGER PRIMARY KEY AUTOINCREMENT," +
237                        Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + " INTEGER NOT NULL," +
238                        Downloads.Impl.RequestHeaders.COLUMN_HEADER + " TEXT NOT NULL," +
239                        Downloads.Impl.RequestHeaders.COLUMN_VALUE + " TEXT NOT NULL" +
240                        ");");
241         }
242     }
243
244     /**
245      * Initializes the content provider when it is created.
246      */
247     @Override
248     public boolean onCreate() {
249         if (mSystemFacade == null) {
250             mSystemFacade = new RealSystemFacade(getContext());
251         }
252
253         mOpenHelper = new DatabaseHelper(getContext());
254         // Initialize the system uid
255         mSystemUid = Process.SYSTEM_UID;
256         // Initialize the default container uid. Package name hardcoded
257         // for now.
258         ApplicationInfo appInfo = null;
259         try {
260             appInfo = getContext().getPackageManager().
261                     getApplicationInfo("com.android.defcontainer", 0);
262         } catch (NameNotFoundException e) {
263             // TODO Auto-generated catch block
264             e.printStackTrace();
265         }
266         if (appInfo != null) {
267             mDefContainerUid = appInfo.uid;
268         }
269         return true;
270     }
271
272     /**
273      * Returns the content-provider-style MIME types of the various
274      * types accessible through this content provider.
275      */
276     @Override
277     public String getType(final Uri uri) {
278         int match = sURIMatcher.match(uri);
279         switch (match) {
280             case DOWNLOADS: {
281                 return DOWNLOAD_LIST_TYPE;
282             }
283             case DOWNLOADS_ID: {
284                 return DOWNLOAD_TYPE;
285             }
286             default: {
287                 if (Constants.LOGV) {
288                     Log.v(Constants.TAG, "calling getType on an unknown URI: " + uri);
289                 }
290                 throw new IllegalArgumentException("Unknown URI: " + uri);
291             }
292         }
293     }
294
295     /**
296      * Inserts a row in the database
297      */
298     @Override
299     public Uri insert(final Uri uri, final ContentValues values) {
300         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
301
302         if (sURIMatcher.match(uri) != DOWNLOADS) {
303             if (Config.LOGD) {
304                 Log.d(Constants.TAG, "calling insert on an unknown/invalid URI: " + uri);
305             }
306             throw new IllegalArgumentException("Unknown/Invalid URI " + uri);
307         }
308
309         ContentValues filteredValues = new ContentValues();
310
311         copyString(Downloads.Impl.COLUMN_URI, values, filteredValues);
312         copyString(Downloads.Impl.COLUMN_APP_DATA, values, filteredValues);
313         copyBoolean(Downloads.Impl.COLUMN_NO_INTEGRITY, values, filteredValues);
314         copyString(Downloads.Impl.COLUMN_FILE_NAME_HINT, values, filteredValues);
315         copyString(Downloads.Impl.COLUMN_MIME_TYPE, values, filteredValues);
316         Integer dest = values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION);
317         if (dest != null) {
318             if (getContext().checkCallingPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED)
319                     != PackageManager.PERMISSION_GRANTED
320                     && dest != Downloads.Impl.DESTINATION_EXTERNAL
321                     && dest != Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE
322                     && dest != Downloads.Impl.DESTINATION_FILE_URI) {
323                 throw new SecurityException("unauthorized destination code");
324             }
325             if (dest == Downloads.Impl.DESTINATION_FILE_URI) {
326                 getContext().enforcePermission(
327                         android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
328                         Binder.getCallingPid(), Binder.getCallingUid(),
329                         "need WRITE_EXTERNAL_STORAGE permission to use DESTINATION_FILE_URI");
330             }
331             filteredValues.put(Downloads.Impl.COLUMN_DESTINATION, dest);
332         }
333         Integer vis = values.getAsInteger(Downloads.Impl.COLUMN_VISIBILITY);
334         if (vis == null) {
335             if (dest == Downloads.Impl.DESTINATION_EXTERNAL) {
336                 filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY,
337                         Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
338             } else {
339                 filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY,
340                         Downloads.Impl.VISIBILITY_HIDDEN);
341             }
342         } else {
343             filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY, vis);
344         }
345         copyInteger(Downloads.Impl.COLUMN_CONTROL, values, filteredValues);
346         filteredValues.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING);
347         filteredValues.put(Downloads.Impl.COLUMN_LAST_MODIFICATION,
348                            mSystemFacade.currentTimeMillis());
349         String pckg = values.getAsString(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE);
350         String clazz = values.getAsString(Downloads.Impl.COLUMN_NOTIFICATION_CLASS);
351         if (pckg != null && clazz != null) {
352             int uid = Binder.getCallingUid();
353             try {
354                 if (uid == 0 ||
355                         getContext().getPackageManager().getApplicationInfo(pckg, 0).uid == uid) {
356                     filteredValues.put(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE, pckg);
357                     filteredValues.put(Downloads.Impl.COLUMN_NOTIFICATION_CLASS, clazz);
358                 }
359             } catch (PackageManager.NameNotFoundException ex) {
360                 /* ignored for now */
361             }
362         }
363         copyString(Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS, values, filteredValues);
364         copyString(Downloads.Impl.COLUMN_COOKIE_DATA, values, filteredValues);
365         copyString(Downloads.Impl.COLUMN_USER_AGENT, values, filteredValues);
366         copyString(Downloads.Impl.COLUMN_REFERER, values, filteredValues);
367         if (getContext().checkCallingPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED)
368                 == PackageManager.PERMISSION_GRANTED) {
369             copyInteger(Downloads.Impl.COLUMN_OTHER_UID, values, filteredValues);
370         }
371         filteredValues.put(Constants.UID, Binder.getCallingUid());
372         if (Binder.getCallingUid() == 0) {
373             copyInteger(Constants.UID, values, filteredValues);
374         }
375         copyString(Downloads.Impl.COLUMN_TITLE, values, filteredValues);
376         copyString(Downloads.Impl.COLUMN_DESCRIPTION, values, filteredValues);
377
378         if (Constants.LOGVV) {
379             Log.v(Constants.TAG, "initiating download with UID "
380                     + filteredValues.getAsInteger(Constants.UID));
381             if (filteredValues.containsKey(Downloads.Impl.COLUMN_OTHER_UID)) {
382                 Log.v(Constants.TAG, "other UID " +
383                         filteredValues.getAsInteger(Downloads.Impl.COLUMN_OTHER_UID));
384             }
385         }
386
387         Context context = getContext();
388         context.startService(new Intent(context, DownloadService.class));
389
390         long rowID = db.insert(DB_TABLE, null, filteredValues);
391         insertRequestHeaders(db, rowID, values);
392
393         Uri ret = null;
394
395         if (rowID != -1) {
396             context.startService(new Intent(context, DownloadService.class));
397             ret = Uri.parse(Downloads.Impl.CONTENT_URI + "/" + rowID);
398             context.getContentResolver().notifyChange(uri, null);
399         } else {
400             if (Config.LOGD) {
401                 Log.d(Constants.TAG, "couldn't insert into downloads database");
402             }
403         }
404
405         return ret;
406     }
407
408     /**
409      * Starts a database query
410      */
411     @Override
412     public Cursor query(final Uri uri, String[] projection,
413              final String selection, final String[] selectionArgs,
414              final String sort) {
415
416         Helpers.validateSelection(selection, sAppReadableColumnsSet);
417
418         SQLiteDatabase db = mOpenHelper.getReadableDatabase();
419
420         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
421
422         int match = sURIMatcher.match(uri);
423         boolean emptyWhere = true;
424         switch (match) {
425             case DOWNLOADS: {
426                 qb.setTables(DB_TABLE);
427                 break;
428             }
429             case DOWNLOADS_ID: {
430                 qb.setTables(DB_TABLE);
431                 qb.appendWhere(Downloads.Impl._ID + "=");
432                 qb.appendWhere(getDownloadIdFromUri(uri));
433                 emptyWhere = false;
434                 break;
435             }
436             case REQUEST_HEADERS_URI:
437                 if (projection != null || selection != null || sort != null) {
438                     throw new UnsupportedOperationException("Request header queries do not support "
439                                                             + "projections, selections or sorting");
440                 }
441                 return queryRequestHeaders(db, uri);
442             default: {
443                 if (Constants.LOGV) {
444                     Log.v(Constants.TAG, "querying unknown URI: " + uri);
445                 }
446                 throw new IllegalArgumentException("Unknown URI: " + uri);
447             }
448         }
449
450         if (shouldRestrictVisibility()) {
451             boolean canSeeAllExternal;
452             if (projection == null) {
453                 projection = sAppReadableColumnsArray;
454                 // sAppReadableColumnsArray includes _DATA, which is not allowed
455                 // to be seen except by the initiating application
456                 canSeeAllExternal = false;
457             } else {
458                 canSeeAllExternal = getContext().checkCallingPermission(
459                         Downloads.Impl.PERMISSION_SEE_ALL_EXTERNAL)
460                         == PackageManager.PERMISSION_GRANTED;
461                 for (int i = 0; i < projection.length; ++i) {
462                     if (!sAppReadableColumnsSet.contains(projection[i])) {
463                         throw new IllegalArgumentException(
464                                 "column " + projection[i] + " is not allowed in queries");
465                     }
466                     canSeeAllExternal = canSeeAllExternal
467                             && !projection[i].equals(Downloads.Impl._DATA);
468                 }
469             }
470             if (!emptyWhere) {
471                 qb.appendWhere(" AND ");
472                 emptyWhere = false;
473             }
474             String validUid = "( " + Constants.UID + "="
475                     + Binder.getCallingUid() + " OR "
476                     + Downloads.Impl.COLUMN_OTHER_UID + "="
477                     + Binder.getCallingUid() + " )";
478             if (canSeeAllExternal) {
479                 qb.appendWhere("( " + validUid + " OR "
480                         + Downloads.Impl.DESTINATION_EXTERNAL + " = "
481                         + Downloads.Impl.COLUMN_DESTINATION + " )");
482             } else {
483                 qb.appendWhere(validUid);
484             }
485         }
486
487         if (Constants.LOGVV) {
488             java.lang.StringBuilder sb = new java.lang.StringBuilder();
489             sb.append("starting query, database is ");
490             if (db != null) {
491                 sb.append("not ");
492             }
493             sb.append("null; ");
494             if (projection == null) {
495                 sb.append("projection is null; ");
496             } else if (projection.length == 0) {
497                 sb.append("projection is empty; ");
498             } else {
499                 for (int i = 0; i < projection.length; ++i) {
500                     sb.append("projection[");
501                     sb.append(i);
502                     sb.append("] is ");
503                     sb.append(projection[i]);
504                     sb.append("; ");
505                 }
506             }
507             sb.append("selection is ");
508             sb.append(selection);
509             sb.append("; ");
510             if (selectionArgs == null) {
511                 sb.append("selectionArgs is null; ");
512             } else if (selectionArgs.length == 0) {
513                 sb.append("selectionArgs is empty; ");
514             } else {
515                 for (int i = 0; i < selectionArgs.length; ++i) {
516                     sb.append("selectionArgs[");
517                     sb.append(i);
518                     sb.append("] is ");
519                     sb.append(selectionArgs[i]);
520                     sb.append("; ");
521                 }
522             }
523             sb.append("sort is ");
524             sb.append(sort);
525             sb.append(".");
526             Log.v(Constants.TAG, sb.toString());
527         }
528
529         Cursor ret = qb.query(db, projection, selection, selectionArgs,
530                               null, null, sort);
531
532         if (ret != null) {
533            ret = new ReadOnlyCursorWrapper(ret);
534         }
535
536         if (ret != null) {
537             ret.setNotificationUri(getContext().getContentResolver(), uri);
538             if (Constants.LOGVV) {
539                 Log.v(Constants.TAG,
540                         "created cursor " + ret + " on behalf of " + Binder.getCallingPid());
541             }
542         } else {
543             if (Constants.LOGV) {
544                 Log.v(Constants.TAG, "query failed in downloads database");
545             }
546         }
547
548         return ret;
549     }
550
551     private String getDownloadIdFromUri(final Uri uri) {
552         return uri.getPathSegments().get(1);
553     }
554
555     /**
556      * Insert request headers for a download into the DB.
557      */
558     private void insertRequestHeaders(SQLiteDatabase db, long downloadId, ContentValues values) {
559         ContentValues rowValues = new ContentValues();
560         rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID, downloadId);
561         for (Map.Entry<String, Object> entry : values.valueSet()) {
562             String key = entry.getKey();
563             if (key.startsWith(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX)) {
564                 String headerLine = entry.getValue().toString();
565                 if (!headerLine.contains(":")) {
566                     throw new IllegalArgumentException("Invalid HTTP header line: " + headerLine);
567                 }
568                 String[] parts = headerLine.split(":", 2);
569                 rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_HEADER, parts[0].trim());
570                 rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_VALUE, parts[1].trim());
571                 db.insert(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, null, rowValues);
572             }
573         }
574     }
575
576     /**
577      * Handle a query for the custom request headers registered for a download.
578      */
579     private Cursor queryRequestHeaders(SQLiteDatabase db, Uri uri) {
580         String where = Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "="
581                        + getDownloadIdFromUri(uri);
582         String[] projection = new String[] {Downloads.Impl.RequestHeaders.COLUMN_HEADER,
583                                             Downloads.Impl.RequestHeaders.COLUMN_VALUE};
584         Cursor cursor = db.query(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, projection, where,
585                                  null, null, null, null);
586         return new ReadOnlyCursorWrapper(cursor);
587     }
588
589     /**
590      * Delete request headers for downloads matching the given query.
591      */
592     private void deleteRequestHeaders(SQLiteDatabase db, String where, String[] whereArgs) {
593         String[] projection = new String[] {Downloads.Impl._ID};
594         Cursor cursor = db.query(DB_TABLE, projection , where, whereArgs, null, null, null, null);
595         try {
596             for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
597                 long id = cursor.getLong(0);
598                 String idWhere = Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "=" + id;
599                 db.delete(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, idWhere, null);
600             }
601         } finally {
602             cursor.close();
603         }
604     }
605
606     /**
607      * @return true if we should restrict this call to viewing only its own downloads
608      */
609     private boolean shouldRestrictVisibility() {
610         int callingUid = Binder.getCallingUid();
611         return Binder.getCallingPid() != Process.myPid() &&
612                 callingUid != mSystemUid &&
613                 callingUid != mDefContainerUid &&
614                 Process.supportsProcesses();
615     }
616
617     /**
618      * Updates a row in the database
619      */
620     @Override
621     public int update(final Uri uri, final ContentValues values,
622             final String where, final String[] whereArgs) {
623
624         Helpers.validateSelection(where, sAppReadableColumnsSet);
625
626         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
627
628         int count;
629         long rowId = 0;
630         boolean startService = false;
631
632         ContentValues filteredValues;
633         if (Binder.getCallingPid() != Process.myPid()) {
634             filteredValues = new ContentValues();
635             copyString(Downloads.Impl.COLUMN_APP_DATA, values, filteredValues);
636             copyInteger(Downloads.Impl.COLUMN_VISIBILITY, values, filteredValues);
637             Integer i = values.getAsInteger(Downloads.Impl.COLUMN_CONTROL);
638             if (i != null) {
639                 filteredValues.put(Downloads.Impl.COLUMN_CONTROL, i);
640                 startService = true;
641             }
642             copyInteger(Downloads.Impl.COLUMN_CONTROL, values, filteredValues);
643             copyString(Downloads.Impl.COLUMN_TITLE, values, filteredValues);
644             copyString(Downloads.Impl.COLUMN_DESCRIPTION, values, filteredValues);
645         } else {
646             filteredValues = values;
647             String filename = values.getAsString(Downloads.Impl._DATA);
648             if (filename != null) {
649                 Cursor c = query(uri, new String[]
650                         { Downloads.Impl.COLUMN_TITLE }, null, null, null);
651                 if (!c.moveToFirst() || c.getString(0) == null) {
652                     values.put(Downloads.Impl.COLUMN_TITLE,
653                             new File(filename).getName());
654                 }
655                 c.close();
656             }
657         }
658         int match = sURIMatcher.match(uri);
659         switch (match) {
660             case DOWNLOADS:
661             case DOWNLOADS_ID: {
662                 String myWhere;
663                 if (where != null) {
664                     if (match == DOWNLOADS) {
665                         myWhere = "( " + where + " )";
666                     } else {
667                         myWhere = "( " + where + " ) AND ";
668                     }
669                 } else {
670                     myWhere = "";
671                 }
672                 if (match == DOWNLOADS_ID) {
673                     String segment = getDownloadIdFromUri(uri);
674                     rowId = Long.parseLong(segment);
675                     myWhere += " ( " + Downloads.Impl._ID + " = " + rowId + " ) ";
676                 }
677                 int callingUid = Binder.getCallingUid();
678                 if (Binder.getCallingPid() != Process.myPid() &&
679                         callingUid != mSystemUid &&
680                         callingUid != mDefContainerUid) {
681                     myWhere += " AND ( " + Constants.UID + "=" +  Binder.getCallingUid() + " OR "
682                             + Downloads.Impl.COLUMN_OTHER_UID + "=" +  Binder.getCallingUid() + " )";
683                 }
684                 if (filteredValues.size() > 0) {
685                     count = db.update(DB_TABLE, filteredValues, myWhere, whereArgs);
686                 } else {
687                     count = 0;
688                 }
689                 break;
690             }
691             default: {
692                 if (Config.LOGD) {
693                     Log.d(Constants.TAG, "updating unknown/invalid URI: " + uri);
694                 }
695                 throw new UnsupportedOperationException("Cannot update URI: " + uri);
696             }
697         }
698         getContext().getContentResolver().notifyChange(uri, null);
699         if (startService) {
700             Context context = getContext();
701             context.startService(new Intent(context, DownloadService.class));
702         }
703         return count;
704     }
705
706     /**
707      * Deletes a row in the database
708      */
709     @Override
710     public int delete(final Uri uri, final String where,
711             final String[] whereArgs) {
712
713         Helpers.validateSelection(where, sAppReadableColumnsSet);
714
715         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
716         int count;
717         int match = sURIMatcher.match(uri);
718         switch (match) {
719             case DOWNLOADS:
720             case DOWNLOADS_ID: {
721                 String myWhere;
722                 if (where != null) {
723                     if (match == DOWNLOADS) {
724                         myWhere = "( " + where + " )";
725                     } else {
726                         myWhere = "( " + where + " ) AND ";
727                     }
728                 } else {
729                     myWhere = "";
730                 }
731                 if (match == DOWNLOADS_ID) {
732                     String segment = getDownloadIdFromUri(uri);
733                     long rowId = Long.parseLong(segment);
734                     myWhere += " ( " + Downloads.Impl._ID + " = " + rowId + " ) ";
735                 }
736                 int callingUid = Binder.getCallingUid();
737                 if (Binder.getCallingPid() != Process.myPid() &&
738                         callingUid != mSystemUid &&
739                         callingUid != mDefContainerUid) {
740                     myWhere += " AND ( " + Constants.UID + "=" +  Binder.getCallingUid() + " OR "
741                             + Downloads.Impl.COLUMN_OTHER_UID + "="
742                             +  Binder.getCallingUid() + " )";
743                 }
744                 deleteRequestHeaders(db, where, whereArgs);
745                 count = db.delete(DB_TABLE, myWhere, whereArgs);
746                 break;
747             }
748             default: {
749                 if (Config.LOGD) {
750                     Log.d(Constants.TAG, "deleting unknown/invalid URI: " + uri);
751                 }
752                 throw new UnsupportedOperationException("Cannot delete URI: " + uri);
753             }
754         }
755         getContext().getContentResolver().notifyChange(uri, null);
756         return count;
757     }
758
759     /**
760      * Remotely opens a file
761      */
762     @Override
763     public ParcelFileDescriptor openFile(Uri uri, String mode)
764             throws FileNotFoundException {
765         if (Constants.LOGVV) {
766             Log.v(Constants.TAG, "openFile uri: " + uri + ", mode: " + mode
767                     + ", uid: " + Binder.getCallingUid());
768             Cursor cursor = query(Downloads.Impl.CONTENT_URI,
769                     new String[] { "_id" }, null, null, "_id");
770             if (cursor == null) {
771                 Log.v(Constants.TAG, "null cursor in openFile");
772             } else {
773                 if (!cursor.moveToFirst()) {
774                     Log.v(Constants.TAG, "empty cursor in openFile");
775                 } else {
776                     do {
777                         Log.v(Constants.TAG, "row " + cursor.getInt(0) + " available");
778                     } while(cursor.moveToNext());
779                 }
780                 cursor.close();
781             }
782             cursor = query(uri, new String[] { "_data" }, null, null, null);
783             if (cursor == null) {
784                 Log.v(Constants.TAG, "null cursor in openFile");
785             } else {
786                 if (!cursor.moveToFirst()) {
787                     Log.v(Constants.TAG, "empty cursor in openFile");
788                 } else {
789                     String filename = cursor.getString(0);
790                     Log.v(Constants.TAG, "filename in openFile: " + filename);
791                     if (new java.io.File(filename).isFile()) {
792                         Log.v(Constants.TAG, "file exists in openFile");
793                     }
794                 }
795                cursor.close();
796             }
797         }
798
799         // This logic is mostly copied form openFileHelper. If openFileHelper eventually
800         //     gets split into small bits (to extract the filename and the modebits),
801         //     this code could use the separate bits and be deeply simplified.
802         Cursor c = query(uri, new String[]{"_data"}, null, null, null);
803         int count = (c != null) ? c.getCount() : 0;
804         if (count != 1) {
805             // If there is not exactly one result, throw an appropriate exception.
806             if (c != null) {
807                 c.close();
808             }
809             if (count == 0) {
810                 throw new FileNotFoundException("No entry for " + uri);
811             }
812             throw new FileNotFoundException("Multiple items at " + uri);
813         }
814
815         c.moveToFirst();
816         String path = c.getString(0);
817         c.close();
818         if (path == null) {
819             throw new FileNotFoundException("No filename found.");
820         }
821         if (!Helpers.isFilenameValid(path)) {
822             throw new FileNotFoundException("Invalid filename.");
823         }
824
825         if (!"r".equals(mode)) {
826             throw new FileNotFoundException("Bad mode for " + uri + ": " + mode);
827         }
828         ParcelFileDescriptor ret = ParcelFileDescriptor.open(new File(path),
829                 ParcelFileDescriptor.MODE_READ_ONLY);
830
831         if (ret == null) {
832             if (Constants.LOGV) {
833                 Log.v(Constants.TAG, "couldn't open file");
834             }
835             throw new FileNotFoundException("couldn't open file");
836         } else {
837             ContentValues values = new ContentValues();
838             values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
839             update(uri, values, null, null);
840         }
841         return ret;
842     }
843
844     private static final void copyInteger(String key, ContentValues from, ContentValues to) {
845         Integer i = from.getAsInteger(key);
846         if (i != null) {
847             to.put(key, i);
848         }
849     }
850
851     private static final void copyBoolean(String key, ContentValues from, ContentValues to) {
852         Boolean b = from.getAsBoolean(key);
853         if (b != null) {
854             to.put(key, b);
855         }
856     }
857
858     private static final void copyString(String key, ContentValues from, ContentValues to) {
859         String s = from.getAsString(key);
860         if (s != null) {
861             to.put(key, s);
862         }
863     }
864
865     private class ReadOnlyCursorWrapper extends CursorWrapper implements CrossProcessCursor {
866         public ReadOnlyCursorWrapper(Cursor cursor) {
867             super(cursor);
868             mCursor = (CrossProcessCursor) cursor;
869         }
870
871         public boolean deleteRow() {
872             throw new SecurityException("Download manager cursors are read-only");
873         }
874
875         public boolean commitUpdates() {
876             throw new SecurityException("Download manager cursors are read-only");
877         }
878
879         public void fillWindow(int pos, CursorWindow window) {
880             mCursor.fillWindow(pos, window);
881         }
882
883         public CursorWindow getWindow() {
884             return mCursor.getWindow();
885         }
886
887         public boolean onMove(int oldPosition, int newPosition) {
888             return mCursor.onMove(oldPosition, newPosition);
889         }
890
891         private CrossProcessCursor mCursor;
892     }
893
894 }