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