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