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