Home | History | Annotate | Download | only in providers
      1 /*
      2  * Copyright (C) 2013 Google Inc.
      3  * Licensed to The Android Open Source Project.
      4  *
      5  * Licensed under the Apache License, Version 2.0 (the "License");
      6  * you may not use this file except in compliance with the License.
      7  * You may obtain a copy of the License at
      8  *
      9  *      http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  * Unless required by applicable law or agreed to in writing, software
     12  * distributed under the License is distributed on an "AS IS" BASIS,
     13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  * See the License for the specific language governing permissions and
     15  * limitations under the License.
     16  */
     17 
     18 package com.android.mail.providers;
     19 
     20 import android.app.DownloadManager;
     21 import android.content.ContentProvider;
     22 import android.content.ContentResolver;
     23 import android.content.ContentValues;
     24 import android.content.Context;
     25 import android.content.Intent;
     26 import android.content.UriMatcher;
     27 import android.database.Cursor;
     28 import android.database.MatrixCursor;
     29 import android.net.Uri;
     30 import android.os.Environment;
     31 import android.os.ParcelFileDescriptor;
     32 import android.os.SystemClock;
     33 import android.text.TextUtils;
     34 
     35 import com.android.ex.photo.provider.PhotoContract;
     36 import com.android.mail.R;
     37 import com.android.mail.utils.LogTag;
     38 import com.android.mail.utils.LogUtils;
     39 import com.android.mail.utils.MimeType;
     40 import com.google.common.collect.Lists;
     41 import com.google.common.collect.Maps;
     42 
     43 import java.io.File;
     44 import java.io.FileInputStream;
     45 import java.io.FileNotFoundException;
     46 import java.io.FileOutputStream;
     47 import java.io.IOException;
     48 import java.io.InputStream;
     49 import java.io.OutputStream;
     50 import java.util.List;
     51 import java.util.Map;
     52 
     53 /**
     54  * A {@link ContentProvider} for attachments created from eml files.
     55  * Supports all of the semantics (query/insert/update/delete/openFile)
     56  * of the regular attachment provider.
     57  *
     58  * One major difference is that all attachment info is stored in memory (with the
     59  * exception of the attachment raw data which is stored in the cache). When
     60  * the process is killed, all of the attachments disappear if they still
     61  * exist.
     62  */
     63 public class EmlAttachmentProvider extends ContentProvider {
     64     private static final String LOG_TAG = LogTag.getLogTag();
     65 
     66     private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
     67     private static boolean sUrisAddedToMatcher = false;
     68 
     69     private static final int ATTACHMENT_LIST = 0;
     70     private static final int ATTACHMENT = 1;
     71     private static final int ATTACHMENT_BY_CID = 2;
     72 
     73     /**
     74      * The buffer size used to copy data from cache to sd card.
     75      */
     76     private static final int BUFFER_SIZE = 4096;
     77 
     78     /** Any IO reads should be limited to this timeout */
     79     private static final long READ_TIMEOUT = 3600 * 1000;
     80 
     81     private static Uri BASE_URI;
     82 
     83     private DownloadManager mDownloadManager;
     84 
     85     /**
     86      * Map that contains a mapping from an attachment list uri to a list of uris.
     87      */
     88     private Map<Uri, List<Uri>> mUriListMap;
     89 
     90     /**
     91      * Map that contains a mapping from an attachment uri to an {@link Attachment} object.
     92      */
     93     private Map<Uri, Attachment> mUriAttachmentMap;
     94 
     95 
     96     @Override
     97     public boolean onCreate() {
     98         final String authority =
     99                 getContext().getResources().getString(R.string.eml_attachment_provider);
    100         BASE_URI = new Uri.Builder().scheme("content").authority(authority).build();
    101 
    102         if (!sUrisAddedToMatcher) {
    103             sUrisAddedToMatcher = true;
    104             sUriMatcher.addURI(authority, "attachments/*/*", ATTACHMENT_LIST);
    105             sUriMatcher.addURI(authority, "attachment/*/*/#", ATTACHMENT);
    106             sUriMatcher.addURI(authority, "attachmentByCid/*/*/*", ATTACHMENT_BY_CID);
    107         }
    108 
    109         mDownloadManager =
    110                 (DownloadManager) getContext().getSystemService(Context.DOWNLOAD_SERVICE);
    111 
    112         mUriListMap = Maps.newHashMap();
    113         mUriAttachmentMap = Maps.newHashMap();
    114         return true;
    115     }
    116 
    117     @Override
    118     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
    119             String sortOrder) {
    120         final int match = sUriMatcher.match(uri);
    121         // ignore other projections
    122         final MatrixCursor cursor = new MatrixCursor(UIProvider.ATTACHMENT_PROJECTION);
    123         final ContentResolver cr = getContext().getContentResolver();
    124 
    125         switch (match) {
    126             case ATTACHMENT_LIST: {
    127                 final List<String> contentTypeQueryParameters =
    128                         uri.getQueryParameters(PhotoContract.ContentTypeParameters.CONTENT_TYPE);
    129                 uri = uri.buildUpon().clearQuery().build();
    130                 final List<Uri> attachmentUris = mUriListMap.get(uri);
    131                 for (final Uri attachmentUri : attachmentUris) {
    132                     addRow(cursor, attachmentUri, contentTypeQueryParameters);
    133                 }
    134                 cursor.setNotificationUri(cr, uri);
    135                 break;
    136             }
    137             case ATTACHMENT: {
    138                 addRow(cursor, mUriAttachmentMap.get(uri));
    139                 cursor.setNotificationUri(cr, getListUriFromAttachmentUri(uri));
    140                 break;
    141             }
    142             case ATTACHMENT_BY_CID: {
    143                 // form the attachment lists uri by clipping off the cid from the given uri
    144                 final Uri attachmentsListUri = getListUriFromAttachmentUri(uri);
    145                 final String cid = uri.getPathSegments().get(3);
    146 
    147                 // find all uris for the parent message
    148                 final List<Uri> attachmentUris = mUriListMap.get(attachmentsListUri);
    149 
    150                 if (attachmentUris != null) {
    151                     // find the attachment that contains the given cid
    152                     for (Uri attachmentsUri : attachmentUris) {
    153                         final Attachment attachment = mUriAttachmentMap.get(attachmentsUri);
    154                         if (TextUtils.equals(cid, attachment.partId)) {
    155                             addRow(cursor, attachment);
    156                             cursor.setNotificationUri(cr, attachmentsListUri);
    157                             break;
    158                         }
    159                     }
    160                 }
    161                 break;
    162             }
    163             default:
    164                 break;
    165         }
    166 
    167         return cursor;
    168     }
    169 
    170     @Override
    171     public String getType(Uri uri) {
    172         final int match = sUriMatcher.match(uri);
    173         switch (match) {
    174             case ATTACHMENT:
    175                 return mUriAttachmentMap.get(uri).getContentType();
    176             default:
    177                 return null;
    178         }
    179     }
    180 
    181     @Override
    182     public Uri insert(Uri uri, ContentValues values) {
    183         final Uri listUri = getListUriFromAttachmentUri(uri);
    184 
    185         // add mapping from uri to attachment
    186         if (mUriAttachmentMap.put(uri, new Attachment(values)) == null) {
    187             // only add uri to list if the list
    188             // get list of attachment uris, creating if necessary
    189             List<Uri> list = mUriListMap.get(listUri);
    190             if (list == null) {
    191                 list = Lists.newArrayList();
    192                 mUriListMap.put(listUri, list);
    193             }
    194 
    195             list.add(uri);
    196         }
    197 
    198         return uri;
    199     }
    200 
    201     @Override
    202     public int delete(Uri uri, String selection, String[] selectionArgs) {
    203         final int match = sUriMatcher.match(uri);
    204         switch (match) {
    205             case ATTACHMENT_LIST:
    206                 // remove from list mapping
    207                 final List<Uri> attachmentUris = mUriListMap.remove(uri);
    208 
    209                 // delete each file and remove each element from the mapping
    210                 for (final Uri attachmentUri : attachmentUris) {
    211                     mUriAttachmentMap.remove(attachmentUri);
    212                 }
    213 
    214                 deleteDirectory(getCacheFileDirectory(uri));
    215                 // return rows affected
    216                 return attachmentUris.size();
    217             default:
    218                 return 0;
    219         }
    220     }
    221 
    222     @Override
    223     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    224         final int match = sUriMatcher.match(uri);
    225         switch (match) {
    226             case ATTACHMENT:
    227                 return copyAttachment(uri, values);
    228             default:
    229                 return 0;
    230         }
    231     }
    232 
    233     /**
    234      * Adds a row to the cursor for the attachment at the specific attachment {@link Uri}
    235      * if the attachment's mime type matches one of the query parameters.
    236      *
    237      * Matching is defined to be starting with one of the query parameters. If no
    238      * parameters exist, all rows are added.
    239      */
    240     private void addRow(MatrixCursor cursor, Uri uri,
    241             List<String> contentTypeQueryParameters) {
    242         final Attachment attachment = mUriAttachmentMap.get(uri);
    243 
    244         if (contentTypeQueryParameters != null && !contentTypeQueryParameters.isEmpty()) {
    245             for (final String type : contentTypeQueryParameters) {
    246                 if (attachment.getContentType().startsWith(type)) {
    247                     addRow(cursor, attachment);
    248                     return;
    249                 }
    250             }
    251         } else {
    252             addRow(cursor, attachment);
    253         }
    254     }
    255 
    256     /**
    257      * Adds a new row to the cursor for the specific attachment.
    258      */
    259     private static void addRow(MatrixCursor cursor, Attachment attachment) {
    260         cursor.newRow()
    261                 .add(attachment.getName())                          // displayName
    262                 .add(attachment.size)                               // size
    263                 .add(attachment.uri)                                // uri
    264                 .add(attachment.getContentType())                   // contentType
    265                 .add(attachment.state)                              // state
    266                 .add(attachment.destination)                        // destination
    267                 .add(attachment.downloadedSize)                     // downloadedSize
    268                 .add(attachment.contentUri)                         // contentUri
    269                 .add(attachment.thumbnailUri)                       // thumbnailUri
    270                 .add(attachment.previewIntentUri)                   // previewIntentUri
    271                 .add(attachment.providerData)                       // providerData
    272                 .add(attachment.supportsDownloadAgain() ? 1 : 0)    // supportsDownloadAgain
    273                 .add(attachment.type)                               // type
    274                 .add(attachment.flags)                              // flags
    275                 .add(attachment.partId);                            // partId (same as RFC822 cid)
    276     }
    277 
    278     /**
    279      * Copies an attachment at the specified {@link Uri}
    280      * from cache to the external downloads directory (usually the sd card).
    281      * @return the number of attachments affected. Should be 1 or 0.
    282      */
    283     private int copyAttachment(Uri uri, ContentValues values) {
    284         final Integer newState = values.getAsInteger(UIProvider.AttachmentColumns.STATE);
    285         final Integer newDestination =
    286                 values.getAsInteger(UIProvider.AttachmentColumns.DESTINATION);
    287         if (newState == null && newDestination == null) {
    288             return 0;
    289         }
    290 
    291         final int destination = newDestination != null ?
    292                 newDestination.intValue() : UIProvider.AttachmentDestination.CACHE;
    293         final boolean saveToSd =
    294                 destination == UIProvider.AttachmentDestination.EXTERNAL;
    295 
    296         final Attachment attachment = mUriAttachmentMap.get(uri);
    297 
    298         // 1. check if already saved to sd (via uri save to sd)
    299         // and return if so (we shouldn't ever be here)
    300 
    301         // if the call was not to save to sd or already saved to sd, just bail out
    302         if (!saveToSd || attachment.isSavedToExternal()) {
    303             return 0;
    304         }
    305 
    306 
    307         // 2. copy file
    308         final String oldFilePath = getFilePath(uri);
    309 
    310         // update the destination before getting the new file path
    311         // otherwise it will just point to the old location.
    312         attachment.destination = UIProvider.AttachmentDestination.EXTERNAL;
    313         final String newFilePath = getFilePath(uri);
    314 
    315         InputStream inputStream = null;
    316         OutputStream outputStream = null;
    317 
    318         try {
    319             try {
    320                 inputStream = new FileInputStream(oldFilePath);
    321             } catch (FileNotFoundException e) {
    322                 LogUtils.e(LOG_TAG, "File not found for file %s", oldFilePath);
    323                 return 0;
    324             }
    325             try {
    326                 outputStream = new FileOutputStream(newFilePath);
    327             } catch (FileNotFoundException e) {
    328                 LogUtils.e(LOG_TAG, "File not found for file %s", newFilePath);
    329                 return 0;
    330             }
    331             try {
    332                 final long now = SystemClock.elapsedRealtime();
    333                 final byte data[] = new byte[BUFFER_SIZE];
    334                 int size = 0;
    335                 while (true) {
    336                     final int len = inputStream.read(data);
    337                     if (len != -1) {
    338                         outputStream.write(data, 0, len);
    339 
    340                         size += len;
    341                     } else {
    342                         break;
    343                     }
    344                     if (SystemClock.elapsedRealtime() - now > READ_TIMEOUT) {
    345                         throw new IOException("Timed out copying attachment.");
    346                     }
    347                 }
    348 
    349                 // if the attachment is an APK, change contentUri to be a direct file uri
    350                 if (MimeType.isInstallable(attachment.getContentType())) {
    351                     attachment.contentUri = Uri.parse("file://" + newFilePath);
    352                 }
    353 
    354                 // 3. add file to download manager
    355 
    356                 try {
    357                     // TODO - make a better description
    358                     final String description = attachment.getName();
    359                     mDownloadManager.addCompletedDownload(attachment.getName(),
    360                             description, true, attachment.getContentType(),
    361                             newFilePath, size, false);
    362                 }
    363                 catch (IllegalArgumentException e) {
    364                     // Even if we cannot save the download to the downloads app,
    365                     // (likely due to a bad mimeType), we still want to save it.
    366                     LogUtils.e(LOG_TAG, e, "Failed to save download to Downloads app.");
    367                 }
    368                 final Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
    369                 intent.setData(Uri.parse("file://" + newFilePath));
    370                 getContext().sendBroadcast(intent);
    371 
    372                 // 4. delete old file
    373                 new File(oldFilePath).delete();
    374             } catch (IOException e) {
    375                 // Error writing file, delete partial file
    376                 LogUtils.e(LOG_TAG, e, "Cannot write to file %s", newFilePath);
    377                 new File(newFilePath).delete();
    378             }
    379         } finally {
    380             try {
    381                 if (inputStream != null) {
    382                     inputStream.close();
    383                 }
    384             } catch (IOException e) {
    385             }
    386             try {
    387                 if (outputStream != null) {
    388                     outputStream.close();
    389                 }
    390             } catch (IOException e) {
    391             }
    392         }
    393 
    394         // 5. notify that the list of attachments has changed so the UI will update
    395         getContext().getContentResolver().notifyChange(
    396                 getListUriFromAttachmentUri(uri), null, false);
    397         return 1;
    398     }
    399 
    400     @Override
    401     public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
    402         final String filePath = getFilePath(uri);
    403 
    404         final int fileMode;
    405 
    406         if ("rwt".equals(mode)) {
    407             fileMode = ParcelFileDescriptor.MODE_READ_WRITE |
    408                     ParcelFileDescriptor.MODE_TRUNCATE |
    409                     ParcelFileDescriptor.MODE_CREATE;
    410         } else if ("rw".equals(mode)) {
    411             fileMode = ParcelFileDescriptor.MODE_READ_WRITE | ParcelFileDescriptor.MODE_CREATE;
    412         } else {
    413             fileMode = ParcelFileDescriptor.MODE_READ_ONLY;
    414         }
    415 
    416         return ParcelFileDescriptor.open(new File(filePath), fileMode);
    417     }
    418 
    419     /**
    420      * Returns an attachment list uri for the specific attachment uri passed.
    421      */
    422     private static Uri getListUriFromAttachmentUri(Uri uri) {
    423         final List<String> segments = uri.getPathSegments();
    424         return BASE_URI.buildUpon()
    425                 .appendPath("attachments")
    426                 .appendPath(segments.get(1))
    427                 .appendPath(segments.get(2))
    428                 .build();
    429     }
    430 
    431     /**
    432      * Returns an attachment list uri for an eml file at the given uri with the given message id.
    433      */
    434     public static Uri getAttachmentsListUri(Uri emlFileUri, String messageId) {
    435         return BASE_URI.buildUpon()
    436                 .appendPath("attachments")
    437                 .appendPath(Integer.toString(emlFileUri.hashCode()))
    438                 .appendPath(messageId)
    439                 .build();
    440     }
    441 
    442     /**
    443      * Returns an attachment uri for an eml file at the given uri with the given message id.
    444      * The consumer of this uri must append a specific CID to it to complete the uri.
    445      */
    446     public static Uri getAttachmentByCidUri(Uri emlFileUri, String messageId) {
    447         return BASE_URI.buildUpon()
    448                 .appendPath("attachmentByCid")
    449                 .appendPath(Integer.toString(emlFileUri.hashCode()))
    450                 .appendPath(messageId)
    451                 .build();
    452     }
    453 
    454     /**
    455      * Returns an attachment uri for an attachment from the given eml file uri with
    456      * the given message id and part id.
    457      */
    458     public static Uri getAttachmentUri(Uri emlFileUri, String messageId, String partId) {
    459         return BASE_URI.buildUpon()
    460                 .appendPath("attachment")
    461                 .appendPath(Integer.toString(emlFileUri.hashCode()))
    462                 .appendPath(messageId)
    463                 .appendPath(partId)
    464                 .build();
    465     }
    466 
    467     /**
    468      * Returns the absolute file path for the attachment at the given uri.
    469      */
    470     private String getFilePath(Uri uri) {
    471         final Attachment attachment = mUriAttachmentMap.get(uri);
    472         final boolean saveToSd =
    473                 attachment.destination == UIProvider.AttachmentDestination.EXTERNAL;
    474         final String pathStart = (saveToSd) ?
    475                 Environment.getExternalStoragePublicDirectory(
    476                 Environment.DIRECTORY_DOWNLOADS).getAbsolutePath() : getCacheDir();
    477 
    478         // we want the root of the downloads directory if the attachment is
    479         // saved to external (or we're saving to external)
    480         final String directoryPath = (saveToSd) ? pathStart : pathStart + uri.getEncodedPath();
    481 
    482         final File directory = new File(directoryPath);
    483         if (!directory.exists()) {
    484             directory.mkdirs();
    485         }
    486         return directoryPath + "/" + attachment.getName();
    487     }
    488 
    489     /**
    490      * Returns the root directory for the attachments for the specific uri.
    491      */
    492     private String getCacheFileDirectory(Uri uri) {
    493         return getCacheDir() + "/" + Uri.encode(uri.getPathSegments().get(1));
    494     }
    495 
    496     /**
    497      * Returns the cache directory for eml attachment files.
    498      */
    499     private String getCacheDir() {
    500         return getContext().getCacheDir().getAbsolutePath().concat("/eml");
    501     }
    502 
    503     /**
    504      * Recursively delete the directory at the passed file path.
    505      */
    506     private void deleteDirectory(String cacheFileDirectory) {
    507         recursiveDelete(new File(cacheFileDirectory));
    508     }
    509 
    510     /**
    511      * Recursively deletes a file or directory.
    512      */
    513     private void recursiveDelete(File file) {
    514         if (file.isDirectory()) {
    515             final File[] children = file.listFiles();
    516             for (final File child : children) {
    517                 recursiveDelete(child);
    518             }
    519         }
    520 
    521         file.delete();
    522     }
    523 }
    524