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