summaryrefslogtreecommitdiffstats
path: root/src/com/android/providers/downloads/DownloadStorageProvider.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/providers/downloads/DownloadStorageProvider.java')
-rw-r--r--src/com/android/providers/downloads/DownloadStorageProvider.java381
1 files changed, 381 insertions, 0 deletions
diff --git a/src/com/android/providers/downloads/DownloadStorageProvider.java b/src/com/android/providers/downloads/DownloadStorageProvider.java
new file mode 100644
index 00000000..ecef54e0
--- /dev/null
+++ b/src/com/android/providers/downloads/DownloadStorageProvider.java
@@ -0,0 +1,381 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+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_FLAGS, Root.COLUMN_ICON,
+ Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID,
+ };
+
+ private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
+ Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
+ Document.COLUMN_SUMMARY, Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS,
+ Document.COLUMN_SIZE,
+ };
+
+ private DownloadManager mDm;
+
+ @Override
+ public boolean onCreate() {
+ mDm = (DownloadManager) getContext().getSystemService(Context.DOWNLOAD_SERVICE);
+ mDm.setAccessAllDownloads(true);
+ return true;
+ }
+
+ private static String[] resolveRootProjection(String[] projection) {
+ return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
+ }
+
+ private static String[] resolveDocumentProjection(String[] projection) {
+ return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
+ }
+
+ private void copyNotificationUri(MatrixCursor result, Cursor cursor) {
+ 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_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);
+ return result;
+ }
+
+ @Override
+ public String createDocument(String docId, String mimeType, String displayName)
+ throws FileNotFoundException {
+ if (Document.MIME_TYPE_DIR.equals(mimeType)) {
+ throw new FileNotFoundException("Directory creation not supported");
+ }
+
+ 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();
+ try {
+ if (mDm.remove(Long.parseLong(docId)) != 1) {
+ throw new IllegalStateException("Failed to delete " + docId);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public Cursor queryDocument(String docId, String[] projection) throws FileNotFoundException {
+ final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
+
+ if (DOC_ID_ROOT.equals(docId)) {
+ includeDefaultDocument(result);
+ } else {
+ // Delegate to real provider
+ final long token = Binder.clearCallingIdentity();
+ Cursor cursor = null;
+ try {
+ cursor = mDm.query(new Query().setFilterById(Long.parseLong(docId)));
+ copyNotificationUri(result, cursor);
+ if (cursor.moveToFirst()) {
+ includeDownloadFromCursor(result, cursor);
+ }
+ } finally {
+ IoUtils.closeQuietly(cursor);
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public Cursor queryChildDocuments(String docId, String[] projection, String sortOrder)
+ throws FileNotFoundException {
+ final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
+
+ // Delegate to real provider
+ final long token = Binder.clearCallingIdentity();
+ Cursor cursor = null;
+ try {
+ cursor = mDm.query(new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true)
+ .setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL));
+ copyNotificationUri(result, cursor);
+ while (cursor.moveToNext()) {
+ includeDownloadFromCursor(result, cursor);
+ }
+ } finally {
+ IoUtils.closeQuietly(cursor);
+ Binder.restoreCallingIdentity(token);
+ }
+ return result;
+ }
+
+ @Override
+ public Cursor queryChildDocumentsForManage(
+ String parentDocumentId, String[] projection, String sortOrder)
+ throws FileNotFoundException {
+ final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
+
+ // Delegate to real provider
+ final long token = Binder.clearCallingIdentity();
+ Cursor cursor = null;
+ try {
+ cursor = mDm.query(
+ new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true));
+ copyNotificationUri(result, cursor);
+ while (cursor.moveToNext()) {
+ includeDownloadFromCursor(result, cursor);
+ }
+ } finally {
+ IoUtils.closeQuietly(cursor);
+ Binder.restoreCallingIdentity(token);
+ }
+ return result;
+ }
+
+ @Override
+ public Cursor queryRecentDocuments(String rootId, String[] projection)
+ throws FileNotFoundException {
+ final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
+
+ // Delegate to real provider
+ final long token = Binder.clearCallingIdentity();
+ Cursor cursor = null;
+ try {
+ cursor = mDm.query(new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true)
+ .setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL));
+ copyNotificationUri(result, cursor);
+ while (cursor.moveToNext() && result.getCount() < 12) {
+ final String mimeType = cursor.getString(
+ cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE));
+ final String uri = cursor.getString(
+ cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIAPROVIDER_URI));
+
+ // Skip images that have been inserted into the MediaStore so we
+ // don't duplicate them in the recents list.
+ if (mimeType == null
+ || (mimeType.startsWith("image/") && !TextUtils.isEmpty(uri))) {
+ continue;
+ }
+
+ includeDownloadFromCursor(result, cursor);
+ }
+ } finally {
+ IoUtils.closeQuietly(cursor);
+ Binder.restoreCallingIdentity(token);
+ }
+ return result;
+ }
+
+ @Override
+ public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal)
+ throws FileNotFoundException {
+ // Delegate to real provider
+ final long token = Binder.clearCallingIdentity();
+ try {
+ final long id = Long.parseLong(docId);
+ final ContentResolver resolver = getContext().getContentResolver();
+ return resolver.openFileDescriptor(mDm.getDownloadUri(id), mode, signal);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public AssetFileDescriptor openDocumentThumbnail(
+ String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
+ // TODO: extend ExifInterface to support fds
+ final ParcelFileDescriptor pfd = openDocument(docId, "r", signal);
+ return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
+ }
+
+ private void includeDefaultDocument(MatrixCursor result) {
+ 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 | Document.FLAG_DIR_SUPPORTS_CREATE);
+ }
+
+ private void includeDownloadFromCursor(MatrixCursor result, Cursor cursor) {
+ final long id = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID));
+ final String docId = String.valueOf(id);
+
+ final String displayName = cursor.getString(
+ cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TITLE));
+ String summary = cursor.getString(
+ cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_DESCRIPTION));
+ String mimeType = cursor.getString(
+ cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE));
+ if (mimeType == null) {
+ // Provide fake MIME type so it's openable
+ mimeType = "vnd.android.document/file";
+ }
+ Long size = cursor.getLong(
+ cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
+ if (size == -1) {
+ size = null;
+ }
+
+ final int status = cursor.getInt(
+ cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS));
+ switch (status) {
+ case DownloadManager.STATUS_SUCCESSFUL:
+ break;
+ case DownloadManager.STATUS_PAUSED:
+ summary = getContext().getString(R.string.download_queued);
+ break;
+ case DownloadManager.STATUS_PENDING:
+ summary = getContext().getString(R.string.download_queued);
+ break;
+ case DownloadManager.STATUS_RUNNING:
+ final long progress = cursor.getLong(cursor.getColumnIndexOrThrow(
+ DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
+ if (size != null) {
+ final long percent = progress * 100 / size;
+ summary = getContext().getString(R.string.download_running_percent, percent);
+ } else {
+ summary = getContext().getString(R.string.download_running);
+ }
+ break;
+ case DownloadManager.STATUS_FAILED:
+ default:
+ summary = getContext().getString(R.string.download_error);
+ break;
+ }
+
+ int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_WRITE;
+ if (mimeType != null && mimeType.startsWith("image/")) {
+ flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
+ }
+
+ final long lastModified = cursor.getLong(
+ cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP));
+
+ final RowBuilder row = result.newRow();
+ row.add(Document.COLUMN_DOCUMENT_ID, docId);
+ row.add(Document.COLUMN_DISPLAY_NAME, displayName);
+ row.add(Document.COLUMN_SUMMARY, summary);
+ row.add(Document.COLUMN_SIZE, size);
+ row.add(Document.COLUMN_MIME_TYPE, mimeType);
+ 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;
+ }
+}