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