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