Delegate to documents UI; improve contents.
[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.app.DownloadManager.Query;
21 import android.content.ContentProvider;
22 import android.content.ContentValues;
23 import android.content.Context;
24 import android.content.UriMatcher;
25 import android.database.Cursor;
26 import android.database.MatrixCursor;
27 import android.net.Uri;
28 import android.os.Binder;
29 import android.os.ParcelFileDescriptor;
30 import android.provider.BaseColumns;
31 import android.provider.DocumentsContract;
32 import android.provider.DocumentsContract.DocumentColumns;
33 import android.provider.DocumentsContract.Documents;
34 import android.provider.DocumentsContract.RootColumns;
35 import android.provider.DocumentsContract.Roots;
36
37 import libcore.io.IoUtils;
38
39 import java.io.FileNotFoundException;
40
41 /**
42  * Presents a {@link DocumentsContract} view of {@link DownloadManager}
43  * contents.
44  */
45 public class DownloadStorageProvider extends ContentProvider {
46     public static final String AUTHORITY = Constants.STORAGE_AUTHORITY;
47     public static final String ROOT = Constants.STORAGE_ROOT;
48
49     private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
50
51     private static final int URI_ROOTS = 1;
52     private static final int URI_ROOTS_ID = 2;
53     private static final int URI_DOCS_ID = 3;
54     private static final int URI_DOCS_ID_CONTENTS = 4;
55
56     private DownloadManager mDm;
57     private DownloadManager.Query mBaseQuery;
58
59     static {
60         sMatcher.addURI(AUTHORITY, "roots", URI_ROOTS);
61         sMatcher.addURI(AUTHORITY, "roots/*", URI_ROOTS_ID);
62         sMatcher.addURI(AUTHORITY, "roots/*/docs/*", URI_DOCS_ID);
63         sMatcher.addURI(AUTHORITY, "roots/*/docs/*/contents", URI_DOCS_ID_CONTENTS);
64     }
65
66     @Override
67     public boolean onCreate() {
68         mDm = (DownloadManager) getContext().getSystemService(Context.DOWNLOAD_SERVICE);
69         mDm.setAccessAllDownloads(true);
70         mBaseQuery = new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true);
71
72         return true;
73     }
74
75     @Override
76     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
77             String sortOrder) {
78
79         // TODO: support custom projections
80         final String[] rootsProjection = new String[] {
81                 BaseColumns._ID, RootColumns.ROOT_ID, RootColumns.ROOT_TYPE, RootColumns.ICON,
82                 RootColumns.TITLE, RootColumns.SUMMARY, RootColumns.AVAILABLE_BYTES };
83         final String[] docsProjection = new String[] {
84                 BaseColumns._ID, DocumentColumns.DISPLAY_NAME, DocumentColumns.SIZE,
85                 DocumentColumns.DOC_ID, DocumentColumns.MIME_TYPE, DocumentColumns.LAST_MODIFIED,
86                 DocumentColumns.FLAGS, DocumentColumns.SUMMARY };
87
88         switch (sMatcher.match(uri)) {
89             case URI_ROOTS: {
90                 final MatrixCursor result = new MatrixCursor(rootsProjection);
91                 includeDefaultRoot(result);
92                 return result;
93             }
94             case URI_ROOTS_ID: {
95                 final MatrixCursor result = new MatrixCursor(rootsProjection);
96                 includeDefaultRoot(result);
97                 return result;
98             }
99             case URI_DOCS_ID: {
100                 final String docId = DocumentsContract.getDocId(uri);
101                 final MatrixCursor result = new MatrixCursor(docsProjection);
102
103                 if (Documents.DOC_ID_ROOT.equals(docId)) {
104                     includeDefaultDocument(result);
105                 } else {
106                     // Delegate to real provider
107                     final long token = Binder.clearCallingIdentity();
108                     Cursor cursor = null;
109                     try {
110                         cursor = mDm.query(
111                                 new Query().setFilterById(getDownloadFromDocument(docId)));
112                         if (cursor.moveToFirst()) {
113                             includeDownloadFromCursor(result, cursor);
114                         }
115                     } finally {
116                         IoUtils.closeQuietly(cursor);
117                         Binder.restoreCallingIdentity(token);
118                     }
119                 }
120                 return result;
121             }
122             case URI_DOCS_ID_CONTENTS: {
123                 final String docId = DocumentsContract.getDocId(uri);
124                 final MatrixCursor result = new MatrixCursor(docsProjection);
125
126                 if (!Documents.DOC_ID_ROOT.equals(docId)) {
127                     throw new UnsupportedOperationException("Unsupported Uri " + uri);
128                 }
129
130                 // Delegate to real provider
131                 final long token = Binder.clearCallingIdentity();
132                 Cursor cursor = null;
133                 try {
134                     cursor = mDm.query(mBaseQuery);
135                     while (cursor.moveToNext()) {
136                         includeDownloadFromCursor(result, cursor);
137                     }
138                 } finally {
139                     IoUtils.closeQuietly(cursor);
140                     Binder.restoreCallingIdentity(token);
141                 }
142                 return result;
143             }
144             default: {
145                 throw new UnsupportedOperationException("Unsupported Uri " + uri);
146             }
147         }
148     }
149
150     private void includeDefaultRoot(MatrixCursor result) {
151         final int rootType = Roots.ROOT_TYPE_SHORTCUT;
152         final String rootId = ROOT;
153         final int icon = 0;
154         final String title = getContext().getString(R.string.root_downloads);
155         final String summary = null;
156         final long availableBytes = -1;
157
158         result.addRow(new Object[] {
159                 rootId.hashCode(), rootId, rootType, icon, title, summary,
160                 availableBytes });
161     }
162
163     private void includeDefaultDocument(MatrixCursor result) {
164         final long id = Long.MIN_VALUE;
165         final String docId = Documents.DOC_ID_ROOT;
166         final String displayName = getContext().getString(R.string.root_downloads);
167         final String summary = null;
168         final String mimeType = Documents.MIME_TYPE_DIR;
169         final long size = -1;
170         final long lastModified = -1;
171         final int flags = 0;
172
173         result.addRow(new Object[] {
174                 id, displayName, size, docId, mimeType, lastModified, flags, summary });
175     }
176
177     private void includeDownloadFromCursor(MatrixCursor result, Cursor cursor) {
178         final long id = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID));
179         final String docId = getDocumentFromDownload(id);
180
181         final String displayName = cursor.getString(
182                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TITLE));
183         String summary = cursor.getString(
184                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_DESCRIPTION));
185         String mimeType = cursor.getString(
186                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE));
187         if (mimeType == null) {
188             mimeType = "application/octet-stream";
189         }
190         Long size = null;
191
192         final int status = cursor.getInt(
193                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS));
194         switch (status) {
195             case DownloadManager.STATUS_SUCCESSFUL:
196                 size = cursor.getLong(
197                         cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
198                 if (size == -1) {
199                     size = null;
200                 }
201                 break;
202             case DownloadManager.STATUS_PAUSED:
203                 mimeType = null;
204                 summary = getContext().getString(R.string.download_queued);
205                 break;
206             case DownloadManager.STATUS_PENDING:
207                 mimeType = null;
208                 summary = getContext().getString(R.string.download_queued);
209                 break;
210             case DownloadManager.STATUS_RUNNING:
211                 mimeType = null;
212                 summary = getContext().getString(R.string.download_running);
213                 break;
214             case DownloadManager.STATUS_FAILED:
215             default:
216                 mimeType = null;
217                 summary = getContext().getString(R.string.download_error);
218                 break;
219         }
220
221         int flags = Documents.FLAG_SUPPORTS_DELETE;
222         if (mimeType != null && mimeType.startsWith("image/")) {
223             flags |= Documents.FLAG_SUPPORTS_THUMBNAIL;
224         }
225
226         final long lastModified = cursor.getLong(
227                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP));
228
229         result.addRow(new Object[] {
230                 id, displayName, size, docId, mimeType, lastModified, flags, summary });
231     }
232
233     @Override
234     public String getType(Uri uri) {
235         switch (sMatcher.match(uri)) {
236             case URI_ROOTS: {
237                 return Roots.MIME_TYPE_DIR;
238             }
239             case URI_ROOTS_ID: {
240                 return Roots.MIME_TYPE_ITEM;
241             }
242             case URI_DOCS_ID: {
243                 final String docId = DocumentsContract.getDocId(uri);
244                 if (Documents.DOC_ID_ROOT.equals(docId)) {
245                     return Documents.MIME_TYPE_DIR;
246                 } else {
247                     // Delegate to real provider
248                     final long token = Binder.clearCallingIdentity();
249                     Cursor cursor = null;
250                     String mimeType = null;
251                     try {
252                         cursor = mDm.query(
253                                 new Query().setFilterById(getDownloadFromDocument(docId)));
254                         if (cursor.moveToFirst()) {
255                             mimeType = cursor.getString(
256                                     cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE));
257                         }
258                     } finally {
259                         IoUtils.closeQuietly(cursor);
260                         Binder.restoreCallingIdentity(token);
261                     }
262
263                     if (mimeType == null) {
264                         mimeType = "application/octet-stream";
265                     }
266                 }
267             }
268             default: {
269                 throw new UnsupportedOperationException("Unsupported Uri " + uri);
270             }
271         }
272     }
273
274     private long getDownloadFromDocument(String docId) {
275         return Long.parseLong(docId.substring(docId.indexOf(':') + 1));
276     }
277
278     private String getDocumentFromDownload(long id) {
279         return "id:" + id;
280     }
281
282     @Override
283     public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
284         switch (sMatcher.match(uri)) {
285             case URI_DOCS_ID: {
286                 final String docId = DocumentsContract.getDocId(uri);
287
288                 if (!"r".equals(mode)) {
289                     throw new IllegalArgumentException("Downloads are read-only");
290                 }
291
292                 // Delegate to real provider
293                 final long token = Binder.clearCallingIdentity();
294                 try {
295                     return mDm.openDownloadedFile(getDownloadFromDocument(docId));
296                 } finally {
297                     Binder.restoreCallingIdentity(token);
298                 }
299             }
300             default: {
301                 throw new UnsupportedOperationException("Unsupported Uri " + uri);
302             }
303         }
304     }
305
306     @Override
307     public Uri insert(Uri uri, ContentValues values) {
308         throw new UnsupportedOperationException("Unsupported Uri " + uri);
309     }
310
311     @Override
312     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
313         throw new UnsupportedOperationException("Unsupported Uri " + uri);
314     }
315
316     @Override
317     public int delete(Uri uri, String selection, String[] selectionArgs) {
318         switch (sMatcher.match(uri)) {
319             case URI_DOCS_ID: {
320                 final String docId = DocumentsContract.getDocId(uri);
321
322                 // Delegate to real provider
323                 // TODO: only storage UI should be allowed to delete?
324                 mDm.remove(getDownloadFromDocument(docId));
325             }
326             default: {
327                 throw new UnsupportedOperationException("Unsupported Uri " + uri);
328             }
329         }
330     }
331 }