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