diff options
Diffstat (limited to 'src/com')
3 files changed, 135 insertions, 17 deletions
diff --git a/src/com/android/providers/downloads/DownloadProvider.java b/src/com/android/providers/downloads/DownloadProvider.java index e0b5842d..999134f0 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); @@ -784,6 +798,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(); @@ -1162,13 +1177,15 @@ 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, Downloads.Impl.COLUMN_ALLOW_WRITE }, null, null, null); String path; + boolean allowWrite; try { int count = (cursor != null) ? cursor.getCount() : 0; if (count != 1) { @@ -1181,10 +1198,9 @@ public final class DownloadProvider extends ContentProvider { cursor.moveToFirst(); path = cursor.getString(0); + allowWrite = cursor.getInt(1) != 0; } finally { - if (cursor != null) { - cursor.close(); - } + IoUtils.closeQuietly(cursor); } if (path == null) { @@ -1193,12 +1209,33 @@ public final class DownloadProvider extends ContentProvider { if (!Helpers.isFilenameValid(path, mDownloadsDataDir)) { throw new FileNotFoundException("Invalid filename: " + path); } - if (!"r".equals(mode)) { + if (!allowWrite && !"r".equals(mode)) { throw new FileNotFoundException("Bad mode for " + uri + ": " + mode); } - ParcelFileDescriptor ret = ParcelFileDescriptor.open(new File(path), - ParcelFileDescriptor.MODE_READ_ONLY); + final File file = new File(path); + + ParcelFileDescriptor ret; + if ("r".equals(mode)) { + ret = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); + } else { + try { + // When finished writing, update size and timestamp + ret = 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); + } + } if (ret == null) { if (Constants.LOGV) { diff --git a/src/com/android/providers/downloads/DownloadStorageProvider.java b/src/com/android/providers/downloads/DownloadStorageProvider.java index 7268c5c6..d982f2a1 100644 --- a/src/com/android/providers/downloads/DownloadStorageProvider.java +++ b/src/com/android/providers/downloads/DownloadStorageProvider.java @@ -18,6 +18,7 @@ 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; @@ -26,16 +27,20 @@ import android.database.MatrixCursor.RowBuilder; import android.graphics.Point; 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} @@ -83,7 +88,8 @@ public class DownloadStorageProvider extends DocumentsProvider { 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 +97,40 @@ 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); + + // 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 +249,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 +272,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) { @@ -288,6 +327,12 @@ public class DownloadStorageProvider extends DocumentsProvider { flags |= Document.FLAG_SUPPORTS_THUMBNAIL; } + final int allowWrite = cursor.getInt( + cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ALLOW_WRITE)); + if (allowWrite != 0) { + flags |= Document.FLAG_SUPPORTS_WRITE; + } + final long lastModified = cursor.getLong( cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP)); @@ -300,4 +345,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/Helpers.java b/src/com/android/providers/downloads/Helpers.java index 33205557..aa763de2 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,7 +344,13 @@ public class Helpers { * Checks whether the filename looks legitimate */ static boolean isFilenameValid(String filename, File downloadsDataDir) { - filename = filename.replaceFirst("/+", "/"); // normalize leading slashes + try { + filename = new File(filename).getCanonicalPath(); + } catch (IOException e) { + Log.w(TAG, "Failed to resolve canonical path: " + e); + return false; + } + return filename.startsWith(Environment.getDownloadCacheDirectory().toString()) || filename.startsWith(downloadsDataDir.toString()) || filename.startsWith(Environment.getExternalStorageDirectory().toString()); |