Request access to the filename column.
[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.ContentResolver;
22 import android.content.Context;
23 import android.content.res.AssetFileDescriptor;
24 import android.database.Cursor;
25 import android.database.MatrixCursor;
26 import android.database.MatrixCursor.RowBuilder;
27 import android.graphics.Point;
28 import android.net.Uri;
29 import android.os.Binder;
30 import android.os.CancellationSignal;
31 import android.os.Environment;
32 import android.os.FileUtils;
33 import android.os.ParcelFileDescriptor;
34 import android.provider.DocumentsContract;
35 import android.provider.DocumentsContract.Document;
36 import android.provider.DocumentsContract.Root;
37 import android.provider.DocumentsProvider;
38 import android.support.provider.DocumentArchiveHelper;
39 import android.text.TextUtils;
40 import android.webkit.MimeTypeMap;
41
42 import libcore.io.IoUtils;
43
44 import java.io.File;
45 import java.io.FileNotFoundException;
46 import java.io.IOException;
47 import java.text.NumberFormat;
48
49 /**
50  * Presents a {@link DocumentsContract} view of {@link DownloadManager}
51  * contents.
52  */
53 public class DownloadStorageProvider extends DocumentsProvider {
54     private static final String AUTHORITY = Constants.STORAGE_AUTHORITY;
55     private static final String DOC_ID_ROOT = Constants.STORAGE_ROOT_ID;
56
57     private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
58             Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON,
59             Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID,
60     };
61
62     private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
63             Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
64             Document.COLUMN_SUMMARY, Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS,
65             Document.COLUMN_SIZE,
66     };
67
68     private DownloadManager mDm;
69     private DocumentArchiveHelper mArchiveHelper;
70
71     @Override
72     public boolean onCreate() {
73         mDm = (DownloadManager) getContext().getSystemService(Context.DOWNLOAD_SERVICE);
74         mDm.setAccessAllDownloads(true);
75         mDm.setAccessFilename(true);
76         mArchiveHelper = new DocumentArchiveHelper(this, ':');
77         return true;
78     }
79
80     private static String[] resolveRootProjection(String[] projection) {
81         return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
82     }
83
84     private static String[] resolveDocumentProjection(String[] projection) {
85         return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
86     }
87
88     private void copyNotificationUri(MatrixCursor result, Cursor cursor) {
89         result.setNotificationUri(getContext().getContentResolver(), cursor.getNotificationUri());
90     }
91
92     static void onDownloadProviderDelete(Context context, long id) {
93         final Uri uri = DocumentsContract.buildDocumentUri(AUTHORITY, Long.toString(id));
94         context.revokeUriPermission(uri, ~0);
95     }
96
97     @Override
98     public Cursor queryRoots(String[] projection) throws FileNotFoundException {
99         final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
100         final RowBuilder row = result.newRow();
101         row.add(Root.COLUMN_ROOT_ID, DOC_ID_ROOT);
102         row.add(Root.COLUMN_FLAGS,
103                 Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_RECENTS | Root.FLAG_SUPPORTS_CREATE);
104         row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher_download);
105         row.add(Root.COLUMN_TITLE, getContext().getString(R.string.root_downloads));
106         row.add(Root.COLUMN_DOCUMENT_ID, DOC_ID_ROOT);
107         return result;
108     }
109
110     @Override
111     public String createDocument(String docId, String mimeType, String displayName)
112             throws FileNotFoundException {
113         displayName = FileUtils.buildValidFatFilename(displayName);
114
115         if (Document.MIME_TYPE_DIR.equals(mimeType)) {
116             throw new FileNotFoundException("Directory creation not supported");
117         }
118
119         final File parent = Environment.getExternalStoragePublicDirectory(
120                 Environment.DIRECTORY_DOWNLOADS);
121         parent.mkdirs();
122
123         // Delegate to real provider
124         final long token = Binder.clearCallingIdentity();
125         try {
126             final File file = FileUtils.buildUniqueFile(parent, mimeType, displayName);
127
128             try {
129                 if (!file.createNewFile()) {
130                     throw new IllegalStateException("Failed to touch " + file);
131                 }
132             } catch (IOException e) {
133                 throw new IllegalStateException("Failed to touch " + file + ": " + e);
134             }
135
136             return Long.toString(mDm.addCompletedDownload(
137                     file.getName(), file.getName(), true, mimeType, file.getAbsolutePath(), 0L,
138                     false, true));
139         } finally {
140             Binder.restoreCallingIdentity(token);
141         }
142     }
143
144     @Override
145     public void deleteDocument(String docId) throws FileNotFoundException {
146         // Delegate to real provider
147         final long token = Binder.clearCallingIdentity();
148         try {
149             if (mDm.remove(Long.parseLong(docId)) != 1) {
150                 throw new IllegalStateException("Failed to delete " + docId);
151             }
152         } finally {
153             Binder.restoreCallingIdentity(token);
154         }
155     }
156
157     @Override
158     public Cursor queryDocument(String docId, String[] projection) throws FileNotFoundException {
159         if (mArchiveHelper.isArchivedDocument(docId)) {
160             return mArchiveHelper.queryDocument(docId, projection);
161         }
162
163         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
164
165         if (DOC_ID_ROOT.equals(docId)) {
166             includeDefaultDocument(result);
167         } else {
168             // Delegate to real provider
169             final long token = Binder.clearCallingIdentity();
170             Cursor cursor = null;
171             try {
172                 cursor = mDm.query(new Query().setFilterById(Long.parseLong(docId)));
173                 copyNotificationUri(result, cursor);
174                 if (cursor.moveToFirst()) {
175                     // We don't know if this queryDocument() call is from Downloads (manage)
176                     // or Files. Safely assume it's Files.
177                     includeDownloadFromCursor(result, cursor, false /* forManage */);
178                 }
179             } finally {
180                 IoUtils.closeQuietly(cursor);
181                 Binder.restoreCallingIdentity(token);
182             }
183         }
184         return result;
185     }
186
187     @Override
188     public Cursor queryChildDocuments(String docId, String[] projection, String sortOrder)
189             throws FileNotFoundException {
190         if (mArchiveHelper.isArchivedDocument(docId) ||
191                 mArchiveHelper.isSupportedArchiveType(getDocumentType(docId))) {
192             return mArchiveHelper.queryChildDocuments(docId, projection, sortOrder);
193         }
194
195         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
196
197         // Delegate to real provider
198         final long token = Binder.clearCallingIdentity();
199         Cursor cursor = null;
200         try {
201             cursor = mDm.query(new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true)
202                     .setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL));
203             copyNotificationUri(result, cursor);
204             while (cursor.moveToNext()) {
205                 includeDownloadFromCursor(result, cursor, false /* forManage */);
206             }
207         } finally {
208             IoUtils.closeQuietly(cursor);
209             Binder.restoreCallingIdentity(token);
210         }
211         return result;
212     }
213
214     @Override
215     public Cursor queryChildDocumentsForManage(
216             String parentDocumentId, String[] projection, String sortOrder)
217             throws FileNotFoundException {
218         if (mArchiveHelper.isArchivedDocument(parentDocumentId)) {
219             return mArchiveHelper.queryDocument(parentDocumentId, projection);
220         }
221
222         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
223
224         // Delegate to real provider
225         final long token = Binder.clearCallingIdentity();
226         Cursor cursor = null;
227         try {
228             cursor = mDm.query(
229                     new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true));
230             copyNotificationUri(result, cursor);
231             while (cursor.moveToNext()) {
232                 includeDownloadFromCursor(result, cursor, true /* forManage */);
233             }
234         } finally {
235             IoUtils.closeQuietly(cursor);
236             Binder.restoreCallingIdentity(token);
237         }
238         return result;
239     }
240
241     @Override
242     public Cursor queryRecentDocuments(String rootId, String[] projection)
243             throws FileNotFoundException {
244         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
245
246         // Delegate to real provider
247         final long token = Binder.clearCallingIdentity();
248         Cursor cursor = null;
249         try {
250             cursor = mDm.query(new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true)
251                     .setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL));
252             copyNotificationUri(result, cursor);
253             while (cursor.moveToNext() && result.getCount() < 12) {
254                 final String mimeType = cursor.getString(
255                         cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE));
256                 final String uri = cursor.getString(
257                         cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIAPROVIDER_URI));
258
259                 // Skip images that have been inserted into the MediaStore so we
260                 // don't duplicate them in the recents list.
261                 if (mimeType == null
262                         || (mimeType.startsWith("image/") && !TextUtils.isEmpty(uri))) {
263                     continue;
264                 }
265
266                 includeDownloadFromCursor(result, cursor, false /* forManage */);
267             }
268         } finally {
269             IoUtils.closeQuietly(cursor);
270             Binder.restoreCallingIdentity(token);
271         }
272         return result;
273     }
274
275     @Override
276     public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal)
277             throws FileNotFoundException {
278         if (mArchiveHelper.isArchivedDocument(docId)) {
279             return mArchiveHelper.openDocument(docId, mode, signal);
280         }
281
282         // Delegate to real provider
283         final long token = Binder.clearCallingIdentity();
284         try {
285             final long id = Long.parseLong(docId);
286             final ContentResolver resolver = getContext().getContentResolver();
287             return resolver.openFileDescriptor(mDm.getDownloadUri(id), mode, signal);
288         } finally {
289             Binder.restoreCallingIdentity(token);
290         }
291     }
292
293     @Override
294     public AssetFileDescriptor openDocumentThumbnail(
295             String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
296         // TODO: extend ExifInterface to support fds
297         final ParcelFileDescriptor pfd = openDocument(docId, "r", signal);
298         return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
299     }
300
301     private void includeDefaultDocument(MatrixCursor result) {
302         final RowBuilder row = result.newRow();
303         row.add(Document.COLUMN_DOCUMENT_ID, DOC_ID_ROOT);
304         row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
305         row.add(Document.COLUMN_FLAGS,
306                 Document.FLAG_DIR_PREFERS_LAST_MODIFIED | Document.FLAG_DIR_SUPPORTS_CREATE);
307     }
308
309     private void includeDownloadFromCursor(MatrixCursor result, Cursor cursor, boolean forManage) {
310         final long id = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID));
311         final String docId = String.valueOf(id);
312
313         final String displayName = cursor.getString(
314                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TITLE));
315         String summary = cursor.getString(
316                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_DESCRIPTION));
317         String mimeType = cursor.getString(
318                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE));
319         if (mimeType == null) {
320             // Provide fake MIME type so it's openable
321             mimeType = "vnd.android.document/file";
322         }
323         Long size = cursor.getLong(
324                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
325         if (size == -1) {
326             size = null;
327         }
328
329         final int status = cursor.getInt(
330                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS));
331         switch (status) {
332             case DownloadManager.STATUS_SUCCESSFUL:
333                 break;
334             case DownloadManager.STATUS_PAUSED:
335                 summary = getContext().getString(R.string.download_queued);
336                 break;
337             case DownloadManager.STATUS_PENDING:
338                 summary = getContext().getString(R.string.download_queued);
339                 break;
340             case DownloadManager.STATUS_RUNNING:
341                 final long progress = cursor.getLong(cursor.getColumnIndexOrThrow(
342                         DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
343                 if (size != null) {
344                     String percent =
345                             NumberFormat.getPercentInstance().format((double) progress / size);
346                     summary = getContext().getString(R.string.download_running_percent, percent);
347                 } else {
348                     summary = getContext().getString(R.string.download_running);
349                 }
350                 break;
351             case DownloadManager.STATUS_FAILED:
352             default:
353                 summary = getContext().getString(R.string.download_error);
354                 break;
355         }
356
357         int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_WRITE;
358         if (mimeType.startsWith("image/")) {
359             flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
360         }
361
362         // TODO: Remove forManage and move the logic to DocumentsUI. b/26321218.
363         if (!forManage && mArchiveHelper.isSupportedArchiveType(mimeType)) {
364             flags |= Document.FLAG_ARCHIVE;
365         }
366
367         final long lastModified = cursor.getLong(
368                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP));
369
370         final RowBuilder row = result.newRow();
371         row.add(Document.COLUMN_DOCUMENT_ID, docId);
372         row.add(Document.COLUMN_DISPLAY_NAME, displayName);
373         row.add(Document.COLUMN_SUMMARY, summary);
374         row.add(Document.COLUMN_SIZE, size);
375         row.add(Document.COLUMN_MIME_TYPE, mimeType);
376         row.add(Document.COLUMN_LAST_MODIFIED, lastModified);
377         row.add(Document.COLUMN_FLAGS, flags);
378
379         final String localFilePath = cursor.getString(
380                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_FILENAME));
381         if (localFilePath != null) {
382             row.add(DocumentArchiveHelper.COLUMN_LOCAL_FILE_PATH, localFilePath);
383         }
384     }
385
386     /**
387      * Remove file extension from name, but only if exact MIME type mapping
388      * exists. This means we can reapply the extension later.
389      */
390     private static String removeExtension(String mimeType, String name) {
391         final int lastDot = name.lastIndexOf('.');
392         if (lastDot >= 0) {
393             final String extension = name.substring(lastDot + 1);
394             final String nameMime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
395             if (mimeType.equals(nameMime)) {
396                 return name.substring(0, lastDot);
397             }
398         }
399         return name;
400     }
401
402     /**
403      * Add file extension to name, but only if exact MIME type mapping exists.
404      */
405     private static String addExtension(String mimeType, String name) {
406         final String extension = MimeTypeMap.getSingleton()
407                 .getExtensionFromMimeType(mimeType);
408         if (extension != null) {
409             return name + "." + extension;
410         }
411         return name;
412     }
413 }