Omit duplicate images, query for management UI.
[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.Context;
22 import android.content.res.AssetFileDescriptor;
23 import android.database.Cursor;
24 import android.database.MatrixCursor;
25 import android.database.MatrixCursor.RowBuilder;
26 import android.graphics.Point;
27 import android.os.Binder;
28 import android.os.CancellationSignal;
29 import android.os.ParcelFileDescriptor;
30 import android.provider.DocumentsContract;
31 import android.provider.DocumentsContract.Document;
32 import android.provider.DocumentsContract.Root;
33 import android.provider.DocumentsProvider;
34 import android.text.TextUtils;
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 DocumentsProvider {
45     private static final String DOC_ID_ROOT = Constants.STORAGE_ROOT_ID;
46
47     private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
48             Root.COLUMN_ROOT_ID, Root.COLUMN_ROOT_TYPE, Root.COLUMN_FLAGS, Root.COLUMN_ICON,
49             Root.COLUMN_TITLE, Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID,
50             Root.COLUMN_AVAILABLE_BYTES,
51     };
52
53     private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
54             Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
55             Document.COLUMN_SUMMARY, Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS,
56             Document.COLUMN_SIZE,
57     };
58
59     private DownloadManager mDm;
60
61     @Override
62     public boolean onCreate() {
63         mDm = (DownloadManager) getContext().getSystemService(Context.DOWNLOAD_SERVICE);
64         mDm.setAccessAllDownloads(true);
65         return true;
66     }
67
68     private static String[] resolveRootProjection(String[] projection) {
69         return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
70     }
71
72     private static String[] resolveDocumentProjection(String[] projection) {
73         return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
74     }
75
76     private void copyNotificationUri(MatrixCursor result, Cursor cursor) {
77         result.setNotificationUri(getContext().getContentResolver(), cursor.getNotificationUri());
78     }
79
80     @Override
81     public Cursor queryRoots(String[] projection) throws FileNotFoundException {
82         final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
83         final RowBuilder row = result.newRow();
84         row.add(Root.COLUMN_ROOT_ID, DOC_ID_ROOT);
85         row.add(Root.COLUMN_ROOT_TYPE, Root.ROOT_TYPE_SHORTCUT);
86         row.add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_RECENTS);
87         row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher_download);
88         row.add(Root.COLUMN_TITLE, getContext().getString(R.string.root_downloads));
89         row.add(Root.COLUMN_DOCUMENT_ID, DOC_ID_ROOT);
90         return result;
91     }
92
93     @Override
94     public void deleteDocument(String docId) throws FileNotFoundException {
95         // Delegate to real provider
96         final long token = Binder.clearCallingIdentity();
97         try {
98             if (mDm.remove(Long.parseLong(docId)) != 1) {
99                 throw new IllegalStateException("Failed to delete " + docId);
100             }
101         } finally {
102             Binder.restoreCallingIdentity(token);
103         }
104     }
105
106     @Override
107     public Cursor queryDocument(String docId, String[] projection) throws FileNotFoundException {
108         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
109
110         if (DOC_ID_ROOT.equals(docId)) {
111             includeDefaultDocument(result);
112         } else {
113             // Delegate to real provider
114             final long token = Binder.clearCallingIdentity();
115             Cursor cursor = null;
116             try {
117                 cursor = mDm.query(new Query().setFilterById(Long.parseLong(docId)));
118                 copyNotificationUri(result, cursor);
119                 if (cursor.moveToFirst()) {
120                     includeDownloadFromCursor(result, cursor);
121                 }
122             } finally {
123                 IoUtils.closeQuietly(cursor);
124                 Binder.restoreCallingIdentity(token);
125             }
126         }
127         return result;
128     }
129
130     @Override
131     public Cursor queryChildDocuments(String docId, String[] projection, String sortOrder)
132             throws FileNotFoundException {
133         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
134
135         // Delegate to real provider
136         final long token = Binder.clearCallingIdentity();
137         Cursor cursor = null;
138         try {
139             cursor = mDm.query(new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true)
140                     .setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL));
141             copyNotificationUri(result, cursor);
142             while (cursor.moveToNext()) {
143                 includeDownloadFromCursor(result, cursor);
144             }
145         } finally {
146             IoUtils.closeQuietly(cursor);
147             Binder.restoreCallingIdentity(token);
148         }
149         return result;
150     }
151
152     @Override
153     public Cursor queryChildDocumentsForManage(
154             String parentDocumentId, String[] projection, String sortOrder)
155             throws FileNotFoundException {
156         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
157
158         // Delegate to real provider
159         final long token = Binder.clearCallingIdentity();
160         Cursor cursor = null;
161         try {
162             cursor = mDm.query(
163                     new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true));
164             copyNotificationUri(result, cursor);
165             while (cursor.moveToNext()) {
166                 includeDownloadFromCursor(result, cursor);
167             }
168         } finally {
169             IoUtils.closeQuietly(cursor);
170             Binder.restoreCallingIdentity(token);
171         }
172         return result;
173     }
174
175     @Override
176     public Cursor queryRecentDocuments(String rootId, String[] projection)
177             throws FileNotFoundException {
178         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
179
180         // Delegate to real provider
181         final long token = Binder.clearCallingIdentity();
182         Cursor cursor = null;
183         try {
184             cursor = mDm.query(new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true)
185                     .setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL));
186             copyNotificationUri(result, cursor);
187             while (cursor.moveToNext() && result.getCount() < 12) {
188                 final String mimeType = cursor.getString(
189                         cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE));
190                 final String uri = cursor.getString(
191                         cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIAPROVIDER_URI));
192
193                 // Skip images that have been inserted into the MediaStore so we
194                 // don't duplicate them in the recents list.
195                 if (mimeType == null
196                         || (mimeType.startsWith("image/") && !TextUtils.isEmpty(uri))) {
197                     continue;
198                 }
199
200                 includeDownloadFromCursor(result, cursor);
201             }
202         } finally {
203             IoUtils.closeQuietly(cursor);
204             Binder.restoreCallingIdentity(token);
205         }
206         return result;
207     }
208
209     @Override
210     public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal)
211             throws FileNotFoundException {
212         if (!"r".equals(mode)) {
213             throw new IllegalArgumentException("Downloads are read-only");
214         }
215
216         // Delegate to real provider
217         final long token = Binder.clearCallingIdentity();
218         try {
219             return mDm.openDownloadedFile(Long.parseLong(docId));
220         } finally {
221             Binder.restoreCallingIdentity(token);
222         }
223     }
224
225     @Override
226     public AssetFileDescriptor openDocumentThumbnail(
227             String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
228         // TODO: extend ExifInterface to support fds
229         final ParcelFileDescriptor pfd = openDocument(docId, "r", signal);
230         return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
231     }
232
233     private void includeDefaultDocument(MatrixCursor result) {
234         final RowBuilder row = result.newRow();
235         row.add(Document.COLUMN_DOCUMENT_ID, DOC_ID_ROOT);
236         row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
237         row.add(Document.COLUMN_FLAGS, Document.FLAG_DIR_PREFERS_LAST_MODIFIED);
238     }
239
240     private void includeDownloadFromCursor(MatrixCursor result, Cursor cursor) {
241         final long id = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID));
242         final String docId = String.valueOf(id);
243
244         final String displayName = cursor.getString(
245                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TITLE));
246         String summary = cursor.getString(
247                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_DESCRIPTION));
248         String mimeType = cursor.getString(
249                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE));
250         if (mimeType == null) {
251             mimeType = "application/octet-stream";
252         }
253         Long size = cursor.getLong(
254                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
255         if (size == -1) {
256             size = null;
257         }
258
259         final int status = cursor.getInt(
260                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS));
261         switch (status) {
262             case DownloadManager.STATUS_SUCCESSFUL:
263                 break;
264             case DownloadManager.STATUS_PAUSED:
265                 summary = getContext().getString(R.string.download_queued);
266                 break;
267             case DownloadManager.STATUS_PENDING:
268                 summary = getContext().getString(R.string.download_queued);
269                 break;
270             case DownloadManager.STATUS_RUNNING:
271                 final long progress = cursor.getLong(cursor.getColumnIndexOrThrow(
272                         DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
273                 if (size != null) {
274                     final long percent = progress * 100 / size;
275                     summary = getContext().getString(R.string.download_running_percent, percent);
276                 } else {
277                     summary = getContext().getString(R.string.download_running);
278                 }
279                 break;
280             case DownloadManager.STATUS_FAILED:
281             default:
282                 summary = getContext().getString(R.string.download_error);
283                 break;
284         }
285
286         int flags = Document.FLAG_SUPPORTS_DELETE;
287         if (mimeType != null && mimeType.startsWith("image/")) {
288             flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
289         }
290
291         final long lastModified = cursor.getLong(
292                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP));
293
294         final RowBuilder row = result.newRow();
295         row.add(Document.COLUMN_DOCUMENT_ID, docId);
296         row.add(Document.COLUMN_DISPLAY_NAME, displayName);
297         row.add(Document.COLUMN_SUMMARY, summary);
298         row.add(Document.COLUMN_SIZE, size);
299         row.add(Document.COLUMN_MIME_TYPE, mimeType);
300         row.add(Document.COLUMN_LAST_MODIFIED, lastModified);
301         row.add(Document.COLUMN_FLAGS, flags);
302     }
303 }