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 final long accountId = Long.parseLong(segments.get(0)); 170 final long id = Long.parseLong(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.getAttachmentUri(accountId, id); 180 Cursor c = query(attachmentUri, 181 new String[] { Columns.DATA }, null, null, null); 182 if (c != null) { 183 try { 184 if (c.moveToFirst()) { 185 attachmentUri = Uri.parse(c.getString(0)); 186 } else { 187 return null; 188 } 189 } finally { 190 c.close(); 191 } 192 } 193 String type = getContext().getContentResolver().getType(attachmentUri); 194 try { 195 InputStream in = 196 getContext().getContentResolver().openInputStream(attachmentUri); 197 Bitmap thumbnail = createThumbnail(type, in); 198 if (thumbnail == null) { 199 return null; 200 } 201 thumbnail = Bitmap.createScaledBitmap(thumbnail, width, height, true); 202 FileOutputStream out = new FileOutputStream(file); 203 thumbnail.compress(Bitmap.CompressFormat.PNG, 100, out); 204 out.close(); 205 in.close(); 206 } catch (IOException ioe) { 207 LogUtils.d(Logging.LOG_TAG, "openFile/thumbnail failed with " + 208 ioe.getMessage()); 209 return null; 210 } catch (OutOfMemoryError oome) { 211 LogUtils.d(Logging.LOG_TAG, "openFile/thumbnail failed with " + 212 oome.getMessage()); 213 return null; 214 } 215 } 216 return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); 217 } 218 else { 219 return ParcelFileDescriptor.open( 220 new File(getContext().getDatabasePath(accountId + ".db_att"), 221 String.valueOf(id)), 222 ParcelFileDescriptor.MODE_READ_ONLY); 223 } 224 } catch (NumberFormatException e) { 225 LogUtils.e(Logging.LOG_TAG, 226 "AttachmentProvider.openFile: Failed to open as id is not a long"); 227 return null; 228 } finally { 229 Binder.restoreCallingIdentity(callingId); 230 } 231 } 232 233 @Override 234 public int delete(Uri uri, String arg1, String[] arg2) { 235 return 0; 236 } 237 238 @Override 239 public Uri insert(Uri uri, ContentValues values) { 240 return null; 241 } 242 243 /** 244 * Returns a cursor based on the data in the attachments table, or null if the attachment 245 * is not recorded in the table. 246 * 247 * Supports REST Uri only, for a single row - selection, selection args, and sortOrder are 248 * ignored (non-null values should probably throw an exception....) 249 */ 250 @Override 251 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 252 String sortOrder) { 253 long callingId = Binder.clearCallingIdentity(); 254 try { 255 if (projection == null) { 256 projection = 257 new String[] { 258 Columns._ID, 259 Columns.DATA, 260 }; 261 } 262 263 List<String> segments = uri.getPathSegments(); 264 String accountId = segments.get(0); 265 String id = segments.get(1); 266 String format = segments.get(2); 267 String name = null; 268 int size = -1; 269 String contentUri = null; 270 271 uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, Long.parseLong(id)); 272 Cursor c = getContext().getContentResolver().query(uri, PROJECTION_QUERY, 273 null, null, null); 274 try { 275 if (c.moveToFirst()) { 276 name = c.getString(0); 277 size = c.getInt(1); 278 contentUri = c.getString(2); 279 } else { 280 return null; 281 } 282 } finally { 283 c.close(); 284 } 285 286 MatrixCursor ret = new MatrixCursorWithCachedColumns(projection); 287 Object[] values = new Object[projection.length]; 288 for (int i = 0, count = projection.length; i < count; i++) { 289 String column = projection[i]; 290 if (Columns._ID.equals(column)) { 291 values[i] = id; 292 } 293 else if (Columns.DATA.equals(column)) { 294 values[i] = contentUri; 295 } 296 else if (Columns.DISPLAY_NAME.equals(column)) { 297 values[i] = name; 298 } 299 else if (Columns.SIZE.equals(column)) { 300 values[i] = size; 301 } 302 } 303 ret.addRow(values); 304 return ret; 305 } finally { 306 Binder.restoreCallingIdentity(callingId); 307 } 308 } 309 310 @Override 311 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 312 return 0; 313 } 314 315 private static Bitmap createThumbnail(String type, InputStream data) { 316 if(MimeUtility.mimeTypeMatches(type, "image/*")) { 317 return createImageThumbnail(data); 318 } 319 return null; 320 } 321 322 private static Bitmap createImageThumbnail(InputStream data) { 323 try { 324 Bitmap bitmap = BitmapFactory.decodeStream(data); 325 return bitmap; 326 } catch (OutOfMemoryError oome) { 327 LogUtils.d(Logging.LOG_TAG, "createImageThumbnail failed with " + oome.getMessage()); 328 return null; 329 } catch (Exception e) { 330 LogUtils.d(Logging.LOG_TAG, "createImageThumbnail failed with " + e.getMessage()); 331 return null; 332 } 333 } 334 335 /** 336 * Need this to suppress warning in unit tests. 337 */ 338 @Override 339 public void shutdown() { 340 // Don't call super.shutdown(), which emits a warning... 341 } 342 } 343