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