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