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.emailcommon.Logging;
     20 import com.android.emailcommon.internet.MimeUtility;
     21 import com.android.emailcommon.provider.EmailContent;
     22 import com.android.emailcommon.provider.EmailContent.Attachment;
     23 import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
     24 import com.android.emailcommon.utility.AttachmentUtilities;
     25 import com.android.emailcommon.utility.AttachmentUtilities.Columns;
     26 
     27 import android.content.ContentProvider;
     28 import android.content.ContentUris;
     29 import android.content.ContentValues;
     30 import android.content.Context;
     31 import android.content.pm.PackageManager;
     32 import android.database.Cursor;
     33 import android.database.MatrixCursor;
     34 import android.graphics.Bitmap;
     35 import android.graphics.BitmapFactory;
     36 import android.net.Uri;
     37 import android.os.Binder;
     38 import android.os.ParcelFileDescriptor;
     39 import android.util.Log;
     40 
     41 import java.io.File;
     42 import java.io.FileNotFoundException;
     43 import java.io.FileOutputStream;
     44 import java.io.IOException;
     45 import java.io.InputStream;
     46 import java.util.List;
     47 
     48 /*
     49  * A simple ContentProvider that allows file access to Email's attachments.
     50  *
     51  * The URI scheme is as follows.  For raw file access:
     52  *   content://com.android.email.attachmentprovider/acct#/attach#/RAW
     53  *
     54  * And for access to thumbnails:
     55  *   content://com.android.email.attachmentprovider/acct#/attach#/THUMBNAIL/width#/height#
     56  *
     57  * The on-disk (storage) schema is as follows.
     58  *
     59  * Attachments are stored at:  <database-path>/account#.db_att/item#
     60  * Thumbnails are stored at:   <cache-path>/thmb_account#_item#
     61  *
     62  * Using the standard application context, account #10 and attachment # 20, this would be:
     63  *      /data/data/com.android.email/databases/10.db_att/20
     64  *      /data/data/com.android.email/cache/thmb_10_20
     65  */
     66 public class AttachmentProvider extends ContentProvider {
     67 
     68     private static final String[] MIME_TYPE_PROJECTION = new String[] {
     69             AttachmentColumns.MIME_TYPE, AttachmentColumns.FILENAME };
     70     private static final int MIME_TYPE_COLUMN_MIME_TYPE = 0;
     71     private static final int MIME_TYPE_COLUMN_FILENAME = 1;
     72 
     73     private static final String[] PROJECTION_QUERY = new String[] { AttachmentColumns.FILENAME,
     74             AttachmentColumns.SIZE, AttachmentColumns.CONTENT_URI };
     75 
     76     @Override
     77     public boolean onCreate() {
     78         /*
     79          * We use the cache dir as a temporary directory (since Android doesn't give us one) so
     80          * on startup we'll clean up any .tmp files from the last run.
     81          */
     82         File[] files = getContext().getCacheDir().listFiles();
     83         for (File file : files) {
     84             String filename = file.getName();
     85             if (filename.endsWith(".tmp") || filename.startsWith("thmb_")) {
     86                 file.delete();
     87             }
     88         }
     89         return true;
     90     }
     91 
     92     /**
     93      * Returns the mime type for a given attachment.  There are three possible results:
     94      *  - If thumbnail Uri, always returns "image/png" (even if there's no attachment)
     95      *  - If the attachment does not exist, returns null
     96      *  - Returns the mime type of the attachment
     97      */
     98     @Override
     99     public String getType(Uri uri) {
    100         long callingId = Binder.clearCallingIdentity();
    101         try {
    102             List<String> segments = uri.getPathSegments();
    103             String id = segments.get(1);
    104             String format = segments.get(2);
    105             if (AttachmentUtilities.FORMAT_THUMBNAIL.equals(format)) {
    106                 return "image/png";
    107             } else {
    108                 uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, Long.parseLong(id));
    109                 Cursor c = getContext().getContentResolver().query(uri, MIME_TYPE_PROJECTION, null,
    110                         null, null);
    111                 try {
    112                     if (c.moveToFirst()) {
    113                         String mimeType = c.getString(MIME_TYPE_COLUMN_MIME_TYPE);
    114                         String fileName = c.getString(MIME_TYPE_COLUMN_FILENAME);
    115                         mimeType = AttachmentUtilities.inferMimeType(fileName, mimeType);
    116                         return mimeType;
    117                     }
    118                 } finally {
    119                     c.close();
    120                 }
    121                 return null;
    122             }
    123         } finally {
    124             Binder.restoreCallingIdentity(callingId);
    125         }
    126     }
    127 
    128     /**
    129      * Open an attachment file.  There are two "formats" - "raw", which returns an actual file,
    130      * and "thumbnail", which attempts to generate a thumbnail image.
    131      *
    132      * Thumbnails are cached for easy space recovery and cleanup.
    133      *
    134      * TODO:  The thumbnail format returns null for its failure cases, instead of throwing
    135      * FileNotFoundException, and should be fixed for consistency.
    136      *
    137      *  @throws FileNotFoundException
    138      */
    139     @Override
    140     public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
    141         // If this is a write, the caller must have the EmailProvider permission, which is
    142         // based on signature only
    143         if (mode.equals("w")) {
    144             Context context = getContext();
    145             if (context.checkCallingPermission(EmailContent.PROVIDER_PERMISSION)
    146                     != PackageManager.PERMISSION_GRANTED) {
    147                 throw new FileNotFoundException();
    148             }
    149             List<String> segments = uri.getPathSegments();
    150             String accountId = segments.get(0);
    151             String id = segments.get(1);
    152             File saveIn =
    153                 AttachmentUtilities.getAttachmentDirectory(context, Long.parseLong(accountId));
    154             if (!saveIn.exists()) {
    155                 saveIn.mkdirs();
    156             }
    157             File newFile = new File(saveIn, id);
    158             return ParcelFileDescriptor.open(
    159                     newFile, ParcelFileDescriptor.MODE_READ_WRITE |
    160                         ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_TRUNCATE);
    161         }
    162         long callingId = Binder.clearCallingIdentity();
    163         try {
    164             List<String> segments = uri.getPathSegments();
    165             String accountId = segments.get(0);
    166             String id = segments.get(1);
    167             String format = segments.get(2);
    168             if (AttachmentUtilities.FORMAT_THUMBNAIL.equals(format)) {
    169                 int width = Integer.parseInt(segments.get(3));
    170                 int height = Integer.parseInt(segments.get(4));
    171                 String filename = "thmb_" + accountId + "_" + id;
    172                 File dir = getContext().getCacheDir();
    173                 File file = new File(dir, filename);
    174                 if (!file.exists()) {
    175                     Uri attachmentUri = AttachmentUtilities.
    176                         getAttachmentUri(Long.parseLong(accountId), Long.parseLong(id));
    177                     Cursor c = query(attachmentUri,
    178                             new String[] { Columns.DATA }, null, null, null);
    179                     if (c != null) {
    180                         try {
    181                             if (c.moveToFirst()) {
    182                                 attachmentUri = Uri.parse(c.getString(0));
    183                             } else {
    184                                 return null;
    185                             }
    186                         } finally {
    187                             c.close();
    188                         }
    189                     }
    190                     String type = getContext().getContentResolver().getType(attachmentUri);
    191                     try {
    192                         InputStream in =
    193                             getContext().getContentResolver().openInputStream(attachmentUri);
    194                         Bitmap thumbnail = createThumbnail(type, in);
    195                         if (thumbnail == null) {
    196                             return null;
    197                         }
    198                         thumbnail = Bitmap.createScaledBitmap(thumbnail, width, height, true);
    199                         FileOutputStream out = new FileOutputStream(file);
    200                         thumbnail.compress(Bitmap.CompressFormat.PNG, 100, out);
    201                         out.close();
    202                         in.close();
    203                     } catch (IOException ioe) {
    204                         Log.d(Logging.LOG_TAG, "openFile/thumbnail failed with " +
    205                                 ioe.getMessage());
    206                         return null;
    207                     } catch (OutOfMemoryError oome) {
    208                         Log.d(Logging.LOG_TAG, "openFile/thumbnail failed with " +
    209                                 oome.getMessage());
    210                         return null;
    211                     }
    212                 }
    213                 return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
    214             }
    215             else {
    216                 return ParcelFileDescriptor.open(
    217                         new File(getContext().getDatabasePath(accountId + ".db_att"), id),
    218                         ParcelFileDescriptor.MODE_READ_ONLY);
    219             }
    220         } finally {
    221             Binder.restoreCallingIdentity(callingId);
    222         }
    223     }
    224 
    225     @Override
    226     public int delete(Uri uri, String arg1, String[] arg2) {
    227         return 0;
    228     }
    229 
    230     @Override
    231     public Uri insert(Uri uri, ContentValues values) {
    232         return null;
    233     }
    234 
    235     /**
    236      * Returns a cursor based on the data in the attachments table, or null if the attachment
    237      * is not recorded in the table.
    238      *
    239      * Supports REST Uri only, for a single row - selection, selection args, and sortOrder are
    240      * ignored (non-null values should probably throw an exception....)
    241      */
    242     @Override
    243     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
    244             String sortOrder) {
    245         long callingId = Binder.clearCallingIdentity();
    246         try {
    247             if (projection == null) {
    248                 projection =
    249                     new String[] {
    250                         Columns._ID,
    251                         Columns.DATA,
    252                 };
    253             }
    254 
    255             List<String> segments = uri.getPathSegments();
    256             String accountId = segments.get(0);
    257             String id = segments.get(1);
    258             String format = segments.get(2);
    259             String name = null;
    260             int size = -1;
    261             String contentUri = null;
    262 
    263             uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, Long.parseLong(id));
    264             Cursor c = getContext().getContentResolver().query(uri, PROJECTION_QUERY,
    265                     null, null, null);
    266             try {
    267                 if (c.moveToFirst()) {
    268                     name = c.getString(0);
    269                     size = c.getInt(1);
    270                     contentUri = c.getString(2);
    271                 } else {
    272                     return null;
    273                 }
    274             } finally {
    275                 c.close();
    276             }
    277 
    278             MatrixCursor ret = new MatrixCursor(projection);
    279             Object[] values = new Object[projection.length];
    280             for (int i = 0, count = projection.length; i < count; i++) {
    281                 String column = projection[i];
    282                 if (Columns._ID.equals(column)) {
    283                     values[i] = id;
    284                 }
    285                 else if (Columns.DATA.equals(column)) {
    286                     values[i] = contentUri;
    287                 }
    288                 else if (Columns.DISPLAY_NAME.equals(column)) {
    289                     values[i] = name;
    290                 }
    291                 else if (Columns.SIZE.equals(column)) {
    292                     values[i] = size;
    293                 }
    294             }
    295             ret.addRow(values);
    296             return ret;
    297         } finally {
    298             Binder.restoreCallingIdentity(callingId);
    299         }
    300     }
    301 
    302     @Override
    303     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    304         return 0;
    305     }
    306 
    307     private Bitmap createThumbnail(String type, InputStream data) {
    308         if(MimeUtility.mimeTypeMatches(type, "image/*")) {
    309             return createImageThumbnail(data);
    310         }
    311         return null;
    312     }
    313 
    314     private Bitmap createImageThumbnail(InputStream data) {
    315         try {
    316             Bitmap bitmap = BitmapFactory.decodeStream(data);
    317             return bitmap;
    318         } catch (OutOfMemoryError oome) {
    319             Log.d(Logging.LOG_TAG, "createImageThumbnail failed with " + oome.getMessage());
    320             return null;
    321         } catch (Exception e) {
    322             Log.d(Logging.LOG_TAG, "createImageThumbnail failed with " + e.getMessage());
    323             return null;
    324         }
    325     }
    326 
    327     /**
    328      * Need this to suppress warning in unit tests.
    329      */
    330     @Override
    331     public void shutdown() {
    332         // Don't call super.shutdown(), which emits a warning...
    333     }
    334 }
    335