1/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.providers.downloads;
18
19import android.app.DownloadManager;
20import android.app.DownloadManager.Request;
21import android.content.ContentProvider;
22import android.content.ContentUris;
23import android.content.ContentValues;
24import android.content.Context;
25import android.content.Intent;
26import android.content.UriMatcher;
27import android.content.pm.ApplicationInfo;
28import android.content.pm.PackageManager;
29import android.content.pm.PackageManager.NameNotFoundException;
30import android.database.Cursor;
31import android.database.DatabaseUtils;
32import android.database.SQLException;
33import android.database.sqlite.SQLiteDatabase;
34import android.database.sqlite.SQLiteOpenHelper;
35import android.net.Uri;
36import android.os.Binder;
37import android.os.Environment;
38import android.os.Handler;
39import android.os.ParcelFileDescriptor;
40import android.os.ParcelFileDescriptor.OnCloseListener;
41import android.os.Process;
42import android.provider.BaseColumns;
43import android.provider.Downloads;
44import android.provider.OpenableColumns;
45import android.text.TextUtils;
46import android.text.format.DateUtils;
47import android.util.Log;
48
49import com.android.internal.util.IndentingPrintWriter;
50import com.google.android.collect.Maps;
51import com.google.common.annotations.VisibleForTesting;
52
53import libcore.io.IoUtils;
54
55import java.io.File;
56import java.io.FileDescriptor;
57import java.io.FileNotFoundException;
58import java.io.IOException;
59import java.io.PrintWriter;
60import java.util.ArrayList;
61import java.util.Arrays;
62import java.util.HashMap;
63import java.util.HashSet;
64import java.util.Iterator;
65import java.util.List;
66import java.util.Map;
67
68/**
69 * Allows application to interact with the download manager.
70 */
71public 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