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