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