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