package com.fsck.k9droidtn.provider; import android.content.ContentProvider; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.MatrixCursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.ParcelFileDescriptor; import android.util.Log; import com.fsck.k9droidtn.Account; import com.fsck.k9droidtn.K9; import com.fsck.k9droidtn.Preferences; import com.fsck.k9droidtn.mail.MessagingException; import com.fsck.k9droidtn.mail.internet.MimeUtility; import com.fsck.k9droidtn.mail.store.LocalStore; import com.fsck.k9droidtn.mail.store.LocalStore.AttachmentInfo; import com.fsck.k9droidtn.mail.store.StorageManager; import java.io.*; import java.util.List; /** * A simple ContentProvider that allows file access to attachments. * * <p> * Warning! We make heavy assumptions about the Uris used by the {@link LocalStore} for an * {@link Account} here. * </p> */ public class AttachmentProvider extends ContentProvider { public static final Uri CONTENT_URI = Uri.parse("content://com.fsck.k9droidtn.attachmentprovider"); private static final String FORMAT_RAW = "RAW"; private static final String FORMAT_VIEW = "VIEW"; private static final String FORMAT_THUMBNAIL = "THUMBNAIL"; private static final String[] DEFAULT_PROJECTION = new String[] { AttachmentProviderColumns._ID, AttachmentProviderColumns.DATA, }; public static class AttachmentProviderColumns { public static final String _ID = "_id"; public static final String DATA = "_data"; public static final String DISPLAY_NAME = "_display_name"; public static final String SIZE = "_size"; } public static Uri getAttachmentUri(Account account, long id) { return getAttachmentUri(account.getUuid(), id, true); } public static Uri getAttachmentUriForViewing(Account account, long id) { return getAttachmentUri(account.getUuid(), id, false); } public static Uri getAttachmentThumbnailUri(Account account, long id, int width, int height) { return CONTENT_URI.buildUpon() .appendPath(account.getUuid()) .appendPath(Long.toString(id)) .appendPath(FORMAT_THUMBNAIL) .appendPath(Integer.toString(width)) .appendPath(Integer.toString(height)) .build(); } private static Uri getAttachmentUri(String db, long id, boolean raw) { return CONTENT_URI.buildUpon() .appendPath(db) .appendPath(Long.toString(id)) .appendPath(raw ? FORMAT_RAW : FORMAT_VIEW) .build(); } public static void clear(Context context) { /* * We use the cache dir as a temporary directory (since Android doesn't give us one) so * on startup we'll clean up any .tmp files from the last run. */ File[] files = context.getCacheDir().listFiles(); for (File file : files) { try { if (K9.DEBUG) { Log.d(K9.LOG_TAG, "Deleting file " + file.getCanonicalPath()); } } catch (IOException ioe) { /* No need to log failure to log */ } file.delete(); } } /** * Delete the thumbnail of an attachment. * * @param context * The application context. * @param accountUuid * The UUID of the account the attachment belongs to. * @param attachmentId * The ID of the attachment the thumbnail was created for. */ public static void deleteThumbnail(Context context, String accountUuid, String attachmentId) { File file = getThumbnailFile(context, accountUuid, attachmentId); if (file.exists()) { file.delete(); } } private static File getThumbnailFile(Context context, String accountUuid, String attachmentId) { String filename = "thmb_" + accountUuid + "_" + attachmentId + ".tmp"; File dir = context.getCacheDir(); return new File(dir, filename); } @Override public boolean onCreate() { /* * We use the cache dir as a temporary directory (since Android doesn't give us one) so * on startup we'll clean up any .tmp files from the last run. */ final File cacheDir = getContext().getCacheDir(); if (cacheDir == null) { return true; } File[] files = cacheDir.listFiles(); if (files == null) { return true; } for (File file : files) { if (file.getName().endsWith(".tmp")) { file.delete(); } } return true; } @Override public String getType(Uri uri) { List<String> segments = uri.getPathSegments(); String dbName = segments.get(0); String id = segments.get(1); String format = segments.get(2); return getType(dbName, id, format); } @Override public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { File file; List<String> segments = uri.getPathSegments(); String accountUuid = segments.get(0); String attachmentId = segments.get(1); String format = segments.get(2); if (FORMAT_THUMBNAIL.equals(format)) { int width = Integer.parseInt(segments.get(3)); int height = Integer.parseInt(segments.get(4)); file = getThumbnailFile(getContext(), accountUuid, attachmentId); if (!file.exists()) { String type = getType(accountUuid, attachmentId, FORMAT_VIEW); try { FileInputStream in = new FileInputStream(getFile(accountUuid, attachmentId)); try { Bitmap thumbnail = createThumbnail(type, in); if (thumbnail != null) { thumbnail = Bitmap.createScaledBitmap(thumbnail, width, height, true); FileOutputStream out = new FileOutputStream(file); try { thumbnail.compress(Bitmap.CompressFormat.PNG, 100, out); } finally { out.close(); } } } finally { try { in.close(); } catch (Throwable ignore) { /* ignore */ } } } catch (IOException ioe) { return null; } } } else { file = getFile(accountUuid, attachmentId); } return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { String[] columnNames = (projection == null) ? DEFAULT_PROJECTION : projection; List<String> segments = uri.getPathSegments(); String dbName = segments.get(0); String id = segments.get(1); // Versions of K-9 before 3.400 had a database name here, not an // account UID, so implement a bit of backcompat if (dbName.endsWith(".db")) { dbName = dbName.substring(0, dbName.length() - 3); } final AttachmentInfo attachmentInfo; try { final Account account = Preferences.getPreferences(getContext()).getAccount(dbName); attachmentInfo = LocalStore.getLocalInstance(account, K9.app).getAttachmentInfo(id); } catch (MessagingException e) { Log.e(K9.LOG_TAG, "Unable to retrieve attachment info from local store for ID: " + id, e); return null; } MatrixCursor ret = new MatrixCursor(columnNames); Object[] values = new Object[columnNames.length]; for (int i = 0, count = columnNames.length; i < count; i++) { String column = columnNames[i]; if (AttachmentProviderColumns._ID.equals(column)) { values[i] = id; } else if (AttachmentProviderColumns.DATA.equals(column)) { values[i] = uri.toString(); } else if (AttachmentProviderColumns.DISPLAY_NAME.equals(column)) { values[i] = attachmentInfo.name; } else if (AttachmentProviderColumns.SIZE.equals(column)) { values[i] = attachmentInfo.size; } } ret.addRow(values); return ret; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { return 0; } @Override public int delete(Uri uri, String arg1, String[] arg2) { return 0; } @Override public Uri insert(Uri uri, ContentValues values) { return null; } private String getType(String dbName, String id, String format) { String type; if (FORMAT_THUMBNAIL.equals(format)) { type = "image/png"; } else { final Account account = Preferences.getPreferences(getContext()).getAccount(dbName); try { final LocalStore localStore = LocalStore.getLocalInstance(account, K9.app); AttachmentInfo attachmentInfo = localStore.getAttachmentInfo(id); if (FORMAT_VIEW.equals(format)) { type = MimeUtility.getMimeTypeForViewing(attachmentInfo.type, attachmentInfo.name); } else { // When accessing the "raw" message we deliver the original MIME type. type = attachmentInfo.type; } } catch (MessagingException e) { Log.e(K9.LOG_TAG, "Unable to retrieve LocalStore for " + account, e); type = null; } } return type; } private File getFile(String dbName, String id) throws FileNotFoundException { Account account = Preferences.getPreferences(getContext()).getAccount(dbName); File attachmentsDir = StorageManager.getInstance(K9.app).getAttachmentDirectory(dbName, account.getLocalStorageProviderId()); File file = new File(attachmentsDir, id); if (!file.exists()) { throw new FileNotFoundException(file.getAbsolutePath()); } return file; } private Bitmap createThumbnail(String type, InputStream data) { if (MimeUtility.mimeTypeMatches(type, "image/*")) { return createImageThumbnail(data); } return null; } private Bitmap createImageThumbnail(InputStream data) { try { Bitmap bitmap = BitmapFactory.decodeStream(data); return bitmap; } catch (OutOfMemoryError oome) { /* * Improperly downloaded images, corrupt bitmaps and the like can commonly * cause OOME due to invalid allocation sizes. We're happy with a null bitmap in * that case. If the system is really out of memory we'll know about it soon * enough. */ return null; } catch (Exception e) { return null; } } }