diff --git a/res/values/strings.xml b/res/values/strings.xml index 8710aaca4..f87f9a4c9 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -655,4 +655,10 @@ Welcome to K-9 Mail setup. K-9 is an open source mail client for Android origin K-9 Mail remote control Allows this application to control K-9 Mail activities and settings. + + Attachments On SD Cards + Whether or not attachments are store on the SD card + No SD card found + + No SD card found diff --git a/res/xml/account_settings_preferences.xml b/res/xml/account_settings_preferences.xml index 79ead78b6..be01f6f08 100644 --- a/res/xml/account_settings_preferences.xml +++ b/res/xml/account_settings_preferences.xml @@ -30,9 +30,16 @@ android:key="account_default" android:title="@string/account_settings_default_label" android:summary="@string/account_settings_default_summary" /> - + + + + + - + - + - + - - - + + + - + diff --git a/src/com/fsck/k9/Account.java b/src/com/fsck/k9/Account.java index d8a0d73d0..aa8aa8fcc 100644 --- a/src/com/fsck/k9/Account.java +++ b/src/com/fsck/k9/Account.java @@ -60,10 +60,11 @@ public class Account implements Serializable boolean mIsSignatureBeforeQuotedText; private String mExpungePolicy = EXPUNGE_IMMEDIATELY; private int mMaxPushFolders; + private boolean mStoreAttachmentsOnSdCard; List identities; - public enum FolderMode + public enum FolderMode { NONE, ALL, FIRST_CLASS, FIRST_AND_SECOND_CLASS, NOT_SECOND_CLASS; } @@ -274,7 +275,6 @@ public class Account implements Serializable mFolderPushMode = FolderMode.FIRST_CLASS; } - try { mFolderTargetMode = FolderMode.valueOf(preferences.getPreferences().getString(mUuid + ".folderTargetMode", @@ -286,6 +286,7 @@ public class Account implements Serializable } mIsSignatureBeforeQuotedText = preferences.getPreferences().getBoolean(mUuid + ".signatureBeforeQuotedText", false); + mStoreAttachmentsOnSdCard = preferences.getPreferences().getBoolean(mUuid + ".storeAttachmentOnSdCard", true); identities = loadIdentities(preferences.getPreferences()); } @@ -530,6 +531,7 @@ public class Account implements Serializable editor.remove(mUuid + ".signatureBeforeQuotedText"); editor.remove(mUuid + ".expungePolicy"); editor.remove(mUuid + ".maxPushFolders"); + editor.remove(mUuid + ".storeAttachmentOnSdCard"); deleteIdentities(preferences.getPreferences(), editor); editor.commit(); } @@ -602,6 +604,7 @@ public class Account implements Serializable editor.putBoolean(mUuid + ".signatureBeforeQuotedText", this.mIsSignatureBeforeQuotedText); editor.putString(mUuid + ".expungePolicy", mExpungePolicy); editor.putInt(mUuid + ".maxPushFolders", mMaxPushFolders); + editor.putBoolean(mUuid + ".storeAttachmentOnSdCard", mStoreAttachmentsOnSdCard); saveIdentities(preferences.getPreferences(), editor); editor.commit(); @@ -965,4 +968,13 @@ public class Account implements Serializable mRing = ring; } + public boolean isStoreAttachmentOnSdCard() + { + return mStoreAttachmentsOnSdCard; + } + + public void setStoreAttachmentOnSdCard(boolean mStoreAttachmentOnSdCard) + { + this.mStoreAttachmentsOnSdCard = mStoreAttachmentOnSdCard; + } } diff --git a/src/com/fsck/k9/MessagingController.java b/src/com/fsck/k9/MessagingController.java index ffa42d23c..99e09c5a7 100644 --- a/src/com/fsck/k9/MessagingController.java +++ b/src/com/fsck/k9/MessagingController.java @@ -31,6 +31,7 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.net.Uri; +import android.os.Environment; import android.os.PowerManager; import android.os.Process; import android.os.PowerManager.WakeLock; @@ -46,7 +47,6 @@ import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessageRemovalListener; import com.fsck.k9.mail.MessageRetrievalListener; -import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Part; import com.fsck.k9.mail.PushReceiver; import com.fsck.k9.mail.Pusher; @@ -54,6 +54,7 @@ import com.fsck.k9.mail.Store; import com.fsck.k9.mail.Transport; import com.fsck.k9.mail.Folder.FolderType; import com.fsck.k9.mail.Folder.OpenMode; +import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.internet.MimeMessage; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.TextBody; @@ -61,6 +62,10 @@ import com.fsck.k9.mail.store.LocalStore; import com.fsck.k9.mail.store.LocalStore.LocalFolder; import com.fsck.k9.mail.store.LocalStore.LocalMessage; import com.fsck.k9.mail.store.LocalStore.PendingCommand; +import com.fsck.k9.mail.transport.EOLConvertingOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; /** * Starts a long running (application) Thread that will run through commands @@ -3858,13 +3863,19 @@ public class MessagingController implements Runnable for (final Account account : accounts) { + if (account.isStoreAttachmentOnSdCard() + && !Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) + { + if (K9.DEBUG) + Log.i(K9.LOG_TAG, "SD card not mounted: skipping synchronizing account " + account.getDescription()); + continue; + } + final long accountInterval = account.getAutomaticCheckIntervalMinutes() * 60 * 1000; if (ignoreLastCheckedTime == false && accountInterval <= 0) { if (K9.DEBUG) Log.i(K9.LOG_TAG, "Skipping synchronizing account " + account.getDescription()); - - continue; } @@ -3947,8 +3958,6 @@ public class MessagingController implements Runnable continue; } - - if (K9.DEBUG) Log.v(K9.LOG_TAG, "Folder " + folder.getName() + " was last synced @ " + new Date(folder.getLastChecked())); @@ -3967,6 +3976,16 @@ public class MessagingController implements Runnable { public void run() { + //Let's be conservative and check the sd card + //in case it was unmounted while we are sync'ing this account + if (account.isStoreAttachmentOnSdCard() + && !Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) + { + if (K9.DEBUG) + Log.i(K9.LOG_TAG, "SD card not mounted: skipping synchronizing: account " + account.getDescription() + " folder=" + folder.getName()); + return; + } + LocalFolder tLocalFolder = null; try { @@ -4010,7 +4029,6 @@ public class MessagingController implements Runnable { synchronizeMailboxSynchronous(account, folder.getName(), listener); } - finally { if (account.isShowOngoing()) @@ -4523,6 +4541,14 @@ public class MessagingController implements Runnable { public void run() { + if (account.isStoreAttachmentOnSdCard() + && !Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) + { + if (K9.DEBUG) + Log.i(K9.LOG_TAG, "SD card not mounted: skipping reception of pushed messages: account=" + account.getDescription() + ", folder=" + remoteFolder.getName() + ", message count=" + messages.size()); + return; + } + LocalFolder localFolder = null; try { diff --git a/src/com/fsck/k9/activity/Accounts.java b/src/com/fsck/k9/activity/Accounts.java index f58c34d1f..06d280ff7 100644 --- a/src/com/fsck/k9/activity/Accounts.java +++ b/src/com/fsck/k9/activity/Accounts.java @@ -9,6 +9,7 @@ import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Bundle; +import android.os.Environment; import android.os.Handler; import android.view.*; import android.view.ContextMenu.ContextMenuInfo; @@ -377,6 +378,13 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC private void onCheckMail(Account account) { + if (account.isStoreAttachmentOnSdCard() + && !Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) + { + Toast.makeText(this, R.string.sd_card_error, Toast.LENGTH_SHORT).show(); + return; + } + MessagingController.getInstance(getApplication()).checkMail(this, account, true, true, null); } diff --git a/src/com/fsck/k9/activity/FolderList.java b/src/com/fsck/k9/activity/FolderList.java index a921f4576..b28017eef 100644 --- a/src/com/fsck/k9/activity/FolderList.java +++ b/src/com/fsck/k9/activity/FolderList.java @@ -8,6 +8,7 @@ import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; import android.os.Bundle; +import android.os.Environment; import android.os.Handler; import android.os.PowerManager; import android.os.PowerManager.WakeLock; @@ -172,6 +173,13 @@ public class FolderList extends K9ListActivity private void checkMail(FolderInfoHolder folder) { + if (mAccount.isStoreAttachmentOnSdCard() + && !Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) + { + Toast.makeText(this, R.string.sd_card_error, Toast.LENGTH_SHORT).show(); + return; + } + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); final WakeLock wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Email - UpdateWorker"); wakeLock.setReferenceCounted(false); @@ -447,6 +455,13 @@ public class FolderList extends K9ListActivity private void checkMail(final Account account) { + if (mAccount.isStoreAttachmentOnSdCard() + && !Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) + { + Toast.makeText(this, R.string.sd_card_error, Toast.LENGTH_SHORT).show(); + return; + } + MessagingController.getInstance(getApplication()).checkMail(this, account, true, true, mAdapter.mListener); } diff --git a/src/com/fsck/k9/activity/MessageList.java b/src/com/fsck/k9/activity/MessageList.java index c2f1bcc3d..4cc3a43fe 100644 --- a/src/com/fsck/k9/activity/MessageList.java +++ b/src/com/fsck/k9/activity/MessageList.java @@ -10,11 +10,9 @@ import android.content.Intent; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.os.Environment; import android.os.Handler; -import android.text.SpannableString; import android.text.Spannable; -import android.text.style.ForegroundColorSpan; -import android.text.style.StyleSpan; import android.text.style.TextAppearanceSpan; import android.util.Config; import android.util.Log; @@ -974,6 +972,13 @@ public class MessageList private void checkMail(Account account, String folderName) { + if (mAccount.isStoreAttachmentOnSdCard() + && !Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) + { + Toast.makeText(this, R.string.sd_card_error, Toast.LENGTH_SHORT).show(); + return; + } + mController.synchronizeMailbox(account, folderName, mAdapter.mListener); sendMail(account); } diff --git a/src/com/fsck/k9/activity/setup/AccountSettings.java b/src/com/fsck/k9/activity/setup/AccountSettings.java index 5bba4ac60..799f27ec3 100644 --- a/src/com/fsck/k9/activity/setup/AccountSettings.java +++ b/src/com/fsck/k9/activity/setup/AccountSettings.java @@ -5,9 +5,12 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; +import android.os.Environment; import android.preference.*; +import android.preference.Preference.OnPreferenceChangeListener; import android.util.Log; import android.view.KeyEvent; +import android.widget.Toast; import com.fsck.k9.*; import com.fsck.k9.activity.ChooseFolder; import com.fsck.k9.activity.ChooseIdentity; @@ -45,6 +48,7 @@ public class AccountSettings extends K9PreferenceActivity private static final String PREFERENCE_DELETE_POLICY = "delete_policy"; private static final String PREFERENCE_EXPUNGE_POLICY = "expunge_policy"; private static final String PREFERENCE_AUTO_EXPAND_FOLDER = "account_setup_auto_expand_folder"; + private static final String PREFERENCE_STORE_ATTACHMENTS_ON_SD_CARD = "account_setup_store_attachment_on_sd_card"; private Account mAccount; @@ -67,6 +71,7 @@ public class AccountSettings extends K9PreferenceActivity private ListPreference mDeletePolicy; private ListPreference mExpungePolicy; private Preference mAutoExpandFolder; + private CheckBoxPreference mStoreAttachmentsOnSdCard; public static void actionSettings(Context context, Account account) @@ -349,6 +354,28 @@ public class AccountSettings extends K9PreferenceActivity return true; } }); + + mStoreAttachmentsOnSdCard = (CheckBoxPreference) findPreference(PREFERENCE_STORE_ATTACHMENTS_ON_SD_CARD); + mStoreAttachmentsOnSdCard.setChecked(mAccount.isStoreAttachmentOnSdCard()); + mStoreAttachmentsOnSdCard.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (newValue instanceof Boolean) + { + Boolean b = (Boolean)newValue; + if (b.booleanValue() + && !Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) + { + Toast.makeText( + AccountSettings.this, + R.string.account_setup_store_attachment_on_sd_card_error, + Toast.LENGTH_SHORT).show(); + return false; + } + } + return true; + } + + }); } @Override @@ -378,6 +405,7 @@ public class AccountSettings extends K9PreferenceActivity mAccount.setFolderTargetMode(Account.FolderMode.valueOf(mTargetMode.getValue())); mAccount.setDeletePolicy(Integer.parseInt(mDeletePolicy.getValue())); mAccount.setExpungePolicy(mExpungePolicy.getValue()); + mAccount.setStoreAttachmentOnSdCard(mStoreAttachmentsOnSdCard.isChecked()); SharedPreferences prefs = mAccountRingtone.getPreferenceManager().getSharedPreferences(); String newRingtone = prefs.getString(PREFERENCE_RINGTONE, null); diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java index 9077e6d87..642fe8131 100644 --- a/src/com/fsck/k9/mail/store/LocalStore.java +++ b/src/com/fsck/k9/mail/store/LocalStore.java @@ -8,8 +8,10 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.net.Uri; +import android.os.Environment; import android.text.util.Regex; import android.util.Log; +import com.fsck.k9.Account; import com.fsck.k9.K9; import com.fsck.k9.Preferences; import com.fsck.k9.Utility; @@ -36,9 +38,11 @@ public class LocalStore extends Store implements Serializable private static final int DB_VERSION = 33; private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.X_DESTROYED, Flag.SEEN }; + private Account mAccount; private String mPath; private SQLiteDatabase mDb; - private File mAttachmentsDir; + private File mInternalAttachmentsDir = null; + private File mExternalAttachmentsDir = null; private Application mApplication; private String uUid = null; @@ -65,6 +69,24 @@ public class LocalStore extends Store implements Serializable public LocalStore(String _uri, Application application) throws MessagingException { mApplication = application; + + //Store should be passed their store + //but let's not break the interface now + for (Account account : Preferences.getPreferences(mApplication).getAccounts()) + { + if (_uri.equals(account.getLocalStoreUri())) + { + mAccount = account; + break; + } + } + if (mAccount == null) + { + //Should not happend + throw new IllegalArgumentException("No account found: uri=" + _uri); + } + + URI uri = null; try { @@ -94,10 +116,20 @@ public class LocalStore extends Store implements Serializable parentDir.mkdirs(); } - mAttachmentsDir = new File(mPath + "_att"); - if (!mAttachmentsDir.exists()) + mInternalAttachmentsDir = new File(mPath + "_att"); + if (!mInternalAttachmentsDir.exists()) { - mAttachmentsDir.mkdirs(); + mInternalAttachmentsDir.mkdirs(); + } + + if (useExternalAttachmentDir()) + { + String externalAttachmentsPath = "/sdcard" + mPath.substring("//data".length()); + mExternalAttachmentsDir = new File(externalAttachmentsPath + "_att"); + if (!mExternalAttachmentsDir.exists()) + { + mExternalAttachmentsDir.mkdirs(); + } } mDb = SQLiteDatabase.openOrCreateDatabase(mPath, null); @@ -235,7 +267,21 @@ public class LocalStore extends Store implements Serializable { long attachmentLength = 0; - File[] files = mAttachmentsDir.listFiles(); + attachmentLength =+ getSize(mInternalAttachmentsDir); + if (useExternalAttachmentDir()) + { + attachmentLength =+ getSize(mExternalAttachmentsDir); + } + + File dbFile = new File(mPath); + return dbFile.length() + attachmentLength; + } + + private long getSize(File attachmentsDir) + { + long attachmentLength = 0; + + File[] files = attachmentsDir.listFiles(); for (File file : files) { if (file.exists()) @@ -244,9 +290,7 @@ public class LocalStore extends Store implements Serializable } } - - File dbFile = new File(mPath); - return dbFile.length() + attachmentLength; + return attachmentLength; } public void compact() throws MessagingException @@ -375,24 +419,13 @@ public class LocalStore extends Store implements Serializable { } - try - { - File[] attachments = mAttachmentsDir.listFiles(); - for (File attachment : attachments) - { - if (attachment.exists()) - { - attachment.delete(); - } - } - if (mAttachmentsDir.exists()) - { - mAttachmentsDir.delete(); - } - } - catch (Exception e) + + delete(mInternalAttachmentsDir); + if (useExternalAttachmentDir()) { + delete(mExternalAttachmentsDir); } + try { new File(mPath).delete(); @@ -403,24 +436,53 @@ public class LocalStore extends Store implements Serializable } } + private void delete(File attachmentsDir) { + try { + File[] attachments = attachmentsDir.listFiles(); + for (File attachment : attachments) + { + if (attachment.exists()) + { + attachment.delete(); + } + } + if (attachmentsDir.exists()) + { + attachmentsDir.delete(); + } + } + catch (Exception e) + { + Log.w(K9.LOG_TAG, null, e); + } + } + public void pruneCachedAttachments() throws MessagingException { pruneCachedAttachments(false); } - /** - * Deletes all cached attachments for the entire store. - */ public void pruneCachedAttachments(boolean force) throws MessagingException { - if (force) { ContentValues cv = new ContentValues(); cv.putNull("content_uri"); mDb.update("attachments", cv, null, null); } - File[] files = mAttachmentsDir.listFiles(); + pruneCachedAttachments(force, mInternalAttachmentsDir); + if (useExternalAttachmentDir()) + { + pruneCachedAttachments(force, mExternalAttachmentsDir); + } + } + + /** + * Deletes all cached attachments for the entire store. + */ + private void pruneCachedAttachments(boolean force, File attachmentsDir) throws MessagingException + { + File[] files = attachmentsDir.listFiles(); for (File file : files) { if (file.exists()) @@ -1719,7 +1781,7 @@ public class LocalStore extends Store implements Serializable * so we copy the data into a cached attachment file. */ InputStream in = attachment.getBody().getInputStream(); - tempAttachmentFile = File.createTempFile("att", null, mAttachmentsDir); + tempAttachmentFile = File.createTempFile("att", null, getAttachmentsDir()); FileOutputStream out = new FileOutputStream(tempAttachmentFile); size = IOUtils.copy(in, out); in.close(); @@ -1784,7 +1846,7 @@ public class LocalStore extends Store implements Serializable if (tempAttachmentFile != null) { - File attachmentFile = new File(mAttachmentsDir, Long.toString(attachmentId)); + File attachmentFile = new File(getAttachmentsDir(), Long.toString(attachmentId)); tempAttachmentFile.renameTo(attachmentFile); contentUri = AttachmentProvider.getAttachmentUri( new File(mPath).getName(), @@ -1942,11 +2004,20 @@ public class LocalStore extends Store implements Serializable long attachmentId = attachmentsCursor.getLong(0); try { - File file = new File(mAttachmentsDir, Long.toString(attachmentId)); + File file; + + file = new File(mInternalAttachmentsDir, Long.toString(attachmentId)); if (file.exists()) { file.delete(); } + + file = new File(mExternalAttachmentsDir, Long.toString(attachmentId)); + if (file.exists()) + { + file.delete(); + } + } catch (Exception e) { @@ -2509,14 +2580,6 @@ public class LocalStore extends Store implements Serializable { return mApplication.getContentResolver().openInputStream(mUri); } - catch (FileNotFoundException fnfe) - { - /* - * Since it's completely normal for us to try to serve up attachments that - * have been blown away, we just return an empty stream. - */ - return new ByteArrayInputStream(new byte[0]); - } catch (IOException ioe) { throw new MessagingException("Invalid attachment.", ioe); @@ -2536,4 +2599,34 @@ public class LocalStore extends Store implements Serializable return mUri; } } + + private File getAttachmentsDir() + { + if (useExternalAttachmentDir()) + { + return mExternalAttachmentsDir; + } + else { + return mInternalAttachmentsDir; + } + } + + private boolean useExternalAttachmentDir() + { + if (mAccount.isStoreAttachmentOnSdCard()) { + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) + { + throw new IllegalStateException("SDCard not mounted"); + + } + else + { + return true; + } + } + else + { + return false; + } + } } diff --git a/src/com/fsck/k9/provider/AttachmentProvider.java b/src/com/fsck/k9/provider/AttachmentProvider.java index 3a5edbe6a..dbc3fc8d3 100644 --- a/src/com/fsck/k9/provider/AttachmentProvider.java +++ b/src/com/fsck/k9/provider/AttachmentProvider.java @@ -9,6 +9,7 @@ import android.database.sqlite.SQLiteDatabase; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; +import android.os.Environment; import android.os.ParcelFileDescriptor; import android.util.Log; import com.fsck.k9.Account; @@ -156,6 +157,29 @@ public class AttachmentProvider extends ContentProvider } } + private File getFile(String dbName, String id) + throws FileNotFoundException + { + try + { + File attachmentsDir = getContext().getDatabasePath(dbName + "_att"); + File file = new File(attachmentsDir, id); + if (!file.exists()) + { + file = new File("/sdcard" + attachmentsDir.getCanonicalPath().substring("/data".length()), id); + if (!file.exists()) { + throw new FileNotFoundException(); + } + } + return file; + } + catch (IOException e) + { + Log.w(K9.LOG_TAG, null, e); + throw new FileNotFoundException(e.getMessage()); + } + } + @Override public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { @@ -176,10 +200,9 @@ public class AttachmentProvider extends ContentProvider String type = getType(attachmentUri); try { - FileInputStream in = new FileInputStream( - new File(getContext().getDatabasePath(dbName + "_att"), id)); + FileInputStream in = new FileInputStream(getFile(dbName, id)); Bitmap thumbnail = createThumbnail(type, in); - thumbnail = thumbnail.createScaledBitmap(thumbnail, width, height, true); + thumbnail = Bitmap.createScaledBitmap(thumbnail, width, height, true); FileOutputStream out = new FileOutputStream(file); thumbnail.compress(Bitmap.CompressFormat.PNG, 100, out); out.close(); @@ -195,7 +218,7 @@ public class AttachmentProvider extends ContentProvider else { return ParcelFileDescriptor.open( - new File(getContext().getDatabasePath(dbName + "_att"), id), + getFile(dbName, id), ParcelFileDescriptor.MODE_READ_ONLY); } }