94e5a997b02768e8313b247e2e18ae3cb091d04b
[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.AppOpsManager;
20 import android.app.DownloadManager;
21 import android.app.DownloadManager.Request;
22 import android.content.ContentProvider;
23 import android.content.ContentUris;
24 import android.content.ContentValues;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.UriMatcher;
28 import android.content.pm.ApplicationInfo;
29 import android.content.pm.PackageManager;
30 import android.content.pm.PackageManager.NameNotFoundException;
31 import android.database.Cursor;
32 import android.database.DatabaseUtils;
33 import android.database.SQLException;
34 import android.database.sqlite.SQLiteDatabase;
35 import android.database.sqlite.SQLiteOpenHelper;
36 import android.net.Uri;
37 import android.os.Binder;
38 import android.os.Handler;
39 import android.os.HandlerThread;
40 import android.os.ParcelFileDescriptor;
41 import android.os.ParcelFileDescriptor.OnCloseListener;
42 import android.os.Process;
43 import android.provider.BaseColumns;
44 import android.provider.Downloads;
45 import android.provider.OpenableColumns;
46 import android.text.TextUtils;
47 import android.text.format.DateUtils;
48 import android.util.Log;
49
50 import libcore.io.IoUtils;
51
52 import com.android.internal.util.IndentingPrintWriter;
53 import com.google.android.collect.Maps;
54 import com.google.common.annotations.VisibleForTesting;
55
56 import java.io.File;
57 import java.io.FileDescriptor;
58 import java.io.FileNotFoundException;
59 import java.io.IOException;
60 import java.io.PrintWriter;
61 import java.util.ArrayList;
62 import java.util.Arrays;
63 import java.util.HashMap;
64 import java.util.HashSet;
65 import java.util.Iterator;
66 import java.util.List;
67 import java.util.Map;
68
69 /**
70  * Allows application to interact with the download manager.
71  */
72 public final class DownloadProvider extends ContentProvider {
73     /** Database filename */
74     private static final String DB_NAME = "downloads.db";
75     /** Current database version */
76     private static final int DB_VERSION = 109;
77     /** Name of table in the database */
78     private static final String DB_TABLE = "downloads";
79
80     /** MIME type for the entire download list */
81     private static final String DOWNLOAD_LIST_TYPE = "vnd.android.cursor.dir/download";
82     /** MIME type for an individual download */
83     private static final String DOWNLOAD_TYPE = "vnd.android.cursor.item/download";
84
85     /** URI matcher used to recognize URIs sent by applications */
86     private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
87     /** URI matcher constant for the URI of all downloads belonging to the calling UID */
88     private static final int MY_DOWNLOADS = 1;
89     /** URI matcher constant for the URI of an individual download belonging to the calling UID */
90     private static final int MY_DOWNLOADS_ID = 2;
91     /** URI matcher constant for the URI of all downloads in the system */
92     private static final int ALL_DOWNLOADS = 3;
93     /** URI matcher constant for the URI of an individual download */
94     private static final int ALL_DOWNLOADS_ID = 4;
95     /** URI matcher constant for the URI of a download's request headers */
96     private static final int REQUEST_HEADERS_URI = 5;
97     /** URI matcher constant for the public URI returned by
98      * {@link DownloadManager#getUriForDownloadedFile(long)} if the given downloaded file
99      * is publicly accessible.
100      */
101     private static final int PUBLIC_DOWNLOAD_ID = 6;
102     static {
103         sURIMatcher.addURI("downloads", "my_downloads", MY_DOWNLOADS);
104         sURIMatcher.addURI("downloads", "my_downloads/#", MY_DOWNLOADS_ID);
105         sURIMatcher.addURI("downloads", "all_downloads", ALL_DOWNLOADS);
106         sURIMatcher.addURI("downloads", "all_downloads/#", ALL_DOWNLOADS_ID);
107         sURIMatcher.addURI("downloads",
108                 "my_downloads/#/" + Downloads.Impl.RequestHeaders.URI_SEGMENT,
109                 REQUEST_HEADERS_URI);
110         sURIMatcher.addURI("downloads",
111                 "all_downloads/#/" + Downloads.Impl.RequestHeaders.URI_SEGMENT,
112                 REQUEST_HEADERS_URI);
113         // temporary, for backwards compatibility
114         sURIMatcher.addURI("downloads", "download", MY_DOWNLOADS);
115         sURIMatcher.addURI("downloads", "download/#", MY_DOWNLOADS_ID);
116         sURIMatcher.addURI("downloads",
117                 "download/#/" + Downloads.Impl.RequestHeaders.URI_SEGMENT,
118                 REQUEST_HEADERS_URI);
119         sURIMatcher.addURI("downloads",
120                 Downloads.Impl.PUBLICLY_ACCESSIBLE_DOWNLOADS_URI_SEGMENT + "/#",
121                 PUBLIC_DOWNLOAD_ID);
122     }
123
124     /** Different base URIs that could be used to access an individual download */
125     private static final Uri[] BASE_URIS = new Uri[] {
126             Downloads.Impl.CONTENT_URI,
127             Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
128     };
129
130     private static final String[] sAppReadableColumnsArray = new String[] {
131         Downloads.Impl._ID,
132         Downloads.Impl.COLUMN_APP_DATA,
133         Downloads.Impl._DATA,
134         Downloads.Impl.COLUMN_MIME_TYPE,
135         Downloads.Impl.COLUMN_VISIBILITY,
136         Downloads.Impl.COLUMN_DESTINATION,
137         Downloads.Impl.COLUMN_CONTROL,
138         Downloads.Impl.COLUMN_STATUS,
139         Downloads.Impl.COLUMN_LAST_MODIFICATION,
140         Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE,
141         Downloads.Impl.COLUMN_NOTIFICATION_CLASS,
142         Downloads.Impl.COLUMN_TOTAL_BYTES,
143         Downloads.Impl.COLUMN_CURRENT_BYTES,
144         Downloads.Impl.COLUMN_TITLE,
145         Downloads.Impl.COLUMN_DESCRIPTION,
146         Downloads.Impl.COLUMN_URI,
147         Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI,
148         Downloads.Impl.COLUMN_FILE_NAME_HINT,
149         Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
150         Downloads.Impl.COLUMN_DELETED,
151         OpenableColumns.DISPLAY_NAME,
152         OpenableColumns.SIZE,
153     };
154
155     private static final HashSet<String> sAppReadableColumnsSet;
156     private static final HashMap<String, String> sColumnsMap;
157
158     static {
159         sAppReadableColumnsSet = new HashSet<String>();
160         for (int i = 0; i < sAppReadableColumnsArray.length; ++i) {
161             sAppReadableColumnsSet.add(sAppReadableColumnsArray[i]);
162         }
163
164         sColumnsMap = Maps.newHashMap();
165         sColumnsMap.put(OpenableColumns.DISPLAY_NAME,
166                 Downloads.Impl.COLUMN_TITLE + " AS " + OpenableColumns.DISPLAY_NAME);
167         sColumnsMap.put(OpenableColumns.SIZE,
168                 Downloads.Impl.COLUMN_TOTAL_BYTES + " AS " + OpenableColumns.SIZE);
169     }
170     private static final List<String> downloadManagerColumnsList =
171             Arrays.asList(DownloadManager.UNDERLYING_COLUMNS);
172
173     private Handler mHandler;
174
175     /** The database that lies underneath this content provider */
176     private SQLiteOpenHelper mOpenHelper = null;
177
178     /** List of uids that can access the downloads */
179     private int mSystemUid = -1;
180     private int mDefContainerUid = -1;
181
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                         Downloads.Impl.COLUMN_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         HandlerThread handlerThread =
446                 new HandlerThread("DownloadProvider handler", Process.THREAD_PRIORITY_BACKGROUND);
447         handlerThread.start();
448         mHandler = new Handler(handlerThread.getLooper());
449
450         mOpenHelper = new DatabaseHelper(getContext());
451         // Initialize the system uid
452         mSystemUid = Process.SYSTEM_UID;
453         // Initialize the default container uid. Package name hardcoded
454         // for now.
455         ApplicationInfo appInfo = null;
456         try {
457             appInfo = getContext().getPackageManager().
458                     getApplicationInfo("com.android.defcontainer", 0);
459         } catch (NameNotFoundException e) {
460             Log.wtf(Constants.TAG, "Could not get ApplicationInfo for com.android.defconatiner", e);
461         }
462         if (appInfo != null) {
463             mDefContainerUid = appInfo.uid;
464         }
465         // start the DownloadService class. don't wait for the 1st download to be issued.
466         // saves us by getting some initialization code in DownloadService out of the way.
467         Context context = getContext();
468         context.startService(new Intent(context, DownloadService.class));
469         return true;
470     }
471
472     /**
473      * Returns the content-provider-style MIME types of the various
474      * types accessible through this content provider.
475      */
476     @Override
477     public String getType(final Uri uri) {
478         int match = sURIMatcher.match(uri);
479         switch (match) {
480             case MY_DOWNLOADS:
481             case ALL_DOWNLOADS: {
482                 return DOWNLOAD_LIST_TYPE;
483             }
484             case MY_DOWNLOADS_ID:
485             case ALL_DOWNLOADS_ID:
486             case PUBLIC_DOWNLOAD_ID: {
487                 // return the mimetype of this id from the database
488                 final String id = getDownloadIdFromUri(uri);
489                 final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
490                 final String mimeType = DatabaseUtils.stringForQuery(db,
491                         "SELECT " + Downloads.Impl.COLUMN_MIME_TYPE + " FROM " + DB_TABLE +
492                         " WHERE " + Downloads.Impl._ID + " = ?",
493                         new String[]{id});
494                 if (TextUtils.isEmpty(mimeType)) {
495                     return DOWNLOAD_TYPE;
496                 } else {
497                     return mimeType;
498                 }
499             }
500             default: {
501                 if (Constants.LOGV) {
502                     Log.v(Constants.TAG, "calling getType on an unknown URI: " + uri);
503                 }
504                 throw new IllegalArgumentException("Unknown URI: " + uri);
505             }
506         }
507     }
508
509     /**
510      * Inserts a row in the database
511      */
512     @Override
513     public Uri insert(final Uri uri, final ContentValues values) {
514         checkInsertPermissions(values);
515         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
516
517         // note we disallow inserting into ALL_DOWNLOADS
518         int match = sURIMatcher.match(uri);
519         if (match != MY_DOWNLOADS) {
520             Log.d(Constants.TAG, "calling insert on an unknown/invalid URI: " + uri);
521             throw new IllegalArgumentException("Unknown/Invalid URI " + uri);
522         }
523
524         // copy some of the input values as it
525         ContentValues filteredValues = new ContentValues();
526         copyString(Downloads.Impl.COLUMN_URI, values, filteredValues);
527         copyString(Downloads.Impl.COLUMN_APP_DATA, values, filteredValues);
528         copyBoolean(Downloads.Impl.COLUMN_NO_INTEGRITY, values, filteredValues);
529         copyString(Downloads.Impl.COLUMN_FILE_NAME_HINT, values, filteredValues);
530         copyString(Downloads.Impl.COLUMN_MIME_TYPE, values, filteredValues);
531         copyBoolean(Downloads.Impl.COLUMN_IS_PUBLIC_API, values, filteredValues);
532
533         boolean isPublicApi =
534                 values.getAsBoolean(Downloads.Impl.COLUMN_IS_PUBLIC_API) == Boolean.TRUE;
535
536         // validate the destination column
537         Integer dest = values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION);
538         if (dest != null) {
539             if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED)
540                     != PackageManager.PERMISSION_GRANTED
541                     && (dest == Downloads.Impl.DESTINATION_CACHE_PARTITION
542                             || dest == Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING
543                             || dest == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION)) {
544                 throw new SecurityException("setting destination to : " + dest +
545                         " not allowed, unless PERMISSION_ACCESS_ADVANCED is granted");
546             }
547             // for public API behavior, if an app has CACHE_NON_PURGEABLE permission, automatically
548             // switch to non-purgeable download
549             boolean hasNonPurgeablePermission =
550                     getContext().checkCallingOrSelfPermission(
551                             Downloads.Impl.PERMISSION_CACHE_NON_PURGEABLE)
552                             == PackageManager.PERMISSION_GRANTED;
553             if (isPublicApi && dest == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE
554                     && hasNonPurgeablePermission) {
555                 dest = Downloads.Impl.DESTINATION_CACHE_PARTITION;
556             }
557             if (dest == Downloads.Impl.DESTINATION_FILE_URI) {
558                 checkFileUriDestination(values);
559
560             } else if (dest == Downloads.Impl.DESTINATION_EXTERNAL) {
561                 getContext().enforceCallingOrSelfPermission(
562                         android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
563                         "No permission to write");
564
565                 final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class);
566                 if (appOps.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE,
567                         getCallingPackage()) != AppOpsManager.MODE_ALLOWED) {
568                     throw new SecurityException("No permission to write");
569                 }
570
571             } else if (dest == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION) {
572                 getContext().enforcePermission(
573                         android.Manifest.permission.ACCESS_CACHE_FILESYSTEM,
574                         Binder.getCallingPid(), Binder.getCallingUid(),
575                         "need ACCESS_CACHE_FILESYSTEM permission to use system cache");
576             }
577             filteredValues.put(Downloads.Impl.COLUMN_DESTINATION, dest);
578         }
579
580         // validate the visibility column
581         Integer vis = values.getAsInteger(Downloads.Impl.COLUMN_VISIBILITY);
582         if (vis == null) {
583             if (dest == Downloads.Impl.DESTINATION_EXTERNAL) {
584                 filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY,
585                         Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
586             } else {
587                 filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY,
588                         Downloads.Impl.VISIBILITY_HIDDEN);
589             }
590         } else {
591             filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY, vis);
592         }
593         // copy the control column as is
594         copyInteger(Downloads.Impl.COLUMN_CONTROL, values, filteredValues);
595
596         /*
597          * requests coming from
598          * DownloadManager.addCompletedDownload(String, String, String,
599          * boolean, String, String, long) need special treatment
600          */
601         if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) ==
602                 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
603             // these requests always are marked as 'completed'
604             filteredValues.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_SUCCESS);
605             filteredValues.put(Downloads.Impl.COLUMN_TOTAL_BYTES,
606                     values.getAsLong(Downloads.Impl.COLUMN_TOTAL_BYTES));
607             filteredValues.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0);
608             copyInteger(Downloads.Impl.COLUMN_MEDIA_SCANNED, values, filteredValues);
609             copyString(Downloads.Impl._DATA, values, filteredValues);
610             copyBoolean(Downloads.Impl.COLUMN_ALLOW_WRITE, values, filteredValues);
611         } else {
612             filteredValues.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING);
613             filteredValues.put(Downloads.Impl.COLUMN_TOTAL_BYTES, -1);
614             filteredValues.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0);
615         }
616
617         // set lastupdate to current time
618         long lastMod = mSystemFacade.currentTimeMillis();
619         filteredValues.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, lastMod);
620
621         // use packagename of the caller to set the notification columns
622         String pckg = values.getAsString(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE);
623         String clazz = values.getAsString(Downloads.Impl.COLUMN_NOTIFICATION_CLASS);
624         if (pckg != null && (clazz != null || isPublicApi)) {
625             int uid = Binder.getCallingUid();
626             try {
627                 if (uid == 0 || mSystemFacade.userOwnsPackage(uid, pckg)) {
628                     filteredValues.put(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE, pckg);
629                     if (clazz != null) {
630                         filteredValues.put(Downloads.Impl.COLUMN_NOTIFICATION_CLASS, clazz);
631                     }
632                 }
633             } catch (PackageManager.NameNotFoundException ex) {
634                 /* ignored for now */
635             }
636         }
637
638         // copy some more columns as is
639         copyString(Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS, values, filteredValues);
640         copyString(Downloads.Impl.COLUMN_COOKIE_DATA, values, filteredValues);
641         copyString(Downloads.Impl.COLUMN_USER_AGENT, values, filteredValues);
642         copyString(Downloads.Impl.COLUMN_REFERER, values, filteredValues);
643
644         // UID, PID columns
645         if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED)
646                 == PackageManager.PERMISSION_GRANTED) {
647             copyInteger(Downloads.Impl.COLUMN_OTHER_UID, values, filteredValues);
648         }
649         filteredValues.put(Constants.UID, Binder.getCallingUid());
650         if (Binder.getCallingUid() == 0) {
651             copyInteger(Constants.UID, values, filteredValues);
652         }
653
654         // copy some more columns as is
655         copyStringWithDefault(Downloads.Impl.COLUMN_TITLE, values, filteredValues, "");
656         copyStringWithDefault(Downloads.Impl.COLUMN_DESCRIPTION, values, filteredValues, "");
657
658         // is_visible_in_downloads_ui column
659         if (values.containsKey(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI)) {
660             copyBoolean(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, values, filteredValues);
661         } else {
662             // by default, make external downloads visible in the UI
663             boolean isExternal = (dest == null || dest == Downloads.Impl.DESTINATION_EXTERNAL);
664             filteredValues.put(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, isExternal);
665         }
666
667         // public api requests and networktypes/roaming columns
668         if (isPublicApi) {
669             copyInteger(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES, values, filteredValues);
670             copyBoolean(Downloads.Impl.COLUMN_ALLOW_ROAMING, values, filteredValues);
671             copyBoolean(Downloads.Impl.COLUMN_ALLOW_METERED, values, filteredValues);
672         }
673
674         if (Constants.LOGVV) {
675             Log.v(Constants.TAG, "initiating download with UID "
676                     + filteredValues.getAsInteger(Constants.UID));
677             if (filteredValues.containsKey(Downloads.Impl.COLUMN_OTHER_UID)) {
678                 Log.v(Constants.TAG, "other UID " +
679                         filteredValues.getAsInteger(Downloads.Impl.COLUMN_OTHER_UID));
680             }
681         }
682
683         long rowID = db.insert(DB_TABLE, null, filteredValues);
684         if (rowID == -1) {
685             Log.d(Constants.TAG, "couldn't insert into downloads database");
686             return null;
687         }
688
689         insertRequestHeaders(db, rowID, values);
690         notifyContentChanged(uri, match);
691
692         // Always start service to handle notifications and/or scanning
693         final Context context = getContext();
694         context.startService(new Intent(context, DownloadService.class));
695
696         return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID);
697     }
698
699     /**
700      * Check that the file URI provided for DESTINATION_FILE_URI is valid.
701      */
702     private void checkFileUriDestination(ContentValues values) {
703         String fileUri = values.getAsString(Downloads.Impl.COLUMN_FILE_NAME_HINT);
704         if (fileUri == null) {
705             throw new IllegalArgumentException(
706                     "DESTINATION_FILE_URI must include a file URI under COLUMN_FILE_NAME_HINT");
707         }
708         Uri uri = Uri.parse(fileUri);
709         String scheme = uri.getScheme();
710         if (scheme == null || !scheme.equals("file")) {
711             throw new IllegalArgumentException("Not a file URI: " + uri);
712         }
713         final String path = uri.getPath();
714         if (path == null) {
715             throw new IllegalArgumentException("Invalid file URI: " + uri);
716         }
717
718         final File file = new File(path);
719         if (Helpers.isFilenameValidInExternalPackage(getContext(), file, getCallingPackage())) {
720             // No permissions required for paths belonging to calling package
721             return;
722         } else if (Helpers.isFilenameValidInExternal(getContext(), file)) {
723             // Otherwise we require write permission
724             getContext().enforceCallingOrSelfPermission(
725                     android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
726                     "No permission to write to " + file);
727
728             final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class);
729             if (appOps.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE,
730                     getCallingPackage()) != AppOpsManager.MODE_ALLOWED) {
731                 throw new SecurityException("No permission to write to " + file);
732             }
733
734         } else {
735             throw new SecurityException("Unsupported path " + file);
736         }
737     }
738
739     /**
740      * Apps with the ACCESS_DOWNLOAD_MANAGER permission can access this provider freely, subject to
741      * constraints in the rest of the code. Apps without that may still access this provider through
742      * the public API, but additional restrictions are imposed. We check those restrictions here.
743      *
744      * @param values ContentValues provided to insert()
745      * @throws SecurityException if the caller has insufficient permissions
746      */
747     private void checkInsertPermissions(ContentValues values) {
748         if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS)
749                 == PackageManager.PERMISSION_GRANTED) {
750             return;
751         }
752
753         getContext().enforceCallingOrSelfPermission(android.Manifest.permission.INTERNET,
754                 "INTERNET permission is required to use the download manager");
755
756         // ensure the request fits within the bounds of a public API request
757         // first copy so we can remove values
758         values = new ContentValues(values);
759
760         // check columns whose values are restricted
761         enforceAllowedValues(values, Downloads.Impl.COLUMN_IS_PUBLIC_API, Boolean.TRUE);
762
763         // validate the destination column
764         if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) ==
765                 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
766             /* this row is inserted by
767              * DownloadManager.addCompletedDownload(String, String, String,
768              * boolean, String, String, long)
769              */
770             values.remove(Downloads.Impl.COLUMN_TOTAL_BYTES);
771             values.remove(Downloads.Impl._DATA);
772             values.remove(Downloads.Impl.COLUMN_STATUS);
773         }
774         enforceAllowedValues(values, Downloads.Impl.COLUMN_DESTINATION,
775                 Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE,
776                 Downloads.Impl.DESTINATION_FILE_URI,
777                 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD);
778
779         if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_NO_NOTIFICATION)
780                 == PackageManager.PERMISSION_GRANTED) {
781             enforceAllowedValues(values, Downloads.Impl.COLUMN_VISIBILITY,
782                     Request.VISIBILITY_HIDDEN,
783                     Request.VISIBILITY_VISIBLE,
784                     Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED,
785                     Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION);
786         } else {
787             enforceAllowedValues(values, Downloads.Impl.COLUMN_VISIBILITY,
788                     Request.VISIBILITY_VISIBLE,
789                     Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED,
790                     Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION);
791         }
792
793         // remove the rest of the columns that are allowed (with any value)
794         values.remove(Downloads.Impl.COLUMN_URI);
795         values.remove(Downloads.Impl.COLUMN_TITLE);
796         values.remove(Downloads.Impl.COLUMN_DESCRIPTION);
797         values.remove(Downloads.Impl.COLUMN_MIME_TYPE);
798         values.remove(Downloads.Impl.COLUMN_FILE_NAME_HINT); // checked later in insert()
799         values.remove(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE); // checked later in insert()
800         values.remove(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES);
801         values.remove(Downloads.Impl.COLUMN_ALLOW_ROAMING);
802         values.remove(Downloads.Impl.COLUMN_ALLOW_METERED);
803         values.remove(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI);
804         values.remove(Downloads.Impl.COLUMN_MEDIA_SCANNED);
805         values.remove(Downloads.Impl.COLUMN_ALLOW_WRITE);
806         Iterator<Map.Entry<String, Object>> iterator = values.valueSet().iterator();
807         while (iterator.hasNext()) {
808             String key = iterator.next().getKey();
809             if (key.startsWith(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX)) {
810                 iterator.remove();
811             }
812         }
813
814         // any extra columns are extraneous and disallowed
815         if (values.size() > 0) {
816             StringBuilder error = new StringBuilder("Invalid columns in request: ");
817             boolean first = true;
818             for (Map.Entry<String, Object> entry : values.valueSet()) {
819                 if (!first) {
820                     error.append(", ");
821                 }
822                 error.append(entry.getKey());
823             }
824             throw new SecurityException(error.toString());
825         }
826     }
827
828     /**
829      * Remove column from values, and throw a SecurityException if the value isn't within the
830      * specified allowedValues.
831      */
832     private void enforceAllowedValues(ContentValues values, String column,
833             Object... allowedValues) {
834         Object value = values.get(column);
835         values.remove(column);
836         for (Object allowedValue : allowedValues) {
837             if (value == null && allowedValue == null) {
838                 return;
839             }
840             if (value != null && value.equals(allowedValue)) {
841                 return;
842             }
843         }
844         throw new SecurityException("Invalid value for " + column + ": " + value);
845     }
846
847     private Cursor queryCleared(Uri uri, String[] projection, String selection,
848             String[] selectionArgs, String sort) {
849         final long token = Binder.clearCallingIdentity();
850         try {
851             return query(uri, projection, selection, selectionArgs, sort);
852         } finally {
853             Binder.restoreCallingIdentity(token);
854         }
855     }
856
857     /**
858      * Starts a database query
859      */
860     @Override
861     public Cursor query(final Uri uri, String[] projection,
862              final String selection, final String[] selectionArgs,
863              final String sort) {
864
865         Helpers.validateSelection(selection, sAppReadableColumnsSet);
866
867         SQLiteDatabase db = mOpenHelper.getReadableDatabase();
868
869         int match = sURIMatcher.match(uri);
870         if (match == -1) {
871             if (Constants.LOGV) {
872                 Log.v(Constants.TAG, "querying unknown URI: " + uri);
873             }
874             throw new IllegalArgumentException("Unknown URI: " + uri);
875         }
876
877         if (match == REQUEST_HEADERS_URI) {
878             if (projection != null || selection != null || sort != null) {
879                 throw new UnsupportedOperationException("Request header queries do not support "
880                                                         + "projections, selections or sorting");
881             }
882             return queryRequestHeaders(db, uri);
883         }
884
885         SqlSelection fullSelection = getWhereClause(uri, selection, selectionArgs, match);
886
887         if (shouldRestrictVisibility()) {
888             if (projection == null) {
889                 projection = sAppReadableColumnsArray.clone();
890             } else {
891                 // check the validity of the columns in projection 
892                 for (int i = 0; i < projection.length; ++i) {
893                     if (!sAppReadableColumnsSet.contains(projection[i]) &&
894                             !downloadManagerColumnsList.contains(projection[i])) {
895                         throw new IllegalArgumentException(
896                                 "column " + projection[i] + " is not allowed in queries");
897                     }
898                 }
899             }
900
901             for (int i = 0; i < projection.length; i++) {
902                 final String newColumn = sColumnsMap.get(projection[i]);
903                 if (newColumn != null) {
904                     projection[i] = newColumn;
905                 }
906             }
907         }
908
909         if (Constants.LOGVV) {
910             logVerboseQueryInfo(projection, selection, selectionArgs, sort, db);
911         }
912
913         Cursor ret = db.query(DB_TABLE, projection, fullSelection.getSelection(),
914                 fullSelection.getParameters(), null, null, sort);
915
916         if (ret != null) {
917             ret.setNotificationUri(getContext().getContentResolver(), uri);
918             if (Constants.LOGVV) {
919                 Log.v(Constants.TAG,
920                         "created cursor " + ret + " on behalf of " + Binder.getCallingPid());
921             }
922         } else {
923             if (Constants.LOGV) {
924                 Log.v(Constants.TAG, "query failed in downloads database");
925             }
926         }
927
928         return ret;
929     }
930
931     private void logVerboseQueryInfo(String[] projection, final String selection,
932             final String[] selectionArgs, final String sort, SQLiteDatabase db) {
933         java.lang.StringBuilder sb = new java.lang.StringBuilder();
934         sb.append("starting query, database is ");
935         if (db != null) {
936             sb.append("not ");
937         }
938         sb.append("null; ");
939         if (projection == null) {
940             sb.append("projection is null; ");
941         } else if (projection.length == 0) {
942             sb.append("projection is empty; ");
943         } else {
944             for (int i = 0; i < projection.length; ++i) {
945                 sb.append("projection[");
946                 sb.append(i);
947                 sb.append("] is ");
948                 sb.append(projection[i]);
949                 sb.append("; ");
950             }
951         }
952         sb.append("selection is ");
953         sb.append(selection);
954         sb.append("; ");
955         if (selectionArgs == null) {
956             sb.append("selectionArgs is null; ");
957         } else if (selectionArgs.length == 0) {
958             sb.append("selectionArgs is empty; ");
959         } else {
960             for (int i = 0; i < selectionArgs.length; ++i) {
961                 sb.append("selectionArgs[");
962                 sb.append(i);
963                 sb.append("] is ");
964                 sb.append(selectionArgs[i]);
965                 sb.append("; ");
966             }
967         }
968         sb.append("sort is ");
969         sb.append(sort);
970         sb.append(".");
971         Log.v(Constants.TAG, sb.toString());
972     }
973
974     private String getDownloadIdFromUri(final Uri uri) {
975         return uri.getPathSegments().get(1);
976     }
977
978     /**
979      * Insert request headers for a download into the DB.
980      */
981     private void insertRequestHeaders(SQLiteDatabase db, long downloadId, ContentValues values) {
982         ContentValues rowValues = new ContentValues();
983         rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID, downloadId);
984         for (Map.Entry<String, Object> entry : values.valueSet()) {
985             String key = entry.getKey();
986             if (key.startsWith(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX)) {
987                 String headerLine = entry.getValue().toString();
988                 if (!headerLine.contains(":")) {
989                     throw new IllegalArgumentException("Invalid HTTP header line: " + headerLine);
990                 }
991                 String[] parts = headerLine.split(":", 2);
992                 rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_HEADER, parts[0].trim());
993                 rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_VALUE, parts[1].trim());
994                 db.insert(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, null, rowValues);
995             }
996         }
997     }
998
999     /**
1000      * Handle a query for the custom request headers registered for a download.
1001      */
1002     private Cursor queryRequestHeaders(SQLiteDatabase db, Uri uri) {
1003         String where = Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "="
1004                        + getDownloadIdFromUri(uri);
1005         String[] projection = new String[] {Downloads.Impl.RequestHeaders.COLUMN_HEADER,
1006                                             Downloads.Impl.RequestHeaders.COLUMN_VALUE};
1007         return db.query(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, projection, where,
1008                         null, null, null, null);
1009     }
1010
1011     /**
1012      * Delete request headers for downloads matching the given query.
1013      */
1014     private void deleteRequestHeaders(SQLiteDatabase db, String where, String[] whereArgs) {
1015         String[] projection = new String[] {Downloads.Impl._ID};
1016         Cursor cursor = db.query(DB_TABLE, projection, where, whereArgs, null, null, null, null);
1017         try {
1018             for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
1019                 long id = cursor.getLong(0);
1020                 String idWhere = Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "=" + id;
1021                 db.delete(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, idWhere, null);
1022             }
1023         } finally {
1024             cursor.close();
1025         }
1026     }
1027
1028     /**
1029      * @return true if we should restrict the columns readable by this caller
1030      */
1031     private boolean shouldRestrictVisibility() {
1032         int callingUid = Binder.getCallingUid();
1033         return Binder.getCallingPid() != Process.myPid() &&
1034                 callingUid != mSystemUid &&
1035                 callingUid != mDefContainerUid;
1036     }
1037
1038     /**
1039      * Updates a row in the database
1040      */
1041     @Override
1042     public int update(final Uri uri, final ContentValues values,
1043             final String where, final String[] whereArgs) {
1044
1045         Helpers.validateSelection(where, sAppReadableColumnsSet);
1046
1047         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1048
1049         int count;
1050         boolean startService = false;
1051
1052         if (values.containsKey(Downloads.Impl.COLUMN_DELETED)) {
1053             if (values.getAsInteger(Downloads.Impl.COLUMN_DELETED) == 1) {
1054                 // some rows are to be 'deleted'. need to start DownloadService.
1055                 startService = true;
1056             }
1057         }
1058
1059         ContentValues filteredValues;
1060         if (Binder.getCallingPid() != Process.myPid()) {
1061             filteredValues = new ContentValues();
1062             copyString(Downloads.Impl.COLUMN_APP_DATA, values, filteredValues);
1063             copyInteger(Downloads.Impl.COLUMN_VISIBILITY, values, filteredValues);
1064             Integer i = values.getAsInteger(Downloads.Impl.COLUMN_CONTROL);
1065             if (i != null) {
1066                 filteredValues.put(Downloads.Impl.COLUMN_CONTROL, i);
1067                 startService = true;
1068             }
1069
1070             copyInteger(Downloads.Impl.COLUMN_CONTROL, values, filteredValues);
1071             copyString(Downloads.Impl.COLUMN_TITLE, values, filteredValues);
1072             copyString(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, values, filteredValues);
1073             copyString(Downloads.Impl.COLUMN_DESCRIPTION, values, filteredValues);
1074             copyInteger(Downloads.Impl.COLUMN_DELETED, values, filteredValues);
1075         } else {
1076             filteredValues = values;
1077             String filename = values.getAsString(Downloads.Impl._DATA);
1078             if (filename != null) {
1079                 Cursor c = null;
1080                 try {
1081                     c = query(uri, new String[]
1082                             { Downloads.Impl.COLUMN_TITLE }, null, null, null);
1083                     if (!c.moveToFirst() || c.getString(0).isEmpty()) {
1084                         values.put(Downloads.Impl.COLUMN_TITLE, new File(filename).getName());
1085                     }
1086                 } finally {
1087                     IoUtils.closeQuietly(c);
1088                 }
1089             }
1090
1091             Integer status = values.getAsInteger(Downloads.Impl.COLUMN_STATUS);
1092             boolean isRestart = status != null && status == Downloads.Impl.STATUS_PENDING;
1093             boolean isUserBypassingSizeLimit =
1094                 values.containsKey(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT);
1095             if (isRestart || isUserBypassingSizeLimit) {
1096                 startService = true;
1097             }
1098         }
1099
1100         int match = sURIMatcher.match(uri);
1101         switch (match) {
1102             case MY_DOWNLOADS:
1103             case MY_DOWNLOADS_ID:
1104             case ALL_DOWNLOADS:
1105             case ALL_DOWNLOADS_ID:
1106                 SqlSelection selection = getWhereClause(uri, where, whereArgs, match);
1107                 if (filteredValues.size() > 0) {
1108                     count = db.update(DB_TABLE, filteredValues, selection.getSelection(),
1109                             selection.getParameters());
1110                 } else {
1111                     count = 0;
1112                 }
1113                 break;
1114
1115             default:
1116                 Log.d(Constants.TAG, "updating unknown/invalid URI: " + uri);
1117                 throw new UnsupportedOperationException("Cannot update URI: " + uri);
1118         }
1119
1120         notifyContentChanged(uri, match);
1121         if (startService) {
1122             Context context = getContext();
1123             context.startService(new Intent(context, DownloadService.class));
1124         }
1125         return count;
1126     }
1127
1128     /**
1129      * Notify of a change through both URIs (/my_downloads and /all_downloads)
1130      * @param uri either URI for the changed download(s)
1131      * @param uriMatch the match ID from {@link #sURIMatcher}
1132      */
1133     private void notifyContentChanged(final Uri uri, int uriMatch) {
1134         Long downloadId = null;
1135         if (uriMatch == MY_DOWNLOADS_ID || uriMatch == ALL_DOWNLOADS_ID) {
1136             downloadId = Long.parseLong(getDownloadIdFromUri(uri));
1137         }
1138         for (Uri uriToNotify : BASE_URIS) {
1139             if (downloadId != null) {
1140                 uriToNotify = ContentUris.withAppendedId(uriToNotify, downloadId);
1141             }
1142             getContext().getContentResolver().notifyChange(uriToNotify, null);
1143         }
1144     }
1145
1146     private SqlSelection getWhereClause(final Uri uri, final String where, final String[] whereArgs,
1147             int uriMatch) {
1148         SqlSelection selection = new SqlSelection();
1149         selection.appendClause(where, whereArgs);
1150         if (uriMatch == MY_DOWNLOADS_ID || uriMatch == ALL_DOWNLOADS_ID ||
1151                 uriMatch == PUBLIC_DOWNLOAD_ID) {
1152             selection.appendClause(Downloads.Impl._ID + " = ?", getDownloadIdFromUri(uri));
1153         }
1154         if ((uriMatch == MY_DOWNLOADS || uriMatch == MY_DOWNLOADS_ID)
1155                 && getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS_ALL)
1156                 != PackageManager.PERMISSION_GRANTED) {
1157             selection.appendClause(
1158                     Constants.UID + "= ? OR " + Downloads.Impl.COLUMN_OTHER_UID + "= ?",
1159                     Binder.getCallingUid(), Binder.getCallingUid());
1160         }
1161         return selection;
1162     }
1163
1164     /**
1165      * Deletes a row in the database
1166      */
1167     @Override
1168     public int delete(final Uri uri, final String where, final String[] whereArgs) {
1169         if (shouldRestrictVisibility()) {
1170             Helpers.validateSelection(where, sAppReadableColumnsSet);
1171         }
1172
1173         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1174         int count;
1175         int match = sURIMatcher.match(uri);
1176         switch (match) {
1177             case MY_DOWNLOADS:
1178             case MY_DOWNLOADS_ID:
1179             case ALL_DOWNLOADS:
1180             case ALL_DOWNLOADS_ID:
1181                 SqlSelection selection = getWhereClause(uri, where, whereArgs, match);
1182                 deleteRequestHeaders(db, selection.getSelection(), selection.getParameters());
1183
1184                 final Cursor cursor = db.query(DB_TABLE, new String[] {
1185                         Downloads.Impl._ID, Downloads.Impl._DATA
1186                 }, selection.getSelection(), selection.getParameters(), null, null, null);
1187                 try {
1188                     while (cursor.moveToNext()) {
1189                         final long id = cursor.getLong(0);
1190                         DownloadStorageProvider.onDownloadProviderDelete(getContext(), id);
1191
1192                         final String path = cursor.getString(1);
1193                         if (!TextUtils.isEmpty(path)) {
1194                             final File file = new File(path);
1195                             if (Helpers.isFilenameValid(getContext(), file)) {
1196                                 Log.v(Constants.TAG, "Deleting " + file + " via provider delete");
1197                                 file.delete();
1198                             }
1199                         }
1200                     }
1201                 } finally {
1202                     IoUtils.closeQuietly(cursor);
1203                 }
1204
1205                 count = db.delete(DB_TABLE, selection.getSelection(), selection.getParameters());
1206                 break;
1207
1208             default:
1209                 Log.d(Constants.TAG, "deleting unknown/invalid URI: " + uri);
1210                 throw new UnsupportedOperationException("Cannot delete URI: " + uri);
1211         }
1212         notifyContentChanged(uri, match);
1213         return count;
1214     }
1215
1216     /**
1217      * Remotely opens a file
1218      */
1219     @Override
1220     public ParcelFileDescriptor openFile(final Uri uri, String mode) throws FileNotFoundException {
1221         if (Constants.LOGVV) {
1222             logVerboseOpenFileInfo(uri, mode);
1223         }
1224
1225         final Cursor cursor = queryCleared(uri, new String[] {
1226                 Downloads.Impl._DATA, Downloads.Impl.COLUMN_STATUS,
1227                 Downloads.Impl.COLUMN_DESTINATION, Downloads.Impl.COLUMN_MEDIA_SCANNED }, null,
1228                 null, null);
1229         final String path;
1230         final boolean shouldScan;
1231         try {
1232             int count = (cursor != null) ? cursor.getCount() : 0;
1233             if (count != 1) {
1234                 // If there is not exactly one result, throw an appropriate exception.
1235                 if (count == 0) {
1236                     throw new FileNotFoundException("No entry for " + uri);
1237                 }
1238                 throw new FileNotFoundException("Multiple items at " + uri);
1239             }
1240
1241             if (cursor.moveToFirst()) {
1242                 final int status = cursor.getInt(1);
1243                 final int destination = cursor.getInt(2);
1244                 final int mediaScanned = cursor.getInt(3);
1245
1246                 path = cursor.getString(0);
1247                 shouldScan = Downloads.Impl.isStatusSuccess(status) && (
1248                         destination == Downloads.Impl.DESTINATION_EXTERNAL
1249                         || destination == Downloads.Impl.DESTINATION_FILE_URI
1250                         || destination == Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD)
1251                         && mediaScanned != 2;
1252             } else {
1253                 throw new FileNotFoundException("Failed moveToFirst");
1254             }
1255         } finally {
1256             IoUtils.closeQuietly(cursor);
1257         }
1258
1259         if (path == null) {
1260             throw new FileNotFoundException("No filename found.");
1261         }
1262
1263         final File file = new File(path);
1264         if (!Helpers.isFilenameValid(getContext(), file)) {
1265             throw new FileNotFoundException("Invalid file: " + file);
1266         }
1267
1268         final int pfdMode = ParcelFileDescriptor.parseMode(mode);
1269         if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY) {
1270             return ParcelFileDescriptor.open(file, pfdMode);
1271         } else {
1272             try {
1273                 // When finished writing, update size and timestamp
1274                 return ParcelFileDescriptor.open(file, pfdMode, mHandler, new OnCloseListener() {
1275                     @Override
1276                     public void onClose(IOException e) {
1277                         final ContentValues values = new ContentValues();
1278                         values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, file.length());
1279                         values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION,
1280                                 System.currentTimeMillis());
1281                         update(uri, values, null, null);
1282
1283                         if (shouldScan) {
1284                             final Intent intent = new Intent(
1285                                     Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
1286                             intent.setData(Uri.fromFile(file));
1287                             getContext().sendBroadcast(intent);
1288                         }
1289                     }
1290                 });
1291             } catch (IOException e) {
1292                 throw new FileNotFoundException("Failed to open for writing: " + e);
1293             }
1294         }
1295     }
1296
1297     @Override
1298     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
1299         final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ", 120);
1300
1301         pw.println("Downloads updated in last hour:");
1302         pw.increaseIndent();
1303
1304         final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
1305         final long modifiedAfter = mSystemFacade.currentTimeMillis() - DateUtils.HOUR_IN_MILLIS;
1306         final Cursor cursor = db.query(DB_TABLE, null,
1307                 Downloads.Impl.COLUMN_LAST_MODIFICATION + ">" + modifiedAfter, null, null, null,
1308                 Downloads.Impl._ID + " ASC");
1309         try {
1310             final String[] cols = cursor.getColumnNames();
1311             final int idCol = cursor.getColumnIndex(BaseColumns._ID);
1312             while (cursor.moveToNext()) {
1313                 pw.println("Download #" + cursor.getInt(idCol) + ":");
1314                 pw.increaseIndent();
1315                 for (int i = 0; i < cols.length; i++) {
1316                     // Omit sensitive data when dumping
1317                     if (Downloads.Impl.COLUMN_COOKIE_DATA.equals(cols[i])) {
1318                         continue;
1319                     }
1320                     pw.printPair(cols[i], cursor.getString(i));
1321                 }
1322                 pw.println();
1323                 pw.decreaseIndent();
1324             }
1325         } finally {
1326             cursor.close();
1327         }
1328
1329         pw.decreaseIndent();
1330     }
1331
1332     private void logVerboseOpenFileInfo(Uri uri, String mode) {
1333         Log.v(Constants.TAG, "openFile uri: " + uri + ", mode: " + mode
1334                 + ", uid: " + Binder.getCallingUid());
1335         Cursor cursor = query(Downloads.Impl.CONTENT_URI,
1336                 new String[] { "_id" }, null, null, "_id");
1337         if (cursor == null) {
1338             Log.v(Constants.TAG, "null cursor in openFile");
1339         } else {
1340             try {
1341                 if (!cursor.moveToFirst()) {
1342                     Log.v(Constants.TAG, "empty cursor in openFile");
1343                 } else {
1344                     do {
1345                         Log.v(Constants.TAG, "row " + cursor.getInt(0) + " available");
1346                     } while(cursor.moveToNext());
1347                 }
1348             } finally {
1349                 cursor.close();
1350             }
1351         }
1352         cursor = query(uri, new String[] { "_data" }, null, null, null);
1353         if (cursor == null) {
1354             Log.v(Constants.TAG, "null cursor in openFile");
1355         } else {
1356             try {
1357                 if (!cursor.moveToFirst()) {
1358                     Log.v(Constants.TAG, "empty cursor in openFile");
1359                 } else {
1360                     String filename = cursor.getString(0);
1361                     Log.v(Constants.TAG, "filename in openFile: " + filename);
1362                     if (new java.io.File(filename).isFile()) {
1363                         Log.v(Constants.TAG, "file exists in openFile");
1364                     }
1365                 }
1366             } finally {
1367                 cursor.close();
1368             }
1369         }
1370     }
1371
1372     private static final void copyInteger(String key, ContentValues from, ContentValues to) {
1373         Integer i = from.getAsInteger(key);
1374         if (i != null) {
1375             to.put(key, i);
1376         }
1377     }
1378
1379     private static final void copyBoolean(String key, ContentValues from, ContentValues to) {
1380         Boolean b = from.getAsBoolean(key);
1381         if (b != null) {
1382             to.put(key, b);
1383         }
1384     }
1385
1386     private static final void copyString(String key, ContentValues from, ContentValues to) {
1387         String s = from.getAsString(key);
1388         if (s != null) {
1389             to.put(key, s);
1390         }
1391     }
1392
1393     private static final void copyStringWithDefault(String key, ContentValues from,
1394             ContentValues to, String defaultValue) {
1395         copyString(key, from, to);
1396         if (!to.containsKey(key)) {
1397             to.put(key, defaultValue);
1398         }
1399     }
1400 }