summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJeff Sharkey <jsharkey@android.com>2013-09-23 21:21:55 (GMT)
committerJeff Sharkey <jsharkey@android.com>2013-09-23 21:25:50 (GMT)
commitc067c8be21dc4a6dc5f49b2b1aed7f91aab47063 (patch)
treea1ed113bae539117db084f51f579ed78fbd19d3b
parente0b9b881187362aea61249952a913d794834b728 (diff)
downloadandroid_packages_providers_DownloadProvider-c067c8be21dc4a6dc5f49b2b1aed7f91aab47063.zip
android_packages_providers_DownloadProvider-c067c8be21dc4a6dc5f49b2b1aed7f91aab47063.tar.gz
android_packages_providers_DownloadProvider-c067c8be21dc4a6dc5f49b2b1aed7f91aab47063.tar.bz2
Allow saving to Downloads.
Add column to mark downloads as being writable, and allow documents to be created under Downloads backend. Update database when writing is finished, and generate unique filenames when they already exist. Check canonical path on incoming _DATA paths. Bug: 10667164, 10892621, 10893268 Change-Id: I8c203b96ff042a895b58686903fcd07fc755a00f
-rw-r--r--src/com/android/providers/downloads/DownloadProvider.java55
-rw-r--r--src/com/android/providers/downloads/DownloadStorageProvider.java87
-rw-r--r--src/com/android/providers/downloads/Helpers.java10
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 e0b5842..999134f 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 7268c5c..d982f2a 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 3320555..aa763de 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());