Home | History | Annotate | Download | only in utility
      1 /*
      2  * Copyright (C) 2011 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.emailcommon.utility;
     18 
     19 import android.app.DownloadManager;
     20 import android.content.ContentResolver;
     21 import android.content.ContentUris;
     22 import android.content.ContentValues;
     23 import android.content.Context;
     24 import android.database.Cursor;
     25 import android.media.MediaScannerConnection;
     26 import android.net.Uri;
     27 import android.os.Environment;
     28 import android.text.TextUtils;
     29 import android.util.Log;
     30 import android.webkit.MimeTypeMap;
     31 
     32 import com.android.emailcommon.Logging;
     33 import com.android.emailcommon.provider.EmailContent.Attachment;
     34 import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
     35 import com.android.emailcommon.provider.EmailContent.Message;
     36 import com.android.emailcommon.provider.EmailContent.MessageColumns;
     37 import com.android.mail.providers.UIProvider;
     38 
     39 import org.apache.commons.io.IOUtils;
     40 
     41 import java.io.File;
     42 import java.io.FileOutputStream;
     43 import java.io.IOException;
     44 import java.io.InputStream;
     45 
     46 public class AttachmentUtilities {
     47     public static final String AUTHORITY = "com.android.email.attachmentprovider";
     48     public static final Uri CONTENT_URI = Uri.parse( "content://" + AUTHORITY);
     49 
     50     public static final String FORMAT_RAW = "RAW";
     51     public static final String FORMAT_THUMBNAIL = "THUMBNAIL";
     52 
     53     public static class Columns {
     54         public static final String _ID = "_id";
     55         public static final String DATA = "_data";
     56         public static final String DISPLAY_NAME = "_display_name";
     57         public static final String SIZE = "_size";
     58     }
     59 
     60     /**
     61      * The MIME type(s) of attachments we're willing to send via attachments.
     62      *
     63      * Any attachments may be added via Intents with Intent.ACTION_SEND or ACTION_SEND_MULTIPLE.
     64      */
     65     public static final String[] ACCEPTABLE_ATTACHMENT_SEND_INTENT_TYPES = new String[] {
     66         "*/*",
     67     };
     68     /**
     69      * The MIME type(s) of attachments we're willing to send from the internal UI.
     70      *
     71      * NOTE:  At the moment it is not possible to open a chooser with a list of filter types, so
     72      * the chooser is only opened with the first item in the list.
     73      */
     74     public static final String[] ACCEPTABLE_ATTACHMENT_SEND_UI_TYPES = new String[] {
     75         "image/*",
     76         "video/*",
     77     };
     78     /**
     79      * The MIME type(s) of attachments we're willing to view.
     80      */
     81     public static final String[] ACCEPTABLE_ATTACHMENT_VIEW_TYPES = new String[] {
     82         "*/*",
     83     };
     84     /**
     85      * The MIME type(s) of attachments we're not willing to view.
     86      */
     87     public static final String[] UNACCEPTABLE_ATTACHMENT_VIEW_TYPES = new String[] {
     88     };
     89     /**
     90      * The MIME type(s) of attachments we're willing to download to SD.
     91      */
     92     public static final String[] ACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES = new String[] {
     93         "*/*",
     94     };
     95     /**
     96      * The MIME type(s) of attachments we're not willing to download to SD.
     97      */
     98     public static final String[] UNACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES = new String[] {
     99     };
    100     /**
    101      * Filename extensions of attachments we're never willing to download (potential malware).
    102      * Entries in this list are compared to the end of the lower-cased filename, so they must
    103      * be lower case, and should not include a "."
    104      */
    105     public static final String[] UNACCEPTABLE_ATTACHMENT_EXTENSIONS = new String[] {
    106         // File types that contain malware
    107         "ade", "adp", "bat", "chm", "cmd", "com", "cpl", "dll", "exe",
    108         "hta", "ins", "isp", "jse", "lib", "mde", "msc", "msp",
    109         "mst", "pif", "scr", "sct", "shb", "sys", "vb", "vbe",
    110         "vbs", "vxd", "wsc", "wsf", "wsh",
    111         // File types of common compression/container formats (again, to avoid malware)
    112         "zip", "gz", "z", "tar", "tgz", "bz2",
    113     };
    114     /**
    115      * Filename extensions of attachments that can be installed.
    116      * Entries in this list are compared to the end of the lower-cased filename, so they must
    117      * be lower case, and should not include a "."
    118      */
    119     public static final String[] INSTALLABLE_ATTACHMENT_EXTENSIONS = new String[] {
    120         "apk",
    121     };
    122     /**
    123      * The maximum size of an attachment we're willing to download (either View or Save)
    124      * Attachments that are base64 encoded (most) will be about 1.375x their actual size
    125      * so we should probably factor that in. A 5MB attachment will generally be around
    126      * 6.8MB downloaded but only 5MB saved.
    127      */
    128     public static final int MAX_ATTACHMENT_DOWNLOAD_SIZE = (5 * 1024 * 1024);
    129     /**
    130      * The maximum size of an attachment we're willing to upload (measured as stored on disk).
    131      * Attachments that are base64 encoded (most) will be about 1.375x their actual size
    132      * so we should probably factor that in. A 5MB attachment will generally be around
    133      * 6.8MB uploaded.
    134      */
    135     public static final int MAX_ATTACHMENT_UPLOAD_SIZE = (5 * 1024 * 1024);
    136 
    137     public static Uri getAttachmentUri(long accountId, long id) {
    138         return CONTENT_URI.buildUpon()
    139         .appendPath(Long.toString(accountId))
    140         .appendPath(Long.toString(id))
    141         .appendPath(FORMAT_RAW)
    142         .build();
    143     }
    144 
    145     public static Uri getAttachmentThumbnailUri(long accountId, long id,
    146             int width, int height) {
    147         return CONTENT_URI.buildUpon()
    148         .appendPath(Long.toString(accountId))
    149         .appendPath(Long.toString(id))
    150         .appendPath(FORMAT_THUMBNAIL)
    151         .appendPath(Integer.toString(width))
    152         .appendPath(Integer.toString(height))
    153         .build();
    154     }
    155 
    156     /**
    157      * Return the filename for a given attachment.  This should be used by any code that is
    158      * going to *write* attachments.
    159      *
    160      * This does not create or write the file, or even the directories.  It simply builds
    161      * the filename that should be used.
    162      */
    163     public static File getAttachmentFilename(Context context, long accountId, long attachmentId) {
    164         return new File(getAttachmentDirectory(context, accountId), Long.toString(attachmentId));
    165     }
    166 
    167     /**
    168      * Return the directory for a given attachment.  This should be used by any code that is
    169      * going to *write* attachments.
    170      *
    171      * This does not create or write the directory.  It simply builds the pathname that should be
    172      * used.
    173      */
    174     public static File getAttachmentDirectory(Context context, long accountId) {
    175         return context.getDatabasePath(accountId + ".db_att");
    176     }
    177 
    178     /**
    179      * Helper to convert unknown or unmapped attachments to something useful based on filename
    180      * extensions. The mime type is inferred based upon the table below. It's not perfect, but
    181      * it helps.
    182      *
    183      * <pre>
    184      *                   |---------------------------------------------------------|
    185      *                   |                  E X T E N S I O N                      |
    186      *                   |---------------------------------------------------------|
    187      *                   | .eml        | known(.png) | unknown(.abc) | none        |
    188      * | M |-----------------------------------------------------------------------|
    189      * | I | none        | msg/rfc822  | image/png   | app/abc       | app/oct-str |
    190      * | M |-------------| (always     |             |               |             |
    191      * | E | app/oct-str |  overrides  |             |               |             |
    192      * | T |-------------|             |             |-----------------------------|
    193      * | Y | text/plain  |             |             | text/plain                  |
    194      * | P |-------------|             |-------------------------------------------|
    195      * | E | any/type    |             | any/type                                  |
    196      * |---|-----------------------------------------------------------------------|
    197      * </pre>
    198      *
    199      * NOTE: Since mime types on Android are case-*sensitive*, return values are always in
    200      * lower case.
    201      *
    202      * @param fileName The given filename
    203      * @param mimeType The given mime type
    204      * @return A likely mime type for the attachment
    205      */
    206     public static String inferMimeType(final String fileName, final String mimeType) {
    207         String resultType = null;
    208         String fileExtension = getFilenameExtension(fileName);
    209         boolean isTextPlain = "text/plain".equalsIgnoreCase(mimeType);
    210 
    211         if ("eml".equals(fileExtension)) {
    212             resultType = "message/rfc822";
    213         } else {
    214             boolean isGenericType =
    215                     isTextPlain || "application/octet-stream".equalsIgnoreCase(mimeType);
    216             // If the given mime type is non-empty and non-generic, return it
    217             if (isGenericType || TextUtils.isEmpty(mimeType)) {
    218                 if (!TextUtils.isEmpty(fileExtension)) {
    219                     // Otherwise, try to find a mime type based upon the file extension
    220                     resultType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension);
    221                     if (TextUtils.isEmpty(resultType)) {
    222                         // Finally, if original mimetype is text/plain, use it; otherwise synthesize
    223                         resultType = isTextPlain ? mimeType : "application/" + fileExtension;
    224                     }
    225                 }
    226             } else {
    227                 resultType = mimeType;
    228             }
    229         }
    230 
    231         // No good guess could be made; use an appropriate generic type
    232         if (TextUtils.isEmpty(resultType)) {
    233             resultType = isTextPlain ? "text/plain" : "application/octet-stream";
    234         }
    235         return resultType.toLowerCase();
    236     }
    237 
    238     /**
    239      * @return mime-type for a {@link Uri}.
    240      *    - Use {@link ContentResolver#getType} for a content: URI.
    241      *    - Use {@link #inferMimeType} for a file: URI.
    242      *    - Otherwise returns null.
    243      */
    244     public static String inferMimeTypeForUri(Context context, Uri uri) {
    245         final String scheme = uri.getScheme();
    246         if (ContentResolver.SCHEME_CONTENT.equals(scheme)) {
    247             return context.getContentResolver().getType(uri);
    248         } else if (ContentResolver.SCHEME_FILE.equals(scheme)) {
    249             return inferMimeType(uri.getLastPathSegment(), "");
    250         } else {
    251             Log.e(Logging.LOG_TAG, "Unable to determine MIME type for uri=" + uri, new Error());
    252             return null;
    253         }
    254     }
    255 
    256     /**
    257      * Extract and return filename's extension, converted to lower case, and not including the "."
    258      *
    259      * @return extension, or null if not found (or null/empty filename)
    260      */
    261     public static String getFilenameExtension(String fileName) {
    262         String extension = null;
    263         if (!TextUtils.isEmpty(fileName)) {
    264             int lastDot = fileName.lastIndexOf('.');
    265             if ((lastDot > 0) && (lastDot < fileName.length() - 1)) {
    266                 extension = fileName.substring(lastDot + 1).toLowerCase();
    267             }
    268         }
    269         return extension;
    270     }
    271 
    272     /**
    273      * Resolve attachment id to content URI.  Returns the resolved content URI (from the attachment
    274      * DB) or, if not found, simply returns the incoming value.
    275      *
    276      * @param attachmentUri
    277      * @return resolved content URI
    278      *
    279      * TODO:  Throws an SQLite exception on a missing DB file (e.g. unknown URI) instead of just
    280      * returning the incoming uri, as it should.
    281      */
    282     public static Uri resolveAttachmentIdToContentUri(ContentResolver resolver, Uri attachmentUri) {
    283         Cursor c = resolver.query(attachmentUri,
    284                 new String[] { Columns.DATA },
    285                 null, null, null);
    286         if (c != null) {
    287             try {
    288                 if (c.moveToFirst()) {
    289                     final String strUri = c.getString(0);
    290                     if (strUri != null) {
    291                         return Uri.parse(strUri);
    292                     }
    293                 }
    294             } finally {
    295                 c.close();
    296             }
    297         }
    298         return attachmentUri;
    299     }
    300 
    301     /**
    302      * In support of deleting a message, find all attachments and delete associated attachment
    303      * files.
    304      * @param context
    305      * @param accountId the account for the message
    306      * @param messageId the message
    307      */
    308     public static void deleteAllAttachmentFiles(Context context, long accountId, long messageId) {
    309         Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, messageId);
    310         Cursor c = context.getContentResolver().query(uri, Attachment.ID_PROJECTION,
    311                 null, null, null);
    312         try {
    313             while (c.moveToNext()) {
    314                 long attachmentId = c.getLong(Attachment.ID_PROJECTION_COLUMN);
    315                 File attachmentFile = getAttachmentFilename(context, accountId, attachmentId);
    316                 // Note, delete() throws no exceptions for basic FS errors (e.g. file not found)
    317                 // it just returns false, which we ignore, and proceed to the next file.
    318                 // This entire loop is best-effort only.
    319                 attachmentFile.delete();
    320             }
    321         } finally {
    322             c.close();
    323         }
    324     }
    325 
    326     /**
    327      * In support of deleting a mailbox, find all messages and delete their attachments.
    328      *
    329      * @param context
    330      * @param accountId the account for the mailbox
    331      * @param mailboxId the mailbox for the messages
    332      */
    333     public static void deleteAllMailboxAttachmentFiles(Context context, long accountId,
    334             long mailboxId) {
    335         Cursor c = context.getContentResolver().query(Message.CONTENT_URI,
    336                 Message.ID_COLUMN_PROJECTION, MessageColumns.MAILBOX_KEY + "=?",
    337                 new String[] { Long.toString(mailboxId) }, null);
    338         try {
    339             while (c.moveToNext()) {
    340                 long messageId = c.getLong(Message.ID_PROJECTION_COLUMN);
    341                 deleteAllAttachmentFiles(context, accountId, messageId);
    342             }
    343         } finally {
    344             c.close();
    345         }
    346     }
    347 
    348     /**
    349      * In support of deleting or wiping an account, delete all related attachments.
    350      *
    351      * @param context
    352      * @param accountId the account to scrub
    353      */
    354     public static void deleteAllAccountAttachmentFiles(Context context, long accountId) {
    355         File[] files = getAttachmentDirectory(context, accountId).listFiles();
    356         if (files == null) return;
    357         for (File file : files) {
    358             boolean result = file.delete();
    359             if (!result) {
    360                 Log.e(Logging.LOG_TAG, "Failed to delete attachment file " + file.getName());
    361             }
    362         }
    363     }
    364 
    365     private static long copyFile(InputStream in, File file) throws IOException {
    366         FileOutputStream out = new FileOutputStream(file);
    367         long size = IOUtils.copy(in, out);
    368         in.close();
    369         out.flush();
    370         out.close();
    371         return size;
    372     }
    373 
    374     /**
    375      * Save the attachment to its final resting place (cache or sd card)
    376      */
    377     public static void saveAttachment(Context context, InputStream in, Attachment attachment) {
    378         Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachment.mId);
    379         ContentValues cv = new ContentValues();
    380         long attachmentId = attachment.mId;
    381         long accountId = attachment.mAccountKey;
    382         String contentUri;
    383         long size;
    384         try {
    385             if (attachment.mUiDestination == UIProvider.AttachmentDestination.CACHE) {
    386                 File saveIn = getAttachmentDirectory(context, accountId);
    387                 if (!saveIn.exists()) {
    388                     saveIn.mkdirs();
    389                 }
    390                 File file = getAttachmentFilename(context, accountId, attachmentId);
    391                 file.createNewFile();
    392                 size = copyFile(in, file);
    393                 contentUri = getAttachmentUri(accountId, attachmentId).toString();
    394             } else if (Utility.isExternalStorageMounted()) {
    395                 File downloads = Environment.getExternalStoragePublicDirectory(
    396                         Environment.DIRECTORY_DOWNLOADS);
    397                 downloads.mkdirs();
    398                 File file = Utility.createUniqueFile(downloads, attachment.mFileName);
    399                 size = copyFile(in, file);
    400                 String absolutePath = file.getAbsolutePath();
    401 
    402                 // Although the download manager can scan media files, scanning only happens
    403                 // after the user clicks on the item in the Downloads app. So, we run the
    404                 // attachment through the media scanner ourselves so it gets added to
    405                 // gallery / music immediately.
    406                 MediaScannerConnection.scanFile(context, new String[] {absolutePath},
    407                         null, null);
    408 
    409                 DownloadManager dm =
    410                         (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
    411                 long id = dm.addCompletedDownload(attachment.mFileName, attachment.mFileName,
    412                         false /* do not use media scanner */,
    413                         attachment.mMimeType, absolutePath, size,
    414                         true /* show notification */);
    415                 contentUri = dm.getUriForDownloadedFile(id).toString();
    416 
    417             } else {
    418                 Log.w(Logging.LOG_TAG, "Trying to save an attachment without external storage?");
    419                 throw new IOException();
    420             }
    421 
    422             // Update the attachment
    423             cv.put(AttachmentColumns.SIZE, size);
    424             cv.put(AttachmentColumns.CONTENT_URI, contentUri);
    425             cv.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.SAVED);
    426         } catch (IOException e) {
    427             // Handle failures here...
    428             cv.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.FAILED);
    429         }
    430         context.getContentResolver().update(uri, cv, null, null);
    431 
    432     }
    433 }
    434