• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
328     /**
329      * Adds the entry from the cursor to the result only if the entry is valid. That is,
330      * if the file exists in the file system.
331      */
includeDownloadFromCursor(MatrixCursor result, Cursor cursor)332     private void includeDownloadFromCursor(MatrixCursor result, Cursor cursor) {
333         final long id = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID));
334         final String docId = String.valueOf(id);
335 
336         final String displayName = cursor.getString(
337                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TITLE));
338         String summary = cursor.getString(
339                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_DESCRIPTION));
340         String mimeType = cursor.getString(
341                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE));
342         if (mimeType == null) {
343             // Provide fake MIME type so it's openable
344             mimeType = "vnd.android.document/file";
345         }
346         Long size = cursor.getLong(
347                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
348         if (size == -1) {
349             size = null;
350         }
351         String localFilePath = cursor.getString(
352                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_FILENAME));
353 
354         int extraFlags = Document.FLAG_PARTIAL;
355         final int status = cursor.getInt(
356                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS));
357         switch (status) {
358             case DownloadManager.STATUS_SUCCESSFUL:
359                 // Verify that the document still exists in external storage. This is necessary
360                 // because files can be deleted from the file system without their entry being
361                 // removed from DownloadsManager.
362                 if (localFilePath == null || !new File(localFilePath).exists()) {
363                     return;
364                 }
365                 extraFlags = Document.FLAG_SUPPORTS_RENAME;  // only successful is non-partial
366                 break;
367             case DownloadManager.STATUS_PAUSED:
368                 summary = getContext().getString(R.string.download_queued);
369                 break;
370             case DownloadManager.STATUS_PENDING:
371                 summary = getContext().getString(R.string.download_queued);
372                 break;
373             case DownloadManager.STATUS_RUNNING:
374                 final long progress = cursor.getLong(cursor.getColumnIndexOrThrow(
375                         DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
376                 if (size != null) {
377                     String percent =
378                             NumberFormat.getPercentInstance().format((double) progress / size);
379                     summary = getContext().getString(R.string.download_running_percent, percent);
380                 } else {
381                     summary = getContext().getString(R.string.download_running);
382                 }
383                 break;
384             case DownloadManager.STATUS_FAILED:
385             default:
386                 summary = getContext().getString(R.string.download_error);
387                 break;
388         }
389 
390         int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_WRITE | extraFlags;
391         if (mimeType.startsWith("image/")) {
392             flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
393         }
394 
395         if (mArchiveHelper.isSupportedArchiveType(mimeType)) {
396             flags |= Document.FLAG_ARCHIVE;
397         }
398 
399         final long lastModified = cursor.getLong(
400                 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP));
401 
402         final RowBuilder row = result.newRow();
403         row.add(Document.COLUMN_DOCUMENT_ID, docId);
404         row.add(Document.COLUMN_DISPLAY_NAME, displayName);
405         row.add(Document.COLUMN_SUMMARY, summary);
406         row.add(Document.COLUMN_SIZE, size);
407         row.add(Document.COLUMN_MIME_TYPE, mimeType);
408         row.add(Document.COLUMN_FLAGS, flags);
409         // Incomplete downloads get a null timestamp.  This prevents thrashy UI when a bunch of
410         // active downloads get sorted by mod time.
411         if (status != DownloadManager.STATUS_RUNNING) {
412             row.add(Document.COLUMN_LAST_MODIFIED, lastModified);
413         }
414 
415         if (localFilePath != null) {
416             row.add(DocumentArchiveHelper.COLUMN_LOCAL_FILE_PATH, localFilePath);
417         }
418     }
419 
420     /**
421      * Remove file extension from name, but only if exact MIME type mapping
422      * exists. This means we can reapply the extension later.
423      */
removeExtension(String mimeType, String name)424     private static String removeExtension(String mimeType, String name) {
425         final int lastDot = name.lastIndexOf('.');
426         if (lastDot >= 0) {
427             final String extension = name.substring(lastDot + 1);
428             final String nameMime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
429             if (mimeType.equals(nameMime)) {
430                 return name.substring(0, lastDot);
431             }
432         }
433         return name;
434     }
435 
436     /**
437      * Add file extension to name, but only if exact MIME type mapping exists.
438      */
addExtension(String mimeType, String name)439     private static String addExtension(String mimeType, String name) {
440         final String extension = MimeTypeMap.getSingleton()
441                 .getExtensionFromMimeType(mimeType);
442         if (extension != null) {
443             return name + "." + extension;
444         }
445         return name;
446     }
447 }
448