Home | History | Annotate | Download | only in provider
      1 /*
      2  * Copyright (C) 2008 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.email.provider;
     18 
     19 import com.android.email.Email;
     20 import com.android.email.mail.internet.MimeUtility;
     21 import com.android.email.provider.EmailContent.Attachment;
     22 import com.android.email.provider.EmailContent.AttachmentColumns;
     23 import com.android.email.provider.EmailContent.Message;
     24 import com.android.email.provider.EmailContent.MessageColumns;
     25 
     26 import android.content.ContentProvider;
     27 import android.content.ContentResolver;
     28 import android.content.ContentUris;
     29 import android.content.ContentValues;
     30 import android.content.Context;
     31 import android.database.Cursor;
     32 import android.database.MatrixCursor;
     33 import android.graphics.Bitmap;
     34 import android.graphics.BitmapFactory;
     35 import android.net.Uri;
     36 import android.os.Binder;
     37 import android.os.ParcelFileDescriptor;
     38 import android.text.TextUtils;
     39 import android.util.Log;
     40 import android.webkit.MimeTypeMap;
     41 
     42 import java.io.File;
     43 import java.io.FileNotFoundException;
     44 import java.io.FileOutputStream;
     45 import java.io.IOException;
     46 import java.io.InputStream;
     47 import java.util.List;
     48 
     49 /*
     50  * A simple ContentProvider that allows file access to Email's attachments.
     51  *
     52  * The URI scheme is as follows.  For raw file access:
     53  *   content://com.android.email.attachmentprovider/acct#/attach#/RAW
     54  *
     55  * And for access to thumbnails:
     56  *   content://com.android.email.attachmentprovider/acct#/attach#/THUMBNAIL/width#/height#
     57  *
     58  * The on-disk (storage) schema is as follows.
     59  *
     60  * Attachments are stored at:  <database-path>/account#.db_att/item#
     61  * Thumbnails are stored at:   <cache-path>/thmb_account#_item#
     62  *
     63  * Using the standard application context, account #10 and attachment # 20, this would be:
     64  *      /data/data/com.android.email/databases/10.db_att/20
     65  *      /data/data/com.android.email/cache/thmb_10_20
     66  */
     67 public class AttachmentProvider extends ContentProvider {
     68 
     69     public static final String AUTHORITY = "com.android.email.attachmentprovider";
     70     public static final Uri CONTENT_URI = Uri.parse( "content://" + AUTHORITY);
     71 
     72     private static final String FORMAT_RAW = "RAW";
     73     private static final String FORMAT_THUMBNAIL = "THUMBNAIL";
     74 
     75     public static class AttachmentProviderColumns {
     76         public static final String _ID = "_id";
     77         public static final String DATA = "_data";
     78         public static final String DISPLAY_NAME = "_display_name";
     79         public static final String SIZE = "_size";
     80     }
     81 
     82     private static final String[] MIME_TYPE_PROJECTION = new String[] {
     83             AttachmentColumns.MIME_TYPE, AttachmentColumns.FILENAME };
     84     private static final int MIME_TYPE_COLUMN_MIME_TYPE = 0;
     85     private static final int MIME_TYPE_COLUMN_FILENAME = 1;
     86 
     87     private static final String[] PROJECTION_QUERY = new String[] { AttachmentColumns.FILENAME,
     88             AttachmentColumns.SIZE, AttachmentColumns.CONTENT_URI };
     89 
     90     public static Uri getAttachmentUri(long accountId, long id) {
     91         return CONTENT_URI.buildUpon()
     92                 .appendPath(Long.toString(accountId))
     93                 .appendPath(Long.toString(id))
     94                 .appendPath(FORMAT_RAW)
     95                 .build();
     96     }
     97 
     98     public static Uri getAttachmentThumbnailUri(long accountId, long id,
     99             int width, int height) {
    100         return CONTENT_URI.buildUpon()
    101                 .appendPath(Long.toString(accountId))
    102                 .appendPath(Long.toString(id))
    103                 .appendPath(FORMAT_THUMBNAIL)
    104                 .appendPath(Integer.toString(width))
    105                 .appendPath(Integer.toString(height))
    106                 .build();
    107     }
    108 
    109     /**
    110      * Return the filename for a given attachment.  This should be used by any code that is
    111      * going to *write* attachments.
    112      *
    113      * This does not create or write the file, or even the directories.  It simply builds
    114      * the filename that should be used.
    115      */
    116     public static File getAttachmentFilename(Context context, long accountId, long attachmentId) {
    117         return new File(getAttachmentDirectory(context, accountId), Long.toString(attachmentId));
    118     }
    119 
    120     /**
    121      * Return the directory for a given attachment.  This should be used by any code that is
    122      * going to *write* attachments.
    123      *
    124      * This does not create or write the directory.  It simply builds the pathname that should be
    125      * used.
    126      */
    127     public static File getAttachmentDirectory(Context context, long accountId) {
    128         return context.getDatabasePath(accountId + ".db_att");
    129     }
    130 
    131     @Override
    132     public boolean onCreate() {
    133         /*
    134          * We use the cache dir as a temporary directory (since Android doesn't give us one) so
    135          * on startup we'll clean up any .tmp files from the last run.
    136          */
    137         File[] files = getContext().getCacheDir().listFiles();
    138         for (File file : files) {
    139             String filename = file.getName();
    140             if (filename.endsWith(".tmp") || filename.startsWith("thmb_")) {
    141                 file.delete();
    142             }
    143         }
    144         return true;
    145     }
    146 
    147     /**
    148      * Returns the mime type for a given attachment.  There are three possible results:
    149      *  - If thumbnail Uri, always returns "image/png" (even if there's no attachment)
    150      *  - If the attachment does not exist, returns null
    151      *  - Returns the mime type of the attachment
    152      */
    153     @Override
    154     public String getType(Uri uri) {
    155         long callingId = Binder.clearCallingIdentity();
    156         try {
    157             List<String> segments = uri.getPathSegments();
    158             String id = segments.get(1);
    159             String format = segments.get(2);
    160             if (FORMAT_THUMBNAIL.equals(format)) {
    161                 return "image/png";
    162             } else {
    163                 uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, Long.parseLong(id));
    164                 Cursor c = getContext().getContentResolver().query(uri, MIME_TYPE_PROJECTION,
    165                         null, null, null);
    166                 try {
    167                     if (c.moveToFirst()) {
    168                         String mimeType = c.getString(MIME_TYPE_COLUMN_MIME_TYPE);
    169                         String fileName = c.getString(MIME_TYPE_COLUMN_FILENAME);
    170                         mimeType = inferMimeType(fileName, mimeType);
    171                         return mimeType;
    172                     }
    173                 } finally {
    174                     c.close();
    175                 }
    176                 return null;
    177             }
    178         } finally {
    179             Binder.restoreCallingIdentity(callingId);
    180         }
    181     }
    182 
    183     /**
    184      * Helper to convert unknown or unmapped attachments to something useful based on filename
    185      * extensions.  Imperfect, but helps.
    186      *
    187      * If the given mime type is non-empty and anything other than "application/octet-stream",
    188      * just return it.  (This is the most common case.)
    189      * If the filename has a recognizable extension and it converts to a mime type, return that.
    190      * If the filename has an unrecognized extension, return "application/extension"
    191      * Otherwise return "application/octet-stream".
    192      *
    193      * @param fileName The given filename
    194      * @param mimeType The given mime type
    195      * @return A likely mime type for the attachment
    196      */
    197     public static String inferMimeType(String fileName, String mimeType) {
    198         // If the given mime type appears to be non-empty and non-generic - return it
    199         if (!TextUtils.isEmpty(mimeType) &&
    200                 !"application/octet-stream".equalsIgnoreCase(mimeType)) {
    201             return mimeType;
    202         }
    203 
    204         // Try to find an extension in the filename
    205         if (!TextUtils.isEmpty(fileName)) {
    206             int lastDot = fileName.lastIndexOf('.');
    207             String extension = null;
    208             if ((lastDot > 0) && (lastDot < fileName.length() - 1)) {
    209                 extension = fileName.substring(lastDot + 1).toLowerCase();
    210             }
    211             if (!TextUtils.isEmpty(extension)) {
    212                 // Extension found.  Look up mime type, or synthesize if none found.
    213                 mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
    214                 if (mimeType == null) {
    215                     mimeType = "application/" + extension;
    216                 }
    217                 return mimeType;
    218             }
    219         }
    220 
    221         // Fallback case - no good guess could be made.
    222         return "application/octet-stream";
    223     }
    224 
    225     /**
    226      * Open an attachment file.  There are two "modes" - "raw", which returns an actual file,
    227      * and "thumbnail", which attempts to generate a thumbnail image.
    228      *
    229      * Thumbnails are cached for easy space recovery and cleanup.
    230      *
    231      * TODO:  The thumbnail mode returns null for its failure cases, instead of throwing
    232      * FileNotFoundException, and should be fixed for consistency.
    233      *
    234      *  @throws FileNotFoundException
    235      */
    236     @Override
    237     public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
    238         long callingId = Binder.clearCallingIdentity();
    239         try {
    240             List<String> segments = uri.getPathSegments();
    241             String accountId = segments.get(0);
    242             String id = segments.get(1);
    243             String format = segments.get(2);
    244             if (FORMAT_THUMBNAIL.equals(format)) {
    245                 int width = Integer.parseInt(segments.get(3));
    246                 int height = Integer.parseInt(segments.get(4));
    247                 String filename = "thmb_" + accountId + "_" + id;
    248                 File dir = getContext().getCacheDir();
    249                 File file = new File(dir, filename);
    250                 if (!file.exists()) {
    251                     Uri attachmentUri =
    252                         getAttachmentUri(Long.parseLong(accountId), Long.parseLong(id));
    253                     Cursor c = query(attachmentUri,
    254                             new String[] { AttachmentProviderColumns.DATA }, null, null, null);
    255                     if (c != null) {
    256                         try {
    257                             if (c.moveToFirst()) {
    258                                 attachmentUri = Uri.parse(c.getString(0));
    259                             } else {
    260                                 return null;
    261                             }
    262                         } finally {
    263                             c.close();
    264                         }
    265                     }
    266                     String type = getContext().getContentResolver().getType(attachmentUri);
    267                     try {
    268                         InputStream in =
    269                             getContext().getContentResolver().openInputStream(attachmentUri);
    270                         Bitmap thumbnail = createThumbnail(type, in);
    271                         if (thumbnail == null) {
    272                             return null;
    273                         }
    274                         thumbnail = Bitmap.createScaledBitmap(thumbnail, width, height, true);
    275                         FileOutputStream out = new FileOutputStream(file);
    276                         thumbnail.compress(Bitmap.CompressFormat.PNG, 100, out);
    277                         out.close();
    278                         in.close();
    279                     } catch (IOException ioe) {
    280                         Log.d(Email.LOG_TAG, "openFile/thumbnail failed with " + ioe.getMessage());
    281                         return null;
    282                     } catch (OutOfMemoryError oome) {
    283                         Log.d(Email.LOG_TAG, "openFile/thumbnail failed with " + oome.getMessage());
    284                         return null;
    285                     }
    286                 }
    287                 return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
    288             }
    289             else {
    290                 return ParcelFileDescriptor.open(
    291                         new File(getContext().getDatabasePath(accountId + ".db_att"), id),
    292                         ParcelFileDescriptor.MODE_READ_ONLY);
    293             }
    294         } finally {
    295             Binder.restoreCallingIdentity(callingId);
    296         }
    297     }
    298 
    299     @Override
    300     public int delete(Uri uri, String arg1, String[] arg2) {
    301         return 0;
    302     }
    303 
    304     @Override
    305     public Uri insert(Uri uri, ContentValues values) {
    306         return null;
    307     }
    308 
    309     /**
    310      * Returns a cursor based on the data in the attachments table, or null if the attachment
    311      * is not recorded in the table.
    312      *
    313      * Supports REST Uri only, for a single row - selection, selection args, and sortOrder are
    314      * ignored (non-null values should probably throw an exception....)
    315      */
    316     @Override
    317     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
    318             String sortOrder) {
    319         long callingId = Binder.clearCallingIdentity();
    320         try {
    321             if (projection == null) {
    322                 projection =
    323                     new String[] {
    324                         AttachmentProviderColumns._ID,
    325                         AttachmentProviderColumns.DATA,
    326                 };
    327             }
    328 
    329             List<String> segments = uri.getPathSegments();
    330             String accountId = segments.get(0);
    331             String id = segments.get(1);
    332             String format = segments.get(2);
    333             String name = null;
    334             int size = -1;
    335             String contentUri = null;
    336 
    337             uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, Long.parseLong(id));
    338             Cursor c = getContext().getContentResolver().query(uri, PROJECTION_QUERY,
    339                     null, null, null);
    340             try {
    341                 if (c.moveToFirst()) {
    342                     name = c.getString(0);
    343                     size = c.getInt(1);
    344                     contentUri = c.getString(2);
    345                 } else {
    346                     return null;
    347                 }
    348             } finally {
    349                 c.close();
    350             }
    351 
    352             MatrixCursor ret = new MatrixCursor(projection);
    353             Object[] values = new Object[projection.length];
    354             for (int i = 0, count = projection.length; i < count; i++) {
    355                 String column = projection[i];
    356                 if (AttachmentProviderColumns._ID.equals(column)) {
    357                     values[i] = id;
    358                 }
    359                 else if (AttachmentProviderColumns.DATA.equals(column)) {
    360                     values[i] = contentUri;
    361                 }
    362                 else if (AttachmentProviderColumns.DISPLAY_NAME.equals(column)) {
    363                     values[i] = name;
    364                 }
    365                 else if (AttachmentProviderColumns.SIZE.equals(column)) {
    366                     values[i] = size;
    367                 }
    368             }
    369             ret.addRow(values);
    370             return ret;
    371         } finally {
    372             Binder.restoreCallingIdentity(callingId);
    373         }
    374     }
    375 
    376     @Override
    377     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    378         return 0;
    379     }
    380 
    381     private Bitmap createThumbnail(String type, InputStream data) {
    382         if(MimeUtility.mimeTypeMatches(type, "image/*")) {
    383             return createImageThumbnail(data);
    384         }
    385         return null;
    386     }
    387 
    388     private Bitmap createImageThumbnail(InputStream data) {
    389         try {
    390             Bitmap bitmap = BitmapFactory.decodeStream(data);
    391             return bitmap;
    392         } catch (OutOfMemoryError oome) {
    393             Log.d(Email.LOG_TAG, "createImageThumbnail failed with " + oome.getMessage());
    394             return null;
    395         } catch (Exception e) {
    396             Log.d(Email.LOG_TAG, "createImageThumbnail failed with " + e.getMessage());
    397             return null;
    398         }
    399     }
    400 
    401     /**
    402      * Resolve attachment id to content URI.  Returns the resolved content URI (from the attachment
    403      * DB) or, if not found, simply returns the incoming value.
    404      *
    405      * @param attachmentUri
    406      * @return resolved content URI
    407      *
    408      * TODO:  Throws an SQLite exception on a missing DB file (e.g. unknown URI) instead of just
    409      * returning the incoming uri, as it should.
    410      */
    411     public static Uri resolveAttachmentIdToContentUri(ContentResolver resolver, Uri attachmentUri) {
    412         Cursor c = resolver.query(attachmentUri,
    413                 new String[] { AttachmentProvider.AttachmentProviderColumns.DATA },
    414                 null, null, null);
    415         if (c != null) {
    416             try {
    417                 if (c.moveToFirst()) {
    418                     final String strUri = c.getString(0);
    419                     if (strUri != null) {
    420                         return Uri.parse(strUri);
    421                     } else {
    422                         Email.log("AttachmentProvider: attachment with null contentUri");
    423                     }
    424                 }
    425             } finally {
    426                 c.close();
    427             }
    428         }
    429         return attachmentUri;
    430     }
    431 
    432     /**
    433      * In support of deleting a message, find all attachments and delete associated attachment
    434      * files.
    435      * @param context
    436      * @param accountId the account for the message
    437      * @param messageId the message
    438      */
    439     public static void deleteAllAttachmentFiles(Context context, long accountId, long messageId) {
    440         Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, messageId);
    441         Cursor c = context.getContentResolver().query(uri, Attachment.ID_PROJECTION,
    442                 null, null, null);
    443         try {
    444             while (c.moveToNext()) {
    445                 long attachmentId = c.getLong(Attachment.ID_PROJECTION_COLUMN);
    446                 File attachmentFile = getAttachmentFilename(context, accountId, attachmentId);
    447                 // Note, delete() throws no exceptions for basic FS errors (e.g. file not found)
    448                 // it just returns false, which we ignore, and proceed to the next file.
    449                 // This entire loop is best-effort only.
    450                 attachmentFile.delete();
    451             }
    452         } finally {
    453             c.close();
    454         }
    455     }
    456 
    457     /**
    458      * In support of deleting a mailbox, find all messages and delete their attachments.
    459      *
    460      * @param context
    461      * @param accountId the account for the mailbox
    462      * @param mailboxId the mailbox for the messages
    463      */
    464     public static void deleteAllMailboxAttachmentFiles(Context context, long accountId,
    465             long mailboxId) {
    466         Cursor c = context.getContentResolver().query(Message.CONTENT_URI,
    467                 Message.ID_COLUMN_PROJECTION, MessageColumns.MAILBOX_KEY + "=?",
    468                 new String[] { Long.toString(mailboxId) }, null);
    469         try {
    470             while (c.moveToNext()) {
    471                 long messageId = c.getLong(Message.ID_PROJECTION_COLUMN);
    472                 deleteAllAttachmentFiles(context, accountId, messageId);
    473             }
    474         } finally {
    475             c.close();
    476         }
    477     }
    478 }
    479