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