summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSteve Howard <showard@google.com>2010-09-12 18:53:31 -0700
committerSteve Howard <showard@google.com>2010-09-14 19:00:40 -0700
commit3d55d829c03fe78ad8cdab119293efb6c6e49c64 (patch)
treefb8feb9c23b83108546048b0488033279de63635
parent33671e9c1e9ffa3776ed987bddeb70a04daa7cfe (diff)
downloadandroid_packages_providers_DownloadProvider-3d55d829c03fe78ad8cdab119293efb6c6e49c64.zip
android_packages_providers_DownloadProvider-3d55d829c03fe78ad8cdab119293efb6c6e49c64.tar.gz
android_packages_providers_DownloadProvider-3d55d829c03fe78ad8cdab119293efb6c6e49c64.tar.bz2
New URI structure with "my_downloads" and "all_downloads"
This change introduces a second view into the download manager database via a set of URIs starting with /all_downloads, renaming the original /download URIs to /my_downloads. In addition to making things more clear, this change allows the downloads UI to grant permissions on individual downloads to viewer apps. The old semantics were: * for ordinary callers, /download included only downloads initiated by the calling UID * for intraprocess calls or calls by root, /download included all downloads The new semantics are * /my_downloads always includes only downloads initiated by the calling UID, and requires only INTERNET permission. It could just as well require no permission, but that's not possible in the framework, since path-permissions can only broaden access, not tighten it. It doesn't matter, because these URIs are useless without INTERNET permission -- if a user can't initiate downloads, there's no reason to read this. * /all_downloads always includes all downloads on the system, and requires the new permission ACCESS_ALL_DOWNLOADS. This permission is currently protectionLevel=signature -- this could be relaxed later to support third-party download managers. All download manager code has been changed to use /all_downloads URIs, except when passing a URI to another app. In making this change across the download manager code, I've taken some liberties in cleaning things up. Other apps are unchanged and will use /my_downloads. Finally, this incorporates changes to DownloadManager to return a content URI for /cache downloads -- the download UI no longer assumes it's a file URI, and it grants permissions to the receiver of the VIEW intent. The public API test has also been updated. I've also fixed some null cursor checking in DownloadManager. Change-Id: I05a501eb4388249fe80c43724405657c950d7238
-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) {