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