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