diff options
Diffstat (limited to 'src/com/android/providers/downloads/DownloadStorageProvider.java')
-rw-r--r-- | src/com/android/providers/downloads/DownloadStorageProvider.java | 381 |
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; + } +} |