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             final long accountId = Long.parseLong(segments.get(0));
    170             final long id = Long.parseLong(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.getAttachmentUri(accountId, id);
    180                     Cursor c = query(attachmentUri,
    181                             new String[] { Columns.DATA }, null, null, null);
    182                     if (c != null) {
    183                         try {
    184                             if (c.moveToFirst()) {
    185                                 attachmentUri = Uri.parse(c.getString(0));
    186                             } else {
    187                                 return null;
    188                             }
    189                         } finally {
    190                             c.close();
    191                         }
    192                     }
    193                     String type = getContext().getContentResolver().getType(attachmentUri);
    194                     try {
    195                         InputStream in =
    196                             getContext().getContentResolver().openInputStream(attachmentUri);
    197                         Bitmap thumbnail = createThumbnail(type, in);
    198                         if (thumbnail == null) {
    199                             return null;
    200                         }
    201                         thumbnail = Bitmap.createScaledBitmap(thumbnail, width, height, true);
    202                         FileOutputStream out = new FileOutputStream(file);
    203                         thumbnail.compress(Bitmap.CompressFormat.PNG, 100, out);
    204                         out.close();
    205                         in.close();
    206                     } catch (IOException ioe) {
    207                         LogUtils.d(Logging.LOG_TAG, "openFile/thumbnail failed with " +
    208                                 ioe.getMessage());
    209                         return null;
    210                     } catch (OutOfMemoryError oome) {
    211                         LogUtils.d(Logging.LOG_TAG, "openFile/thumbnail failed with " +
    212                                 oome.getMessage());
    213                         return null;
    214                     }
    215                 }
    216                 return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
    217             }
    218             else {
    219                 return ParcelFileDescriptor.open(
    220                         new File(getContext().getDatabasePath(accountId + ".db_att"),
    221                                 String.valueOf(id)),
    222                         ParcelFileDescriptor.MODE_READ_ONLY);
    223             }
    224         } catch (NumberFormatException e) {
    225             LogUtils.e(Logging.LOG_TAG,
    226                     "AttachmentProvider.openFile: Failed to open as id is not a long");
    227             return null;
    228         } finally {
    229             Binder.restoreCallingIdentity(callingId);
    230         }
    231     }
    232 
    233     @Override
    234     public int delete(Uri uri, String arg1, String[] arg2) {
    235         return 0;
    236     }
    237 
    238     @Override
    239     public Uri insert(Uri uri, ContentValues values) {
    240         return null;
    241     }
    242 
    243     /**
    244      * Returns a cursor based on the data in the attachments table, or null if the attachment
    245      * is not recorded in the table.
    246      *
    247      * Supports REST Uri only, for a single row - selection, selection args, and sortOrder are
    248      * ignored (non-null values should probably throw an exception....)
    249      */
    250     @Override
    251     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
    252             String sortOrder) {
    253         long callingId = Binder.clearCallingIdentity();
    254         try {
    255             if (projection == null) {
    256                 projection =
    257                     new String[] {
    258                         Columns._ID,
    259                         Columns.DATA,
    260                 };
    261             }
    262 
    263             List<String> segments = uri.getPathSegments();
    264             String accountId = segments.get(0);
    265             String id = segments.get(1);
    266             String format = segments.get(2);
    267             String name = null;
    268             int size = -1;
    269             String contentUri = null;
    270 
    271             uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, Long.parseLong(id));
    272             Cursor c = getContext().getContentResolver().query(uri, PROJECTION_QUERY,
    273                     null, null, null);
    274             try {
    275                 if (c.moveToFirst()) {
    276                     name = c.getString(0);
    277                     size = c.getInt(1);
    278                     contentUri = c.getString(2);
    279                 } else {
    280                     return null;
    281                 }
    282             } finally {
    283                 c.close();
    284             }
    285 
    286             MatrixCursor ret = new MatrixCursorWithCachedColumns(projection);
    287             Object[] values = new Object[projection.length];
    288             for (int i = 0, count = projection.length; i < count; i++) {
    289                 String column = projection[i];
    290                 if (Columns._ID.equals(column)) {
    291                     values[i] = id;
    292                 }
    293                 else if (Columns.DATA.equals(column)) {
    294                     values[i] = contentUri;
    295                 }
    296                 else if (Columns.DISPLAY_NAME.equals(column)) {
    297                     values[i] = name;
    298                 }
    299                 else if (Columns.SIZE.equals(column)) {
    300                     values[i] = size;
    301                 }
    302             }
    303             ret.addRow(values);
    304             return ret;
    305         } finally {
    306             Binder.restoreCallingIdentity(callingId);
    307         }
    308     }
    309 
    310     @Override
    311     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    312         return 0;
    313     }
    314 
    315     private static Bitmap createThumbnail(String type, InputStream data) {
    316         if(MimeUtility.mimeTypeMatches(type, "image/*")) {
    317             return createImageThumbnail(data);
    318         }
    319         return null;
    320     }
    321 
    322     private static Bitmap createImageThumbnail(InputStream data) {
    323         try {
    324             Bitmap bitmap = BitmapFactory.decodeStream(data);
    325             return bitmap;
    326         } catch (OutOfMemoryError oome) {
    327             LogUtils.d(Logging.LOG_TAG, "createImageThumbnail failed with " + oome.getMessage());
    328             return null;
    329         } catch (Exception e) {
    330             LogUtils.d(Logging.LOG_TAG, "createImageThumbnail failed with " + e.getMessage());
    331             return null;
    332         }
    333     }
    334 
    335     /**
    336      * Need this to suppress warning in unit tests.
    337      */
    338     @Override
    339     public void shutdown() {
    340         // Don't call super.shutdown(), which emits a warning...
    341     }
    342 }
    343