• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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();
sColumnsMap.put(OpenableColumns.DISPLAY_NAME, Downloads.Impl.COLUMN_TITLE + " AS " + OpenableColumns.DISPLAY_NAME)173         sColumnsMap.put(OpenableColumns.DISPLAY_NAME,
174                 Downloads.Impl.COLUMN_TITLE + " AS " + OpenableColumns.DISPLAY_NAME);
sColumnsMap.put(OpenableColumns.SIZE, Downloads.Impl.COLUMN_TOTAL_BYTES + " AS " + OpenableColumns.SIZE)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 
appendClause(String newClause, final T... parameters)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 
getSelection()218         public String getSelection() {
219             return mWhereClause.toString();
220         }
221 
getParameters()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 {
DatabaseHelper(final Context context)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
onCreate(final SQLiteDatabase db)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
onUpgrade(final SQLiteDatabase db, int oldV, final int newV)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          */
upgradeTo(SQLiteDatabase db, int version)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          */
fillNullValues(SQLiteDatabase db)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 
fillNullValuesForColumn(SQLiteDatabase db, ContentValues values)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          */
makeCacheDownloadsInvisible(SQLiteDatabase db)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          */
addColumn(SQLiteDatabase db, String dbTable, String columnName, String columnDefinition)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          */
createDownloadsTable(SQLiteDatabase db)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 
createHeadersTable(SQLiteDatabase db)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
onCreate()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         try {
477             while (cursor.moveToNext()) {
478                 grantAllDownloadsPermission(cursor.getLong(0), cursor.getInt(1));
479             }
480         } finally {
481             cursor.close();
482         }
483 
484         return true;
485     }
486 
487     /**
488      * Returns the content-provider-style MIME types of the various
489      * types accessible through this content provider.
490      */
491     @Override
getType(final Uri uri)492     public String getType(final Uri uri) {
493         int match = sURIMatcher.match(uri);
494         switch (match) {
495             case MY_DOWNLOADS:
496             case ALL_DOWNLOADS: {
497                 return DOWNLOAD_LIST_TYPE;
498             }
499             case MY_DOWNLOADS_ID:
500             case ALL_DOWNLOADS_ID:
501             case PUBLIC_DOWNLOAD_ID: {
502                 // return the mimetype of this id from the database
503                 final String id = getDownloadIdFromUri(uri);
504                 final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
505                 final String mimeType = DatabaseUtils.stringForQuery(db,
506                         "SELECT " + Downloads.Impl.COLUMN_MIME_TYPE + " FROM " + DB_TABLE +
507                         " WHERE " + Downloads.Impl._ID + " = ?",
508                         new String[]{id});
509                 if (TextUtils.isEmpty(mimeType)) {
510                     return DOWNLOAD_TYPE;
511                 } else {
512                     return mimeType;
513                 }
514             }
515             default: {
516                 if (Constants.LOGV) {
517                     Log.v(Constants.TAG, "calling getType on an unknown URI: " + uri);
518                 }
519                 throw new IllegalArgumentException("Unknown URI: " + uri);
520             }
521         }
522     }
523 
524     /**
525      * Inserts a row in the database
526      */
527     @Override
insert(final Uri uri, final ContentValues values)528     public Uri insert(final Uri uri, final ContentValues values) {
529         checkInsertPermissions(values);
530         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
531 
532         // note we disallow inserting into ALL_DOWNLOADS
533         int match = sURIMatcher.match(uri);
534         if (match != MY_DOWNLOADS) {
535             Log.d(Constants.TAG, "calling insert on an unknown/invalid URI: " + uri);
536             throw new IllegalArgumentException("Unknown/Invalid URI " + uri);
537         }
538 
539         // copy some of the input values as it
540         ContentValues filteredValues = new ContentValues();
541         copyString(Downloads.Impl.COLUMN_URI, values, filteredValues);
542         copyString(Downloads.Impl.COLUMN_APP_DATA, values, filteredValues);
543         copyBoolean(Downloads.Impl.COLUMN_NO_INTEGRITY, values, filteredValues);
544         copyString(Downloads.Impl.COLUMN_FILE_NAME_HINT, values, filteredValues);
545         copyString(Downloads.Impl.COLUMN_MIME_TYPE, values, filteredValues);
546         copyBoolean(Downloads.Impl.COLUMN_IS_PUBLIC_API, values, filteredValues);
547 
548         boolean isPublicApi =
549                 values.getAsBoolean(Downloads.Impl.COLUMN_IS_PUBLIC_API) == Boolean.TRUE;
550 
551         // validate the destination column
552         Integer dest = values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION);
553         if (dest != null) {
554             if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED)
555                     != PackageManager.PERMISSION_GRANTED
556                     && (dest == Downloads.Impl.DESTINATION_CACHE_PARTITION
557                             || dest == Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING
558                             || dest == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION)) {
559                 throw new SecurityException("setting destination to : " + dest +
560                         " not allowed, unless PERMISSION_ACCESS_ADVANCED is granted");
561             }
562             // for public API behavior, if an app has CACHE_NON_PURGEABLE permission, automatically
563             // switch to non-purgeable download
564             boolean hasNonPurgeablePermission =
565                     getContext().checkCallingOrSelfPermission(
566                             Downloads.Impl.PERMISSION_CACHE_NON_PURGEABLE)
567                             == PackageManager.PERMISSION_GRANTED;
568             if (isPublicApi && dest == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE
569                     && hasNonPurgeablePermission) {
570                 dest = Downloads.Impl.DESTINATION_CACHE_PARTITION;
571             }
572             if (dest == Downloads.Impl.DESTINATION_FILE_URI) {
573                 checkFileUriDestination(values);
574 
575             } else if (dest == Downloads.Impl.DESTINATION_EXTERNAL) {
576                 getContext().enforceCallingOrSelfPermission(
577                         android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
578                         "No permission to write");
579 
580                 final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class);
581                 if (appOps.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE,
582                         getCallingPackage()) != AppOpsManager.MODE_ALLOWED) {
583                     throw new SecurityException("No permission to write");
584                 }
585 
586             } else if (dest == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION) {
587                 getContext().enforcePermission(
588                         android.Manifest.permission.ACCESS_CACHE_FILESYSTEM,
589                         Binder.getCallingPid(), Binder.getCallingUid(),
590                         "need ACCESS_CACHE_FILESYSTEM permission to use system cache");
591             }
592             filteredValues.put(Downloads.Impl.COLUMN_DESTINATION, dest);
593         }
594 
595         // validate the visibility column
596         Integer vis = values.getAsInteger(Downloads.Impl.COLUMN_VISIBILITY);
597         if (vis == null) {
598             if (dest == Downloads.Impl.DESTINATION_EXTERNAL) {
599                 filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY,
600                         Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
601             } else {
602                 filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY,
603                         Downloads.Impl.VISIBILITY_HIDDEN);
604             }
605         } else {
606             filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY, vis);
607         }
608         // copy the control column as is
609         copyInteger(Downloads.Impl.COLUMN_CONTROL, values, filteredValues);
610 
611         /*
612          * requests coming from
613          * DownloadManager.addCompletedDownload(String, String, String,
614          * boolean, String, String, long) need special treatment
615          */
616         if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) ==
617                 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
618             // these requests always are marked as 'completed'
619             filteredValues.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_SUCCESS);
620             filteredValues.put(Downloads.Impl.COLUMN_TOTAL_BYTES,
621                     values.getAsLong(Downloads.Impl.COLUMN_TOTAL_BYTES));
622             filteredValues.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0);
623             copyInteger(Downloads.Impl.COLUMN_MEDIA_SCANNED, values, filteredValues);
624             copyString(Downloads.Impl._DATA, values, filteredValues);
625             copyBoolean(Downloads.Impl.COLUMN_ALLOW_WRITE, values, filteredValues);
626         } else {
627             filteredValues.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING);
628             filteredValues.put(Downloads.Impl.COLUMN_TOTAL_BYTES, -1);
629             filteredValues.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0);
630         }
631 
632         // set lastupdate to current time
633         long lastMod = mSystemFacade.currentTimeMillis();
634         filteredValues.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, lastMod);
635 
636         // use packagename of the caller to set the notification columns
637         String pckg = values.getAsString(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE);
638         String clazz = values.getAsString(Downloads.Impl.COLUMN_NOTIFICATION_CLASS);
639         if (pckg != null && (clazz != null || isPublicApi)) {
640             int uid = Binder.getCallingUid();
641             try {
642                 if (uid == 0 || mSystemFacade.userOwnsPackage(uid, pckg)) {
643                     filteredValues.put(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE, pckg);
644                     if (clazz != null) {
645                         filteredValues.put(Downloads.Impl.COLUMN_NOTIFICATION_CLASS, clazz);
646                     }
647                 }
648             } catch (PackageManager.NameNotFoundException ex) {
649                 /* ignored for now */
650             }
651         }
652 
653         // copy some more columns as is
654         copyString(Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS, values, filteredValues);
655         copyString(Downloads.Impl.COLUMN_COOKIE_DATA, values, filteredValues);
656         copyString(Downloads.Impl.COLUMN_USER_AGENT, values, filteredValues);
657         copyString(Downloads.Impl.COLUMN_REFERER, values, filteredValues);
658 
659         // UID, PID columns
660         if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED)
661                 == PackageManager.PERMISSION_GRANTED) {
662             copyInteger(Downloads.Impl.COLUMN_OTHER_UID, values, filteredValues);
663         }
664         filteredValues.put(Constants.UID, Binder.getCallingUid());
665         if (Binder.getCallingUid() == 0) {
666             copyInteger(Constants.UID, values, filteredValues);
667         }
668 
669         // copy some more columns as is
670         copyStringWithDefault(Downloads.Impl.COLUMN_TITLE, values, filteredValues, "");
671         copyStringWithDefault(Downloads.Impl.COLUMN_DESCRIPTION, values, filteredValues, "");
672 
673         // is_visible_in_downloads_ui column
674         if (values.containsKey(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI)) {
675             copyBoolean(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, values, filteredValues);
676         } else {
677             // by default, make external downloads visible in the UI
678             boolean isExternal = (dest == null || dest == Downloads.Impl.DESTINATION_EXTERNAL);
679             filteredValues.put(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, isExternal);
680         }
681 
682         // public api requests and networktypes/roaming columns
683         if (isPublicApi) {
684             copyInteger(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES, values, filteredValues);
685             copyBoolean(Downloads.Impl.COLUMN_ALLOW_ROAMING, values, filteredValues);
686             copyBoolean(Downloads.Impl.COLUMN_ALLOW_METERED, values, filteredValues);
687             copyInteger(Downloads.Impl.COLUMN_FLAGS, values, filteredValues);
688         }
689 
690         if (Constants.LOGVV) {
691             Log.v(Constants.TAG, "initiating download with UID "
692                     + filteredValues.getAsInteger(Constants.UID));
693             if (filteredValues.containsKey(Downloads.Impl.COLUMN_OTHER_UID)) {
694                 Log.v(Constants.TAG, "other UID " +
695                         filteredValues.getAsInteger(Downloads.Impl.COLUMN_OTHER_UID));
696             }
697         }
698 
699         long rowID = db.insert(DB_TABLE, null, filteredValues);
700         if (rowID == -1) {
701             Log.d(Constants.TAG, "couldn't insert into downloads database");
702             return null;
703         }
704 
705         insertRequestHeaders(db, rowID, values);
706         grantAllDownloadsPermission(rowID, Binder.getCallingUid());
707         notifyContentChanged(uri, match);
708 
709         final long token = Binder.clearCallingIdentity();
710         try {
711             Helpers.scheduleJob(getContext(), rowID);
712         } finally {
713             Binder.restoreCallingIdentity(token);
714         }
715 
716         if (values.getAsInteger(COLUMN_DESTINATION) == DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD
717                 && values.getAsInteger(COLUMN_MEDIA_SCANNED) == 0) {
718             DownloadScanner.requestScanBlocking(getContext(), rowID, values.getAsString(_DATA),
719                     values.getAsString(COLUMN_MIME_TYPE));
720         }
721 
722         return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID);
723     }
724 
725     /**
726      * Check that the file URI provided for DESTINATION_FILE_URI is valid.
727      */
checkFileUriDestination(ContentValues values)728     private void checkFileUriDestination(ContentValues values) {
729         String fileUri = values.getAsString(Downloads.Impl.COLUMN_FILE_NAME_HINT);
730         if (fileUri == null) {
731             throw new IllegalArgumentException(
732                     "DESTINATION_FILE_URI must include a file URI under COLUMN_FILE_NAME_HINT");
733         }
734         Uri uri = Uri.parse(fileUri);
735         String scheme = uri.getScheme();
736         if (scheme == null || !scheme.equals("file")) {
737             throw new IllegalArgumentException("Not a file URI: " + uri);
738         }
739         final String path = uri.getPath();
740         if (path == null) {
741             throw new IllegalArgumentException("Invalid file URI: " + uri);
742         }
743 
744         final File file;
745         try {
746             file = new File(path).getCanonicalFile();
747         } catch (IOException e) {
748             throw new SecurityException(e);
749         }
750 
751         if (Helpers.isFilenameValidInExternalPackage(getContext(), file, getCallingPackage())) {
752             // No permissions required for paths belonging to calling package
753             return;
754         } else if (Helpers.isFilenameValidInExternal(getContext(), file)) {
755             // Otherwise we require write permission
756             getContext().enforceCallingOrSelfPermission(
757                     android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
758                     "No permission to write to " + file);
759 
760             final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class);
761             if (appOps.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE,
762                     getCallingPackage()) != AppOpsManager.MODE_ALLOWED) {
763                 throw new SecurityException("No permission to write to " + file);
764             }
765 
766         } else {
767             throw new SecurityException("Unsupported path " + file);
768         }
769     }
770 
771     /**
772      * Apps with the ACCESS_DOWNLOAD_MANAGER permission can access this provider freely, subject to
773      * constraints in the rest of the code. Apps without that may still access this provider through
774      * the public API, but additional restrictions are imposed. We check those restrictions here.
775      *
776      * @param values ContentValues provided to insert()
777      * @throws SecurityException if the caller has insufficient permissions
778      */
checkInsertPermissions(ContentValues values)779     private void checkInsertPermissions(ContentValues values) {
780         if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS)
781                 == PackageManager.PERMISSION_GRANTED) {
782             return;
783         }
784 
785         getContext().enforceCallingOrSelfPermission(android.Manifest.permission.INTERNET,
786                 "INTERNET permission is required to use the download manager");
787 
788         // ensure the request fits within the bounds of a public API request
789         // first copy so we can remove values
790         values = new ContentValues(values);
791 
792         // check columns whose values are restricted
793         enforceAllowedValues(values, Downloads.Impl.COLUMN_IS_PUBLIC_API, Boolean.TRUE);
794 
795         // validate the destination column
796         if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) ==
797                 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
798             /* this row is inserted by
799              * DownloadManager.addCompletedDownload(String, String, String,
800              * boolean, String, String, long)
801              */
802             values.remove(Downloads.Impl.COLUMN_TOTAL_BYTES);
803             values.remove(Downloads.Impl._DATA);
804             values.remove(Downloads.Impl.COLUMN_STATUS);
805         }
806         enforceAllowedValues(values, Downloads.Impl.COLUMN_DESTINATION,
807                 Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE,
808                 Downloads.Impl.DESTINATION_FILE_URI,
809                 Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD);
810 
811         if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_NO_NOTIFICATION)
812                 == PackageManager.PERMISSION_GRANTED) {
813             enforceAllowedValues(values, Downloads.Impl.COLUMN_VISIBILITY,
814                     Request.VISIBILITY_HIDDEN,
815                     Request.VISIBILITY_VISIBLE,
816                     Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED,
817                     Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION);
818         } else {
819             enforceAllowedValues(values, Downloads.Impl.COLUMN_VISIBILITY,
820                     Request.VISIBILITY_VISIBLE,
821                     Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED,
822                     Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION);
823         }
824 
825         // remove the rest of the columns that are allowed (with any value)
826         values.remove(Downloads.Impl.COLUMN_URI);
827         values.remove(Downloads.Impl.COLUMN_TITLE);
828         values.remove(Downloads.Impl.COLUMN_DESCRIPTION);
829         values.remove(Downloads.Impl.COLUMN_MIME_TYPE);
830         values.remove(Downloads.Impl.COLUMN_FILE_NAME_HINT); // checked later in insert()
831         values.remove(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE); // checked later in insert()
832         values.remove(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES);
833         values.remove(Downloads.Impl.COLUMN_ALLOW_ROAMING);
834         values.remove(Downloads.Impl.COLUMN_ALLOW_METERED);
835         values.remove(Downloads.Impl.COLUMN_FLAGS);
836         values.remove(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI);
837         values.remove(Downloads.Impl.COLUMN_MEDIA_SCANNED);
838         values.remove(Downloads.Impl.COLUMN_ALLOW_WRITE);
839         Iterator<Map.Entry<String, Object>> iterator = values.valueSet().iterator();
840         while (iterator.hasNext()) {
841             String key = iterator.next().getKey();
842             if (key.startsWith(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX)) {
843                 iterator.remove();
844             }
845         }
846 
847         // any extra columns are extraneous and disallowed
848         if (values.size() > 0) {
849             StringBuilder error = new StringBuilder("Invalid columns in request: ");
850             boolean first = true;
851             for (Map.Entry<String, Object> entry : values.valueSet()) {
852                 if (!first) {
853                     error.append(", ");
854                 }
855                 error.append(entry.getKey());
856             }
857             throw new SecurityException(error.toString());
858         }
859     }
860 
861     /**
862      * Remove column from values, and throw a SecurityException if the value isn't within the
863      * specified allowedValues.
864      */
enforceAllowedValues(ContentValues values, String column, Object... allowedValues)865     private void enforceAllowedValues(ContentValues values, String column,
866             Object... allowedValues) {
867         Object value = values.get(column);
868         values.remove(column);
869         for (Object allowedValue : allowedValues) {
870             if (value == null && allowedValue == null) {
871                 return;
872             }
873             if (value != null && value.equals(allowedValue)) {
874                 return;
875             }
876         }
877         throw new SecurityException("Invalid value for " + column + ": " + value);
878     }
879 
queryCleared(Uri uri, String[] projection, String selection, String[] selectionArgs, String sort)880     private Cursor queryCleared(Uri uri, String[] projection, String selection,
881             String[] selectionArgs, String sort) {
882         final long token = Binder.clearCallingIdentity();
883         try {
884             return query(uri, projection, selection, selectionArgs, sort);
885         } finally {
886             Binder.restoreCallingIdentity(token);
887         }
888     }
889 
890     /**
891      * Starts a database query
892      */
893     @Override
query(final Uri uri, String[] projection, final String selection, final String[] selectionArgs, final String sort)894     public Cursor query(final Uri uri, String[] projection,
895              final String selection, final String[] selectionArgs,
896              final String sort) {
897 
898         Helpers.validateSelection(selection, sAppReadableColumnsSet);
899 
900         SQLiteDatabase db = mOpenHelper.getReadableDatabase();
901 
902         int match = sURIMatcher.match(uri);
903         if (match == -1) {
904             if (Constants.LOGV) {
905                 Log.v(Constants.TAG, "querying unknown URI: " + uri);
906             }
907             throw new IllegalArgumentException("Unknown URI: " + uri);
908         }
909 
910         if (match == REQUEST_HEADERS_URI) {
911             if (projection != null || selection != null || sort != null) {
912                 throw new UnsupportedOperationException("Request header queries do not support "
913                                                         + "projections, selections or sorting");
914             }
915             return queryRequestHeaders(db, uri);
916         }
917 
918         SqlSelection fullSelection = getWhereClause(uri, selection, selectionArgs, match);
919 
920         if (shouldRestrictVisibility()) {
921             if (projection == null) {
922                 projection = sAppReadableColumnsArray.clone();
923             } else {
924                 // check the validity of the columns in projection
925                 for (int i = 0; i < projection.length; ++i) {
926                     if (!sAppReadableColumnsSet.contains(projection[i]) &&
927                             !downloadManagerColumnsList.contains(projection[i])) {
928                         throw new IllegalArgumentException(
929                                 "column " + projection[i] + " is not allowed in queries");
930                     }
931                 }
932             }
933 
934             for (int i = 0; i < projection.length; i++) {
935                 final String newColumn = sColumnsMap.get(projection[i]);
936                 if (newColumn != null) {
937                     projection[i] = newColumn;
938                 }
939             }
940         }
941 
942         if (Constants.LOGVV) {
943             logVerboseQueryInfo(projection, selection, selectionArgs, sort, db);
944         }
945 
946         Cursor ret = db.query(DB_TABLE, projection, fullSelection.getSelection(),
947                 fullSelection.getParameters(), null, null, sort);
948 
949         if (ret != null) {
950             ret.setNotificationUri(getContext().getContentResolver(), uri);
951             if (Constants.LOGVV) {
952                 Log.v(Constants.TAG,
953                         "created cursor " + ret + " on behalf of " + Binder.getCallingPid());
954             }
955         } else {
956             if (Constants.LOGV) {
957                 Log.v(Constants.TAG, "query failed in downloads database");
958             }
959         }
960 
961         return ret;
962     }
963 
logVerboseQueryInfo(String[] projection, final String selection, final String[] selectionArgs, final String sort, SQLiteDatabase db)964     private void logVerboseQueryInfo(String[] projection, final String selection,
965             final String[] selectionArgs, final String sort, SQLiteDatabase db) {
966         java.lang.StringBuilder sb = new java.lang.StringBuilder();
967         sb.append("starting query, database is ");
968         if (db != null) {
969             sb.append("not ");
970         }
971         sb.append("null; ");
972         if (projection == null) {
973             sb.append("projection is null; ");
974         } else if (projection.length == 0) {
975             sb.append("projection is empty; ");
976         } else {
977             for (int i = 0; i < projection.length; ++i) {
978                 sb.append("projection[");
979                 sb.append(i);
980                 sb.append("] is ");
981                 sb.append(projection[i]);
982                 sb.append("; ");
983             }
984         }
985         sb.append("selection is ");
986         sb.append(selection);
987         sb.append("; ");
988         if (selectionArgs == null) {
989             sb.append("selectionArgs is null; ");
990         } else if (selectionArgs.length == 0) {
991             sb.append("selectionArgs is empty; ");
992         } else {
993             for (int i = 0; i < selectionArgs.length; ++i) {
994                 sb.append("selectionArgs[");
995                 sb.append(i);
996                 sb.append("] is ");
997                 sb.append(selectionArgs[i]);
998                 sb.append("; ");
999             }
1000         }
1001         sb.append("sort is ");
1002         sb.append(sort);
1003         sb.append(".");
1004         Log.v(Constants.TAG, sb.toString());
1005     }
1006 
getDownloadIdFromUri(final Uri uri)1007     private String getDownloadIdFromUri(final Uri uri) {
1008         return uri.getPathSegments().get(1);
1009     }
1010 
1011     /**
1012      * Insert request headers for a download into the DB.
1013      */
insertRequestHeaders(SQLiteDatabase db, long downloadId, ContentValues values)1014     private void insertRequestHeaders(SQLiteDatabase db, long downloadId, ContentValues values) {
1015         ContentValues rowValues = new ContentValues();
1016         rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID, downloadId);
1017         for (Map.Entry<String, Object> entry : values.valueSet()) {
1018             String key = entry.getKey();
1019             if (key.startsWith(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX)) {
1020                 String headerLine = entry.getValue().toString();
1021                 if (!headerLine.contains(":")) {
1022                     throw new IllegalArgumentException("Invalid HTTP header line: " + headerLine);
1023                 }
1024                 String[] parts = headerLine.split(":", 2);
1025                 rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_HEADER, parts[0].trim());
1026                 rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_VALUE, parts[1].trim());
1027                 db.insert(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, null, rowValues);
1028             }
1029         }
1030     }
1031 
1032     /**
1033      * Handle a query for the custom request headers registered for a download.
1034      */
queryRequestHeaders(SQLiteDatabase db, Uri uri)1035     private Cursor queryRequestHeaders(SQLiteDatabase db, Uri uri) {
1036         String where = Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "="
1037                        + getDownloadIdFromUri(uri);
1038         String[] projection = new String[] {Downloads.Impl.RequestHeaders.COLUMN_HEADER,
1039                                             Downloads.Impl.RequestHeaders.COLUMN_VALUE};
1040         return db.query(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, projection, where,
1041                         null, null, null, null);
1042     }
1043 
1044     /**
1045      * Delete request headers for downloads matching the given query.
1046      */
deleteRequestHeaders(SQLiteDatabase db, String where, String[] whereArgs)1047     private void deleteRequestHeaders(SQLiteDatabase db, String where, String[] whereArgs) {
1048         String[] projection = new String[] {Downloads.Impl._ID};
1049         Cursor cursor = db.query(DB_TABLE, projection, where, whereArgs, null, null, null, null);
1050         try {
1051             for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
1052                 long id = cursor.getLong(0);
1053                 String idWhere = Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "=" + id;
1054                 db.delete(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, idWhere, null);
1055             }
1056         } finally {
1057             cursor.close();
1058         }
1059     }
1060 
1061     /**
1062      * @return true if we should restrict the columns readable by this caller
1063      */
shouldRestrictVisibility()1064     private boolean shouldRestrictVisibility() {
1065         int callingUid = Binder.getCallingUid();
1066         return Binder.getCallingPid() != Process.myPid() &&
1067                 callingUid != mSystemUid &&
1068                 callingUid != mDefContainerUid;
1069     }
1070 
1071     /**
1072      * Updates a row in the database
1073      */
1074     @Override
update(final Uri uri, final ContentValues values, final String where, final String[] whereArgs)1075     public int update(final Uri uri, final ContentValues values,
1076             final String where, final String[] whereArgs) {
1077 
1078         Helpers.validateSelection(where, sAppReadableColumnsSet);
1079 
1080         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1081 
1082         int count;
1083         boolean updateSchedule = false;
1084 
1085         ContentValues filteredValues;
1086         if (Binder.getCallingPid() != Process.myPid()) {
1087             filteredValues = new ContentValues();
1088             copyString(Downloads.Impl.COLUMN_APP_DATA, values, filteredValues);
1089             copyInteger(Downloads.Impl.COLUMN_VISIBILITY, values, filteredValues);
1090             Integer i = values.getAsInteger(Downloads.Impl.COLUMN_CONTROL);
1091             if (i != null) {
1092                 filteredValues.put(Downloads.Impl.COLUMN_CONTROL, i);
1093                 updateSchedule = true;
1094             }
1095 
1096             copyInteger(Downloads.Impl.COLUMN_CONTROL, values, filteredValues);
1097             copyString(Downloads.Impl.COLUMN_TITLE, values, filteredValues);
1098             copyString(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, values, filteredValues);
1099             copyString(Downloads.Impl.COLUMN_DESCRIPTION, values, filteredValues);
1100             copyInteger(Downloads.Impl.COLUMN_DELETED, values, filteredValues);
1101         } else {
1102             filteredValues = values;
1103             String filename = values.getAsString(Downloads.Impl._DATA);
1104             if (filename != null) {
1105                 Cursor c = null;
1106                 try {
1107                     c = query(uri, new String[]
1108                             { Downloads.Impl.COLUMN_TITLE }, null, null, null);
1109                     if (!c.moveToFirst() || c.getString(0).isEmpty()) {
1110                         values.put(Downloads.Impl.COLUMN_TITLE, new File(filename).getName());
1111                     }
1112                 } finally {
1113                     IoUtils.closeQuietly(c);
1114                 }
1115             }
1116 
1117             Integer status = values.getAsInteger(Downloads.Impl.COLUMN_STATUS);
1118             boolean isRestart = status != null && status == Downloads.Impl.STATUS_PENDING;
1119             boolean isUserBypassingSizeLimit =
1120                 values.containsKey(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT);
1121             if (isRestart || isUserBypassingSizeLimit) {
1122                 updateSchedule = true;
1123             }
1124         }
1125 
1126         int match = sURIMatcher.match(uri);
1127         switch (match) {
1128             case MY_DOWNLOADS:
1129             case MY_DOWNLOADS_ID:
1130             case ALL_DOWNLOADS:
1131             case ALL_DOWNLOADS_ID:
1132                 if (filteredValues.size() == 0) {
1133                     count = 0;
1134                     break;
1135                 }
1136 
1137                 final SqlSelection selection = getWhereClause(uri, where, whereArgs, match);
1138                 count = db.update(DB_TABLE, filteredValues, selection.getSelection(),
1139                         selection.getParameters());
1140                 if (updateSchedule) {
1141                     final long token = Binder.clearCallingIdentity();
1142                     try {
1143                         try (Cursor cursor = db.query(DB_TABLE, new String[] { _ID },
1144                                 selection.getSelection(), selection.getParameters(),
1145                                 null, null, null)) {
1146                             while (cursor.moveToNext()) {
1147                                 Helpers.scheduleJob(getContext(), cursor.getInt(0));
1148                             }
1149                         }
1150                     } finally {
1151                         Binder.restoreCallingIdentity(token);
1152                     }
1153                 }
1154                 break;
1155 
1156             default:
1157                 Log.d(Constants.TAG, "updating unknown/invalid URI: " + uri);
1158                 throw new UnsupportedOperationException("Cannot update URI: " + uri);
1159         }
1160 
1161         notifyContentChanged(uri, match);
1162         return count;
1163     }
1164 
1165     /**
1166      * Notify of a change through both URIs (/my_downloads and /all_downloads)
1167      * @param uri either URI for the changed download(s)
1168      * @param uriMatch the match ID from {@link #sURIMatcher}
1169      */
notifyContentChanged(final Uri uri, int uriMatch)1170     private void notifyContentChanged(final Uri uri, int uriMatch) {
1171         Long downloadId = null;
1172         if (uriMatch == MY_DOWNLOADS_ID || uriMatch == ALL_DOWNLOADS_ID) {
1173             downloadId = Long.parseLong(getDownloadIdFromUri(uri));
1174         }
1175         for (Uri uriToNotify : BASE_URIS) {
1176             if (downloadId != null) {
1177                 uriToNotify = ContentUris.withAppendedId(uriToNotify, downloadId);
1178             }
1179             getContext().getContentResolver().notifyChange(uriToNotify, null);
1180         }
1181     }
1182 
getWhereClause(final Uri uri, final String where, final String[] whereArgs, int uriMatch)1183     private SqlSelection getWhereClause(final Uri uri, final String where, final String[] whereArgs,
1184             int uriMatch) {
1185         SqlSelection selection = new SqlSelection();
1186         selection.appendClause(where, whereArgs);
1187         if (uriMatch == MY_DOWNLOADS_ID || uriMatch == ALL_DOWNLOADS_ID ||
1188                 uriMatch == PUBLIC_DOWNLOAD_ID) {
1189             selection.appendClause(Downloads.Impl._ID + " = ?", getDownloadIdFromUri(uri));
1190         }
1191         if ((uriMatch == MY_DOWNLOADS || uriMatch == MY_DOWNLOADS_ID)
1192                 && getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS_ALL)
1193                 != PackageManager.PERMISSION_GRANTED) {
1194             selection.appendClause(
1195                     Constants.UID + "= ? OR " + Downloads.Impl.COLUMN_OTHER_UID + "= ?",
1196                     Binder.getCallingUid(), Binder.getCallingUid());
1197         }
1198         return selection;
1199     }
1200 
1201     /**
1202      * Deletes a row in the database
1203      */
1204     @Override
delete(final Uri uri, final String where, final String[] whereArgs)1205     public int delete(final Uri uri, final String where, final String[] whereArgs) {
1206         if (shouldRestrictVisibility()) {
1207             Helpers.validateSelection(where, sAppReadableColumnsSet);
1208         }
1209 
1210         final JobScheduler scheduler = getContext().getSystemService(JobScheduler.class);
1211         final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
1212         int count;
1213         int match = sURIMatcher.match(uri);
1214         switch (match) {
1215             case MY_DOWNLOADS:
1216             case MY_DOWNLOADS_ID:
1217             case ALL_DOWNLOADS:
1218             case ALL_DOWNLOADS_ID:
1219                 final SqlSelection selection = getWhereClause(uri, where, whereArgs, match);
1220                 deleteRequestHeaders(db, selection.getSelection(), selection.getParameters());
1221 
1222                 try (Cursor cursor = db.query(DB_TABLE, new String[] {
1223                         _ID, _DATA, COLUMN_MEDIAPROVIDER_URI
1224                 }, selection.getSelection(), selection.getParameters(), null, null, null)) {
1225                     while (cursor.moveToNext()) {
1226                         final long id = cursor.getLong(0);
1227                         scheduler.cancel((int) id);
1228                         revokeAllDownloadsPermission(id);
1229                         DownloadStorageProvider.onDownloadProviderDelete(getContext(), id);
1230 
1231                         final String path = cursor.getString(1);
1232                         if (!TextUtils.isEmpty(path)) {
1233                             try {
1234                                 final File file = new File(path).getCanonicalFile();
1235                                 if (Helpers.isFilenameValid(getContext(), file)) {
1236                                     Log.v(Constants.TAG,
1237                                             "Deleting " + file + " via provider delete");
1238                                     file.delete();
1239                                 }
1240                             } catch (IOException ignored) {
1241                             }
1242                         }
1243 
1244                         final String mediaUri = cursor.getString(2);
1245                         if (!TextUtils.isEmpty(mediaUri)) {
1246                             final long token = Binder.clearCallingIdentity();
1247                             try {
1248                                 getContext().getContentResolver().delete(Uri.parse(mediaUri), null,
1249                                         null);
1250                             } finally {
1251                                 Binder.restoreCallingIdentity(token);
1252                             }
1253                         }
1254                     }
1255                 }
1256 
1257                 count = db.delete(DB_TABLE, selection.getSelection(), selection.getParameters());
1258                 break;
1259 
1260             default:
1261                 Log.d(Constants.TAG, "deleting unknown/invalid URI: " + uri);
1262                 throw new UnsupportedOperationException("Cannot delete URI: " + uri);
1263         }
1264         notifyContentChanged(uri, match);
1265         return count;
1266     }
1267 
1268     /**
1269      * Remotely opens a file
1270      */
1271     @Override
openFile(final Uri uri, String mode)1272     public ParcelFileDescriptor openFile(final Uri uri, String mode) throws FileNotFoundException {
1273         if (Constants.LOGVV) {
1274             logVerboseOpenFileInfo(uri, mode);
1275         }
1276 
1277         // Perform normal query to enforce caller identity access before
1278         // clearing it to reach internal-only columns
1279         final Cursor probeCursor = query(uri, new String[] {
1280                 Downloads.Impl._DATA }, null, null, null);
1281         try {
1282             if ((probeCursor == null) || (probeCursor.getCount() == 0)) {
1283                 throw new FileNotFoundException(
1284                         "No file found for " + uri + " as UID " + Binder.getCallingUid());
1285             }
1286         } finally {
1287             IoUtils.closeQuietly(probeCursor);
1288         }
1289 
1290         final Cursor cursor = queryCleared(uri, new String[] {
1291                 Downloads.Impl._DATA, Downloads.Impl.COLUMN_STATUS,
1292                 Downloads.Impl.COLUMN_DESTINATION, Downloads.Impl.COLUMN_MEDIA_SCANNED }, null,
1293                 null, null);
1294         final String path;
1295         final boolean shouldScan;
1296         try {
1297             int count = (cursor != null) ? cursor.getCount() : 0;
1298             if (count != 1) {
1299                 // If there is not exactly one result, throw an appropriate exception.
1300                 if (count == 0) {
1301                     throw new FileNotFoundException("No entry for " + uri);
1302                 }
1303                 throw new FileNotFoundException("Multiple items at " + uri);
1304             }
1305 
1306             if (cursor.moveToFirst()) {
1307                 final int status = cursor.getInt(1);
1308                 final int destination = cursor.getInt(2);
1309                 final int mediaScanned = cursor.getInt(3);
1310 
1311                 path = cursor.getString(0);
1312                 shouldScan = Downloads.Impl.isStatusSuccess(status) && (
1313                         destination == Downloads.Impl.DESTINATION_EXTERNAL
1314                         || destination == Downloads.Impl.DESTINATION_FILE_URI
1315                         || destination == Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD)
1316                         && mediaScanned != 2;
1317             } else {
1318                 throw new FileNotFoundException("Failed moveToFirst");
1319             }
1320         } finally {
1321             IoUtils.closeQuietly(cursor);
1322         }
1323 
1324         if (path == null) {
1325             throw new FileNotFoundException("No filename found.");
1326         }
1327 
1328         final File file;
1329         try {
1330             file = new File(path).getCanonicalFile();
1331         } catch (IOException e) {
1332             throw new FileNotFoundException(e.getMessage());
1333         }
1334 
1335         if (!Helpers.isFilenameValid(getContext(), file)) {
1336             throw new FileNotFoundException("Invalid file: " + file);
1337         }
1338 
1339         final int pfdMode = ParcelFileDescriptor.parseMode(mode);
1340         if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY) {
1341             return ParcelFileDescriptor.open(file, pfdMode);
1342         } else {
1343             try {
1344                 // When finished writing, update size and timestamp
1345                 return ParcelFileDescriptor.open(file, pfdMode, Helpers.getAsyncHandler(),
1346                         new OnCloseListener() {
1347                     @Override
1348                     public void onClose(IOException e) {
1349                         final ContentValues values = new ContentValues();
1350                         values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, file.length());
1351                         values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION,
1352                                 System.currentTimeMillis());
1353                         update(uri, values, null, null);
1354 
1355                         if (shouldScan) {
1356                             final Intent intent = new Intent(
1357                                     Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
1358                             intent.setData(Uri.fromFile(file));
1359                             getContext().sendBroadcast(intent);
1360                         }
1361                     }
1362                 });
1363             } catch (IOException e) {
1364                 throw new FileNotFoundException("Failed to open for writing: " + e);
1365             }
1366         }
1367     }
1368 
1369     @Override
1370     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
1371         final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ", 120);
1372 
1373         pw.println("Downloads updated in last hour:");
1374         pw.increaseIndent();
1375 
1376         final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
1377         final long modifiedAfter = mSystemFacade.currentTimeMillis() - DateUtils.HOUR_IN_MILLIS;
1378         final Cursor cursor = db.query(DB_TABLE, null,
1379                 Downloads.Impl.COLUMN_LAST_MODIFICATION + ">" + modifiedAfter, null, null, null,
1380                 Downloads.Impl._ID + " ASC");
1381         try {
1382             final String[] cols = cursor.getColumnNames();
1383             final int idCol = cursor.getColumnIndex(BaseColumns._ID);
1384             while (cursor.moveToNext()) {
1385                 pw.println("Download #" + cursor.getInt(idCol) + ":");
1386                 pw.increaseIndent();
1387                 for (int i = 0; i < cols.length; i++) {
1388                     // Omit sensitive data when dumping
1389                     if (Downloads.Impl.COLUMN_COOKIE_DATA.equals(cols[i])) {
1390                         continue;
1391                     }
1392                     pw.printPair(cols[i], cursor.getString(i));
1393                 }
1394                 pw.println();
1395                 pw.decreaseIndent();
1396             }
1397         } finally {
1398             cursor.close();
1399         }
1400 
1401         pw.decreaseIndent();
1402     }
1403 
1404     private void logVerboseOpenFileInfo(Uri uri, String mode) {
1405         Log.v(Constants.TAG, "openFile uri: " + uri + ", mode: " + mode
1406                 + ", uid: " + Binder.getCallingUid());
1407         Cursor cursor = query(Downloads.Impl.CONTENT_URI,
1408                 new String[] { "_id" }, null, null, "_id");
1409         if (cursor == null) {
1410             Log.v(Constants.TAG, "null cursor in openFile");
1411         } else {
1412             try {
1413                 if (!cursor.moveToFirst()) {
1414                     Log.v(Constants.TAG, "empty cursor in openFile");
1415                 } else {
1416                     do {
1417                         Log.v(Constants.TAG, "row " + cursor.getInt(0) + " available");
1418                     } while(cursor.moveToNext());
1419                 }
1420             } finally {
1421                 cursor.close();
1422             }
1423         }
1424         cursor = query(uri, new String[] { "_data" }, null, null, null);
1425         if (cursor == null) {
1426             Log.v(Constants.TAG, "null cursor in openFile");
1427         } else {
1428             try {
1429                 if (!cursor.moveToFirst()) {
1430                     Log.v(Constants.TAG, "empty cursor in openFile");
1431                 } else {
1432                     String filename = cursor.getString(0);
1433                     Log.v(Constants.TAG, "filename in openFile: " + filename);
1434                     if (new java.io.File(filename).isFile()) {
1435                         Log.v(Constants.TAG, "file exists in openFile");
1436                     }
1437                 }
1438             } finally {
1439                 cursor.close();
1440             }
1441         }
1442     }
1443 
1444     private static final void copyInteger(String key, ContentValues from, ContentValues to) {
1445         Integer i = from.getAsInteger(key);
1446         if (i != null) {
1447             to.put(key, i);
1448         }
1449     }
1450 
1451     private static final void copyBoolean(String key, ContentValues from, ContentValues to) {
1452         Boolean b = from.getAsBoolean(key);
1453         if (b != null) {
1454             to.put(key, b);
1455         }
1456     }
1457 
1458     private static final void copyString(String key, ContentValues from, ContentValues to) {
1459         String s = from.getAsString(key);
1460         if (s != null) {
1461             to.put(key, s);
1462         }
1463     }
1464 
1465     private static final void copyStringWithDefault(String key, ContentValues from,
1466             ContentValues to, String defaultValue) {
1467         copyString(key, from, to);
1468         if (!to.containsKey(key)) {
1469             to.put(key, defaultValue);
1470         }
1471     }
1472 
1473     private void grantAllDownloadsPermission(long id, int uid) {
1474         final String[] packageNames = getContext().getPackageManager().getPackagesForUid(uid);
1475         if (packageNames == null || packageNames.length == 0) return;
1476 
1477         // We only need to grant to the first package, since the
1478         // platform internally tracks based on UIDs
1479         final Uri uri = ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id);
1480         getContext().grantUriPermission(packageNames[0], uri,
1481                 Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
1482     }
1483 
1484     private void revokeAllDownloadsPermission(long id) {
1485         final Uri uri = ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id);
1486         getContext().revokeUriPermission(uri, ~0);
1487     }
1488 }
1489