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