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