diff options
Diffstat (limited to 'src')
5 files changed, 169 insertions, 52 deletions
diff --git a/src/com/android/providers/downloads/DownloadProvider.java b/src/com/android/providers/downloads/DownloadProvider.java index e0b5842d..ad3cf7ac 100644 --- a/src/com/android/providers/downloads/DownloadProvider.java +++ b/src/com/android/providers/downloads/DownloadProvider.java @@ -35,7 +35,9 @@ import android.database.sqlite.SQLiteOpenHelper; 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.os.SELinux; import android.provider.BaseColumns; @@ -49,6 +51,8 @@ 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; @@ -69,7 +73,7 @@ public final class DownloadProvider extends ContentProvider { /** Database filename */ private static final String DB_NAME = "downloads.db"; /** Current database version */ - private static final int DB_VERSION = 108; + private static final int DB_VERSION = 109; /** Name of table in the database */ private static final String DB_TABLE = "downloads"; @@ -166,6 +170,8 @@ public final class DownloadProvider extends ContentProvider { private static final List<String> downloadManagerColumnsList = Arrays.asList(DownloadManager.UNDERLYING_COLUMNS); + private Handler mHandler; + /** The database that lies underneath this content provider */ private SQLiteOpenHelper mOpenHelper = null; @@ -319,6 +325,11 @@ public final class DownloadProvider extends ContentProvider { "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); } @@ -432,6 +443,8 @@ public final class DownloadProvider extends ContentProvider { mSystemFacade = new RealSystemFacade(getContext()); } + mHandler = new Handler(); + mOpenHelper = new DatabaseHelper(getContext()); // Initialize the system uid mSystemUid = Process.SYSTEM_UID; @@ -590,6 +603,7 @@ public final class DownloadProvider extends ContentProvider { 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); @@ -669,23 +683,12 @@ public final class DownloadProvider extends ContentProvider { } insertRequestHeaders(db, rowID, values); - /* - * requests coming from - * DownloadManager.addCompletedDownload(String, String, String, - * boolean, String, String, long) need special treatment - */ - Context context = getContext(); - if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) == - Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) { - // When notification is requested, kick off service to process all - // relevant downloads. - if (Downloads.Impl.isNotificationToBeDisplayed(vis)) { - context.startService(new Intent(context, DownloadService.class)); - } - } else { - 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); } @@ -784,6 +787,7 @@ public final class DownloadProvider extends ContentProvider { 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(); @@ -1147,6 +1151,19 @@ public final class DownloadProvider extends ContentProvider { 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; @@ -1162,12 +1179,12 @@ public final class DownloadProvider extends ContentProvider { * 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; @@ -1182,9 +1199,7 @@ public final class DownloadProvider extends ContentProvider { cursor.moveToFirst(); path = cursor.getString(0); } finally { - if (cursor != null) { - cursor.close(); - } + IoUtils.closeQuietly(cursor); } if (path == null) { @@ -1193,20 +1208,28 @@ public final class DownloadProvider extends ContentProvider { if (!Helpers.isFilenameValid(path, mDownloadsDataDir)) { throw new FileNotFoundException("Invalid filename: " + path); } - if (!"r".equals(mode)) { - throw new FileNotFoundException("Bad mode for " + uri + ": " + mode); - } - - ParcelFileDescriptor ret = ParcelFileDescriptor.open(new File(path), - ParcelFileDescriptor.MODE_READ_ONLY); - if (ret == null) { - if (Constants.LOGV) { - Log.v(Constants.TAG, "couldn't open file"); + final File file = new File(path); + 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); } - throw new FileNotFoundException("couldn't open file"); } - return ret; } @Override diff --git a/src/com/android/providers/downloads/DownloadStorageProvider.java b/src/com/android/providers/downloads/DownloadStorageProvider.java index 7268c5c6..d9a0aa63 100644 --- a/src/com/android/providers/downloads/DownloadStorageProvider.java +++ b/src/com/android/providers/downloads/DownloadStorageProvider.java @@ -18,36 +18,42 @@ package com.android.providers.downloads; import android.app.DownloadManager; import android.app.DownloadManager.Query; +import android.content.ContentResolver; import android.content.Context; import android.content.res.AssetFileDescriptor; import android.database.Cursor; import android.database.MatrixCursor; import android.database.MatrixCursor.RowBuilder; import android.graphics.Point; +import android.net.Uri; import android.os.Binder; import android.os.CancellationSignal; +import android.os.Environment; import android.os.ParcelFileDescriptor; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import android.provider.DocumentsContract.Root; import android.provider.DocumentsProvider; import android.text.TextUtils; +import android.webkit.MimeTypeMap; import libcore.io.IoUtils; +import java.io.File; import java.io.FileNotFoundException; +import java.io.IOException; /** * Presents a {@link DocumentsContract} view of {@link DownloadManager} * contents. */ public class DownloadStorageProvider extends DocumentsProvider { + private static final String AUTHORITY = Constants.STORAGE_AUTHORITY; private static final String DOC_ID_ROOT = Constants.STORAGE_ROOT_ID; private static final String[] DEFAULT_ROOT_PROJECTION = new String[] { - Root.COLUMN_ROOT_ID, Root.COLUMN_ROOT_TYPE, Root.COLUMN_FLAGS, Root.COLUMN_ICON, - Root.COLUMN_TITLE, Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID, - Root.COLUMN_AVAILABLE_BYTES, + Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, + Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID, }; private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] { @@ -77,13 +83,18 @@ public class DownloadStorageProvider extends DocumentsProvider { result.setNotificationUri(getContext().getContentResolver(), cursor.getNotificationUri()); } + static void onDownloadProviderDelete(Context context, long id) { + final Uri uri = DocumentsContract.buildDocumentUri(AUTHORITY, Long.toString(id)); + context.revokeUriPermission(uri, ~0); + } + @Override public Cursor queryRoots(String[] projection) throws FileNotFoundException { final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection)); final RowBuilder row = result.newRow(); row.add(Root.COLUMN_ROOT_ID, DOC_ID_ROOT); - row.add(Root.COLUMN_ROOT_TYPE, Root.ROOT_TYPE_SHORTCUT); - row.add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_RECENTS); + row.add(Root.COLUMN_FLAGS, + Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_RECENTS | Root.FLAG_SUPPORTS_CREATE); row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher_download); row.add(Root.COLUMN_TITLE, getContext().getString(R.string.root_downloads)); row.add(Root.COLUMN_DOCUMENT_ID, DOC_ID_ROOT); @@ -91,6 +102,41 @@ public class DownloadStorageProvider extends DocumentsProvider { } @Override + public String createDocument(String docId, String mimeType, String displayName) + throws FileNotFoundException { + final File parent = Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS); + parent.mkdirs(); + + // Delegate to real provider + final long token = Binder.clearCallingIdentity(); + try { + displayName = removeExtension(mimeType, displayName); + File file = new File(parent, addExtension(mimeType, displayName)); + + // If conflicting file, try adding counter suffix + int n = 0; + while (file.exists() && n++ < 32) { + file = new File(parent, addExtension(mimeType, displayName + " (" + n + ")")); + } + + try { + if (!file.createNewFile()) { + throw new IllegalStateException("Failed to touch " + file); + } + } catch (IOException e) { + throw new IllegalStateException("Failed to touch " + file + ": " + e); + } + + return Long.toString(mDm.addCompletedDownload( + file.getName(), file.getName(), false, mimeType, file.getAbsolutePath(), 0L, + false, true)); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override public void deleteDocument(String docId) throws FileNotFoundException { // Delegate to real provider final long token = Binder.clearCallingIdentity(); @@ -209,14 +255,12 @@ public class DownloadStorageProvider extends DocumentsProvider { @Override public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal) throws FileNotFoundException { - if (!"r".equals(mode)) { - throw new IllegalArgumentException("Downloads are read-only"); - } - // Delegate to real provider final long token = Binder.clearCallingIdentity(); try { - return mDm.openDownloadedFile(Long.parseLong(docId)); + final long id = Long.parseLong(docId); + final ContentResolver resolver = getContext().getContentResolver(); + return resolver.openFileDescriptor(mDm.getDownloadUri(id), mode, signal); } finally { Binder.restoreCallingIdentity(token); } @@ -234,7 +278,8 @@ public class DownloadStorageProvider extends DocumentsProvider { final RowBuilder row = result.newRow(); row.add(Document.COLUMN_DOCUMENT_ID, DOC_ID_ROOT); row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); - row.add(Document.COLUMN_FLAGS, Document.FLAG_DIR_PREFERS_LAST_MODIFIED); + row.add(Document.COLUMN_FLAGS, + Document.FLAG_DIR_PREFERS_LAST_MODIFIED | Document.FLAG_DIR_SUPPORTS_CREATE); } private void includeDownloadFromCursor(MatrixCursor result, Cursor cursor) { @@ -248,7 +293,8 @@ public class DownloadStorageProvider extends DocumentsProvider { String mimeType = cursor.getString( cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE)); if (mimeType == null) { - mimeType = "application/octet-stream"; + // Provide fake MIME type so it's openable + mimeType = "vnd.android.document/file"; } Long size = cursor.getLong( cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)); @@ -283,7 +329,7 @@ public class DownloadStorageProvider extends DocumentsProvider { break; } - int flags = Document.FLAG_SUPPORTS_DELETE; + int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_WRITE; if (mimeType != null && mimeType.startsWith("image/")) { flags |= Document.FLAG_SUPPORTS_THUMBNAIL; } @@ -300,4 +346,32 @@ public class DownloadStorageProvider extends DocumentsProvider { row.add(Document.COLUMN_LAST_MODIFIED, lastModified); row.add(Document.COLUMN_FLAGS, flags); } + + /** + * Remove file extension from name, but only if exact MIME type mapping + * exists. This means we can reapply the extension later. + */ + private static String removeExtension(String mimeType, String name) { + final int lastDot = name.lastIndexOf('.'); + if (lastDot >= 0) { + final String extension = name.substring(lastDot + 1); + final String nameMime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + if (mimeType.equals(nameMime)) { + return name.substring(0, lastDot); + } + } + return name; + } + + /** + * Add file extension to name, but only if exact MIME type mapping exists. + */ + private static String addExtension(String mimeType, String name) { + final String extension = MimeTypeMap.getSingleton() + .getExtensionFromMimeType(mimeType); + if (extension != null) { + return name + "." + extension; + } + return name; + } } diff --git a/src/com/android/providers/downloads/DownloadThread.java b/src/com/android/providers/downloads/DownloadThread.java index def4d7fe..93f8d650 100644 --- a/src/com/android/providers/downloads/DownloadThread.java +++ b/src/com/android/providers/downloads/DownloadThread.java @@ -519,7 +519,7 @@ public class DownloadThread implements Runnable { throw new StopRequestException( Downloads.Impl.STATUS_PAUSED_BY_APP, "download paused by owner"); } - if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED) { + if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED || mInfo.mDeleted) { throw new StopRequestException(Downloads.Impl.STATUS_CANCELED, "download canceled"); } } diff --git a/src/com/android/providers/downloads/Helpers.java b/src/com/android/providers/downloads/Helpers.java index 33205557..013faf27 100644 --- a/src/com/android/providers/downloads/Helpers.java +++ b/src/com/android/providers/downloads/Helpers.java @@ -16,6 +16,8 @@ package com.android.providers.downloads; +import static com.android.providers.downloads.Constants.TAG; + import android.content.Context; import android.net.Uri; import android.os.Environment; @@ -342,10 +344,26 @@ public class Helpers { * Checks whether the filename looks legitimate */ static boolean isFilenameValid(String filename, File downloadsDataDir) { - filename = filename.replaceFirst("/+", "/"); // normalize leading slashes - return filename.startsWith(Environment.getDownloadCacheDirectory().toString()) - || filename.startsWith(downloadsDataDir.toString()) - || filename.startsWith(Environment.getExternalStorageDirectory().toString()); + final String[] whitelist; + try { + filename = new File(filename).getCanonicalPath(); + whitelist = new String[] { + downloadsDataDir.getCanonicalPath(), + Environment.getDownloadCacheDirectory().getCanonicalPath(), + Environment.getExternalStorageDirectory().getCanonicalPath(), + }; + } catch (IOException e) { + Log.w(TAG, "Failed to resolve canonical path: " + e); + return false; + } + + for (String test : whitelist) { + if (filename.startsWith(test)) { + return true; + } + } + + return false; } /** diff --git a/src/com/android/providers/downloads/OpenHelper.java b/src/com/android/providers/downloads/OpenHelper.java index af7a37f2..4eb319c4 100644 --- a/src/com/android/providers/downloads/OpenHelper.java +++ b/src/com/android/providers/downloads/OpenHelper.java @@ -78,7 +78,6 @@ public class OpenHelper { mimeType = DownloadDrmHelper.getOriginalMimeType(context, file, mimeType); final Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); if ("application/vnd.android.package-archive".equals(mimeType)) { // PackageInstaller doesn't like content URIs, so open file @@ -90,9 +89,12 @@ public class OpenHelper { intent.putExtra(Intent.EXTRA_REFERRER, getRefererUri(context, id)); intent.putExtra(Intent.EXTRA_ORIGINATING_UID, getOriginatingUid(context, id)); } else if ("file".equals(localUri.getScheme())) { + intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); intent.setDataAndType( ContentUris.withAppendedId(ALL_DOWNLOADS_CONTENT_URI, id), mimeType); } else { + intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.setDataAndType(localUri, mimeType); } |