• 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 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
onCreate()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
getType(Uri uri)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
openFile(Uri uri, String mode)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
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         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
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)307     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
308         return 0;
309     }
310 
createThumbnail(String type, InputStream data)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 
createImageThumbnail(InputStream data)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
shutdown()335     public void shutdown() {
336         // Don't call super.shutdown(), which emits a warning...
337     }
338 }
339