summaryrefslogtreecommitdiffstats
path: root/src/com/android/providers/downloads
diff options
context:
space:
mode:
authorJeff Sharkey <jsharkey@android.com>2013-09-23 14:21:55 -0700
committerJeff Sharkey <jsharkey@android.com>2013-09-23 14:25:50 -0700
commitc067c8be21dc4a6dc5f49b2b1aed7f91aab47063 (patch)
treea1ed113bae539117db084f51f579ed78fbd19d3b /src/com/android/providers/downloads
parente0b9b881187362aea61249952a913d794834b728 (diff)
downloadandroid_packages_providers_DownloadProvider-c067c8be21dc4a6dc5f49b2b1aed7f91aab47063.tar.gz
android_packages_providers_DownloadProvider-c067c8be21dc4a6dc5f49b2b1aed7f91aab47063.tar.bz2
android_packages_providers_DownloadProvider-c067c8be21dc4a6dc5f49b2b1aed7f91aab47063.zip
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
Diffstat (limited to 'src/com/android/providers/downloads')
-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 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());