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