package com.android.providers.downloads;
import android.app.DownloadManager;
+import android.app.DownloadManager.Request;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.net.Uri;
import android.os.Binder;
import android.os.Environment;
+import android.os.Handler;
import android.os.ParcelFileDescriptor;
+import android.os.ParcelFileDescriptor.OnCloseListener;
import android.os.Process;
+import android.provider.BaseColumns;
import android.provider.Downloads;
+import android.provider.OpenableColumns;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
import android.util.Log;
+import com.android.internal.util.IndentingPrintWriter;
+import com.google.android.collect.Maps;
import com.google.common.annotations.VisibleForTesting;
+import libcore.io.IoUtils;
+
import java.io.File;
+import java.io.FileDescriptor;
import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
-
/**
* Allows application to interact with the download manager.
*/
/** Database filename */
private static final String DB_NAME = "downloads.db";
/** Current database version */
- private static final int DB_VERSION = 107;
+ private static final int DB_VERSION = 109;
/** Name of table in the database */
private static final String DB_TABLE = "downloads";
Downloads.Impl.COLUMN_FILE_NAME_HINT,
Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
Downloads.Impl.COLUMN_DELETED,
+ OpenableColumns.DISPLAY_NAME,
+ OpenableColumns.SIZE,
};
- private static HashSet<String> sAppReadableColumnsSet;
+ private static final HashSet<String> sAppReadableColumnsSet;
+ private static final HashMap<String, String> sColumnsMap;
+
static {
sAppReadableColumnsSet = new HashSet<String>();
for (int i = 0; i < sAppReadableColumnsArray.length; ++i) {
sAppReadableColumnsSet.add(sAppReadableColumnsArray[i]);
}
+
+ sColumnsMap = Maps.newHashMap();
+ sColumnsMap.put(OpenableColumns.DISPLAY_NAME,
+ Downloads.Impl.COLUMN_TITLE + " AS " + OpenableColumns.DISPLAY_NAME);
+ sColumnsMap.put(OpenableColumns.SIZE,
+ Downloads.Impl.COLUMN_TOTAL_BYTES + " AS " + OpenableColumns.SIZE);
}
private static final List<String> downloadManagerColumnsList =
Arrays.asList(DownloadManager.UNDERLYING_COLUMNS);
- // TODO is there a better way to get this package name
- private static final Object GSF_PACKAGE_NAME = "com.google.android.gsf";
+ private Handler mHandler;
/** The database that lies underneath this content provider */
private SQLiteOpenHelper mOpenHelper = null;
/** List of uids that can access the downloads */
private int mSystemUid = -1;
private int mDefContainerUid = -1;
- private File mDownloadsDataDir;
@VisibleForTesting
SystemFacade mSystemFacade;
addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ERROR_MSG, "TEXT");
break;
+ case 108:
+ addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ALLOW_METERED,
+ "INTEGER NOT NULL DEFAULT 1");
+ break;
+
+ case 109:
+ addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ALLOW_WRITE,
+ "BOOLEAN NOT NULL DEFAULT 0");
+ break;
+
default:
throw new IllegalStateException("Don't know how to upgrade to " + version);
}
Downloads.Impl.COLUMN_VISIBILITY + " INTEGER, " +
Downloads.Impl.COLUMN_CONTROL + " INTEGER, " +
Downloads.Impl.COLUMN_STATUS + " INTEGER, " +
- Constants.FAILED_CONNECTIONS + " INTEGER, " +
+ Downloads.Impl.COLUMN_FAILED_CONNECTIONS + " INTEGER, " +
Downloads.Impl.COLUMN_LAST_MODIFICATION + " BIGINT, " +
Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE + " TEXT, " +
Downloads.Impl.COLUMN_NOTIFICATION_CLASS + " TEXT, " +
mSystemFacade = new RealSystemFacade(getContext());
}
+ mHandler = new Handler();
+
mOpenHelper = new DatabaseHelper(getContext());
// Initialize the system uid
mSystemUid = Process.SYSTEM_UID;
appInfo = getContext().getPackageManager().
getApplicationInfo("com.android.defcontainer", 0);
} catch (NameNotFoundException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
+ Log.wtf(Constants.TAG, "Could not get ApplicationInfo for com.android.defconatiner", e);
}
if (appInfo != null) {
mDefContainerUid = appInfo.uid;
// saves us by getting some initialization code in DownloadService out of the way.
Context context = getContext();
context.startService(new Intent(context, DownloadService.class));
- mDownloadsDataDir = StorageManager.getInstance(getContext()).getDownloadDataDirectory();
return true;
}
public String getType(final Uri uri) {
int match = sURIMatcher.match(uri);
switch (match) {
- case MY_DOWNLOADS: {
+ case MY_DOWNLOADS:
+ case ALL_DOWNLOADS: {
return DOWNLOAD_LIST_TYPE;
}
- case MY_DOWNLOADS_ID: {
- return DOWNLOAD_TYPE;
- }
+ case MY_DOWNLOADS_ID:
+ case ALL_DOWNLOADS_ID:
case PUBLIC_DOWNLOAD_ID: {
// return the mimetype of this id from the database
final String id = getDownloadIdFromUri(uri);
final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
- return DatabaseUtils.stringForQuery(db,
+ final String mimeType = DatabaseUtils.stringForQuery(db,
"SELECT " + Downloads.Impl.COLUMN_MIME_TYPE + " FROM " + DB_TABLE +
" WHERE " + Downloads.Impl._ID + " = ?",
new String[]{id});
+ if (TextUtils.isEmpty(mimeType)) {
+ return DOWNLOAD_TYPE;
+ } else {
+ return mimeType;
+ }
}
default: {
if (Constants.LOGV) {
// validate the destination column
Integer dest = values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION);
if (dest != null) {
- if (getContext().checkCallingPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED)
+ if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED)
!= PackageManager.PERMISSION_GRANTED
&& (dest == Downloads.Impl.DESTINATION_CACHE_PARTITION
|| dest == Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING
// for public API behavior, if an app has CACHE_NON_PURGEABLE permission, automatically
// switch to non-purgeable download
boolean hasNonPurgeablePermission =
- getContext().checkCallingPermission(
+ getContext().checkCallingOrSelfPermission(
Downloads.Impl.PERMISSION_CACHE_NON_PURGEABLE)
== PackageManager.PERMISSION_GRANTED;
if (isPublicApi && dest == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE
/*
* requests coming from
- * DownloadManager.completedDownload(String, String, String, boolean, String,
- * String, long) need special treatment
+ * DownloadManager.addCompletedDownload(String, String, String,
+ * boolean, String, String, long) need special treatment
*/
if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) ==
Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
filteredValues.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0);
copyInteger(Downloads.Impl.COLUMN_MEDIA_SCANNED, values, filteredValues);
copyString(Downloads.Impl._DATA, values, filteredValues);
+ copyBoolean(Downloads.Impl.COLUMN_ALLOW_WRITE, values, filteredValues);
} else {
filteredValues.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING);
filteredValues.put(Downloads.Impl.COLUMN_TOTAL_BYTES, -1);
}
// set lastupdate to current time
- filteredValues.put(Downloads.Impl.COLUMN_LAST_MODIFICATION,
- mSystemFacade.currentTimeMillis());
+ long lastMod = mSystemFacade.currentTimeMillis();
+ filteredValues.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, lastMod);
// use packagename of the caller to set the notification columns
String pckg = values.getAsString(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE);
copyString(Downloads.Impl.COLUMN_REFERER, values, filteredValues);
// UID, PID columns
- if (getContext().checkCallingPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED)
+ if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED)
== PackageManager.PERMISSION_GRANTED) {
copyInteger(Downloads.Impl.COLUMN_OTHER_UID, values, filteredValues);
}
if (isPublicApi) {
copyInteger(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES, values, filteredValues);
copyBoolean(Downloads.Impl.COLUMN_ALLOW_ROAMING, values, filteredValues);
- }
-
- // TODO: replace this hack with something cleaner
- if (pckg.equals(GSF_PACKAGE_NAME) &&
- (getContext().checkCallingPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED)
- == PackageManager.PERMISSION_GRANTED)) {
- filteredValues.put(Constants.OTA_UPDATE, Boolean.TRUE);
+ copyBoolean(Downloads.Impl.COLUMN_ALLOW_METERED, values, filteredValues);
}
if (Constants.LOGVV) {
}
}
- Context context = getContext();
- context.startService(new Intent(context, DownloadService.class));
-
long rowID = db.insert(DB_TABLE, null, filteredValues);
if (rowID == -1) {
Log.d(Constants.TAG, "couldn't insert into downloads database");
}
insertRequestHeaders(db, rowID, values);
- context.startService(new Intent(context, DownloadService.class));
notifyContentChanged(uri, match);
+
+ // Always start service to handle notifications and/or scanning
+ final Context context = getContext();
+ context.startService(new Intent(context, DownloadService.class));
+
return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID);
}
if (scheme == null || !scheme.equals("file")) {
throw new IllegalArgumentException("Not a file URI: " + uri);
}
- String path = uri.getPath();
+ final String path = uri.getPath();
if (path == null) {
throw new IllegalArgumentException("Invalid file URI: " + uri);
}
- String externalPath = Environment.getExternalStorageDirectory().getAbsolutePath();
- if (!path.startsWith(externalPath)) {
- throw new SecurityException("Destination must be on external storage: " + uri);
+ try {
+ final String canonicalPath = new File(path).getCanonicalPath();
+ final String externalPath = Environment.getExternalStorageDirectory().getAbsolutePath();
+ if (!canonicalPath.startsWith(externalPath)) {
+ throw new SecurityException("Destination must be on external storage: " + uri);
+ }
+ } catch (IOException e) {
+ throw new SecurityException("Problem resolving path: " + uri);
}
}
if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) ==
Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
/* this row is inserted by
- * DownloadManager.completedDownload(String, String, String, boolean, String,
- * String, long)
+ * DownloadManager.addCompletedDownload(String, String, String,
+ * boolean, String, String, long)
*/
values.remove(Downloads.Impl.COLUMN_TOTAL_BYTES);
values.remove(Downloads.Impl._DATA);
if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_NO_NOTIFICATION)
== PackageManager.PERMISSION_GRANTED) {
enforceAllowedValues(values, Downloads.Impl.COLUMN_VISIBILITY,
- Downloads.Impl.VISIBILITY_HIDDEN, Downloads.Impl.VISIBILITY_VISIBLE);
+ Request.VISIBILITY_HIDDEN,
+ Request.VISIBILITY_VISIBLE,
+ Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED,
+ Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION);
} else {
enforceAllowedValues(values, Downloads.Impl.COLUMN_VISIBILITY,
- Downloads.Impl.VISIBILITY_VISIBLE);
+ Request.VISIBILITY_VISIBLE,
+ Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED,
+ Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION);
}
// remove the rest of the columns that are allowed (with any value)
values.remove(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE); // checked later in insert()
values.remove(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES);
values.remove(Downloads.Impl.COLUMN_ALLOW_ROAMING);
+ values.remove(Downloads.Impl.COLUMN_ALLOW_METERED);
values.remove(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI);
values.remove(Downloads.Impl.COLUMN_MEDIA_SCANNED);
-
+ values.remove(Downloads.Impl.COLUMN_ALLOW_WRITE);
Iterator<Map.Entry<String, Object>> iterator = values.valueSet().iterator();
while (iterator.hasNext()) {
String key = iterator.next().getKey();
if (shouldRestrictVisibility()) {
if (projection == null) {
- projection = sAppReadableColumnsArray;
+ projection = sAppReadableColumnsArray.clone();
} else {
// check the validity of the columns in projection
for (int i = 0; i < projection.length; ++i) {
}
}
}
+
+ for (int i = 0; i < projection.length; i++) {
+ final String newColumn = sColumnsMap.get(projection[i]);
+ if (newColumn != null) {
+ projection[i] = newColumn;
+ }
+ }
}
if (Constants.LOGVV) {
int callingUid = Binder.getCallingUid();
return Binder.getCallingPid() != Process.myPid() &&
callingUid != mSystemUid &&
- callingUid != mDefContainerUid &&
- Process.supportsProcesses();
+ callingUid != mDefContainerUid;
}
/**
filteredValues = values;
String filename = values.getAsString(Downloads.Impl._DATA);
if (filename != null) {
- Cursor c = query(uri, new String[]
- { Downloads.Impl.COLUMN_TITLE }, null, null, null);
- if (!c.moveToFirst() || c.getString(0).isEmpty()) {
- values.put(Downloads.Impl.COLUMN_TITLE, new File(filename).getName());
+ Cursor c = null;
+ try {
+ c = query(uri, new String[]
+ { Downloads.Impl.COLUMN_TITLE }, null, null, null);
+ if (!c.moveToFirst() || c.getString(0).isEmpty()) {
+ values.put(Downloads.Impl.COLUMN_TITLE, new File(filename).getName());
+ }
+ } finally {
+ IoUtils.closeQuietly(c);
}
- c.close();
}
Integer status = values.getAsInteger(Downloads.Impl.COLUMN_STATUS);
selection.appendClause(Downloads.Impl._ID + " = ?", getDownloadIdFromUri(uri));
}
if ((uriMatch == MY_DOWNLOADS || uriMatch == MY_DOWNLOADS_ID)
- && getContext().checkCallingPermission(Downloads.Impl.PERMISSION_ACCESS_ALL)
+ && getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS_ALL)
!= PackageManager.PERMISSION_GRANTED) {
selection.appendClause(
Constants.UID + "= ? OR " + Downloads.Impl.COLUMN_OTHER_UID + "= ?",
- Binder.getCallingUid(), Binder.getCallingPid());
+ Binder.getCallingUid(), Binder.getCallingUid());
}
return selection;
}
public int delete(final Uri uri, final String where,
final String[] whereArgs) {
- Helpers.validateSelection(where, sAppReadableColumnsSet);
+ if (shouldRestrictVisibility()) {
+ Helpers.validateSelection(where, sAppReadableColumnsSet);
+ }
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
int count;
case ALL_DOWNLOADS_ID:
SqlSelection selection = getWhereClause(uri, where, whereArgs, match);
deleteRequestHeaders(db, selection.getSelection(), selection.getParameters());
+
+ final Cursor cursor = db.query(DB_TABLE, new String[] {
+ Downloads.Impl._ID }, selection.getSelection(), selection.getParameters(),
+ null, null, null);
+ try {
+ while (cursor.moveToNext()) {
+ final long id = cursor.getLong(0);
+ DownloadStorageProvider.onDownloadProviderDelete(getContext(), id);
+ }
+ } finally {
+ IoUtils.closeQuietly(cursor);
+ }
+
count = db.delete(DB_TABLE, selection.getSelection(), selection.getParameters());
break;
* Remotely opens a file
*/
@Override
- public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
+ public ParcelFileDescriptor openFile(final Uri uri, String mode) throws FileNotFoundException {
if (Constants.LOGVV) {
logVerboseOpenFileInfo(uri, mode);
}
- Cursor cursor = query(uri, new String[] {"_data"}, null, null, null);
+ final Cursor cursor = query(uri, new String[] { Downloads.Impl._DATA }, null, null, null);
String path;
try {
int count = (cursor != null) ? cursor.getCount() : 0;
cursor.moveToFirst();
path = cursor.getString(0);
} finally {
- if (cursor != null) {
- cursor.close();
- }
+ IoUtils.closeQuietly(cursor);
}
if (path == null) {
throw new FileNotFoundException("No filename found.");
}
- if (!Helpers.isFilenameValid(path, mDownloadsDataDir)) {
- throw new FileNotFoundException("Invalid filename: " + path);
+
+ final File file = new File(path);
+ if (!Helpers.isFilenameValid(getContext(), file)) {
+ throw new FileNotFoundException("Invalid file: " + file);
}
- if (!"r".equals(mode)) {
- throw new FileNotFoundException("Bad mode for " + uri + ": " + mode);
+
+ if ("r".equals(mode)) {
+ return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
+ } else {
+ try {
+ // When finished writing, update size and timestamp
+ return ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode(mode),
+ mHandler, new OnCloseListener() {
+ @Override
+ public void onClose(IOException e) {
+ final ContentValues values = new ContentValues();
+ values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, file.length());
+ values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION,
+ System.currentTimeMillis());
+ update(uri, values, null, null);
+ }
+ });
+ } catch (IOException e) {
+ throw new FileNotFoundException("Failed to open for writing: " + e);
+ }
}
+ }
- ParcelFileDescriptor ret = ParcelFileDescriptor.open(new File(path),
- ParcelFileDescriptor.MODE_READ_ONLY);
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ", 120);
- if (ret == null) {
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "couldn't open file");
+ pw.println("Downloads updated in last hour:");
+ pw.increaseIndent();
+
+ final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+ final long modifiedAfter = mSystemFacade.currentTimeMillis() - DateUtils.HOUR_IN_MILLIS;
+ final Cursor cursor = db.query(DB_TABLE, null,
+ Downloads.Impl.COLUMN_LAST_MODIFICATION + ">" + modifiedAfter, null, null, null,
+ Downloads.Impl._ID + " ASC");
+ try {
+ final String[] cols = cursor.getColumnNames();
+ final int idCol = cursor.getColumnIndex(BaseColumns._ID);
+ while (cursor.moveToNext()) {
+ pw.println("Download #" + cursor.getInt(idCol) + ":");
+ pw.increaseIndent();
+ for (int i = 0; i < cols.length; i++) {
+ // Omit sensitive data when dumping
+ if (Downloads.Impl.COLUMN_COOKIE_DATA.equals(cols[i])) {
+ continue;
+ }
+ pw.printPair(cols[i], cursor.getString(i));
+ }
+ pw.println();
+ pw.decreaseIndent();
}
- throw new FileNotFoundException("couldn't open file");
+ } finally {
+ cursor.close();
}
- return ret;
+
+ pw.decreaseIndent();
}
private void logVerboseOpenFileInfo(Uri uri, String mode) {
if (cursor == null) {
Log.v(Constants.TAG, "null cursor in openFile");
} else {
- if (!cursor.moveToFirst()) {
- Log.v(Constants.TAG, "empty cursor in openFile");
- } else {
- do {
- Log.v(Constants.TAG, "row " + cursor.getInt(0) + " available");
- } while(cursor.moveToNext());
+ try {
+ if (!cursor.moveToFirst()) {
+ Log.v(Constants.TAG, "empty cursor in openFile");
+ } else {
+ do {
+ Log.v(Constants.TAG, "row " + cursor.getInt(0) + " available");
+ } while(cursor.moveToNext());
+ }
+ } finally {
+ cursor.close();
}
- cursor.close();
}
cursor = query(uri, new String[] { "_data" }, null, null, null);
if (cursor == null) {
Log.v(Constants.TAG, "null cursor in openFile");
} else {
- if (!cursor.moveToFirst()) {
- Log.v(Constants.TAG, "empty cursor in openFile");
- } else {
- String filename = cursor.getString(0);
- Log.v(Constants.TAG, "filename in openFile: " + filename);
- if (new java.io.File(filename).isFile()) {
- Log.v(Constants.TAG, "file exists in openFile");
+ try {
+ if (!cursor.moveToFirst()) {
+ Log.v(Constants.TAG, "empty cursor in openFile");
+ } else {
+ String filename = cursor.getString(0);
+ Log.v(Constants.TAG, "filename in openFile: " + filename);
+ if (new java.io.File(filename).isFile()) {
+ Log.v(Constants.TAG, "file exists in openFile");
+ }
}
+ } finally {
+ cursor.close();
}
- cursor.close();
}
}