7b6d1521b39d4166fc4ac9c4897142a647807b83
[android/platform/packages/providers/DownloadProvider.git] / src / com / android / providers / downloads / DownloadStorageProvider.java
1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.android.providers.downloads;
18
19 import android.app.DownloadManager;
20 import android.content.ContentProvider;
21 import android.content.ContentUris;
22 import android.content.ContentValues;
23 import android.content.UriMatcher;
24 import android.database.Cursor;
25 import android.database.MatrixCursor;
26 import android.net.Uri;
27 import android.os.Binder;
28 import android.os.ParcelFileDescriptor;
29 import android.provider.BaseColumns;
30 import android.provider.DocumentsContract;
31 import android.provider.DocumentsContract.DocumentColumns;
32 import android.provider.DocumentsContract.RootColumns;
33 import android.provider.Downloads;
34 import android.util.Log;
35
36 import libcore.io.IoUtils;
37
38 import java.io.FileNotFoundException;
39
40 /**
41  * Presents a {@link DocumentsContract} view of {@link DownloadManager}
42  * contents.
43  */
44 public class DownloadStorageProvider extends ContentProvider {
45     private static final String AUTHORITY = "com.android.providers.downloads.storage";
46
47     private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
48
49     private static final int URI_ROOTS = 1;
50     private static final int URI_ROOTS_ID = 2;
51     private static final int URI_DOCS_ID = 3;
52     private static final int URI_DOCS_ID_CONTENTS = 4;
53
54     static {
55         sMatcher.addURI(AUTHORITY, "roots", URI_ROOTS);
56         sMatcher.addURI(AUTHORITY, "roots/*", URI_ROOTS_ID);
57         sMatcher.addURI(AUTHORITY, "roots/*/docs/*", URI_DOCS_ID);
58         sMatcher.addURI(AUTHORITY, "roots/*/docs/*/contents", URI_DOCS_ID_CONTENTS);
59     }
60
61     @Override
62     public boolean onCreate() {
63         return true;
64     }
65
66     @Override
67     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
68             String sortOrder) {
69
70         // TODO: support custom projections
71         final String[] rootsProjection = new String[] {
72                 BaseColumns._ID, RootColumns.ROOT_ID, RootColumns.ROOT_TYPE, RootColumns.ICON,
73                 RootColumns.TITLE, RootColumns.SUMMARY, RootColumns.AVAILABLE_BYTES };
74         final String[] docsProjection = new String[] {
75                 BaseColumns._ID, DocumentColumns.DISPLAY_NAME, DocumentColumns.SIZE,
76                 DocumentColumns.DOC_ID, DocumentColumns.MIME_TYPE, DocumentColumns.LAST_MODIFIED,
77                 DocumentColumns.FLAGS, DocumentColumns.SUMMARY };
78
79         switch (sMatcher.match(uri)) {
80             case URI_ROOTS: {
81                 final MatrixCursor result = new MatrixCursor(rootsProjection);
82                 includeDefaultRoot(result);
83                 return result;
84             }
85             case URI_ROOTS_ID: {
86                 final MatrixCursor result = new MatrixCursor(rootsProjection);
87                 includeDefaultRoot(result);
88                 return result;
89             }
90             case URI_DOCS_ID: {
91                 final String docId = DocumentsContract.getDocId(uri);
92                 final MatrixCursor result = new MatrixCursor(docsProjection);
93
94                 if (DocumentsContract.ROOT_DOC_ID.equals(docId)) {
95                     includeDefaultDocument(result);
96                 } else {
97                     // Delegate to real provider
98                     final long token = Binder.clearCallingIdentity();
99                     Cursor cursor = null;
100                     try {
101                         final Uri downloadUri = getDownloadUriFromDocument(docId);
102                         cursor = getContext()
103                                 .getContentResolver().query(downloadUri, null, null, null, null);
104                         if (cursor.moveToFirst()) {
105                             includeDownloadFromCursor(result, cursor);
106                         }
107                     } finally {
108                         IoUtils.closeQuietly(cursor);
109                         Binder.restoreCallingIdentity(token);
110                     }
111                 }
112                 return result;
113             }
114             case URI_DOCS_ID_CONTENTS: {
115                 final String docId = DocumentsContract.getDocId(uri);
116                 final MatrixCursor result = new MatrixCursor(docsProjection);
117
118                 if (!DocumentsContract.ROOT_DOC_ID.equals(docId)) {
119                     throw new UnsupportedOperationException("Unsupported Uri " + uri);
120                 }
121
122                 // Delegate to real provider
123                 // TODO: filter visible downloads?
124                 final long token = Binder.clearCallingIdentity();
125                 Cursor cursor = null;
126                 try {
127                     cursor = getContext().getContentResolver()
128                             .query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, null, null, null, null);
129                     while (cursor.moveToNext()) {
130                         includeDownloadFromCursor(result, cursor);
131                     }
132                 } finally {
133                     IoUtils.closeQuietly(cursor);
134                     Binder.restoreCallingIdentity(token);
135                 }
136                 return result;
137             }
138             default: {
139                 throw new UnsupportedOperationException("Unsupported Uri " + uri);
140             }
141         }
142     }
143
144     private void includeDefaultRoot(MatrixCursor result) {
145         final int rootType = DocumentsContract.ROOT_TYPE_SHORTCUT;
146         final String rootId = "downloads";
147         final int icon = 0;
148         final String title = getContext().getString(R.string.root_downloads);
149         final String summary = null;
150         final long availableBytes = -1;
151
152         result.addRow(new Object[] {
153                 rootId.hashCode(), rootId, rootType, icon, title, summary,
154                 availableBytes });
155     }
156
157     private void includeDefaultDocument(MatrixCursor result) {
158         final long id = Long.MIN_VALUE;
159         final String docId = DocumentsContract.ROOT_DOC_ID;
160         final String displayName = getContext().getString(R.string.root_downloads);
161         final String summary = null;
162         final String mimeType = DocumentsContract.MIME_TYPE_DIRECTORY;
163         final long size = -1;
164         final long lastModified = -1;
165         final int flags = 0;
166
167         result.addRow(new Object[] {
168                 id, displayName, size, docId, mimeType, lastModified, flags, summary });
169     }
170
171     private void includeDownloadFromCursor(MatrixCursor result, Cursor cursor) {
172         final long id = cursor.getLong(cursor.getColumnIndexOrThrow(Downloads.Impl._ID));
173         final String docId = getDocumentFromDownload(id);
174
175         final String displayName = cursor.getString(
176                 cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_TITLE));
177         final String summary = cursor.getString(
178                 cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_DESCRIPTION));
179         String mimeType = cursor.getString(
180                 cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_MIME_TYPE));
181         if (mimeType == null) {
182             mimeType = "application/octet-stream";
183         }
184
185         final int status = cursor.getInt(
186                 cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_STATUS));
187         final long size;
188         if (Downloads.Impl.isStatusCompleted(status)) {
189             size = cursor.getLong(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_TOTAL_BYTES));
190         } else {
191             size = -1;
192         }
193
194         int flags = DocumentsContract.FLAG_SUPPORTS_DELETE;
195         if (mimeType.startsWith("image/")) {
196             flags |= DocumentsContract.FLAG_SUPPORTS_THUMBNAIL;
197         }
198
199         final long lastModified = cursor.getLong(
200                 cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_LAST_MODIFICATION));
201
202         result.addRow(new Object[] {
203                 id, displayName, size, docId, mimeType, lastModified, flags, summary });
204     }
205
206     @Override
207     public String getType(Uri uri) {
208         switch (sMatcher.match(uri)) {
209             case URI_DOCS_ID: {
210                 final String docId = DocumentsContract.getDocId(uri);
211                 if (DocumentsContract.ROOT_DOC_ID.equals(docId)) {
212                     return DocumentsContract.MIME_TYPE_DIRECTORY;
213                 } else {
214                     // Delegate to real provider
215                     final long token = Binder.clearCallingIdentity();
216                     Cursor cursor = null;
217                     String mimeType = null;
218                     try {
219                         final Uri downloadUri = getDownloadUriFromDocument(docId);
220                         cursor = getContext().getContentResolver()
221                                 .query(downloadUri, null, null, null, null);
222                         if (cursor.moveToFirst()) {
223                             mimeType = cursor.getString(
224                                     cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_MIME_TYPE));
225                         }
226                     } finally {
227                         IoUtils.closeQuietly(cursor);
228                         Binder.restoreCallingIdentity(token);
229                     }
230
231                     if (mimeType == null) {
232                         mimeType = "application/octet-stream";
233                     }
234                 }
235             }
236             default: {
237                 throw new UnsupportedOperationException("Unsupported Uri " + uri);
238             }
239         }
240     }
241
242     private Uri getDownloadUriFromDocument(String docId) {
243         return ContentUris.withAppendedId(
244                 Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, getDownloadFromDocument(docId));
245     }
246
247     private long getDownloadFromDocument(String docId) {
248         return Long.parseLong(docId.substring(docId.indexOf(':') + 1));
249     }
250
251     private String getDocumentFromDownload(long id) {
252         return "id:" + id;
253     }
254
255     @Override
256     public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
257         switch (sMatcher.match(uri)) {
258             case URI_DOCS_ID: {
259                 final String docId = DocumentsContract.getDocId(uri);
260
261                 // Delegate to real provider
262                 final long token = Binder.clearCallingIdentity();
263                 try {
264                     final Uri downloadUri = getDownloadUriFromDocument(docId);
265                     return getContext().getContentResolver().openFileDescriptor(downloadUri, mode);
266                 } finally {
267                     Binder.restoreCallingIdentity(token);
268                 }
269             }
270             default: {
271                 throw new UnsupportedOperationException("Unsupported Uri " + uri);
272             }
273         }
274     }
275
276     @Override
277     public Uri insert(Uri uri, ContentValues values) {
278         throw new UnsupportedOperationException("Unsupported Uri " + uri);
279     }
280
281     @Override
282     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
283         throw new UnsupportedOperationException("Unsupported Uri " + uri);
284     }
285
286     @Override
287     public int delete(Uri uri, String selection, String[] selectionArgs) {
288         switch (sMatcher.match(uri)) {
289             case URI_DOCS_ID: {
290                 final String docId = DocumentsContract.getDocId(uri);
291
292                 // Delegate to real provider
293                 // TODO: only storage UI should be allowed to delete?
294                 final Uri downloadUri = getDownloadUriFromDocument(docId);
295                 getContext().getContentResolver().delete(downloadUri, null, null);
296             }
297             default: {
298                 throw new UnsupportedOperationException("Unsupported Uri " + uri);
299             }
300         }
301     }
302 }