Restore the appropriate SELinux context to the downloads dir.
[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.app.DownloadManager;
20 import android.app.DownloadManager.Request;
21 import android.content.ContentProvider;
22 import android.content.ContentUris;
23 import android.content.ContentValues;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.UriMatcher;
27 import android.content.pm.ApplicationInfo;
28 import android.content.pm.PackageManager;
29 import android.content.pm.PackageManager.NameNotFoundException;
30 import android.database.Cursor;
31 import android.database.DatabaseUtils;
32 import android.database.SQLException;
33 import android.database.sqlite.SQLiteDatabase;
34 import android.database.sqlite.SQLiteOpenHelper;
35 import android.net.Uri;
36 import android.os.Binder;
37 import android.os.Environment;
38 import android.os.ParcelFileDescriptor;
39 import android.os.Process;
40 import android.os.SELinux;
41 import android.provider.BaseColumns;
42 import android.provider.Downloads;
43 import android.provider.OpenableColumns;
44 import android.text.TextUtils;
45 import android.text.format.DateUtils;
46 import android.util.Log;
47
48 import com.android.internal.util.IndentingPrintWriter;
49 import com.google.android.collect.Maps;
50 import com.google.common.annotations.VisibleForTesting;
51
52 import java.io.File;
53 import java.io.FileDescriptor;
54 import java.io.FileNotFoundException;
55 import java.io.IOException;
56 import java.io.PrintWriter;
57 import java.util.ArrayList;
58 import java.util.Arrays;
59 import java.util.HashMap;
60 import java.util.HashSet;
61 import java.util.Iterator;
62 import java.util.List;
63 import java.util.Map;
64
65 /**
66  * Allows application to interact with the download manager.
67  */
68 public final class DownloadProvider extends ContentProvider {
69     /** Database filename */
70     private static final String DB_NAME = "downloads.db";
71     /** Current database version */
72     private static final int DB_VERSION = 108;
73     /** Name of table in the database */
74     private static final String DB_TABLE = "downloads";
75
76     /** MIME type for the entire download list */
77     private static final String DOWNLOAD_LIST_TYPE = "vnd.android.cursor.dir/download";
78     /** MIME type for an individual download */
79     private static final String DOWNLOAD_TYPE = "vnd.android.cursor.item/download";
80
81     /** URI matcher used to recognize URIs sent by applications */
82     private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
83     /** URI matcher constant for the URI of all downloads belonging to the calling UID */
84     private static final int MY_DOWNLOADS = 1;
85     /** URI matcher constant for the URI of an individual download belonging to the calling UID */
86     private static final int MY_DOWNLOADS_ID = 2;
87     /** URI matcher constant for the URI of all downloads in the system */
88     private static final int ALL_DOWNLOADS = 3;
89     /** URI matcher constant for the URI of an individual download */
90     private static final int ALL_DOWNLOADS_ID = 4;
91     /** URI matcher constant for the URI of a download's request headers */
92     private static final int REQUEST_HEADERS_URI = 5;
93     /** URI matcher constant for the public URI returned by
94      * {@link DownloadManager#getUriForDownloadedFile(long)} if the given downloaded file
95      * is publicly accessible.
96      */
97     private static final int PUBLIC_DOWNLOAD_ID = 6;
98     static {
99         sURIMatcher.addURI("downloads", "my_downloads", MY_DOWNLOADS);
100         sURIMatcher.addURI("downloads", "my_downloads/#", MY_DOWNLOADS_ID);
101         sURIMatcher.addURI("downloads", "all_downloads", ALL_DOWNLOADS);
102         sURIMatcher.addURI("downloads", "all_downloads/#", ALL_DOWNLOADS_ID);
103         sURIMatcher.addURI("downloads",
104                 "my_downloads/#/" + Downloads.Impl.RequestHeaders.URI_SEGMENT,
105                 REQUEST_HEADERS_URI);
106         sURIMatcher.addURI("downloads",
107                 "all_downloads/#/" + Downloads.Impl.RequestHeaders.URI_SEGMENT,
108                 REQUEST_HEADERS_URI);
109         // temporary, for backwards compatibility
110         sURIMatcher.addURI("downloads", "download", MY_DOWNLOADS);
111         sURIMatcher.addURI("downloads", "download/#", MY_DOWNLOADS_ID);
112         sURIMatcher.addURI("downloads",
113                 "download/#/" + Downloads.Impl.RequestHeaders.URI_SEGMENT,
114                 REQUEST_HEADERS_URI);
115         sURIMatcher.addURI("downloads",
116                 Downloads.Impl.PUBLICLY_ACCESSIBLE_DOWNLOADS_URI_SEGMENT + "/#",
117                 PUBLIC_DOWNLOAD_ID);
118     }
119
120     /** Different base URIs that could be used to access an individual download */
121     private static final Uri[] BASE_URIS = new Uri[] {
122             Downloads.Impl.CONTENT_URI,
123             Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
124     };
125
126     private static final String[] sAppReadableColumnsArray = new String[] {
127         Downloads.Impl._ID,
128         Downloads.Impl.COLUMN_APP_DATA,
129         Downloads.Impl._DATA,
130         Downloads.Impl.COLUMN_MIME_TYPE,
131         Downloads.Impl.COLUMN_VISIBILITY,
132         Downloads.Impl.COLUMN_DESTINATION,
133         Downloads.Impl.COLUMN_CONTROL,
134         Downloads.Impl.COLUMN_STATUS,
135         Downloads.Impl.COLUMN_LAST_MODIFICATION,
136         Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE,
137         Downloads.Impl.COLUMN_NOTIFICATION_CLASS,
138         Downloads.Impl.COLUMN_TOTAL_BYTES,
139         Downloads.Impl.COLUMN_CURRENT_BYTES,
140         Downloads.Impl.COLUMN_TITLE,
141         Downloads.Impl.COLUMN_DESCRIPTION,
142         Downloads.Impl.COLUMN_URI,
143         Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI,
144         Downloads.Impl.COLUMN_FILE_NAME_HINT,
145         Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
146         Downloads.Impl.COLUMN_DELETED,
147         OpenableColumns.DISPLAY_NAME,
148         OpenableColumns.SIZE,
149     };
150
151     private static final HashSet<String> sAppReadableColumnsSet;
152     private static final HashMap<String, String> sColumnsMap;
153
154     static {
155         sAppReadableColumnsSet = new HashSet<String>();
156         for (int i = 0; i < sAppReadableColumnsArray.length; ++i) {
157             sAppReadableColumnsSet.add(sAppReadableColumnsArray[i]);
158         }
159
160         sColumnsMap = Maps.newHashMap();
161         sColumnsMap.put(OpenableColumns.DISPLAY_NAME,
162                 Downloads.Impl.COLUMN_TITLE + " AS " + OpenableColumns.DISPLAY_NAME);
163         sColumnsMap.put(OpenableColumns.SIZE,
164                 Downloads.Impl.COLUMN_TOTAL_BYTES + " AS " + OpenableColumns.SIZE);
165     }
166     private static final List<String> downloadManagerColumnsList =
167             Arrays.asList(DownloadManager.UNDERLYING_COLUMNS);
168
169     /** The database that lies underneath this content provider */
170     private SQLiteOpenHelper mOpenHelper = null;
171
172     /** List of uids that can access the downloads */
173     private int mSystemUid = -1;
174     private int mDefContainerUid = -1;
175     private File mDownloadsDataDir;
176
177     @VisibleForTesting
178     SystemFacade mSystemFacade;
179
180     /**
181      * This class encapsulates a SQL where clause and its parameters.  It makes it possible for
182      * shared methods (like {@link DownloadProvider#getWhereClause(Uri, String, String[], int)})
183      * to return both pieces of information, and provides some utility logic to ease piece-by-piece
184      * construction of selections.
185      */
186     private static class SqlSelection {
187         public StringBuilder mWhereClause = new StringBuilder();
188         public List<String> mParameters = new ArrayList<String>();
189
190         public <T> void appendClause(String newClause, final T... parameters) {
191             if (newClause == null || newClause.isEmpty()) {
192                 return;
193             }
194             if (mWhereClause.length() != 0) {
195                 mWhereClause.append(" AND ");
196             }
197             mWhereClause.append("(");
198             mWhereClause.append(newClause);
199             mWhereClause.append(")");
200             if (parameters != null) {
201                 for (Object parameter : parameters) {
202                     mParameters.add(parameter.toString());
203                 }
204             }
205         }
206
207         public String getSelection() {
208             return mWhereClause.toString();
209         }
210
211         public String[] getParameters() {
212             String[] array = new String[mParameters.size()];
213             return mParameters.toArray(array);
214         }
215     }
216
217     /**
218      * Creates and updated database on demand when opening it.
219      * Helper class to create database the first time the provider is
220      * initialized and upgrade it when a new version of the provider needs
221      * an updated version of the database.
222      */
223     private final class DatabaseHelper extends SQLiteOpenHelper {
224         public DatabaseHelper(final Context context) {
225             super(context, DB_NAME, null, DB_VERSION);
226         }
227
228         /**
229          * Creates database the first time we try to open it.
230          */
231         @Override
232         public void onCreate(final SQLiteDatabase db) {
233             if (Constants.LOGVV) {
234                 Log.v(Constants.TAG, "populating new database");
235             }
236             onUpgrade(db, 0, DB_VERSION);
237         }
238
239         /**
240          * Updates the database format when a content provider is used
241          * with a database that was created with a different format.
242          *
243          * Note: to support downgrades, creating a table should always drop it first if it already
244          * exists.
245          */
246         @Override
247         public void onUpgrade(final SQLiteDatabase db, int oldV, final int newV) {
248             if (oldV == 31) {
249                 // 31 and 100 are identical, just in different codelines. Upgrading from 31 is the
250                 // same as upgrading from 100.
251                 oldV = 100;
252             } else if (oldV < 100) {
253                 // no logic to upgrade from these older version, just recreate the DB
254                 Log.i(Constants.TAG, "Upgrading downloads database from version " + oldV
255                       + " to version " + newV + ", which will destroy all old data");
256                 oldV = 99;
257             } else if (oldV > newV) {
258                 // user must have downgraded software; we have no way to know how to downgrade the
259                 // DB, so just recreate it
260                 Log.i(Constants.TAG, "Downgrading downloads database from version " + oldV
261                       + " (current version is " + newV + "), destroying all old data");
262                 oldV = 99;
263             }
264
265             for (int version = oldV + 1; version <= newV; version++) {
266                 upgradeTo(db, version);
267             }
268         }
269
270         /**
271          * Upgrade database from (version - 1) to version.
272          */
273         private void upgradeTo(SQLiteDatabase db, int version) {
274             switch (version) {
275                 case 100:
276                     createDownloadsTable(db);
277                     break;
278
279                 case 101:
280                     createHeadersTable(db);
281                     break;
282
283                 case 102:
284                     addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_IS_PUBLIC_API,
285                               "INTEGER NOT NULL DEFAULT 0");
286                     addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ALLOW_ROAMING,
287                               "INTEGER NOT NULL DEFAULT 0");
288                     addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES,
289                               "INTEGER NOT NULL DEFAULT 0");
290                     break;
291
292                 case 103:
293                     addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI,
294                               "INTEGER NOT NULL DEFAULT 1");
295                     makeCacheDownloadsInvisible(db);
296                     break;
297
298                 case 104:
299                     addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT,
300                             "INTEGER NOT NULL DEFAULT 0");
301                     break;
302
303                 case 105:
304                     fillNullValues(db);
305                     break;
306
307                 case 106:
308                     addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, "TEXT");
309                     addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_DELETED,
310                             "BOOLEAN NOT NULL DEFAULT 0");
311                     break;
312
313                 case 107:
314                     addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ERROR_MSG, "TEXT");
315                     break;
316
317                 case 108:
318                     addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ALLOW_METERED,
319                             "INTEGER NOT NULL DEFAULT 1");
320                     break;
321
322                 default:
323                     throw new IllegalStateException("Don't know how to upgrade to " + version);
324             }
325         }
326
327         /**
328          * insert() now ensures these four columns are never null for new downloads, so this method
329          * makes that true for existing columns, so that code can rely on this assumption.
330          */
331         private void fillNullValues(SQLiteDatabase db) {
332             ContentValues values = new ContentValues();
333             values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0);
334             fillNullValuesForColumn(db, values);
335             values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, -1);
336             fillNullValuesForColumn(db, values);
337             values.put(Downloads.Impl.COLUMN_TITLE, "");
338             fillNullValuesForColumn(db, values);
339             values.put(Downloads.Impl.COLUMN_DESCRIPTION, "");
340             fillNullValuesForColumn(db, values);
341         }
342
343         private void fillNullValuesForColumn(SQLiteDatabase db, ContentValues values) {
344             String column = values.valueSet().iterator().next().getKey();
345             db.update(DB_TABLE, values, column + " is null", null);
346             values.clear();
347         }
348
349         /**
350          * Set all existing downloads to the cache partition to be invisible in the downloads UI.
351          */
352         private void makeCacheDownloadsInvisible(SQLiteDatabase db) {
353             ContentValues values = new ContentValues();
354             values.put(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, false);
355             String cacheSelection = Downloads.Impl.COLUMN_DESTINATION
356                     + " != " + Downloads.Impl.DESTINATION_EXTERNAL;
357             db.update(DB_TABLE, values, cacheSelection, null);
358         }
359
360         /**
361          * Add a column to a table using ALTER TABLE.
362          * @param dbTable name of the table
363          * @param columnName name of the column to add
364          * @param columnDefinition SQL for the column definition
365          */
366         private void addColumn(SQLiteDatabase db, String dbTable, String columnName,
367                                String columnDefinition) {
368             db.execSQL("ALTER TABLE " + dbTable + " ADD COLUMN " + columnName + " "
369                        + columnDefinition);
370         }
371
372         /**
373          * Creates the table that'll hold the download information.
374          */
375         private void createDownloadsTable(SQLiteDatabase db) {
376             try {
377                 db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);
378                 db.execSQL("CREATE TABLE " + DB_TABLE + "(" +
379                         Downloads.Impl._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
380                         Downloads.Impl.COLUMN_URI + " TEXT, " +
381                         Constants.RETRY_AFTER_X_REDIRECT_COUNT + " INTEGER, " +
382                         Downloads.Impl.COLUMN_APP_DATA + " TEXT, " +
383                         Downloads.Impl.COLUMN_NO_INTEGRITY + " BOOLEAN, " +
384                         Downloads.Impl.COLUMN_FILE_NAME_HINT + " TEXT, " +
385                         Constants.OTA_UPDATE + " BOOLEAN, " +
386                         Downloads.Impl._DATA + " TEXT, " +
387                         Downloads.Impl.COLUMN_MIME_TYPE + " TEXT, " +
388                         Downloads.Impl.COLUMN_DESTINATION + " INTEGER, " +
389                         Constants.NO_SYSTEM_FILES + " BOOLEAN, " +
390                         Downloads.Impl.COLUMN_VISIBILITY + " INTEGER, " +
391                         Downloads.Impl.COLUMN_CONTROL + " INTEGER, " +
392                         Downloads.Impl.COLUMN_STATUS + " INTEGER, " +
393                         Downloads.Impl.COLUMN_FAILED_CONNECTIONS + " INTEGER, " +
394                         Downloads.Impl.COLUMN_LAST_MODIFICATION + " BIGINT, " +
395                         Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE + " TEXT, " +
396                         Downloads.Impl.COLUMN_NOTIFICATION_CLASS + " TEXT, " +
397                         Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS + " TEXT, " +
398                         Downloads.Impl.COLUMN_COOKIE_DATA + " TEXT, " +
399                         Downloads.Impl.COLUMN_USER_AGENT + " TEXT, " +
400                         Downloads.Impl.COLUMN_REFERER + " TEXT, " +
401                         Downloads.Impl.COLUMN_TOTAL_BYTES + " INTEGER, " +
402                         Downloads.Impl.COLUMN_CURRENT_BYTES + " INTEGER, " +
403                         Constants.ETAG + " TEXT, " +
404                         Constants.UID + " INTEGER, " +
405                         Downloads.Impl.COLUMN_OTHER_UID + " INTEGER, " +
406                         Downloads.Impl.COLUMN_TITLE + " TEXT, " +
407                         Downloads.Impl.COLUMN_DESCRIPTION + " TEXT, " +
408                         Constants.MEDIA_SCANNED + " BOOLEAN);");
409             } catch (SQLException ex) {
410                 Log.e(Constants.TAG, "couldn't create table in downloads database");
411                 throw ex;
412             }
413         }
414
415         private void createHeadersTable(SQLiteDatabase db) {
416             db.execSQL("DROP TABLE IF EXISTS " + Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE);
417             db.execSQL("CREATE TABLE " + Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE + "(" +
418                        "id INTEGER PRIMARY KEY AUTOINCREMENT," +
419                        Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + " INTEGER NOT NULL," +
420                        Downloads.Impl.RequestHeaders.COLUMN_HEADER + " TEXT NOT NULL," +
421                        Downloads.Impl.RequestHeaders.COLUMN_VALUE + " TEXT NOT NULL" +
422                        ");");
423         }
424     }
425
426     /**
427      * Initializes the content provider when it is created.
428      */
429     @Override
430     public boolean onCreate() {
431         if (mSystemFacade == null) {
432             mSystemFacade = new RealSystemFacade(getContext());
433         }
434
435         mOpenHelper = new DatabaseHelper(getContext());
436         // Initialize the system uid
437         mSystemUid = Process.SYSTEM_UID;
438         // Initialize the default container uid. Package name hardcoded
439         // for now.
440         ApplicationInfo appInfo = null;
441         try {
442             appInfo = getContext().getPackageManager().
443                     getApplicationInfo("com.android.defcontainer", 0);
444         } catch (NameNotFoundException e) {
445             Log.wtf(Constants.TAG, "Could not get ApplicationInfo for com.android.defconatiner", e);
446         }
447         if (appInfo != null) {
448             mDefContainerUid = appInfo.uid;
449         }
450         // start the DownloadService class. don't wait for the 1st download to be issued.
451         // saves us by getting some initialization code in DownloadService out of the way.
452         Context context = getContext();
453         context.startService(new Intent(context, DownloadService.class));
454         mDownloadsDataDir = StorageManager.getDownloadDataDirectory(getContext());
455         try {
456             SELinux.restorecon(mDownloadsDataDir.getCanonicalPath());
457         } catch (IOException e) {
458             Log.wtf(Constants.TAG, "Could not get canonical path for download directory", e);
459         }
460         return true;
461     }
462
463     /**
464      * Returns the content-provider-style MIME types of the various
465      * types accessible through this content provider.
466      */
467     @Override
468     public String getType(final Uri uri) {
469         int match = sURIMatcher.match(uri);
470         switch (match) {
471             case MY_DOWNLOADS:
472             case ALL_DOWNLOADS: {
473                 return DOWNLOAD_LIST_TYPE;
474             }
475             case MY_DOWNLOADS_ID:
476             case ALL_DOWNLOADS_ID:
477             case PUBLIC_DOWNLOAD_ID: {
478                 // return the mimetype of this id from the database
479                 final String id = getDownloadIdFromUri(uri);
480                 final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
481                 final String mimeType = DatabaseUtils.stringForQuery(db,
482                         "SELECT " + Downloads.Impl.COLUMN_MIME_TYPE + " FROM " + DB_TABLE +
483                         " WHERE " + Downloads.Impl._ID + " = ?",
484                         new String[]{id});
485                 if (TextUtils.isEmpty(mimeType)) {
486                     return DOWNLOAD_TYPE;
487                 } else {
488                     return mimeType;
489                 }
490             }
491             default: {
492                 if (Constants.LOGV) {
493                     Log.v(Constants.TAG, "calling getType on an unknown URI: " + uri);
494                 }
495                 throw new IllegalArgumentException("Unknown URI: " + uri);
496             }
497         }
498     }
499
500     /**
501      * Inserts a row in the database
502      */
503     @Override
504     public Uri insert(final Uri uri, final ContentValues values) {
505         checkInsertPermissions(values);
506         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
507
508         // note we disallow inserting into ALL_DOWNLOADS
509         int match = sURIMatcher.match(uri);
510         if (match != MY_DOWNLOADS) {
511             Log.d(Constants.TAG, "calling insert on an unknown/invalid URI: " + uri);
512             throw new IllegalArgumentException("Unknown/Invalid URI " + uri);
513         }
514
515         // copy some of the input values as it
516         ContentValues filteredValues = new ContentValues();
517         copyString(Downloads.Impl.COLUMN_URI, values, filteredValues);
518         copyString(Downloads.Impl.COLUMN_APP_DATA, values, filteredValues);
519         copyBoolean(Downloads.Impl.COLUMN_NO_INTEGRITY, values, filteredValues);
520         copyString(Downloads.Impl.COLUMN_FILE_NAME_HINT, values, filteredValues);
521         copyString(Downloads.Impl.COLUMN_MIME_TYPE, values, filteredValues);
522         copyBoolean(Downloads.Impl.COLUMN_IS_PUBLIC_API, values, filteredValues);
523
524         boolean isPublicApi =
525                 values.getAsBoolean(Downloads.Impl.COLUMN_IS_PUBLIC_API) == Boolean.TRUE;
526
527         // validate the destination column
528         Integer dest = values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION);
529         if (dest != null) {
530             if (getContext().checkCallingPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED)
531                     != PackageManager.PERMISSION_GRANTED
532                     && (dest == Downloads.Impl.DESTINATION_CACHE_PARTITION
533                             || dest == Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING
534                             || dest == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION)) {
535                 throw new SecurityException("setting destination to : " + dest +
536                         " not allowed, unless PERMISSION_ACCESS_ADVANCED is granted");
537             }
538             // for public API behavior, if an app has CACHE_NON_PURGEABLE permission, automatically
539             // switch to non-purgeable download
540             boolean hasNonPurgeablePermission =
541                     getContext().checkCallingPermission(
542                             Downloads.Impl.PERMISSION_CACHE_NON_PURGEABLE)
543                             == PackageManager.PERMISSION_GRANTED;
544             if (isPublicApi && dest == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE
545                     && hasNonPurgeablePermission) {
546                 dest = Downloads.Impl.DESTINATION_CACHE_PARTITION;
547             }
548             if (dest == Downloads.Impl.DESTINATION_FILE_URI) {
549                 getContext().enforcePermission(
550                         android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
551                         Binder.getCallingPid(), Binder.getCallingUid(),
552                         "need WRITE_EXTERNAL_STORAGE permission to use DESTINATION_FILE_URI");
553                 checkFileUriDestination(values);
554             } else if (dest == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION) {
555                 getContext().enforcePermission(
556                         android.Manifest.permission.ACCESS_CACHE_FILESYSTEM,
557                         Binder.getCallingPid(), Binder.getCallingUid(),
558                         "need ACCESS_CACHE_FILESYSTEM permission to use system cache");
559             }
560             filteredValues.put(Downloads.Impl.COLUMN_DESTINATION, dest);
561         }
562
563         // validate the visibility column
564         Integer vis = values.getAsInteger(Downloads.Impl.COLUMN_VISIBILITY);
565         if (vis == null) {
566             if (dest == Downloads.Impl.DESTINATION_EXTERNAL) {
567                 filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY,
568                         Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
569             } else {
570                 filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY,
571                         Downloads.Impl.VISIBILITY_HIDDEN);
572             }
573         } else {
574             filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY, vis);
575         }
576         // copy the control column as is
577         copyInteger(Downloads.Impl.COLUMN_CONTROL, values, filteredValues);
578
579         /*
580          * requests coming from
581          * DownloadManager.addCompletedDownload(String, String, String,
582          * boolean, String, String, long) need special treatment
583          */
584         if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) ==
585                 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
586             // these requests always are marked as 'completed'
587             filteredValues.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_SUCCESS);
588             filteredValues.put(Downloads.Impl.COLUMN_TOTAL_BYTES,
589                     values.getAsLong(Downloads.Impl.COLUMN_TOTAL_BYTES));
590             filteredValues.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0);
591             copyInteger(Downloads.Impl.COLUMN_MEDIA_SCANNED, values, filteredValues);
592             copyString(Downloads.Impl._DATA, values, filteredValues);
593         } else {
594             filteredValues.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING);
595             filteredValues.put(Downloads.Impl.COLUMN_TOTAL_BYTES, -1);
596             filteredValues.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0);
597         }
598
599         // set lastupdate to current time
600         long lastMod = mSystemFacade.currentTimeMillis();
601         filteredValues.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, lastMod);
602
603         // use packagename of the caller to set the notification columns
604         String pckg = values.getAsString(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE);
605         String clazz = values.getAsString(Downloads.Impl.COLUMN_NOTIFICATION_CLASS);
606         if (pckg != null && (clazz != null || isPublicApi)) {
607             int uid = Binder.getCallingUid();
608             try {
609                 if (uid == 0 || mSystemFacade.userOwnsPackage(uid, pckg)) {
610                     filteredValues.put(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE, pckg);
611                     if (clazz != null) {
612                         filteredValues.put(Downloads.Impl.COLUMN_NOTIFICATION_CLASS, clazz);
613                     }
614                 }
615             } catch (PackageManager.NameNotFoundException ex) {
616                 /* ignored for now */
617             }
618         }
619
620         // copy some more columns as is
621         copyString(Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS, values, filteredValues);
622         copyString(Downloads.Impl.COLUMN_COOKIE_DATA, values, filteredValues);
623         copyString(Downloads.Impl.COLUMN_USER_AGENT, values, filteredValues);
624         copyString(Downloads.Impl.COLUMN_REFERER, values, filteredValues);
625
626         // UID, PID columns
627         if (getContext().checkCallingPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED)
628                 == PackageManager.PERMISSION_GRANTED) {
629             copyInteger(Downloads.Impl.COLUMN_OTHER_UID, values, filteredValues);
630         }
631         filteredValues.put(Constants.UID, Binder.getCallingUid());
632         if (Binder.getCallingUid() == 0) {
633             copyInteger(Constants.UID, values, filteredValues);
634         }
635
636         // copy some more columns as is
637         copyStringWithDefault(Downloads.Impl.COLUMN_TITLE, values, filteredValues, "");
638         copyStringWithDefault(Downloads.Impl.COLUMN_DESCRIPTION, values, filteredValues, "");
639
640         // is_visible_in_downloads_ui column
641         if (values.containsKey(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI)) {
642             copyBoolean(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, values, filteredValues);
643         } else {
644             // by default, make external downloads visible in the UI
645             boolean isExternal = (dest == null || dest == Downloads.Impl.DESTINATION_EXTERNAL);
646             filteredValues.put(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, isExternal);
647         }
648
649         // public api requests and networktypes/roaming columns
650         if (isPublicApi) {
651             copyInteger(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES, values, filteredValues);
652             copyBoolean(Downloads.Impl.COLUMN_ALLOW_ROAMING, values, filteredValues);
653             copyBoolean(Downloads.Impl.COLUMN_ALLOW_METERED, values, filteredValues);
654         }
655
656         if (Constants.LOGVV) {
657             Log.v(Constants.TAG, "initiating download with UID "
658                     + filteredValues.getAsInteger(Constants.UID));
659             if (filteredValues.containsKey(Downloads.Impl.COLUMN_OTHER_UID)) {
660                 Log.v(Constants.TAG, "other UID " +
661                         filteredValues.getAsInteger(Downloads.Impl.COLUMN_OTHER_UID));
662             }
663         }
664
665         long rowID = db.insert(DB_TABLE, null, filteredValues);
666         if (rowID == -1) {
667             Log.d(Constants.TAG, "couldn't insert into downloads database");
668             return null;
669         }
670
671         insertRequestHeaders(db, rowID, values);
672         /*
673          * requests coming from
674          * DownloadManager.addCompletedDownload(String, String, String,
675          * boolean, String, String, long) need special treatment
676          */
677         Context context = getContext();
678         if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) ==
679                 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
680             // When notification is requested, kick off service to process all
681             // relevant downloads.
682             if (Downloads.Impl.isNotificationToBeDisplayed(vis)) {
683                 context.startService(new Intent(context, DownloadService.class));
684             }
685         } else {
686             context.startService(new Intent(context, DownloadService.class));
687         }
688         notifyContentChanged(uri, match);
689         return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID);
690     }
691
692     /**
693      * Check that the file URI provided for DESTINATION_FILE_URI is valid.
694      */
695     private void checkFileUriDestination(ContentValues values) {
696         String fileUri = values.getAsString(Downloads.Impl.COLUMN_FILE_NAME_HINT);
697         if (fileUri == null) {
698             throw new IllegalArgumentException(
699                     "DESTINATION_FILE_URI must include a file URI under COLUMN_FILE_NAME_HINT");
700         }
701         Uri uri = Uri.parse(fileUri);
702         String scheme = uri.getScheme();
703         if (scheme == null || !scheme.equals("file")) {
704             throw new IllegalArgumentException("Not a file URI: " + uri);
705         }
706         final String path = uri.getPath();
707         if (path == null) {
708             throw new IllegalArgumentException("Invalid file URI: " + uri);
709         }
710         try {
711             final String canonicalPath = new File(path).getCanonicalPath();
712             final String externalPath = Environment.getExternalStorageDirectory().getAbsolutePath();
713             if (!canonicalPath.startsWith(externalPath)) {
714                 throw new SecurityException("Destination must be on external storage: " + uri);
715             }
716         } catch (IOException e) {
717             throw new SecurityException("Problem resolving path: " + uri);
718         }
719     }
720
721     /**
722      * Apps with the ACCESS_DOWNLOAD_MANAGER permission can access this provider freely, subject to
723      * constraints in the rest of the code. Apps without that may still access this provider through
724      * the public API, but additional restrictions are imposed. We check those restrictions here.
725      *
726      * @param values ContentValues provided to insert()
727      * @throws SecurityException if the caller has insufficient permissions
728      */
729     private void checkInsertPermissions(ContentValues values) {
730         if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS)
731                 == PackageManager.PERMISSION_GRANTED) {
732             return;
733         }
734
735         getContext().enforceCallingOrSelfPermission(android.Manifest.permission.INTERNET,
736                 "INTERNET permission is required to use the download manager");
737
738         // ensure the request fits within the bounds of a public API request
739         // first copy so we can remove values
740         values = new ContentValues(values);
741
742         // check columns whose values are restricted
743         enforceAllowedValues(values, Downloads.Impl.COLUMN_IS_PUBLIC_API, Boolean.TRUE);
744
745         // validate the destination column
746         if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) ==
747                 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
748             /* this row is inserted by
749              * DownloadManager.addCompletedDownload(String, String, String,
750              * boolean, String, String, long)
751              */
752             values.remove(Downloads.Impl.COLUMN_TOTAL_BYTES);
753             values.remove(Downloads.Impl._DATA);
754             values.remove(Downloads.Impl.COLUMN_STATUS);
755         }
756         enforceAllowedValues(values, Downloads.Impl.COLUMN_DESTINATION,
757                 Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE,
758                 Downloads.Impl.DESTINATION_FILE_URI,
759                 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD);
760
761         if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_NO_NOTIFICATION)
762                 == PackageManager.PERMISSION_GRANTED) {
763             enforceAllowedValues(values, Downloads.Impl.COLUMN_VISIBILITY,
764                     Request.VISIBILITY_HIDDEN,
765                     Request.VISIBILITY_VISIBLE,
766                     Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED,
767                     Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION);
768         } else {
769             enforceAllowedValues(values, Downloads.Impl.COLUMN_VISIBILITY,
770                     Request.VISIBILITY_VISIBLE,
771                     Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED,
772                     Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION);
773         }
774
775         // remove the rest of the columns that are allowed (with any value)
776         values.remove(Downloads.Impl.COLUMN_URI);
777         values.remove(Downloads.Impl.COLUMN_TITLE);
778         values.remove(Downloads.Impl.COLUMN_DESCRIPTION);
779         values.remove(Downloads.Impl.COLUMN_MIME_TYPE);
780         values.remove(Downloads.Impl.COLUMN_FILE_NAME_HINT); // checked later in insert()
781         values.remove(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE); // checked later in insert()
782         values.remove(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES);
783         values.remove(Downloads.Impl.COLUMN_ALLOW_ROAMING);
784         values.remove(Downloads.Impl.COLUMN_ALLOW_METERED);
785         values.remove(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI);
786         values.remove(Downloads.Impl.COLUMN_MEDIA_SCANNED);
787         Iterator<Map.Entry<String, Object>> iterator = values.valueSet().iterator();
788         while (iterator.hasNext()) {
789             String key = iterator.next().getKey();
790             if (key.startsWith(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX)) {
791                 iterator.remove();
792             }
793         }
794
795         // any extra columns are extraneous and disallowed
796         if (values.size() > 0) {
797             StringBuilder error = new StringBuilder("Invalid columns in request: ");
798             boolean first = true;
799             for (Map.Entry<String, Object> entry : values.valueSet()) {
800                 if (!first) {
801                     error.append(", ");
802                 }
803                 error.append(entry.getKey());
804             }
805             throw new SecurityException(error.toString());
806         }
807     }
808
809     /**
810      * Remove column from values, and throw a SecurityException if the value isn't within the
811      * specified allowedValues.
812      */
813     private void enforceAllowedValues(ContentValues values, String column,
814             Object... allowedValues) {
815         Object value = values.get(column);
816         values.remove(column);
817         for (Object allowedValue : allowedValues) {
818             if (value == null && allowedValue == null) {
819                 return;
820             }
821             if (value != null && value.equals(allowedValue)) {
822                 return;
823             }
824         }
825         throw new SecurityException("Invalid value for " + column + ": " + value);
826     }
827
828     /**
829      * Starts a database query
830      */
831     @Override
832     public Cursor query(final Uri uri, String[] projection,
833              final String selection, final String[] selectionArgs,
834              final String sort) {
835
836         Helpers.validateSelection(selection, sAppReadableColumnsSet);
837
838         SQLiteDatabase db = mOpenHelper.getReadableDatabase();
839
840         int match = sURIMatcher.match(uri);
841         if (match == -1) {
842             if (Constants.LOGV) {
843                 Log.v(Constants.TAG, "querying unknown URI: " + uri);
844             }
845             throw new IllegalArgumentException("Unknown URI: " + uri);
846         }
847
848         if (match == REQUEST_HEADERS_URI) {
849             if (projection != null || selection != null || sort != null) {
850                 throw new UnsupportedOperationException("Request header queries do not support "
851                                                         + "projections, selections or sorting");
852             }
853             return queryRequestHeaders(db, uri);
854         }
855
856         SqlSelection fullSelection = getWhereClause(uri, selection, selectionArgs, match);
857
858         if (shouldRestrictVisibility()) {
859             if (projection == null) {
860                 projection = sAppReadableColumnsArray.clone();
861             } else {
862                 // check the validity of the columns in projection 
863                 for (int i = 0; i < projection.length; ++i) {
864                     if (!sAppReadableColumnsSet.contains(projection[i]) &&
865                             !downloadManagerColumnsList.contains(projection[i])) {
866                         throw new IllegalArgumentException(
867                                 "column " + projection[i] + " is not allowed in queries");
868                     }
869                 }
870             }
871
872             for (int i = 0; i < projection.length; i++) {
873                 final String newColumn = sColumnsMap.get(projection[i]);
874                 if (newColumn != null) {
875                     projection[i] = newColumn;
876                 }
877             }
878         }
879
880         if (Constants.LOGVV) {
881             logVerboseQueryInfo(projection, selection, selectionArgs, sort, db);
882         }
883
884         Cursor ret = db.query(DB_TABLE, projection, fullSelection.getSelection(),
885                 fullSelection.getParameters(), null, null, sort);
886
887         if (ret != null) {
888             ret.setNotificationUri(getContext().getContentResolver(), uri);
889             if (Constants.LOGVV) {
890                 Log.v(Constants.TAG,
891                         "created cursor " + ret + " on behalf of " + Binder.getCallingPid());
892             }
893         } else {
894             if (Constants.LOGV) {
895                 Log.v(Constants.TAG, "query failed in downloads database");
896             }
897         }
898
899         return ret;
900     }
901
902     private void logVerboseQueryInfo(String[] projection, final String selection,
903             final String[] selectionArgs, final String sort, SQLiteDatabase db) {
904         java.lang.StringBuilder sb = new java.lang.StringBuilder();
905         sb.append("starting query, database is ");
906         if (db != null) {
907             sb.append("not ");
908         }
909         sb.append("null; ");
910         if (projection == null) {
911             sb.append("projection is null; ");
912         } else if (projection.length == 0) {
913             sb.append("projection is empty; ");
914         } else {
915             for (int i = 0; i < projection.length; ++i) {
916                 sb.append("projection[");
917                 sb.append(i);
918                 sb.append("] is ");
919                 sb.append(projection[i]);
920                 sb.append("; ");
921             }
922         }
923         sb.append("selection is ");
924         sb.append(selection);
925         sb.append("; ");
926         if (selectionArgs == null) {
927             sb.append("selectionArgs is null; ");
928         } else if (selectionArgs.length == 0) {
929             sb.append("selectionArgs is empty; ");
930         } else {
931             for (int i = 0; i < selectionArgs.length; ++i) {
932                 sb.append("selectionArgs[");
933                 sb.append(i);
934                 sb.append("] is ");
935                 sb.append(selectionArgs[i]);
936                 sb.append("; ");
937             }
938         }
939         sb.append("sort is ");
940         sb.append(sort);
941         sb.append(".");
942         Log.v(Constants.TAG, sb.toString());
943     }
944
945     private String getDownloadIdFromUri(final Uri uri) {
946         return uri.getPathSegments().get(1);
947     }
948
949     /**
950      * Insert request headers for a download into the DB.
951      */
952     private void insertRequestHeaders(SQLiteDatabase db, long downloadId, ContentValues values) {
953         ContentValues rowValues = new ContentValues();
954         rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID, downloadId);
955         for (Map.Entry<String, Object> entry : values.valueSet()) {
956             String key = entry.getKey();
957             if (key.startsWith(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX)) {
958                 String headerLine = entry.getValue().toString();
959                 if (!headerLine.contains(":")) {
960                     throw new IllegalArgumentException("Invalid HTTP header line: " + headerLine);
961                 }
962                 String[] parts = headerLine.split(":", 2);
963                 rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_HEADER, parts[0].trim());
964                 rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_VALUE, parts[1].trim());
965                 db.insert(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, null, rowValues);
966             }
967         }
968     }
969
970     /**
971      * Handle a query for the custom request headers registered for a download.
972      */
973     private Cursor queryRequestHeaders(SQLiteDatabase db, Uri uri) {
974         String where = Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "="
975                        + getDownloadIdFromUri(uri);
976         String[] projection = new String[] {Downloads.Impl.RequestHeaders.COLUMN_HEADER,
977                                             Downloads.Impl.RequestHeaders.COLUMN_VALUE};
978         return db.query(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, projection, where,
979                         null, null, null, null);
980     }
981
982     /**
983      * Delete request headers for downloads matching the given query.
984      */
985     private void deleteRequestHeaders(SQLiteDatabase db, String where, String[] whereArgs) {
986         String[] projection = new String[] {Downloads.Impl._ID};
987         Cursor cursor = db.query(DB_TABLE, projection, where, whereArgs, null, null, null, null);
988         try {
989             for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
990                 long id = cursor.getLong(0);
991                 String idWhere = Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "=" + id;
992                 db.delete(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, idWhere, null);
993             }
994         } finally {
995             cursor.close();
996         }
997     }
998
999     /**
1000      * @return true if we should restrict the columns readable by this caller
1001      */
1002     private boolean shouldRestrictVisibility() {
1003         int callingUid = Binder.getCallingUid();
1004         return Binder.getCallingPid() != Process.myPid() &&
1005                 callingUid != mSystemUid &&
1006                 callingUid != mDefContainerUid;
1007     }
1008
1009     /**
1010      * Updates a row in the database
1011      */
1012     @Override
1013     public int update(final Uri uri, final ContentValues values,
1014             final String where, final String[] whereArgs) {
1015
1016         Helpers.validateSelection(where, sAppReadableColumnsSet);
1017
1018         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1019
1020         int count;
1021         boolean startService = false;
1022
1023         if (values.containsKey(Downloads.Impl.COLUMN_DELETED)) {
1024             if (values.getAsInteger(Downloads.Impl.COLUMN_DELETED) == 1) {
1025                 // some rows are to be 'deleted'. need to start DownloadService.
1026                 startService = true;
1027             }
1028         }
1029
1030         ContentValues filteredValues;
1031         if (Binder.getCallingPid() != Process.myPid()) {
1032             filteredValues = new ContentValues();
1033             copyString(Downloads.Impl.COLUMN_APP_DATA, values, filteredValues);
1034             copyInteger(Downloads.Impl.COLUMN_VISIBILITY, values, filteredValues);
1035             Integer i = values.getAsInteger(Downloads.Impl.COLUMN_CONTROL);
1036             if (i != null) {
1037                 filteredValues.put(Downloads.Impl.COLUMN_CONTROL, i);
1038                 startService = true;
1039             }
1040
1041             copyInteger(Downloads.Impl.COLUMN_CONTROL, values, filteredValues);
1042             copyString(Downloads.Impl.COLUMN_TITLE, values, filteredValues);
1043             copyString(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, values, filteredValues);
1044             copyString(Downloads.Impl.COLUMN_DESCRIPTION, values, filteredValues);
1045             copyInteger(Downloads.Impl.COLUMN_DELETED, values, filteredValues);
1046         } else {
1047             filteredValues = values;
1048             String filename = values.getAsString(Downloads.Impl._DATA);
1049             if (filename != null) {
1050                 Cursor c = query(uri, new String[]
1051                         { Downloads.Impl.COLUMN_TITLE }, null, null, null);
1052                 if (!c.moveToFirst() || c.getString(0).isEmpty()) {
1053                     values.put(Downloads.Impl.COLUMN_TITLE, new File(filename).getName());
1054                 }
1055                 c.close();
1056             }
1057
1058             Integer status = values.getAsInteger(Downloads.Impl.COLUMN_STATUS);
1059             boolean isRestart = status != null && status == Downloads.Impl.STATUS_PENDING;
1060             boolean isUserBypassingSizeLimit =
1061                 values.containsKey(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT);
1062             if (isRestart || isUserBypassingSizeLimit) {
1063                 startService = true;
1064             }
1065         }
1066
1067         int match = sURIMatcher.match(uri);
1068         switch (match) {
1069             case MY_DOWNLOADS:
1070             case MY_DOWNLOADS_ID:
1071             case ALL_DOWNLOADS:
1072             case ALL_DOWNLOADS_ID:
1073                 SqlSelection selection = getWhereClause(uri, where, whereArgs, match);
1074                 if (filteredValues.size() > 0) {
1075                     count = db.update(DB_TABLE, filteredValues, selection.getSelection(),
1076                             selection.getParameters());
1077                 } else {
1078                     count = 0;
1079                 }
1080                 break;
1081
1082             default:
1083                 Log.d(Constants.TAG, "updating unknown/invalid URI: " + uri);
1084                 throw new UnsupportedOperationException("Cannot update URI: " + uri);
1085         }
1086
1087         notifyContentChanged(uri, match);
1088         if (startService) {
1089             Context context = getContext();
1090             context.startService(new Intent(context, DownloadService.class));
1091         }
1092         return count;
1093     }
1094
1095     /**
1096      * Notify of a change through both URIs (/my_downloads and /all_downloads)
1097      * @param uri either URI for the changed download(s)
1098      * @param uriMatch the match ID from {@link #sURIMatcher}
1099      */
1100     private void notifyContentChanged(final Uri uri, int uriMatch) {
1101         Long downloadId = null;
1102         if (uriMatch == MY_DOWNLOADS_ID || uriMatch == ALL_DOWNLOADS_ID) {
1103             downloadId = Long.parseLong(getDownloadIdFromUri(uri));
1104         }
1105         for (Uri uriToNotify : BASE_URIS) {
1106             if (downloadId != null) {
1107                 uriToNotify = ContentUris.withAppendedId(uriToNotify, downloadId);
1108             }
1109             getContext().getContentResolver().notifyChange(uriToNotify, null);
1110         }
1111     }
1112
1113     private SqlSelection getWhereClause(final Uri uri, final String where, final String[] whereArgs,
1114             int uriMatch) {
1115         SqlSelection selection = new SqlSelection();
1116         selection.appendClause(where, whereArgs);
1117         if (uriMatch == MY_DOWNLOADS_ID || uriMatch == ALL_DOWNLOADS_ID ||
1118                 uriMatch == PUBLIC_DOWNLOAD_ID) {
1119             selection.appendClause(Downloads.Impl._ID + " = ?", getDownloadIdFromUri(uri));
1120         }
1121         if ((uriMatch == MY_DOWNLOADS || uriMatch == MY_DOWNLOADS_ID)
1122                 && getContext().checkCallingPermission(Downloads.Impl.PERMISSION_ACCESS_ALL)
1123                 != PackageManager.PERMISSION_GRANTED) {
1124             selection.appendClause(
1125                     Constants.UID + "= ? OR " + Downloads.Impl.COLUMN_OTHER_UID + "= ?",
1126                     Binder.getCallingUid(), Binder.getCallingUid());
1127         }
1128         return selection;
1129     }
1130
1131     /**
1132      * Deletes a row in the database
1133      */
1134     @Override
1135     public int delete(final Uri uri, final String where,
1136             final String[] whereArgs) {
1137
1138         Helpers.validateSelection(where, sAppReadableColumnsSet);
1139
1140         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1141         int count;
1142         int match = sURIMatcher.match(uri);
1143         switch (match) {
1144             case MY_DOWNLOADS:
1145             case MY_DOWNLOADS_ID:
1146             case ALL_DOWNLOADS:
1147             case ALL_DOWNLOADS_ID:
1148                 SqlSelection selection = getWhereClause(uri, where, whereArgs, match);
1149                 deleteRequestHeaders(db, selection.getSelection(), selection.getParameters());
1150                 count = db.delete(DB_TABLE, selection.getSelection(), selection.getParameters());
1151                 break;
1152
1153             default:
1154                 Log.d(Constants.TAG, "deleting unknown/invalid URI: " + uri);
1155                 throw new UnsupportedOperationException("Cannot delete URI: " + uri);
1156         }
1157         notifyContentChanged(uri, match);
1158         return count;
1159     }
1160
1161     /**
1162      * Remotely opens a file
1163      */
1164     @Override
1165     public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
1166         if (Constants.LOGVV) {
1167             logVerboseOpenFileInfo(uri, mode);
1168         }
1169
1170         Cursor cursor = query(uri, new String[] {"_data"}, null, null, null);
1171         String path;
1172         try {
1173             int count = (cursor != null) ? cursor.getCount() : 0;
1174             if (count != 1) {
1175                 // If there is not exactly one result, throw an appropriate exception.
1176                 if (count == 0) {
1177                     throw new FileNotFoundException("No entry for " + uri);
1178                 }
1179                 throw new FileNotFoundException("Multiple items at " + uri);
1180             }
1181
1182             cursor.moveToFirst();
1183             path = cursor.getString(0);
1184         } finally {
1185             if (cursor != null) {
1186                 cursor.close();
1187             }
1188         }
1189
1190         if (path == null) {
1191             throw new FileNotFoundException("No filename found.");
1192         }
1193         if (!Helpers.isFilenameValid(path, mDownloadsDataDir)) {
1194             throw new FileNotFoundException("Invalid filename: " + path);
1195         }
1196         if (!"r".equals(mode)) {
1197             throw new FileNotFoundException("Bad mode for " + uri + ": " + mode);
1198         }
1199
1200         ParcelFileDescriptor ret = ParcelFileDescriptor.open(new File(path),
1201                 ParcelFileDescriptor.MODE_READ_ONLY);
1202
1203         if (ret == null) {
1204             if (Constants.LOGV) {
1205                 Log.v(Constants.TAG, "couldn't open file");
1206             }
1207             throw new FileNotFoundException("couldn't open file");
1208         }
1209         return ret;
1210     }
1211
1212     @Override
1213     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
1214         final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ", 120);
1215
1216         pw.println("Downloads updated in last hour:");
1217         pw.increaseIndent();
1218
1219         final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
1220         final long modifiedAfter = mSystemFacade.currentTimeMillis() - DateUtils.HOUR_IN_MILLIS;
1221         final Cursor cursor = db.query(DB_TABLE, null,
1222                 Downloads.Impl.COLUMN_LAST_MODIFICATION + ">" + modifiedAfter, null, null, null,
1223                 Downloads.Impl._ID + " ASC");
1224         try {
1225             final String[] cols = cursor.getColumnNames();
1226             final int idCol = cursor.getColumnIndex(BaseColumns._ID);
1227             while (cursor.moveToNext()) {
1228                 pw.println("Download #" + cursor.getInt(idCol) + ":");
1229                 pw.increaseIndent();
1230                 for (int i = 0; i < cols.length; i++) {
1231                     // Omit sensitive data when dumping
1232                     if (Downloads.Impl.COLUMN_COOKIE_DATA.equals(cols[i])) {
1233                         continue;
1234                     }
1235                     pw.printPair(cols[i], cursor.getString(i));
1236                 }
1237                 pw.println();
1238                 pw.decreaseIndent();
1239             }
1240         } finally {
1241             cursor.close();
1242         }
1243
1244         pw.decreaseIndent();
1245     }
1246
1247     private void logVerboseOpenFileInfo(Uri uri, String mode) {
1248         Log.v(Constants.TAG, "openFile uri: " + uri + ", mode: " + mode
1249                 + ", uid: " + Binder.getCallingUid());
1250         Cursor cursor = query(Downloads.Impl.CONTENT_URI,
1251                 new String[] { "_id" }, null, null, "_id");
1252         if (cursor == null) {
1253             Log.v(Constants.TAG, "null cursor in openFile");
1254         } else {
1255             if (!cursor.moveToFirst()) {
1256                 Log.v(Constants.TAG, "empty cursor in openFile");
1257             } else {
1258                 do {
1259                     Log.v(Constants.TAG, "row " + cursor.getInt(0) + " available");
1260                 } while(cursor.moveToNext());
1261             }
1262             cursor.close();
1263         }
1264         cursor = query(uri, new String[] { "_data" }, null, null, null);
1265         if (cursor == null) {
1266             Log.v(Constants.TAG, "null cursor in openFile");
1267         } else {
1268             if (!cursor.moveToFirst()) {
1269                 Log.v(Constants.TAG, "empty cursor in openFile");
1270             } else {
1271                 String filename = cursor.getString(0);
1272                 Log.v(Constants.TAG, "filename in openFile: " + filename);
1273                 if (new java.io.File(filename).isFile()) {
1274                     Log.v(Constants.TAG, "file exists in openFile");
1275                 }
1276             }
1277             cursor.close();
1278         }
1279     }
1280
1281     private static final void copyInteger(String key, ContentValues from, ContentValues to) {
1282         Integer i = from.getAsInteger(key);
1283         if (i != null) {
1284             to.put(key, i);
1285         }
1286     }
1287
1288     private static final void copyBoolean(String key, ContentValues from, ContentValues to) {
1289         Boolean b = from.getAsBoolean(key);
1290         if (b != null) {
1291             to.put(key, b);
1292         }
1293     }
1294
1295     private static final void copyString(String key, ContentValues from, ContentValues to) {
1296         String s = from.getAsString(key);
1297         if (s != null) {
1298             to.put(key, s);
1299         }
1300     }
1301
1302     private static final void copyStringWithDefault(String key, ContentValues from,
1303             ContentValues to, String defaultValue) {
1304         copyString(key, from, to);
1305         if (!to.containsKey(key)) {
1306             to.put(key, defaultValue);
1307         }
1308     }
1309 }