summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJeff Sharkey <jsharkey@android.com>2013-08-07 18:33:57 -0700
committerJeff Sharkey <jsharkey@android.com>2013-08-07 18:39:07 -0700
commit6a09c3294997dc9e3c83b8431f7b496b19670014 (patch)
tree759a839d9a50b999468c9b263a4085bfdc984586
parent56a03b18e3691824f8012049466954a668b68ba8 (diff)
downloadandroid_packages_providers_DownloadProvider-6a09c3294997dc9e3c83b8431f7b496b19670014.zip
android_packages_providers_DownloadProvider-6a09c3294997dc9e3c83b8431f7b496b19670014.tar.gz
android_packages_providers_DownloadProvider-6a09c3294997dc9e3c83b8431f7b496b19670014.tar.bz2
First pass at Downloads storage provider.
Offers a view of Downloads through the lens of DocumentsContract for surfacing in new storage UI. Change-Id: I4373c2498b4b82bfee2300a00f8d0bb734bf574c
-rw-r--r--AndroidManifest.xml15
-rw-r--r--res/mipmap-hdpi/ic_launcher_download.pngbin0 -> 10593 bytes
-rw-r--r--res/mipmap-mdpi/ic_launcher_download.pngbin0 -> 23509 bytes
-rw-r--r--res/mipmap-xhdpi/ic_launcher_download.pngbin0 -> 32416 bytes
-rw-r--r--res/mipmap-xxhdpi/ic_launcher_download.pngbin0 -> 21228 bytes
-rw-r--r--res/values/strings.xml3
-rw-r--r--res/xml/document_provider.xml19
-rw-r--r--src/com/android/providers/downloads/DownloadStorageProvider.java302
8 files changed, 338 insertions, 1 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 3024a17..f1ad40e 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -56,7 +56,8 @@
<uses-permission android:name="android.permission.MODIFY_NETWORK_ACCOUNTING" />
<application android:process="android.process.media"
- android:label="@string/app_label">
+ android:label="@string/app_label"
+ android:icon="@mipmap/ic_launcher_download">
<provider android:name=".DownloadProvider"
android:authorities="downloads" android:exported="true">
@@ -77,6 +78,18 @@
downloaded files with other viewers -->
<grant-uri-permission android:pathPrefix="/my_downloads/"/>
</provider>
+
+ <provider
+ android:name=".DownloadStorageProvider"
+ android:authorities="com.android.providers.downloads.storage"
+ android:grantUriPermissions="true"
+ android:exported="true"
+ android:permission="android.permission.MANAGE_DOCUMENTS">
+ <meta-data
+ android:name="android.content.DOCUMENT_PROVIDER"
+ android:resource="@xml/document_provider" />
+ </provider>
+
<service android:name=".DownloadService"
android:permission="android.permission.ACCESS_DOWNLOAD_MANAGER" />
<receiver android:name=".DownloadReceiver" android:exported="false">
diff --git a/res/mipmap-hdpi/ic_launcher_download.png b/res/mipmap-hdpi/ic_launcher_download.png
new file mode 100644
index 0000000..3f092d3
--- /dev/null
+++ b/res/mipmap-hdpi/ic_launcher_download.png
Binary files differ
diff --git a/res/mipmap-mdpi/ic_launcher_download.png b/res/mipmap-mdpi/ic_launcher_download.png
new file mode 100644
index 0000000..76652fb
--- /dev/null
+++ b/res/mipmap-mdpi/ic_launcher_download.png
Binary files differ
diff --git a/res/mipmap-xhdpi/ic_launcher_download.png b/res/mipmap-xhdpi/ic_launcher_download.png
new file mode 100644
index 0000000..7d7b1b1
--- /dev/null
+++ b/res/mipmap-xhdpi/ic_launcher_download.png
Binary files differ
diff --git a/res/mipmap-xxhdpi/ic_launcher_download.png b/res/mipmap-xxhdpi/ic_launcher_download.png
new file mode 100644
index 0000000..0921c12
--- /dev/null
+++ b/res/mipmap-xxhdpi/ic_launcher_download.png
Binary files differ
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 3a060e2..2981047 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -213,4 +213,7 @@
[CHAR LIMIT=200] -->
<string name="download_no_application_title">Can\'t open file</string>
+ <!-- Label describing Downloads as a storage root [CHAR LIMIT=32] -->
+ <string name="root_downloads">Downloads</string>
+
</resources>
diff --git a/res/xml/document_provider.xml b/res/xml/document_provider.xml
new file mode 100644
index 0000000..77891cb
--- /dev/null
+++ b/res/xml/document_provider.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<documents-provider xmlns:android="http://schemas.android.com/apk/res/android"
+ android:customRoots="true">
+</documents-provider>
diff --git a/src/com/android/providers/downloads/DownloadStorageProvider.java b/src/com/android/providers/downloads/DownloadStorageProvider.java
new file mode 100644
index 0000000..7b6d152
--- /dev/null
+++ b/src/com/android/providers/downloads/DownloadStorageProvider.java
@@ -0,0 +1,302 @@
+/*
+ * 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.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.ParcelFileDescriptor;
+import android.provider.BaseColumns;
+import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.DocumentColumns;
+import android.provider.DocumentsContract.RootColumns;
+import android.provider.Downloads;
+import android.util.Log;
+
+import libcore.io.IoUtils;
+
+import java.io.FileNotFoundException;
+
+/**
+ * Presents a {@link DocumentsContract} view of {@link DownloadManager}
+ * contents.
+ */
+public class DownloadStorageProvider extends ContentProvider {
+ private static final String AUTHORITY = "com.android.providers.downloads.storage";
+
+ private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+ private static final int URI_ROOTS = 1;
+ private static final int URI_ROOTS_ID = 2;
+ private static final int URI_DOCS_ID = 3;
+ private static final int URI_DOCS_ID_CONTENTS = 4;
+
+ static {
+ sMatcher.addURI(AUTHORITY, "roots", URI_ROOTS);
+ sMatcher.addURI(AUTHORITY, "roots/*", URI_ROOTS_ID);
+ sMatcher.addURI(AUTHORITY, "roots/*/docs/*", URI_DOCS_ID);
+ sMatcher.addURI(AUTHORITY, "roots/*/docs/*/contents", URI_DOCS_ID_CONTENTS);
+ }
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+
+ // TODO: support custom projections
+ final String[] rootsProjection = new String[] {
+ BaseColumns._ID, RootColumns.ROOT_ID, RootColumns.ROOT_TYPE, RootColumns.ICON,
+ RootColumns.TITLE, RootColumns.SUMMARY, RootColumns.AVAILABLE_BYTES };
+ final String[] docsProjection = new String[] {
+ BaseColumns._ID, DocumentColumns.DISPLAY_NAME, DocumentColumns.SIZE,
+ DocumentColumns.DOC_ID, DocumentColumns.MIME_TYPE, DocumentColumns.LAST_MODIFIED,
+ DocumentColumns.FLAGS, DocumentColumns.SUMMARY };
+
+ switch (sMatcher.match(uri)) {
+ case URI_ROOTS: {
+ final MatrixCursor result = new MatrixCursor(rootsProjection);
+ includeDefaultRoot(result);
+ return result;
+ }
+ case URI_ROOTS_ID: {
+ final MatrixCursor result = new MatrixCursor(rootsProjection);
+ includeDefaultRoot(result);
+ return result;
+ }
+ case URI_DOCS_ID: {
+ final String docId = DocumentsContract.getDocId(uri);
+ final MatrixCursor result = new MatrixCursor(docsProjection);
+
+ if (DocumentsContract.ROOT_DOC_ID.equals(docId)) {
+ includeDefaultDocument(result);
+ } else {
+ // Delegate to real provider
+ final long token = Binder.clearCallingIdentity();
+ Cursor cursor = null;
+ try {
+ final Uri downloadUri = getDownloadUriFromDocument(docId);
+ cursor = getContext()
+ .getContentResolver().query(downloadUri, null, null, null, null);
+ if (cursor.moveToFirst()) {
+ includeDownloadFromCursor(result, cursor);
+ }
+ } finally {
+ IoUtils.closeQuietly(cursor);
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ return result;
+ }
+ case URI_DOCS_ID_CONTENTS: {
+ final String docId = DocumentsContract.getDocId(uri);
+ final MatrixCursor result = new MatrixCursor(docsProjection);
+
+ if (!DocumentsContract.ROOT_DOC_ID.equals(docId)) {
+ throw new UnsupportedOperationException("Unsupported Uri " + uri);
+ }
+
+ // Delegate to real provider
+ // TODO: filter visible downloads?
+ final long token = Binder.clearCallingIdentity();
+ Cursor cursor = null;
+ try {
+ cursor = getContext().getContentResolver()
+ .query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, null, null, null, null);
+ while (cursor.moveToNext()) {
+ includeDownloadFromCursor(result, cursor);
+ }
+ } finally {
+ IoUtils.closeQuietly(cursor);
+ Binder.restoreCallingIdentity(token);
+ }
+ return result;
+ }
+ default: {
+ throw new UnsupportedOperationException("Unsupported Uri " + uri);
+ }
+ }
+ }
+
+ private void includeDefaultRoot(MatrixCursor result) {
+ final int rootType = DocumentsContract.ROOT_TYPE_SHORTCUT;
+ final String rootId = "downloads";
+ final int icon = 0;
+ final String title = getContext().getString(R.string.root_downloads);
+ final String summary = null;
+ final long availableBytes = -1;
+
+ result.addRow(new Object[] {
+ rootId.hashCode(), rootId, rootType, icon, title, summary,
+ availableBytes });
+ }
+
+ private void includeDefaultDocument(MatrixCursor result) {
+ final long id = Long.MIN_VALUE;
+ final String docId = DocumentsContract.ROOT_DOC_ID;
+ final String displayName = getContext().getString(R.string.root_downloads);
+ final String summary = null;
+ final String mimeType = DocumentsContract.MIME_TYPE_DIRECTORY;
+ final long size = -1;
+ final long lastModified = -1;
+ final int flags = 0;
+
+ result.addRow(new Object[] {
+ id, displayName, size, docId, mimeType, lastModified, flags, summary });
+ }
+
+ private void includeDownloadFromCursor(MatrixCursor result, Cursor cursor) {
+ final long id = cursor.getLong(cursor.getColumnIndexOrThrow(Downloads.Impl._ID));
+ final String docId = getDocumentFromDownload(id);
+
+ final String displayName = cursor.getString(
+ cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_TITLE));
+ final String summary = cursor.getString(
+ cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_DESCRIPTION));
+ String mimeType = cursor.getString(
+ cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_MIME_TYPE));
+ if (mimeType == null) {
+ mimeType = "application/octet-stream";
+ }
+
+ final int status = cursor.getInt(
+ cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_STATUS));
+ final long size;
+ if (Downloads.Impl.isStatusCompleted(status)) {
+ size = cursor.getLong(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_TOTAL_BYTES));
+ } else {
+ size = -1;
+ }
+
+ int flags = DocumentsContract.FLAG_SUPPORTS_DELETE;
+ if (mimeType.startsWith("image/")) {
+ flags |= DocumentsContract.FLAG_SUPPORTS_THUMBNAIL;
+ }
+
+ final long lastModified = cursor.getLong(
+ cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_LAST_MODIFICATION));
+
+ result.addRow(new Object[] {
+ id, displayName, size, docId, mimeType, lastModified, flags, summary });
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ switch (sMatcher.match(uri)) {
+ case URI_DOCS_ID: {
+ final String docId = DocumentsContract.getDocId(uri);
+ if (DocumentsContract.ROOT_DOC_ID.equals(docId)) {
+ return DocumentsContract.MIME_TYPE_DIRECTORY;
+ } else {
+ // Delegate to real provider
+ final long token = Binder.clearCallingIdentity();
+ Cursor cursor = null;
+ String mimeType = null;
+ try {
+ final Uri downloadUri = getDownloadUriFromDocument(docId);
+ cursor = getContext().getContentResolver()
+ .query(downloadUri, null, null, null, null);
+ if (cursor.moveToFirst()) {
+ mimeType = cursor.getString(
+ cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_MIME_TYPE));
+ }
+ } finally {
+ IoUtils.closeQuietly(cursor);
+ Binder.restoreCallingIdentity(token);
+ }
+
+ if (mimeType == null) {
+ mimeType = "application/octet-stream";
+ }
+ }
+ }
+ default: {
+ throw new UnsupportedOperationException("Unsupported Uri " + uri);
+ }
+ }
+ }
+
+ private Uri getDownloadUriFromDocument(String docId) {
+ return ContentUris.withAppendedId(
+ Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, getDownloadFromDocument(docId));
+ }
+
+ private long getDownloadFromDocument(String docId) {
+ return Long.parseLong(docId.substring(docId.indexOf(':') + 1));
+ }
+
+ private String getDocumentFromDownload(long id) {
+ return "id:" + id;
+ }
+
+ @Override
+ public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
+ switch (sMatcher.match(uri)) {
+ case URI_DOCS_ID: {
+ final String docId = DocumentsContract.getDocId(uri);
+
+ // Delegate to real provider
+ final long token = Binder.clearCallingIdentity();
+ try {
+ final Uri downloadUri = getDownloadUriFromDocument(docId);
+ return getContext().getContentResolver().openFileDescriptor(downloadUri, mode);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ default: {
+ throw new UnsupportedOperationException("Unsupported Uri " + uri);
+ }
+ }
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ throw new UnsupportedOperationException("Unsupported Uri " + uri);
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ throw new UnsupportedOperationException("Unsupported Uri " + uri);
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ switch (sMatcher.match(uri)) {
+ case URI_DOCS_ID: {
+ final String docId = DocumentsContract.getDocId(uri);
+
+ // Delegate to real provider
+ // TODO: only storage UI should be allowed to delete?
+ final Uri downloadUri = getDownloadUriFromDocument(docId);
+ getContext().getContentResolver().delete(downloadUri, null, null);
+ }
+ default: {
+ throw new UnsupportedOperationException("Unsupported Uri " + uri);
+ }
+ }
+ }
+}