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