• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.mail.internet.MimeUtility;
20 import com.android.email.provider.EmailContent.Attachment;
21 import com.android.email.provider.EmailContent.AttachmentColumns;
22 import com.android.email.provider.EmailContent.Message;
23 import com.android.email.provider.EmailContent.MessageColumns;
24 
25 import android.content.ContentProvider;
26 import android.content.ContentResolver;
27 import android.content.ContentUris;
28 import android.content.ContentValues;
29 import android.content.Context;
30 import android.database.Cursor;
31 import android.database.MatrixCursor;
32 import android.graphics.Bitmap;
33 import android.graphics.BitmapFactory;
34 import android.net.Uri;
35 import android.os.ParcelFileDescriptor;
36 
37 import java.io.File;
38 import java.io.FileNotFoundException;
39 import java.io.FileOutputStream;
40 import java.io.IOException;
41 import java.io.InputStream;
42 import java.util.List;
43 
44 /*
45  * A simple ContentProvider that allows file access to Email's attachments.
46  *
47  * The URI scheme is as follows.  For raw file access:
48  *   content://com.android.email.attachmentprovider/acct#/attach#/RAW
49  *
50  * And for access to thumbnails:
51  *   content://com.android.email.attachmentprovider/acct#/attach#/THUMBNAIL/width#/height#
52  *
53  * The on-disk (storage) schema is as follows.
54  *
55  * Attachments are stored at:  <database-path>/account#.db_att/item#
56  * Thumbnails are stored at:   <cache-path>/thmb_account#_item#
57  *
58  * Using the standard application context, account #10 and attachment # 20, this would be:
59  *      /data/data/com.android.email/databases/10.db_att/20
60  *      /data/data/com.android.email/cache/thmb_10_20
61  */
62 public class AttachmentProvider extends ContentProvider {
63 
64     public static final String AUTHORITY = "com.android.email.attachmentprovider";
65     public static final Uri CONTENT_URI = Uri.parse( "content://" + AUTHORITY);
66 
67     private static final String FORMAT_RAW = "RAW";
68     private static final String FORMAT_THUMBNAIL = "THUMBNAIL";
69 
70     public static class AttachmentProviderColumns {
71         public static final String _ID = "_id";
72         public static final String DATA = "_data";
73         public static final String DISPLAY_NAME = "_display_name";
74         public static final String SIZE = "_size";
75     }
76 
77     private String[] PROJECTION_MIME_TYPE = new String[] { AttachmentColumns.MIME_TYPE };
78     private String[] PROJECTION_QUERY = new String[] { AttachmentColumns.FILENAME,
79             AttachmentColumns.SIZE, AttachmentColumns.CONTENT_URI };
80 
getAttachmentUri(long accountId, long id)81     public static Uri getAttachmentUri(long accountId, long id) {
82         return CONTENT_URI.buildUpon()
83                 .appendPath(Long.toString(accountId))
84                 .appendPath(Long.toString(id))
85                 .appendPath(FORMAT_RAW)
86                 .build();
87     }
88 
getAttachmentThumbnailUri(long accountId, long id, int width, int height)89     public static Uri getAttachmentThumbnailUri(long accountId, long id,
90             int width, int height) {
91         return CONTENT_URI.buildUpon()
92                 .appendPath(Long.toString(accountId))
93                 .appendPath(Long.toString(id))
94                 .appendPath(FORMAT_THUMBNAIL)
95                 .appendPath(Integer.toString(width))
96                 .appendPath(Integer.toString(height))
97                 .build();
98     }
99 
100     /**
101      * Return the filename for a given attachment.  This should be used by any code that is
102      * going to *write* attachments.
103      *
104      * This does not create or write the file, or even the directories.  It simply builds
105      * the filename that should be used.
106      */
getAttachmentFilename(Context context, long accountId, long attachmentId)107     public static File getAttachmentFilename(Context context, long accountId, long attachmentId) {
108         return new File(getAttachmentDirectory(context, accountId), Long.toString(attachmentId));
109     }
110 
111     /**
112      * Return the directory for a given attachment.  This should be used by any code that is
113      * going to *write* attachments.
114      *
115      * This does not create or write the directory.  It simply builds the pathname that should be
116      * used.
117      */
getAttachmentDirectory(Context context, long accountId)118     public static File getAttachmentDirectory(Context context, long accountId) {
119         return context.getDatabasePath(accountId + ".db_att");
120     }
121 
122     @Override
onCreate()123     public boolean onCreate() {
124         /*
125          * We use the cache dir as a temporary directory (since Android doesn't give us one) so
126          * on startup we'll clean up any .tmp files from the last run.
127          */
128         File[] files = getContext().getCacheDir().listFiles();
129         for (File file : files) {
130             String filename = file.getName();
131             if (filename.endsWith(".tmp") || filename.startsWith("thmb_")) {
132                 file.delete();
133             }
134         }
135         return true;
136     }
137 
138     /**
139      * Returns the mime type for a given attachment.  There are three possible results:
140      *  - If thumbnail Uri, always returns "image/png" (even if there's no attachment)
141      *  - If the attachment does not exist, returns null
142      *  - Returns the mime type of the attachment
143      */
144     @Override
getType(Uri uri)145     public String getType(Uri uri) {
146         List<String> segments = uri.getPathSegments();
147         String accountId = segments.get(0);
148         String id = segments.get(1);
149         String format = segments.get(2);
150         if (FORMAT_THUMBNAIL.equals(format)) {
151             return "image/png";
152         } else {
153             uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, Long.parseLong(id));
154             Cursor c = getContext().getContentResolver().query(uri, PROJECTION_MIME_TYPE,
155                     null, null, null);
156             try {
157                 if (c.moveToFirst()) {
158                     return c.getString(0);
159                 }
160             } finally {
161                 c.close();
162             }
163             return null;
164         }
165     }
166 
167     /**
168      * Open an attachment file.  There are two "modes" - "raw", which returns an actual file,
169      * and "thumbnail", which attempts to generate a thumbnail image.
170      *
171      * Thumbnails are cached for easy space recovery and cleanup.
172      *
173      * TODO:  The thumbnail mode returns null for its failure cases, instead of throwing
174      * FileNotFoundException, and should be fixed for consistency.
175      *
176      *  @throws FileNotFoundException
177      */
178     @Override
openFile(Uri uri, String mode)179     public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
180         List<String> segments = uri.getPathSegments();
181         String accountId = segments.get(0);
182         String id = segments.get(1);
183         String format = segments.get(2);
184         if (FORMAT_THUMBNAIL.equals(format)) {
185             int width = Integer.parseInt(segments.get(3));
186             int height = Integer.parseInt(segments.get(4));
187             String filename = "thmb_" + accountId + "_" + id;
188             File dir = getContext().getCacheDir();
189             File file = new File(dir, filename);
190             if (!file.exists()) {
191                 Uri attachmentUri = getAttachmentUri(Long.parseLong(accountId), Long.parseLong(id));
192                 Cursor c = query(attachmentUri,
193                         new String[] { AttachmentProviderColumns.DATA }, null, null, null);
194                 if (c != null) {
195                     try {
196                         if (c.moveToFirst()) {
197                             attachmentUri = Uri.parse(c.getString(0));
198                         } else {
199                             return null;
200                         }
201                     } finally {
202                         c.close();
203                     }
204                 }
205                 String type = getContext().getContentResolver().getType(attachmentUri);
206                 try {
207                     InputStream in =
208                         getContext().getContentResolver().openInputStream(attachmentUri);
209                     Bitmap thumbnail = createThumbnail(type, in);
210                     thumbnail = Bitmap.createScaledBitmap(thumbnail, width, height, true);
211                     FileOutputStream out = new FileOutputStream(file);
212                     thumbnail.compress(Bitmap.CompressFormat.PNG, 100, out);
213                     out.close();
214                     in.close();
215                 }
216                 catch (IOException ioe) {
217                     return null;
218                 }
219             }
220             return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
221         }
222         else {
223             return ParcelFileDescriptor.open(
224                     new File(getContext().getDatabasePath(accountId + ".db_att"), id),
225                     ParcelFileDescriptor.MODE_READ_ONLY);
226         }
227     }
228 
229     @Override
delete(Uri uri, String arg1, String[] arg2)230     public int delete(Uri uri, String arg1, String[] arg2) {
231         return 0;
232     }
233 
234     @Override
insert(Uri uri, ContentValues values)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
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)247     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
248             String sortOrder) {
249         if (projection == null) {
250             projection =
251                 new String[] {
252                     AttachmentProviderColumns._ID,
253                     AttachmentProviderColumns.DATA,
254                     };
255         }
256 
257         List<String> segments = uri.getPathSegments();
258         String accountId = segments.get(0);
259         String id = segments.get(1);
260         String format = segments.get(2);
261         String name = null;
262         int size = -1;
263         String contentUri = null;
264 
265         uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, Long.parseLong(id));
266         Cursor c = getContext().getContentResolver().query(uri, PROJECTION_QUERY,
267                 null, null, null);
268         try {
269             if (c.moveToFirst()) {
270                 name = c.getString(0);
271                 size = c.getInt(1);
272                 contentUri = c.getString(2);
273             } else {
274                 return null;
275             }
276         } finally {
277             c.close();
278         }
279 
280         MatrixCursor ret = new MatrixCursor(projection);
281         Object[] values = new Object[projection.length];
282         for (int i = 0, count = projection.length; i < count; i++) {
283             String column = projection[i];
284             if (AttachmentProviderColumns._ID.equals(column)) {
285                 values[i] = id;
286             }
287             else if (AttachmentProviderColumns.DATA.equals(column)) {
288                 values[i] = contentUri;
289             }
290             else if (AttachmentProviderColumns.DISPLAY_NAME.equals(column)) {
291                 values[i] = name;
292             }
293             else if (AttachmentProviderColumns.SIZE.equals(column)) {
294                 values[i] = size;
295             }
296         }
297         ret.addRow(values);
298         return ret;
299     }
300 
301     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)302     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
303         return 0;
304     }
305 
createThumbnail(String type, InputStream data)306     private Bitmap createThumbnail(String type, InputStream data) {
307         if(MimeUtility.mimeTypeMatches(type, "image/*")) {
308             return createImageThumbnail(data);
309         }
310         return null;
311     }
312 
createImageThumbnail(InputStream data)313     private Bitmap createImageThumbnail(InputStream data) {
314         try {
315             Bitmap bitmap = BitmapFactory.decodeStream(data);
316             return bitmap;
317         }
318         catch (OutOfMemoryError oome) {
319             /*
320              * Improperly downloaded images, corrupt bitmaps and the like can commonly
321              * cause OOME due to invalid allocation sizes. We're happy with a null bitmap in
322              * that case. If the system is really out of memory we'll know about it soon
323              * enough.
324              */
325             return null;
326         }
327         catch (Exception e) {
328             return null;
329         }
330     }
331     /**
332      * Resolve attachment id to content URI.  Returns the resolved content URI (from the attachment
333      * DB) or, if not found, simply returns the incoming value.
334      *
335      * @param attachmentUri
336      * @return resolved content URI
337      *
338      * TODO:  Throws an SQLite exception on a missing DB file (e.g. unknown URI) instead of just
339      * returning the incoming uri, as it should.
340      */
resolveAttachmentIdToContentUri(ContentResolver resolver, Uri attachmentUri)341     public static Uri resolveAttachmentIdToContentUri(ContentResolver resolver, Uri attachmentUri) {
342         Cursor c = resolver.query(attachmentUri,
343                 new String[] { AttachmentProvider.AttachmentProviderColumns.DATA },
344                 null, null, null);
345         if (c != null) {
346             try {
347                 if (c.moveToFirst()) {
348                     return Uri.parse(c.getString(0));
349                 }
350             } finally {
351                 c.close();
352             }
353         }
354         return attachmentUri;
355     }
356 
357     /**
358      * In support of deleting a message, find all attachments and delete associated attachment
359      * files.
360      * @param context
361      * @param accountId the account for the message
362      * @param messageId the message
363      */
deleteAllAttachmentFiles(Context context, long accountId, long messageId)364     public static void deleteAllAttachmentFiles(Context context, long accountId, long messageId) {
365         Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, messageId);
366         Cursor c = context.getContentResolver().query(uri, Attachment.ID_PROJECTION,
367                 null, null, null);
368         try {
369             while (c.moveToNext()) {
370                 long attachmentId = c.getLong(Attachment.ID_PROJECTION_COLUMN);
371                 File attachmentFile = getAttachmentFilename(context, accountId, attachmentId);
372                 // Note, delete() throws no exceptions for basic FS errors (e.g. file not found)
373                 // it just returns false, which we ignore, and proceed to the next file.
374                 // This entire loop is best-effort only.
375                 attachmentFile.delete();
376             }
377         } finally {
378             c.close();
379         }
380     }
381 
382     /**
383      * In support of deleting a mailbox, find all messages and delete their attachments.
384      *
385      * @param context
386      * @param accountId the account for the mailbox
387      * @param mailboxId the mailbox for the messages
388      */
deleteAllMailboxAttachmentFiles(Context context, long accountId, long mailboxId)389     public static void deleteAllMailboxAttachmentFiles(Context context, long accountId,
390             long mailboxId) {
391         Cursor c = context.getContentResolver().query(Message.CONTENT_URI,
392                 Message.ID_COLUMN_PROJECTION, MessageColumns.MAILBOX_KEY + "=?",
393                 new String[] { Long.toString(mailboxId) }, null);
394         try {
395             while (c.moveToNext()) {
396                 long messageId = c.getLong(Message.ID_PROJECTION_COLUMN);
397                 deleteAllAttachmentFiles(context, accountId, messageId);
398             }
399         } finally {
400             c.close();
401         }
402     }
403 }
404