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.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 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
    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
    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
    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
    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
    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
    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 
    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     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      */
    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      */
    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