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