• 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.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
onCreate()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
getType(Uri uri)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
openFile(Uri uri, String mode)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
delete(Uri uri, String arg1, String[] arg2)226     public int delete(Uri uri, String arg1, String[] arg2) {
227         return 0;
228     }
229 
230     @Override
insert(Uri uri, ContentValues values)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
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)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
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)303     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
304         return 0;
305     }
306 
createThumbnail(String type, InputStream data)307     private Bitmap createThumbnail(String type, InputStream data) {
308         if(MimeUtility.mimeTypeMatches(type, "image/*")) {
309             return createImageThumbnail(data);
310         }
311         return null;
312     }
313 
createImageThumbnail(InputStream data)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
shutdown()331     public void shutdown() {
332         // Don't call super.shutdown(), which emits a warning...
333     }
334 }
335