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
onCreate()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 
resolveRootProjection(String[] projection)80     private static String[] resolveRootProjection(String[] projection) {
81         return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
82     }
83 
resolveDocumentProjection(String[] projection)84     private static String[] resolveDocumentProjection(String[] projection) {
85         return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
86     }
87 
copyNotificationUri(MatrixCursor result, Cursor cursor)88     private void copyNotificationUri(MatrixCursor result, Cursor cursor) {
89         result.setNotificationUri(getContext().getContentResolver(), cursor.getNotificationUri());
90     }
91 
onDownloadProviderDelete(Context context, long id)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
queryRoots(String[] projection)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
createDocument(String docId, String mimeType, String displayName)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
deleteDocument(String docId)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
renameDocument(String documentId, String displayName)158     public String renameDocument(String documentId, String displayName)
159             throws FileNotFoundException {
160         displayName = FileUtils.buildValidFatFilename(displayName);
161 
162         final long token = Binder.clearCallingIdentity();
163         try {
164             final long id = Long.parseLong(documentId);
165 
166             if (!mDm.rename(getContext(), id, displayName)) {
167                 throw new IllegalStateException(
168                         "Failed to rename to " + displayName + " in downloadsManager");
169             }
170         } finally {
171             Binder.restoreCallingIdentity(token);
172         }
173         return null;
174     }
175 
176     @Override
queryDocument(String docId, String[] projection)177     public Cursor queryDocument(String docId, String[] projection) throws FileNotFoundException {
178         if (mArchiveHelper.isArchivedDocument(docId)) {
179             return mArchiveHelper.queryDocument(docId, projection);
180         }
181 
182         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
183 
184         if (DOC_ID_ROOT.equals(docId)) {
185             includeDefaultDocument(result);
186         } else {
187             // Delegate to real provider
188             final long token = Binder.clearCallingIdentity();
189             Cursor cursor = null;
190             try {
191                 cursor = mDm.query(new Query().setFilterById(Long.parseLong(docId)));
192                 copyNotificationUri(result, cursor);
193                 if (cursor.moveToFirst()) {
194                     // We don't know if this queryDocument() call is from Downloads (manage)
195                     // or Files. Safely assume it's Files.
196                     includeDownloadFromCursor(result, cursor);
197                 }
198             } finally {
199                 IoUtils.closeQuietly(cursor);
200                 Binder.restoreCallingIdentity(token);
201             }
202         }
203         return result;
204     }
205 
206     @Override
queryChildDocuments(String docId, String[] projection, String sortOrder)207     public Cursor queryChildDocuments(String docId, String[] projection, String sortOrder)
208             throws FileNotFoundException {
209         if (mArchiveHelper.isArchivedDocument(docId) ||
210                 mArchiveHelper.isSupportedArchiveType(getDocumentType(docId))) {
211             return mArchiveHelper.queryChildDocuments(docId, projection, sortOrder);
212         }
213 
214         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
215 
216         // Delegate to real provider
217         final long token = Binder.clearCallingIdentity();
218         Cursor cursor = null;
219         try {
220             cursor = mDm.query(new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true)
221                     .setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL));
222             copyNotificationUri(result, cursor);
223             while (cursor.moveToNext()) {
224                 includeDownloadFromCursor(result, cursor);
225             }
226         } finally {
227             IoUtils.closeQuietly(cursor);
228             Binder.restoreCallingIdentity(token);
229         }
230         return result;
231     }
232 
233     @Override
queryChildDocumentsForManage( String parentDocumentId, String[] projection, String sortOrder)234     public Cursor queryChildDocumentsForManage(
235             String parentDocumentId, String[] projection, String sortOrder)
236             throws FileNotFoundException {
237         if (mArchiveHelper.isArchivedDocument(parentDocumentId)) {
238             return mArchiveHelper.queryDocument(parentDocumentId, projection);
239         }
240 
241         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
242 
243         // Delegate to real provider
244         final long token = Binder.clearCallingIdentity();
245         Cursor cursor = null;
246         try {
247             cursor = mDm.query(
248                     new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true));
249             copyNotificationUri(result, cursor);
250             while (cursor.moveToNext()) {
251                 includeDownloadFromCursor(result, cursor);
252             }
253         } finally {
254             IoUtils.closeQuietly(cursor);
255             Binder.restoreCallingIdentity(token);
256         }
257         return result;
258     }
259 
260     @Override
queryRecentDocuments(String rootId, String[] projection)261     public Cursor queryRecentDocuments(String rootId, String[] projection)
262             throws FileNotFoundException {
263         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
264 
265         // Delegate to real provider
266         final long token = Binder.clearCallingIdentity();
267         Cursor cursor = null;
268         try {
269             cursor = mDm.query(new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true)
270                     .setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL));
271             copyNotificationUri(result, cursor);
272             while (cursor.moveToNext() && result.getCount() < 12) {
273                 final String mimeType = cursor.getString(
274                         cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE));
275                 final String uri = cursor.getString(
276                         cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIAPROVIDER_URI));
277 
278                 // Skip images that have been inserted into the MediaStore so we
279                 // don't duplicate them in the recents list.
280                 if (mimeType == null
281                         || (mimeType.startsWith("image/") && !TextUtils.isEmpty(uri))) {
282                     continue;
283                 }
284 
285                 includeDownloadFromCursor(result, cursor);
286             }
287         } finally {
288             IoUtils.closeQuietly(cursor);
289             Binder.restoreCallingIdentity(token);
290         }
291         return result;
292     }
293 
294     @Override
openDocument(String docId, String mode, CancellationSignal signal)295     public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal)
296             throws FileNotFoundException {
297         if (mArchiveHelper.isArchivedDocument(docId)) {
298             return mArchiveHelper.openDocument(docId, mode, signal);
299         }
300 
301         // Delegate to real provider
302         final long token = Binder.clearCallingIdentity();
303         try {
304             final long id = Long.parseLong(docId);
305             final ContentResolver resolver = getContext().getContentResolver();
306             return resolver.openFileDescriptor(mDm.getDownloadUri(id), mode, signal);
307         } finally {
308             Binder.restoreCallingIdentity(token);
309         }
310     }
311 
312     @Override
openDocumentThumbnail( String docId, Point sizeHint, CancellationSignal signal)313     public AssetFileDescriptor openDocumentThumbnail(
314             String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
315         // TODO: extend ExifInterface to support fds
316         final ParcelFileDescriptor pfd = openDocument(docId, "r", signal);
317         return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
318     }
319 
includeDefaultDocument(MatrixCursor result)320     private void includeDefaultDocument(MatrixCursor result) {
321         final RowBuilder row = result.newRow();
322         row.add(Document.COLUMN_DOCUMENT_ID, DOC_ID_ROOT);
323         row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
324         row.add(Document.COLUMN_FLAGS,
325                 Document.FLAG_DIR_PREFERS_LAST_MODIFIED | Document.FLAG_DIR_SUPPORTS_CREATE);
326     }
327 
includeDownloadFromCursor(MatrixCursor result, Cursor cursor)328     private void includeDownloadFromCursor(MatrixCursor result, Cursor cursor) {
329         final long id = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID));
330         final String docId = String.valueOf(id);
331 
332         final String displayName = cursor.getString(
333                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TITLE));
334         String summary = cursor.getString(
335                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_DESCRIPTION));
336         String mimeType = cursor.getString(
337                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE));
338         if (mimeType == null) {
339             // Provide fake MIME type so it's openable
340             mimeType = "vnd.android.document/file";
341         }
342         Long size = cursor.getLong(
343                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
344         if (size == -1) {
345             size = null;
346         }
347 
348         int extraFlags = Document.FLAG_PARTIAL;
349         final int status = cursor.getInt(
350                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS));
351         switch (status) {
352             case DownloadManager.STATUS_SUCCESSFUL:
353                 extraFlags = Document.FLAG_SUPPORTS_RENAME;  // only successful is non-partial
354                 break;
355             case DownloadManager.STATUS_PAUSED:
356                 summary = getContext().getString(R.string.download_queued);
357                 break;
358             case DownloadManager.STATUS_PENDING:
359                 summary = getContext().getString(R.string.download_queued);
360                 break;
361             case DownloadManager.STATUS_RUNNING:
362                 final long progress = cursor.getLong(cursor.getColumnIndexOrThrow(
363                         DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
364                 if (size != null) {
365                     String percent =
366                             NumberFormat.getPercentInstance().format((double) progress / size);
367                     summary = getContext().getString(R.string.download_running_percent, percent);
368                 } else {
369                     summary = getContext().getString(R.string.download_running);
370                 }
371                 break;
372             case DownloadManager.STATUS_FAILED:
373             default:
374                 summary = getContext().getString(R.string.download_error);
375                 break;
376         }
377 
378         int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_WRITE | extraFlags;
379         if (mimeType.startsWith("image/")) {
380             flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
381         }
382 
383         if (mArchiveHelper.isSupportedArchiveType(mimeType)) {
384             flags |= Document.FLAG_ARCHIVE;
385         }
386 
387         final long lastModified = cursor.getLong(
388                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP));
389 
390         final RowBuilder row = result.newRow();
391         row.add(Document.COLUMN_DOCUMENT_ID, docId);
392         row.add(Document.COLUMN_DISPLAY_NAME, displayName);
393         row.add(Document.COLUMN_SUMMARY, summary);
394         row.add(Document.COLUMN_SIZE, size);
395         row.add(Document.COLUMN_MIME_TYPE, mimeType);
396         row.add(Document.COLUMN_FLAGS, flags);
397         // Incomplete downloads get a null timestamp.  This prevents thrashy UI when a bunch of
398         // active downloads get sorted by mod time.
399         if (status != DownloadManager.STATUS_RUNNING) {
400             row.add(Document.COLUMN_LAST_MODIFIED, lastModified);
401         }
402 
403         final String localFilePath = cursor.getString(
404                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_FILENAME));
405         if (localFilePath != null) {
406             row.add(DocumentArchiveHelper.COLUMN_LOCAL_FILE_PATH, localFilePath);
407         }
408     }
409 
410     /**
411      * Remove file extension from name, but only if exact MIME type mapping
412      * exists. This means we can reapply the extension later.
413      */
removeExtension(String mimeType, String name)414     private static String removeExtension(String mimeType, String name) {
415         final int lastDot = name.lastIndexOf('.');
416         if (lastDot >= 0) {
417             final String extension = name.substring(lastDot + 1);
418             final String nameMime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
419             if (mimeType.equals(nameMime)) {
420                 return name.substring(0, lastDot);
421             }
422         }
423         return name;
424     }
425 
426     /**
427      * Add file extension to name, but only if exact MIME type mapping exists.
428      */
addExtension(String mimeType, String name)429     private static String addExtension(String mimeType, String name) {
430         final String extension = MimeTypeMap.getSingleton()
431                 .getExtensionFromMimeType(mimeType);
432         if (extension != null) {
433             return name + "." + extension;
434         }
435         return name;
436     }
437 }
438