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