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