summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AndroidManifest.xml20
-rw-r--r--res/values/strings.xml11
-rw-r--r--src/com/android/providers/downloads/DownloadInfo.java21
-rw-r--r--src/com/android/providers/downloads/DownloadNotification.java5
-rw-r--r--src/com/android/providers/downloads/DownloadProvider.java371
-rw-r--r--src/com/android/providers/downloads/DownloadService.java38
-rw-r--r--src/com/android/providers/downloads/DownloadThread.java27
-rw-r--r--src/com/android/providers/downloads/Helpers.java5
-rw-r--r--tests/src/com/android/providers/downloads/AbstractDownloadManagerFunctionalTest.java3
-rw-r--r--tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java16
-rw-r--r--ui/AndroidManifest.xml2
-rw-r--r--ui/src/com/android/providers/downloads/ui/DownloadList.java36
12 files changed, 307 insertions, 248 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 8431d1e..9da6fc8 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -33,6 +33,14 @@
android:description="@string/permdesc_downloadWithoutNotification"
android:protectionLevel="signatureOrSystem"/>
+ <!-- Allows an app to access all downloads in the system via the /all_downloads/ URIs. The
+ protection level could be relaxed in the future to support third-party download
+ managers. -->
+ <permission android:name="android.permission.ACCESS_ALL_DOWNLOADS"
+ android:label="@string/permlab_accessAllDownloads"
+ android:description="@string/permdesc_accessAllDownloads"
+ android:protectionLevel="signature"/>
+
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_DOWNLOAD_MANAGER" />
<uses-permission android:name="android.permission.ACCESS_DRM" />
@@ -42,11 +50,21 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INSTALL_DRM" />
+ <uses-permission android:name="android.permission.ACCESS_ALL_DOWNLOADS" />
<application android:process="android.process.media"
android:label="@string/app_label">
<provider android:name=".DownloadProvider"
- android:authorities="downloads" />
+ android:authorities="downloads"
+ android:permission="android.permission.ACCESS_ALL_DOWNLOADS">
+ <!-- Anyone can access /my_downloads, the provider internally restricts access by UID for
+ these URIs -->
+ <path-permission android:pathPrefix="/my_downloads"
+ android:permission="android.permission.INTERNET"/>
+ <!-- Apps with access to /all_downloads/... can grant permissions, allowing them to share
+ downloaded files with other viewers -->
+ <grant-uri-permission android:pathPrefix="/all_downloads/"/>
+ </provider>
<service android:name=".DownloadService"
android:permission="android.permission.ACCESS_DOWNLOAD_MANAGER" />
<receiver android:name=".DownloadReceiver" android:exported="false">
diff --git a/res/values/strings.xml b/res/values/strings.xml
index b0d95ce..1623fbe 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -98,6 +98,17 @@
to download files through the download manager without any notification
being shown to the user.</string>
+ <!-- The label for the permission to access all downloads in the download
+ manager, not just those owned by the calling user [CHAR LIMIT=50] -->
+ <string name="permlab_accessAllDownloads">Access all system
+ downloads</string>
+
+ <!-- The full sentence description for the permission to access all
+ downloads in the download manager, not just those owned by the calling user
+ [CHAR LIMIT=160] -->
+ <string name="permdesc_accessAllDownloads">Allows the application to view
+ and modify all initiated by any application on the system.</string>
+
<!-- This is the title that is used when displaying the notification
for a download that doesn't have a title associated with it. -->
diff --git a/src/com/android/providers/downloads/DownloadInfo.java b/src/com/android/providers/downloads/DownloadInfo.java
index 4380059..0cf025b 100644
--- a/src/com/android/providers/downloads/DownloadInfo.java
+++ b/src/com/android/providers/downloads/DownloadInfo.java
@@ -131,9 +131,8 @@ public class DownloadInfo {
}
private void readRequestHeaders(long downloadId) {
- Uri headerUri = Downloads.Impl.CONTENT_URI.buildUpon()
- .appendPath(Long.toString(downloadId))
- .appendPath(Downloads.Impl.RequestHeaders.URI_SEGMENT).build();
+ Uri headerUri = Uri.withAppendedPath(
+ getAllDownloadsUri(), Downloads.Impl.RequestHeaders.URI_SEGMENT);
Cursor cursor = mContext.getContentResolver().query(headerUri, null, null, null, null);
try {
int headerIndex =
@@ -159,7 +158,7 @@ public class DownloadInfo {
return Collections.unmodifiableMap(mRequestHeaders);
}
- public void sendIntentIfRequested(Uri contentUri) {
+ public void sendIntentIfRequested() {
if (mPackage == null) {
return;
}
@@ -181,7 +180,7 @@ public class DownloadInfo {
// We only send the content: URI, for security reasons. Otherwise, malicious
// applications would have an easier time spoofing download results by
// sending spoofed intents.
- intent.setData(contentUri);
+ intent.setData(getMyDownloadsUri());
}
mSystemFacade.sendBroadcast(intent);
}
@@ -374,9 +373,7 @@ public class DownloadInfo {
mStatus = Impl.STATUS_RUNNING;
ContentValues values = new ContentValues();
values.put(Impl.COLUMN_STATUS, mStatus);
- mContext.getContentResolver().update(
- ContentUris.withAppendedId(Impl.CONTENT_URI, mId),
- values, null, null);
+ mContext.getContentResolver().update(getAllDownloadsUri(), values, null, null);
}
DownloadThread downloader = new DownloadThread(mContext, mSystemFacade, this);
mHasActiveThread = true;
@@ -388,4 +385,12 @@ public class DownloadInfo {
|| mDestination == Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING
|| mDestination == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE);
}
+
+ public Uri getMyDownloadsUri() {
+ return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, mId);
+ }
+
+ public Uri getAllDownloadsUri() {
+ return ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, mId);
+ }
}
diff --git a/src/com/android/providers/downloads/DownloadNotification.java b/src/com/android/providers/downloads/DownloadNotification.java
index 472a5f3..38def59 100644
--- a/src/com/android/providers/downloads/DownloadNotification.java
+++ b/src/com/android/providers/downloads/DownloadNotification.java
@@ -18,6 +18,7 @@ package com.android.providers.downloads;
import android.app.Notification;
import android.app.PendingIntent;
+import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
@@ -194,7 +195,7 @@ class DownloadNotification {
Intent intent = new Intent(Constants.ACTION_LIST);
intent.setClassName("com.android.providers.downloads",
DownloadReceiver.class.getName());
- intent.setData(Uri.parse(Downloads.Impl.CONTENT_URI + "/" + item.mId));
+ intent.setData(ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, item.mId));
intent.putExtra("multiple", item.mTitleCount > 1);
n.contentIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
@@ -223,7 +224,7 @@ class DownloadNotification {
title = mContext.getResources().getString(
R.string.download_unknown_title);
}
- Uri contentUri = Uri.parse(Downloads.Impl.CONTENT_URI + "/" + id);
+ Uri contentUri = ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, id);
String caption;
Intent intent;
if (Downloads.Impl.isStatusError(download.mStatus)) {
diff --git a/src/com/android/providers/downloads/DownloadProvider.java b/src/com/android/providers/downloads/DownloadProvider.java
index d957989..17f3d81 100644
--- a/src/com/android/providers/downloads/DownloadProvider.java
+++ b/src/com/android/providers/downloads/DownloadProvider.java
@@ -17,6 +17,7 @@
package com.android.providers.downloads;
import android.content.ContentProvider;
+import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
@@ -54,7 +55,6 @@ import java.util.Map;
* Allows application to interact with the download manager.
*/
public final class DownloadProvider extends ContentProvider {
-
/** Database filename */
private static final String DB_NAME = "downloads.db";
/** Current database version */
@@ -69,19 +69,35 @@ public final class DownloadProvider extends ContentProvider {
/** URI matcher used to recognize URIs sent by applications */
private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
- /** URI matcher constant for the URI of the entire download list */
- private static final int DOWNLOADS = 1;
+ /** URI matcher constant for the URI of all downloads belonging to the calling UID */
+ private static final int MY_DOWNLOADS = 1;
+ /** URI matcher constant for the URI of an individual download belonging to the calling UID */
+ private static final int MY_DOWNLOADS_ID = 2;
+ /** URI matcher constant for the URI of all downloads in the system */
+ private static final int ALL_DOWNLOADS = 3;
/** URI matcher constant for the URI of an individual download */
- private static final int DOWNLOADS_ID = 2;
+ private static final int ALL_DOWNLOADS_ID = 4;
/** URI matcher constant for the URI of a download's request headers */
- private static final int REQUEST_HEADERS_URI = 3;
+ private static final int REQUEST_HEADERS_URI = 5;
static {
- sURIMatcher.addURI("downloads", "download", DOWNLOADS);
- sURIMatcher.addURI("downloads", "download/#", DOWNLOADS_ID);
- sURIMatcher.addURI("downloads", "download/#/" + Downloads.Impl.RequestHeaders.URI_SEGMENT,
- REQUEST_HEADERS_URI);
+ sURIMatcher.addURI("downloads", "my_downloads", MY_DOWNLOADS);
+ sURIMatcher.addURI("downloads", "my_downloads/#", MY_DOWNLOADS_ID);
+ sURIMatcher.addURI("downloads", "all_downloads", ALL_DOWNLOADS);
+ sURIMatcher.addURI("downloads", "all_downloads/#", ALL_DOWNLOADS_ID);
+ sURIMatcher.addURI("downloads",
+ "my_downloads/#/" + Downloads.Impl.RequestHeaders.URI_SEGMENT,
+ REQUEST_HEADERS_URI);
+ sURIMatcher.addURI("downloads",
+ "all_downloads/#/" + Downloads.Impl.RequestHeaders.URI_SEGMENT,
+ REQUEST_HEADERS_URI);
}
+ /** Different base URIs that could be used to access an individual download */
+ private static final Uri[] BASE_URIS = new Uri[] {
+ Downloads.Impl.CONTENT_URI,
+ Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
+ };
+
private static final String[] sAppReadableColumnsArray = new String[] {
Downloads.Impl._ID,
Downloads.Impl.COLUMN_APP_DATA,
@@ -319,10 +335,10 @@ public final class DownloadProvider extends ContentProvider {
public String getType(final Uri uri) {
int match = sURIMatcher.match(uri);
switch (match) {
- case DOWNLOADS: {
+ case MY_DOWNLOADS: {
return DOWNLOAD_LIST_TYPE;
}
- case DOWNLOADS_ID: {
+ case MY_DOWNLOADS_ID: {
return DOWNLOAD_TYPE;
}
default: {
@@ -342,10 +358,10 @@ public final class DownloadProvider extends ContentProvider {
checkInsertPermissions(values);
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
- if (sURIMatcher.match(uri) != DOWNLOADS) {
- if (Config.LOGD) {
- Log.d(Constants.TAG, "calling insert on an unknown/invalid URI: " + uri);
- }
+ // note we disallow inserting into ALL_DOWNLOADS
+ int match = sURIMatcher.match(uri);
+ if (match != MY_DOWNLOADS) {
+ Log.d(Constants.TAG, "calling insert on an unknown/invalid URI: " + uri);
throw new IllegalArgumentException("Unknown/Invalid URI " + uri);
}
@@ -463,21 +479,15 @@ public final class DownloadProvider extends ContentProvider {
context.startService(new Intent(context, DownloadService.class));
long rowID = db.insert(DB_TABLE, null, filteredValues);
- insertRequestHeaders(db, rowID, values);
-
- Uri ret = null;
-
- if (rowID != -1) {
- context.startService(new Intent(context, DownloadService.class));
- ret = Uri.parse(Downloads.Impl.CONTENT_URI + "/" + rowID);
- context.getContentResolver().notifyChange(uri, null);
- } else {
- if (Config.LOGD) {
- Log.d(Constants.TAG, "couldn't insert into downloads database");
- }
+ if (rowID == -1) {
+ Log.d(Constants.TAG, "couldn't insert into downloads database");
+ return null;
}
- return ret;
+ insertRequestHeaders(db, rowID, values);
+ context.startService(new Intent(context, DownloadService.class));
+ notifyContentChanged(uri, match);
+ return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID);
}
/**
@@ -600,33 +610,27 @@ public final class DownloadProvider extends ContentProvider {
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ qb.setTables(DB_TABLE);
int match = sURIMatcher.match(uri);
- boolean emptyWhere = true;
- switch (match) {
- case DOWNLOADS: {
- qb.setTables(DB_TABLE);
- break;
- }
- case DOWNLOADS_ID: {
- qb.setTables(DB_TABLE);
- qb.appendWhere(Downloads.Impl._ID + "=");
- qb.appendWhere(getDownloadIdFromUri(uri));
- emptyWhere = false;
- break;
+ if (match == -1) {
+ if (Constants.LOGV) {
+ Log.v(Constants.TAG, "querying unknown URI: " + uri);
}
- case REQUEST_HEADERS_URI:
- if (projection != null || selection != null || sort != null) {
- throw new UnsupportedOperationException("Request header queries do not support "
- + "projections, selections or sorting");
- }
- return queryRequestHeaders(db, uri);
- default: {
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "querying unknown URI: " + uri);
- }
- throw new IllegalArgumentException("Unknown URI: " + uri);
+ throw new IllegalArgumentException("Unknown URI: " + uri);
+ }
+
+ if (match == REQUEST_HEADERS_URI) {
+ if (projection != null || selection != null || sort != null) {
+ throw new UnsupportedOperationException("Request header queries do not support "
+ + "projections, selections or sorting");
}
+ return queryRequestHeaders(db, uri);
+ }
+
+ String where = getWhereClause(uri, null, match);
+ if (!where.isEmpty()) {
+ qb.appendWhere(where);
}
if (shouldRestrictVisibility()) {
@@ -640,53 +644,10 @@ public final class DownloadProvider extends ContentProvider {
}
}
}
- if (!emptyWhere) {
- qb.appendWhere(" AND ");
- emptyWhere = false;
- }
- qb.appendWhere(getRestrictedUidClause());
}
if (Constants.LOGVV) {
- java.lang.StringBuilder sb = new java.lang.StringBuilder();
- sb.append("starting query, database is ");
- if (db != null) {
- sb.append("not ");
- }
- sb.append("null; ");
- if (projection == null) {
- sb.append("projection is null; ");
- } else if (projection.length == 0) {
- sb.append("projection is empty; ");
- } else {
- for (int i = 0; i < projection.length; ++i) {
- sb.append("projection[");
- sb.append(i);
- sb.append("] is ");
- sb.append(projection[i]);
- sb.append("; ");
- }
- }
- sb.append("selection is ");
- sb.append(selection);
- sb.append("; ");
- if (selectionArgs == null) {
- sb.append("selectionArgs is null; ");
- } else if (selectionArgs.length == 0) {
- sb.append("selectionArgs is empty; ");
- } else {
- for (int i = 0; i < selectionArgs.length; ++i) {
- sb.append("selectionArgs[");
- sb.append(i);
- sb.append("] is ");
- sb.append(selectionArgs[i]);
- sb.append("; ");
- }
- }
- sb.append("sort is ");
- sb.append(sort);
- sb.append(".");
- Log.v(Constants.TAG, sb.toString());
+ logVerboseQueryInfo(projection, selection, selectionArgs, sort, db);
}
Cursor ret = qb.query(db, projection, selection, selectionArgs,
@@ -711,6 +672,49 @@ public final class DownloadProvider extends ContentProvider {
return ret;
}
+ private void logVerboseQueryInfo(String[] projection, final String selection,
+ final String[] selectionArgs, final String sort, SQLiteDatabase db) {
+ java.lang.StringBuilder sb = new java.lang.StringBuilder();
+ sb.append("starting query, database is ");
+ if (db != null) {
+ sb.append("not ");
+ }
+ sb.append("null; ");
+ if (projection == null) {
+ sb.append("projection is null; ");
+ } else if (projection.length == 0) {
+ sb.append("projection is empty; ");
+ } else {
+ for (int i = 0; i < projection.length; ++i) {
+ sb.append("projection[");
+ sb.append(i);
+ sb.append("] is ");
+ sb.append(projection[i]);
+ sb.append("; ");
+ }
+ }
+ sb.append("selection is ");
+ sb.append(selection);
+ sb.append("; ");
+ if (selectionArgs == null) {
+ sb.append("selectionArgs is null; ");
+ } else if (selectionArgs.length == 0) {
+ sb.append("selectionArgs is empty; ");
+ } else {
+ for (int i = 0; i < selectionArgs.length; ++i) {
+ sb.append("selectionArgs[");
+ sb.append(i);
+ sb.append("] is ");
+ sb.append(selectionArgs[i]);
+ sb.append("; ");
+ }
+ }
+ sb.append("sort is ");
+ sb.append(sort);
+ sb.append(".");
+ Log.v(Constants.TAG, sb.toString());
+ }
+
private String getDownloadIdFromUri(final Uri uri) {
return uri.getPathSegments().get(1);
}
@@ -767,7 +771,7 @@ public final class DownloadProvider extends ContentProvider {
}
/**
- * @return true if we should restrict this caller to viewing only its own downloads
+ * @return true if we should restrict the columns readable by this caller
*/
private boolean shouldRestrictVisibility() {
int callingUid = Binder.getCallingUid();
@@ -831,26 +835,27 @@ public final class DownloadProvider extends ContentProvider {
startService = true;
}
}
+
int match = sURIMatcher.match(uri);
switch (match) {
- case DOWNLOADS:
- case DOWNLOADS_ID: {
- String fullWhere = getWhereClause(uri, where);
+ case MY_DOWNLOADS:
+ case MY_DOWNLOADS_ID:
+ case ALL_DOWNLOADS:
+ case ALL_DOWNLOADS_ID:
+ String fullWhere = getWhereClause(uri, where, match);
if (filteredValues.size() > 0) {
count = db.update(DB_TABLE, filteredValues, fullWhere, whereArgs);
} else {
count = 0;
}
break;
- }
- default: {
- if (Config.LOGD) {
- Log.d(Constants.TAG, "updating unknown/invalid URI: " + uri);
- }
+
+ default:
+ Log.d(Constants.TAG, "updating unknown/invalid URI: " + uri);
throw new UnsupportedOperationException("Cannot update URI: " + uri);
- }
}
- getContext().getContentResolver().notifyChange(uri, null);
+
+ notifyContentChanged(uri, match);
if (startService) {
Context context = getContext();
context.startService(new Intent(context, DownloadService.class));
@@ -858,17 +863,34 @@ public final class DownloadProvider extends ContentProvider {
return count;
}
- private String getWhereClause(final Uri uri, final String where) {
+ /**
+ * Notify of a change through both URIs (/my_downloads and /all_downloads)
+ * @param uri either URI for the changed download(s)
+ * @param uriMatch the match ID from {@link #sURIMatcher}
+ */
+ private void notifyContentChanged(final Uri uri, int uriMatch) {
+ Long downloadId = null;
+ if (uriMatch == MY_DOWNLOADS_ID || uriMatch == ALL_DOWNLOADS_ID) {
+ downloadId = Long.parseLong(getDownloadIdFromUri(uri));
+ }
+ for (Uri uriToNotify : BASE_URIS) {
+ if (downloadId != null) {
+ uriToNotify = ContentUris.withAppendedId(uriToNotify, downloadId);
+ }
+ getContext().getContentResolver().notifyChange(uriToNotify, null);
+ }
+ }
+
+ private String getWhereClause(final Uri uri, final String where, int uriMatch) {
StringBuilder myWhere = new StringBuilder();
if (where != null) {
myWhere.append("( " + where + " )");
}
- if (sURIMatcher.match(uri) == DOWNLOADS_ID) {
- String segment = getDownloadIdFromUri(uri);
- long rowId = Long.parseLong(segment);
- appendClause(myWhere, " ( " + Downloads.Impl._ID + " = " + rowId + " ) ");
+ if (uriMatch == MY_DOWNLOADS_ID || uriMatch == ALL_DOWNLOADS_ID) {
+ appendClause(myWhere,
+ " ( " + Downloads.Impl._ID + " = " + getDownloadIdFromUri(uri) + " ) ");
}
- if (shouldRestrictVisibility()) {
+ if (uriMatch == MY_DOWNLOADS || uriMatch == MY_DOWNLOADS_ID) {
appendClause(myWhere, getRestrictedUidClause());
}
return myWhere.toString();
@@ -887,21 +909,20 @@ public final class DownloadProvider extends ContentProvider {
int count;
int match = sURIMatcher.match(uri);
switch (match) {
- case DOWNLOADS:
- case DOWNLOADS_ID: {
- String fullWhere = getWhereClause(uri, where);
+ case MY_DOWNLOADS:
+ case MY_DOWNLOADS_ID:
+ case ALL_DOWNLOADS:
+ case ALL_DOWNLOADS_ID:
+ String fullWhere = getWhereClause(uri, where, match);
deleteRequestHeaders(db, fullWhere, whereArgs);
count = db.delete(DB_TABLE, fullWhere, whereArgs);
break;
- }
- default: {
- if (Config.LOGD) {
- Log.d(Constants.TAG, "deleting unknown/invalid URI: " + uri);
- }
+
+ default:
+ Log.d(Constants.TAG, "deleting unknown/invalid URI: " + uri);
throw new UnsupportedOperationException("Cannot delete URI: " + uri);
- }
}
- getContext().getContentResolver().notifyChange(uri, null);
+ notifyContentChanged(uri, match);
return count;
}
@@ -916,71 +937,41 @@ public final class DownloadProvider extends ContentProvider {
* Remotely opens a file
*/
@Override
- public ParcelFileDescriptor openFile(Uri uri, String mode)
- throws FileNotFoundException {
+ public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
if (Constants.LOGVV) {
- Log.v(Constants.TAG, "openFile uri: " + uri + ", mode: " + mode
- + ", uid: " + Binder.getCallingUid());
- Cursor cursor = query(Downloads.Impl.CONTENT_URI,
- new String[] { "_id" }, null, null, "_id");
- 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());
- }
- 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");
- }
- }
- cursor.close();
- }
+ logVerboseOpenFileInfo(uri, mode);
}
- // This logic is mostly copied form openFileHelper. If openFileHelper eventually
- // gets split into small bits (to extract the filename and the modebits),
- // this code could use the separate bits and be deeply simplified.
- Cursor c = query(uri, new String[]{"_data"}, null, null, null);
- int count = (c != null) ? c.getCount() : 0;
- if (count != 1) {
- // If there is not exactly one result, throw an appropriate exception.
- if (c != null) {
- c.close();
+ Cursor cursor = query(uri, new String[] {"_data"}, null, null, null);
+ String path;
+ try {
+ int count = (cursor != null) ? cursor.getCount() : 0;
+ if (count != 1) {
+ // If there is not exactly one result, throw an appropriate exception.
+ if (count == 0) {
+ throw new FileNotFoundException("No entry for " + uri);
+ }
+ throw new FileNotFoundException("Multiple items at " + uri);
}
- if (count == 0) {
- throw new FileNotFoundException("No entry for " + uri);
+
+ cursor.moveToFirst();
+ path = cursor.getString(0);
+ } finally {
+ if (cursor != null) {
+ cursor.close();
}
- throw new FileNotFoundException("Multiple items at " + uri);
}
- c.moveToFirst();
- String path = c.getString(0);
- c.close();
if (path == null) {
throw new FileNotFoundException("No filename found.");
}
if (!Helpers.isFilenameValid(path)) {
throw new FileNotFoundException("Invalid filename.");
}
-
if (!"r".equals(mode)) {
throw new FileNotFoundException("Bad mode for " + uri + ": " + mode);
}
+
ParcelFileDescriptor ret = ParcelFileDescriptor.open(new File(path),
ParcelFileDescriptor.MODE_READ_ONLY);
@@ -989,14 +980,44 @@ public final class DownloadProvider extends ContentProvider {
Log.v(Constants.TAG, "couldn't open file");
}
throw new FileNotFoundException("couldn't open file");
- } else {
- ContentValues values = new ContentValues();
- values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
- update(uri, values, null, null);
}
return ret;
}
+ private void logVerboseOpenFileInfo(Uri uri, String mode) {
+ Log.v(Constants.TAG, "openFile uri: " + uri + ", mode: " + mode
+ + ", uid: " + Binder.getCallingUid());
+ Cursor cursor = query(Downloads.Impl.CONTENT_URI,
+ new String[] { "_id" }, null, null, "_id");
+ 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());
+ }
+ 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");
+ }
+ }
+ cursor.close();
+ }
+ }
+
private static final void copyInteger(String key, ContentValues from, ContentValues to) {
Integer i = from.getAsInteger(key);
if (i != null) {
diff --git a/src/com/android/providers/downloads/DownloadService.java b/src/com/android/providers/downloads/DownloadService.java
index 6d9ee22..b85fb90 100644
--- a/src/com/android/providers/downloads/DownloadService.java
+++ b/src/com/android/providers/downloads/DownloadService.java
@@ -211,7 +211,7 @@ public class DownloadService extends Service {
mDownloads = Lists.newArrayList();
mObserver = new DownloadManagerContentObserver();
- getContentResolver().registerContentObserver(Downloads.Impl.CONTENT_URI,
+ getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
true, mObserver);
mMediaScannerService = null;
@@ -313,7 +313,7 @@ public class DownloadService extends Service {
}
long now = mSystemFacade.currentTimeMillis();
- Cursor cursor = getContentResolver().query(Downloads.Impl.CONTENT_URI,
+ Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
null, null, null, Downloads.Impl._ID);
if (cursor == null) {
@@ -482,7 +482,7 @@ public class DownloadService extends Service {
// when running the simulator).
return;
}
- HashSet<String> fileSet = new HashSet();
+ HashSet<String> fileSet = new HashSet<String>();
for (int i = 0; i < files.length; i++) {
if (files[i].getName().equals(Constants.KNOWN_SPURIOUS_FILENAME)) {
continue;
@@ -493,7 +493,7 @@ public class DownloadService extends Service {
fileSet.add(files[i].getPath());
}
- Cursor cursor = getContentResolver().query(Downloads.Impl.CONTENT_URI,
+ Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
new String[] { Downloads.Impl._DATA }, null, null, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
@@ -517,7 +517,7 @@ public class DownloadService extends Service {
* Drops old rows from the database to prevent it from growing too large
*/
private void trimDatabase() {
- Cursor cursor = getContentResolver().query(Downloads.Impl.CONTENT_URI,
+ Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
new String[] { Downloads.Impl._ID },
Downloads.Impl.COLUMN_STATUS + " >= '200'", null,
Downloads.Impl.COLUMN_LAST_MODIFICATION);
@@ -530,9 +530,9 @@ public class DownloadService extends Service {
int numDelete = cursor.getCount() - Constants.MAX_DOWNLOADS;
int columnId = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
while (numDelete > 0) {
- getContentResolver().delete(
- ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI,
- cursor.getLong(columnId)), null, null);
+ Uri downloadUri = ContentUris.withAppendedId(
+ Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, cursor.getLong(columnId));
+ getContentResolver().delete(downloadUri, null, null);
if (!cursor.moveToNext()) {
break;
}
@@ -601,16 +601,13 @@ public class DownloadService extends Service {
//Log.i(Constants.TAG, "*** QUERY " + mimetypeIntent + ": " + list);
if (ri == null) {
- if (Config.LOGD) {
- Log.d(Constants.TAG, "no application to handle MIME type " + info.mMimeType);
- }
+ Log.d(Constants.TAG, "no application to handle MIME type " + info.mMimeType);
info.mStatus = Downloads.Impl.STATUS_NOT_ACCEPTABLE;
- Uri uri = ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, info.mId);
ContentValues values = new ContentValues();
values.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_NOT_ACCEPTABLE);
- getContentResolver().update(uri, values, null, null);
- info.sendIntentIfRequested(uri);
+ getContentResolver().update(info.getAllDownloadsUri(), values, null, null);
+ info.sendIntentIfRequested();
return;
}
}
@@ -624,7 +621,7 @@ public class DownloadService extends Service {
* Updates the local copy of the info about a download.
*/
private void updateDownload(Cursor cursor, int arrayPos, long now) {
- DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
+ DownloadInfo info = mDownloads.get(arrayPos);
int statusColumn = cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_STATUS);
int failedColumn = cursor.getColumnIndexOrThrow(Constants.FAILED_CONNECTIONS);
info.mId = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl._ID));
@@ -785,7 +782,7 @@ public class DownloadService extends Service {
* Returns true if the file has been properly scanned.
*/
private boolean scanFile(Cursor cursor, int arrayPos) {
- DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
+ DownloadInfo info = mDownloads.get(arrayPos);
synchronized (this) {
if (mMediaScannerService != null) {
try {
@@ -796,16 +793,11 @@ public class DownloadService extends Service {
if (cursor != null) {
ContentValues values = new ContentValues();
values.put(Constants.MEDIA_SCANNED, 1);
- getContentResolver().update(ContentUris.withAppendedId(
- Downloads.Impl.CONTENT_URI, cursor.getLong(
- cursor.getColumnIndexOrThrow(Downloads.Impl._ID))),
- values, null, null);
+ getContentResolver().update(info.getAllDownloadsUri(), values, null, null);
}
return true;
} catch (RemoteException e) {
- if (Config.LOGD) {
- Log.d(Constants.TAG, "Failed to scan file " + info.mFileName);
- }
+ Log.d(Constants.TAG, "Failed to scan file " + info.mFileName);
}
}
}
diff --git a/src/com/android/providers/downloads/DownloadThread.java b/src/com/android/providers/downloads/DownloadThread.java
index 8a8a0da..b2353e1 100644
--- a/src/com/android/providers/downloads/DownloadThread.java
+++ b/src/com/android/providers/downloads/DownloadThread.java
@@ -16,12 +16,10 @@
package com.android.providers.downloads;
-import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.drm.mobile1.DrmRawContent;
-import android.net.Uri;
import android.net.http.AndroidHttpClient;
import android.os.FileUtils;
import android.os.PowerManager;
@@ -85,14 +83,12 @@ public class DownloadThread extends Thread {
public int mRetryAfter = 0;
public int mRedirectCount = 0;
public String mNewUri;
- public Uri mContentUri;
public boolean mGotData = false;
public String mRequestUri;
public State(DownloadInfo info) {
mMimeType = sanitizeMimeType(info.mMimeType);
mRedirectCount = info.mRedirectCount;
- mContentUri = Uri.parse(Downloads.Impl.CONTENT_URI + "/" + info.mId);
mRequestUri = info.mUri;
mFilename = info.mFileName;
}
@@ -405,8 +401,7 @@ public class DownloadThread extends Thread {
> Constants.MIN_PROGRESS_TIME) {
ContentValues values = new ContentValues();
values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
- mContext.getContentResolver().update(
- state.mContentUri, values, null, null);
+ mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
innerState.mBytesNotified = innerState.mBytesSoFar;
innerState.mTimeLastNotification = now;
}
@@ -450,7 +445,7 @@ public class DownloadThread extends Thread {
if (innerState.mHeaderContentLength == null) {
values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, innerState.mBytesSoFar);
}
- mContext.getContentResolver().update(state.mContentUri, values, null, null);
+ mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
boolean lengthMismatched = (innerState.mHeaderContentLength != null)
&& (innerState.mBytesSoFar != Integer.parseInt(innerState.mHeaderContentLength));
@@ -488,7 +483,7 @@ public class DownloadThread extends Thread {
logNetworkState();
ContentValues values = new ContentValues();
values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
- mContext.getContentResolver().update(state.mContentUri, values, null, null);
+ mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
if (cannotResume(innerState)) {
Log.d(Constants.TAG, "download IOException for download " + mInfo.mId, ex);
Log.d(Constants.TAG, "can't resume interrupted download with no ETag");
@@ -572,7 +567,7 @@ public class DownloadThread extends Thread {
values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType);
}
values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mInfo.mTotalBytes);
- mContext.getContentResolver().update(state.mContentUri, values, null, null);
+ mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
}
/**
@@ -868,7 +863,7 @@ public class DownloadThread extends Thread {
notifyThroughDatabase(
status, countRetry, retryAfter, redirectCount, gotData, filename, uri, mimeType);
if (Downloads.Impl.isStatusCompleted(status)) {
- notifyThroughIntent();
+ mInfo.sendIntentIfRequested();
}
}
@@ -892,17 +887,7 @@ public class DownloadThread extends Thread {
values.put(Constants.FAILED_CONNECTIONS, mInfo.mNumFailed + 1);
}
- mContext.getContentResolver().update(ContentUris.withAppendedId(
- Downloads.Impl.CONTENT_URI, mInfo.mId), values, null, null);
- }
-
- /**
- * Notifies the initiating app if it requested it. That way, it can know that the
- * download completed even if it's not actively watching the cursor.
- */
- private void notifyThroughIntent() {
- Uri uri = Uri.parse(Downloads.Impl.CONTENT_URI + "/" + mInfo.mId);
- mInfo.sendIntentIfRequested(uri);
+ mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
}
/**
diff --git a/src/com/android/providers/downloads/Helpers.java b/src/com/android/providers/downloads/Helpers.java
index 42a49f1..f8900d9 100644
--- a/src/com/android/providers/downloads/Helpers.java
+++ b/src/com/android/providers/downloads/Helpers.java
@@ -469,7 +469,7 @@ public class Helpers {
*/
public static final boolean discardPurgeableFiles(Context context, long targetBytes) {
Cursor cursor = context.getContentResolver().query(
- Downloads.Impl.CONTENT_URI,
+ Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
null,
"( " +
Downloads.Impl.COLUMN_STATUS + " = '" + Downloads.Impl.STATUS_SUCCESS + "' AND " +
@@ -493,7 +493,8 @@ public class Helpers {
file.delete();
long id = cursor.getLong(cursor.getColumnIndex(Downloads.Impl._ID));
context.getContentResolver().delete(
- ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, id), null, null);
+ ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id),
+ null, null);
cursor.moveToNext();
}
} finally {
diff --git a/tests/src/com/android/providers/downloads/AbstractDownloadManagerFunctionalTest.java b/tests/src/com/android/providers/downloads/AbstractDownloadManagerFunctionalTest.java
index 3e4bccc..d04fd2d 100644
--- a/tests/src/com/android/providers/downloads/AbstractDownloadManagerFunctionalTest.java
+++ b/tests/src/com/android/providers/downloads/AbstractDownloadManagerFunctionalTest.java
@@ -165,7 +165,8 @@ public abstract class AbstractDownloadManagerFunctionalTest extends
}
private boolean isDatabaseEmpty() {
- Cursor cursor = mResolver.query(Downloads.CONTENT_URI, null, null, null, null);
+ Cursor cursor = mResolver.query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
+ null, null, null, null);
try {
return cursor.getCount() == 0;
} finally {
diff --git a/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java b/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
index e48ce22..d577e2c 100644
--- a/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
+++ b/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
@@ -29,6 +29,8 @@ import tests.http.RecordedRequest;
import java.io.File;
import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.util.List;
@@ -89,9 +91,8 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
assertEquals(REQUEST_PATH, request.getPath());
Uri localUri = Uri.parse(download.getStringField(DownloadManager.COLUMN_LOCAL_URI));
- assertEquals("file", localUri.getScheme());
- assertStartsWith("//" + Environment.getDownloadCacheDirectory().getPath(),
- localUri.getSchemeSpecificPart());
+ assertEquals("content", localUri.getScheme());
+ checkUriContent(localUri);
assertEquals("text/plain", download.getStringField(DownloadManager.COLUMN_MEDIA_TYPE));
int size = FILE_CONTENT.length();
@@ -103,6 +104,15 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
checkCompleteDownload(download);
}
+ private void checkUriContent(Uri uri) throws FileNotFoundException, IOException {
+ InputStream inputStream = mResolver.openInputStream(uri);
+ try {
+ assertEquals(FILE_CONTENT, readStream(inputStream));
+ } finally {
+ inputStream.close();
+ }
+ }
+
public void testTitleAndDescription() throws Exception {
Download download = enqueueRequest(getRequest()
.setTitle("my title")
diff --git a/ui/AndroidManifest.xml b/ui/AndroidManifest.xml
index 71fad40..31f1483 100644
--- a/ui/AndroidManifest.xml
+++ b/ui/AndroidManifest.xml
@@ -3,8 +3,8 @@
package="com.android.providers.downloads.ui"
android:sharedUserId="android.media">
- <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SEND_DOWNLOAD_COMPLETED_INTENTS" />
+ <uses-permission android:name="android.permission.ACCESS_ALL_DOWNLOADS" />
<application android:process="android.process.media"
android:label="@string/app_label">
diff --git a/ui/src/com/android/providers/downloads/ui/DownloadList.java b/ui/src/com/android/providers/downloads/ui/DownloadList.java
index dd9a608..fce2f16 100644
--- a/ui/src/com/android/providers/downloads/ui/DownloadList.java
+++ b/ui/src/com/android/providers/downloads/ui/DownloadList.java
@@ -30,6 +30,7 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.provider.Downloads;
+import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -47,7 +48,8 @@ import android.widget.Toast;
import com.android.providers.downloads.ui.DownloadItem.DownloadSelectListener;
-import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
@@ -58,6 +60,8 @@ import java.util.Set;
public class DownloadList extends Activity
implements OnChildClickListener, OnItemClickListener, DownloadSelectListener,
OnClickListener, OnCancelListener {
+ private static final String LOG_TAG = "DownloadList";
+
private ExpandableListView mDateOrderedListView;
private ListView mSizeOrderedListView;
private View mEmptyView;
@@ -103,6 +107,7 @@ public class DownloadList extends Activity
setupViews();
mDownloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
+ mDownloadManager.setAccessAllDownloads(true);
DownloadManager.Query baseQuery = new DownloadManager.Query()
.setOnlyIncludeVisibleInDownloadsUi(true);
mDateSortedCursor = mDownloadManager.query(baseQuery);
@@ -112,7 +117,7 @@ public class DownloadList extends Activity
// only attach everything to the listbox if we can access the download database. Otherwise,
// just show it empty
- if (mDateSortedCursor != null && mSizeSortedCursor != null) {
+ if (haveCursors()) {
startManagingCursor(mDateSortedCursor);
startManagingCursor(mSizeSortedCursor);
@@ -160,19 +165,23 @@ public class DownloadList extends Activity
((Button) findViewById(R.id.deselect_all)).setOnClickListener(this);
}
+ private boolean haveCursors() {
+ return mDateSortedCursor != null && mSizeSortedCursor != null;
+ }
+
@Override
protected void onResume() {
super.onResume();
- if (mDateSortedCursor != null) {
+ if (haveCursors()) {
mDateSortedCursor.registerContentObserver(mContentObserver);
+ refresh();
}
- refresh();
}
@Override
protected void onPause() {
super.onPause();
- if (mDateSortedCursor != null) {
+ if (haveCursors()) {
mDateSortedCursor.unregisterContentObserver(mContentObserver);
}
}
@@ -207,7 +216,7 @@ public class DownloadList extends Activity
@Override
public boolean onCreateOptionsMenu(Menu menu) {
- if (mDateSortedCursor != null) {
+ if (haveCursors()) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.download_menu, menu);
}
@@ -243,7 +252,7 @@ public class DownloadList extends Activity
mDateOrderedListView.setVisibility(View.GONE);
mSizeOrderedListView.setVisibility(View.GONE);
- if (mDateSortedCursor.getCount() == 0) {
+ if (mDateSortedCursor == null || mDateSortedCursor.getCount() == 0) {
mEmptyView.setVisibility(View.VISIBLE);
} else {
mEmptyView.setVisibility(View.GONE);
@@ -290,15 +299,20 @@ public class DownloadList extends Activity
* Send an Intent to open the download currently pointed to by the given cursor.
*/
private void openCurrentDownload(Cursor cursor) {
- Uri fileUri = Uri.parse(cursor.getString(mLocalUriColumnId));
- if (!new File(fileUri.getPath()).exists()) {
+ Uri localUri = Uri.parse(cursor.getString(mLocalUriColumnId));
+ try {
+ getContentResolver().openFileDescriptor(localUri, "r").close();
+ } catch (FileNotFoundException exc) {
+ Log.d(LOG_TAG, "Failed to open download " + cursor.getLong(mIdColumnId), exc);
showFailedDialog(cursor.getLong(mIdColumnId), R.string.dialog_file_missing_body);
return;
+ } catch (IOException exc) {
+ // close() failed, not a problem
}
Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setDataAndType(fileUri, cursor.getString(mMediaTypeColumnId));
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.setDataAndType(localUri, cursor.getString(mMediaTypeColumnId));
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION);
try {
startActivity(intent);
} catch (ActivityNotFoundException ex) {