AttachmentProvider.java revision dc78a769fce18d259eccc602c4623fa74cdf5319
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.emailcommon.Logging; 20import com.android.emailcommon.internet.MimeUtility; 21import com.android.emailcommon.provider.EmailContent; 22import com.android.emailcommon.provider.EmailContent.Attachment; 23import com.android.emailcommon.provider.EmailContent.AttachmentColumns; 24import com.android.emailcommon.utility.AttachmentUtilities; 25import com.android.emailcommon.utility.AttachmentUtilities.Columns; 26 27import android.content.ContentProvider; 28import android.content.ContentUris; 29import android.content.ContentValues; 30import android.content.Context; 31import android.content.pm.PackageManager; 32import android.database.Cursor; 33import android.database.MatrixCursor; 34import android.graphics.Bitmap; 35import android.graphics.BitmapFactory; 36import android.net.Uri; 37import android.os.Binder; 38import android.os.Debug; 39import android.os.ParcelFileDescriptor; 40import android.util.Log; 41 42import java.io.File; 43import java.io.FileNotFoundException; 44import java.io.FileOutputStream; 45import java.io.IOException; 46import java.io.InputStream; 47import 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.email.attachmentprovider/acct#/attach#/RAW 54 * 55 * And for access to thumbnails: 56 * content://com.android.email.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 */ 67public 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 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 File[] files = getContext().getCacheDir().listFiles(); 84 for (File file : files) { 85 String filename = file.getName(); 86 if (filename.endsWith(".tmp") || filename.startsWith("thmb_")) { 87 file.delete(); 88 } 89 } 90 return true; 91 } 92 93 /** 94 * Returns the mime type for a given attachment. There are three possible results: 95 * - If thumbnail Uri, always returns "image/png" (even if there's no attachment) 96 * - If the attachment does not exist, returns null 97 * - Returns the mime type of the attachment 98 */ 99 @Override 100 public String getType(Uri uri) { 101 long callingId = Binder.clearCallingIdentity(); 102 try { 103 List<String> segments = uri.getPathSegments(); 104 String id = segments.get(1); 105 String format = segments.get(2); 106 if (AttachmentUtilities.FORMAT_THUMBNAIL.equals(format)) { 107 return "image/png"; 108 } else { 109 uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, Long.parseLong(id)); 110 Cursor c = getContext().getContentResolver().query(uri, MIME_TYPE_PROJECTION, null, 111 null, null); 112 try { 113 if (c.moveToFirst()) { 114 String mimeType = c.getString(MIME_TYPE_COLUMN_MIME_TYPE); 115 String fileName = c.getString(MIME_TYPE_COLUMN_FILENAME); 116 mimeType = AttachmentUtilities.inferMimeType(fileName, mimeType); 117 return mimeType; 118 } 119 } finally { 120 c.close(); 121 } 122 return null; 123 } 124 } finally { 125 Binder.restoreCallingIdentity(callingId); 126 } 127 } 128 129 /** 130 * Open an attachment file. There are two "formats" - "raw", which returns an actual file, 131 * and "thumbnail", which attempts to generate a thumbnail image. 132 * 133 * Thumbnails are cached for easy space recovery and cleanup. 134 * 135 * TODO: The thumbnail format returns null for its failure cases, instead of throwing 136 * FileNotFoundException, and should be fixed for consistency. 137 * 138 * @throws FileNotFoundException 139 */ 140 @Override 141 public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { 142 // If this is a write, the caller must have the EmailProvider permission, which is 143 // based on signature only 144 if (mode.equals("w")) { 145 Context context = getContext(); 146 if (context.checkCallingPermission(EmailContent.PROVIDER_PERMISSION) 147 != PackageManager.PERMISSION_GRANTED) { 148 throw new FileNotFoundException(); 149 } 150 List<String> segments = uri.getPathSegments(); 151 String accountId = segments.get(0); 152 String id = segments.get(1); 153 File saveIn = 154 AttachmentUtilities.getAttachmentDirectory(context, Long.parseLong(accountId)); 155 if (!saveIn.exists()) { 156 saveIn.mkdirs(); 157 } 158 File newFile = new File(saveIn, id); 159 return ParcelFileDescriptor.open( 160 newFile, ParcelFileDescriptor.MODE_READ_WRITE | 161 ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_TRUNCATE); 162 } 163 long callingId = Binder.clearCallingIdentity(); 164 try { 165 List<String> segments = uri.getPathSegments(); 166 String accountId = segments.get(0); 167 String id = segments.get(1); 168 String format = segments.get(2); 169 if (AttachmentUtilities.FORMAT_THUMBNAIL.equals(format)) { 170 int width = Integer.parseInt(segments.get(3)); 171 int height = Integer.parseInt(segments.get(4)); 172 String filename = "thmb_" + accountId + "_" + id; 173 File dir = getContext().getCacheDir(); 174 File file = new File(dir, filename); 175 if (!file.exists()) { 176 Uri attachmentUri = AttachmentUtilities. 177 getAttachmentUri(Long.parseLong(accountId), Long.parseLong(id)); 178 Cursor c = query(attachmentUri, 179 new String[] { Columns.DATA }, null, null, null); 180 if (c != null) { 181 try { 182 if (c.moveToFirst()) { 183 attachmentUri = Uri.parse(c.getString(0)); 184 } else { 185 return null; 186 } 187 } finally { 188 c.close(); 189 } 190 } 191 String type = getContext().getContentResolver().getType(attachmentUri); 192 try { 193 InputStream in = 194 getContext().getContentResolver().openInputStream(attachmentUri); 195 Bitmap thumbnail = createThumbnail(type, in); 196 if (thumbnail == null) { 197 return null; 198 } 199 thumbnail = Bitmap.createScaledBitmap(thumbnail, width, height, true); 200 FileOutputStream out = new FileOutputStream(file); 201 thumbnail.compress(Bitmap.CompressFormat.PNG, 100, out); 202 out.close(); 203 in.close(); 204 } catch (IOException ioe) { 205 Log.d(Logging.LOG_TAG, "openFile/thumbnail failed with " + 206 ioe.getMessage()); 207 return null; 208 } catch (OutOfMemoryError oome) { 209 Log.d(Logging.LOG_TAG, "openFile/thumbnail failed with " + 210 oome.getMessage()); 211 return null; 212 } 213 } 214 return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); 215 } 216 else { 217 return ParcelFileDescriptor.open( 218 new File(getContext().getDatabasePath(accountId + ".db_att"), id), 219 ParcelFileDescriptor.MODE_READ_ONLY); 220 } 221 } finally { 222 Binder.restoreCallingIdentity(callingId); 223 } 224 } 225 226 @Override 227 public int delete(Uri uri, String arg1, String[] arg2) { 228 return 0; 229 } 230 231 @Override 232 public Uri insert(Uri uri, ContentValues values) { 233 return null; 234 } 235 236 /** 237 * Returns a cursor based on the data in the attachments table, or null if the attachment 238 * is not recorded in the table. 239 * 240 * Supports REST Uri only, for a single row - selection, selection args, and sortOrder are 241 * ignored (non-null values should probably throw an exception....) 242 */ 243 @Override 244 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 245 String sortOrder) { 246 long callingId = Binder.clearCallingIdentity(); 247 try { 248 if (projection == null) { 249 projection = 250 new String[] { 251 Columns._ID, 252 Columns.DATA, 253 }; 254 } 255 256 List<String> segments = uri.getPathSegments(); 257 String accountId = segments.get(0); 258 String id = segments.get(1); 259 String format = segments.get(2); 260 String name = null; 261 int size = -1; 262 String contentUri = null; 263 264 uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, Long.parseLong(id)); 265 Cursor c = getContext().getContentResolver().query(uri, PROJECTION_QUERY, 266 null, null, null); 267 try { 268 if (c.moveToFirst()) { 269 name = c.getString(0); 270 size = c.getInt(1); 271 contentUri = c.getString(2); 272 } else { 273 return null; 274 } 275 } finally { 276 c.close(); 277 } 278 279 MatrixCursor ret = new MatrixCursor(projection); 280 Object[] values = new Object[projection.length]; 281 for (int i = 0, count = projection.length; i < count; i++) { 282 String column = projection[i]; 283 if (Columns._ID.equals(column)) { 284 values[i] = id; 285 } 286 else if (Columns.DATA.equals(column)) { 287 values[i] = contentUri; 288 } 289 else if (Columns.DISPLAY_NAME.equals(column)) { 290 values[i] = name; 291 } 292 else if (Columns.SIZE.equals(column)) { 293 values[i] = size; 294 } 295 } 296 ret.addRow(values); 297 return ret; 298 } finally { 299 Binder.restoreCallingIdentity(callingId); 300 } 301 } 302 303 @Override 304 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 305 return 0; 306 } 307 308 private Bitmap createThumbnail(String type, InputStream data) { 309 if(MimeUtility.mimeTypeMatches(type, "image/*")) { 310 return createImageThumbnail(data); 311 } 312 return null; 313 } 314 315 private Bitmap createImageThumbnail(InputStream data) { 316 try { 317 Bitmap bitmap = BitmapFactory.decodeStream(data); 318 return bitmap; 319 } catch (OutOfMemoryError oome) { 320 Log.d(Logging.LOG_TAG, "createImageThumbnail failed with " + oome.getMessage()); 321 return null; 322 } catch (Exception e) { 323 Log.d(Logging.LOG_TAG, "createImageThumbnail failed with " + e.getMessage()); 324 return null; 325 } 326 } 327 328 /** 329 * Need this to suppress warning in unit tests. 330 */ 331 @Override 332 public void shutdown() { 333 // Don't call super.shutdown(), which emits a warning... 334 } 335} 336