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