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