From 14055691a3d59a95eac01d22aa57e4cb640e60bd Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Sat, 13 Nov 2010 21:40:56 +0000 Subject: [PATCH] Merge branch 'mail-on-sd' * mail-on-sd: (40 commits) Added more comments to explain how the locking mecanism works for LocalStore Fixed wrong method being called during experimental provider initialization (since provider isn't enabled, that didn't harm) Add more comments about how the various StorageProviders work and how they're enabled find src/com/fsck/ -name \*.java|xargs astyle --style=ansi --mode=java --indent-switches --indent=spaces=4 --convert-tabs French localization for storage related settings Remove unused SD card strings (replaced with storage indirection) Merge mail-on-sd branch from trunk Reset mail service on storage mount (even if no account uses the storage, to be improved) find src/com/fsck/ -name \*.java|xargs astyle --style=ansi --mode=java --indent-switches --indent=spaces=4 --convert-tabs Migraion -> Migration move the Storage location preference into preferences rather than the wizard. Made LocalStore log less verbose Added @Override compile checks Added ACTION_SHUTDOWN broadcast receiver to properly initiate shutdown sequence (not yet implemented) and cancel any scheduled Intent Be more consistent about which SQLiteDatabase variable is used (from instance variable to argument variable) to make code more refactoring-friendly (class is already big, code extraction should be easier if not referencing the instance variable). Added transaction timing logging Factorised storage lock/transaction handling code for regular operations. Use DB transactions to batch modifications (makes code more robust / could improve performances) Merge mail-on-sd branch from trunk Update issue 888 Added DB close on unmount / DB open on mount Update issue 888 Back to account list when underlying storage not available/unmounting in MessageView / MessageList ... --- .project | 2 +- AndroidManifest.xml | 23 + res/values-de/strings.xml | 7 + res/values-fr/strings.xml | 10 + res/values/strings.xml | 8 + res/xml/account_settings_preferences.xml | 11 + src/com/fsck/k9/Account.java | 136 +- src/com/fsck/k9/K9.java | 64 +- src/com/fsck/k9/Preferences.java | 41 +- src/com/fsck/k9/activity/Accounts.java | 53 +- src/com/fsck/k9/activity/ChooseAccount.java | 23 +- .../fsck/k9/activity/FolderInfoHolder.java | 4 + src/com/fsck/k9/activity/FolderList.java | 16 + .../fsck/k9/activity/LauncherShortcuts.java | 21 +- src/com/fsck/k9/activity/MessageCompose.java | 22 +- src/com/fsck/k9/activity/MessageList.java | 52 + src/com/fsck/k9/activity/MessageView.java | 51 +- .../k9/activity/setup/AccountSettings.java | 41 + .../activity/setup/AccountSetupIncoming.java | 3 + .../k9/controller/MessagingController.java | 160 +- src/com/fsck/k9/mail/Message.java | 5 +- src/com/fsck/k9/mail/Store.java | 32 +- .../fsck/k9/mail/internet/MimeMessage.java | 16 +- src/com/fsck/k9/mail/store/LocalStore.java | 3564 +++++++++++------ .../fsck/k9/mail/store/StorageManager.java | 830 ++++ .../store/UnavailableAccountException.java | 47 + .../store/UnavailableStorageException.java | 32 + .../fsck/k9/provider/AttachmentProvider.java | 137 +- src/com/fsck/k9/provider/MessageProvider.java | 17 +- src/com/fsck/k9/service/BootReceiver.java | 39 +- src/com/fsck/k9/service/MailService.java | 9 +- .../k9/service/RemoteControlReceiver.java | 1 + .../fsck/k9/service/RemoteControlService.java | 1 + src/com/fsck/k9/service/ShutdownReceiver.java | 44 + .../fsck/k9/service/StorageGoneReceiver.java | 51 + src/com/fsck/k9/service/StorageReceiver.java | 43 + 36 files changed, 4293 insertions(+), 1323 deletions(-) create mode 100644 src/com/fsck/k9/mail/store/StorageManager.java create mode 100644 src/com/fsck/k9/mail/store/UnavailableAccountException.java create mode 100644 src/com/fsck/k9/mail/store/UnavailableStorageException.java create mode 100644 src/com/fsck/k9/service/ShutdownReceiver.java create mode 100644 src/com/fsck/k9/service/StorageGoneReceiver.java create mode 100644 src/com/fsck/k9/service/StorageReceiver.java diff --git a/.project b/.project index cad7a45a2..ef8dc8b06 100644 --- a/.project +++ b/.project @@ -1,6 +1,6 @@ - k9mail + k9mail-sdcard diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 884242c26..f438e0f9d 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -315,6 +315,29 @@ + + + + + + + + Alle Mail-Header herunterladen Alle Header lokal speichern + Nutze SD-Karte + Speichere die Mails auf der SD-Karte + Externes Medium (SD-Karte) + Interner Speicher + %1$s zusätzlicher interner Speicher + Speicherort + Ordner bereinigen (Expunge) Sofort nach Verschieben oder Kopieren Bei jedem Abrufen diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index f7a916a61..3a4b0dbe8 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -364,6 +364,12 @@ Autre Téléchargement des entêtes de messages Enregistrer toutes les entêtes localement + + Stockage externe (carte SD) + Stockage interne + Stockage additionnel %1$s + Emplacement du stockage + Élimination des messages Immédiatement après avoir supprimé ou déplacé Pendant chaque récupération @@ -502,6 +508,10 @@ Fréquence de vérification du dossier Fréquence de vérification pour 2ème classe + + Stockage + + Couleur du compte Choisir la couleur du compte tel qu\'affichée dans les listes de dossiers ou de comptes Couleur de la DEL de notification diff --git a/res/values/strings.xml b/res/values/strings.xml index 625555373..3539ee7e6 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -398,6 +398,11 @@ Welcome to K-9 Mail setup. K-9 is an open source mail client for Android origin Download headers Save all message headers locally + External storage (SD card) + Regular internal storage + %1$s additional internal storage + Storage location + Expunge deleted messages Immediately When polling @@ -558,6 +563,9 @@ Welcome to K-9 Mail setup. K-9 is an open source mail client for Android origin Folder poll frequency 2nd class check frequency + Storage + + Account color Choose the color of the account used in folder and account list diff --git a/res/xml/account_settings_preferences.xml b/res/xml/account_settings_preferences.xml index 335fe6ce5..e60650734 100644 --- a/res/xml/account_settings_preferences.xml +++ b/res/xml/account_settings_preferences.xml @@ -283,6 +283,17 @@ android:dialogTitle="@string/account_settings_searchable_label" /> + + + + null if not available + * @throws MessagingException + * @see {@link #isAvailable(Context)} + */ public AccountStats getStats(Context context) throws MessagingException { + if (!isAvailable(context)) + { + return null; + } long startTime = System.currentTimeMillis(); AccountStats stats = new AccountStats(); int unreadMessageCount = 0; @@ -772,16 +807,45 @@ public class Account implements BaseAccount mRingNotified = ringNotified; } - public synchronized String getLocalStoreUri() + public String getLocalStorageProviderId() { - return mLocalStoreUri; + return mLocalStorageProviderId; } - public synchronized void setLocalStoreUri(String localStoreUri) + public void setLocalStorageProviderId(String id) { - this.mLocalStoreUri = localStoreUri; + + if (!mLocalStorageProviderId.equals(id)) + { + + boolean successful = false; + try + { + switchLocalStorage(id); + successful = true; + } + catch (MessagingException e) + { + } + finally + { + // if migration to/from SD-card failed once, it will fail again. + if (!successful) + { + return; + } + } + + mLocalStorageProviderId = id; + } + } +// public synchronized void setLocalStoreUri(String localStoreUri) +// { +// this.mLocalStoreUri = localStoreUri; +// } + /** * Returns -1 for never. */ @@ -1316,6 +1380,26 @@ public class Account implements BaseAccount mSaveAllHeaders = saveAllHeaders; } + /** + * Are we storing out localStore on the SD-card instead of the local device + * memory?
+ * Only to be called durin initial account-setup!
+ * Side-effect: changes {@link #mLocalStorageProviderId}. + * + * @param context + * @param newStorageProviderId + * Never null. + * @throws MessagingException + */ + public void switchLocalStorage(String newStorageProviderId) throws MessagingException + { + if (this.mLocalStoreMigrationListener != null && !mLocalStorageProviderId.equals(newStorageProviderId)) + { + mLocalStoreMigrationListener.onLocalStoreMigration(mLocalStorageProviderId, + newStorageProviderId); + } + } + public synchronized boolean goToUnreadMessageSearch() { return goToUnreadMessageSearch; @@ -1468,6 +1552,24 @@ public class Account implements BaseAccount lastSelectedFolderName = folderName; } + public boolean isInUse() + { + return mIsInUse; + } + + /** + * Set a listener to be informed when the underlying {@link StorageProvider} + * of the {@link LocalStore} of this account changes. (e.g. via + * {@link #switchLocalStorage(Context, String)}) + * + * @param listener + * @see #switchLocalStorage(Context, String) + */ + public void setLocalStoreMigrationListener(LocalStoreMigrationListener listener) + { + this.mLocalStoreMigrationListener = listener; + } + public synchronized CryptoProvider getCryptoProvider() { if (mCryptoProvider == null) @@ -1482,4 +1584,18 @@ public class Account implements BaseAccount return mNotificationSetting; } + /** + * @return true if our {@link StorageProvider} is ready. (e.g. + * card inserted) + */ + public boolean isAvailable(Context context) + { + String localStorageProviderId = getLocalStorageProviderId(); + if (localStorageProviderId == null) + { + return true; // defaults to internal memory + } + return StorageManager.getInstance(K9.app).isReady(localStorageProviderId); + } + } diff --git a/src/com/fsck/k9/K9.java b/src/com/fsck/k9/K9.java index 61dab28cc..ba6e93c39 100644 --- a/src/com/fsck/k9/K9.java +++ b/src/com/fsck/k9/K9.java @@ -5,16 +5,21 @@ import java.io.File; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.SynchronousQueue; import android.app.Application; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.net.Uri; +import android.os.Handler; +import android.os.Looper; import android.util.Log; import android.webkit.WebSettings; @@ -27,6 +32,8 @@ import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.internet.BinaryTempFileBody; import com.fsck.k9.service.BootReceiver; import com.fsck.k9.service.MailService; +import com.fsck.k9.service.ShutdownReceiver; +import com.fsck.k9.service.StorageGoneReceiver; public class K9 extends Application { @@ -305,7 +312,7 @@ public class K9 extends Application */ public static void setServicesEnabled(Context context) { - int acctLength = Preferences.getPreferences(context).getAccounts().length; + int acctLength = Preferences.getPreferences(context).getAvailableAccounts().size(); setServicesEnabled(context, acctLength > 0, null); @@ -313,7 +320,7 @@ public class K9 extends Application public static void setServicesEnabled(Context context, Integer wakeLockId) { - setServicesEnabled(context, Preferences.getPreferences(context).getAccounts().length > 0, wakeLockId); + setServicesEnabled(context, Preferences.getPreferences(context).getAvailableAccounts().size() > 0, wakeLockId); } public static void setServicesEnabled(Context context, boolean enabled, Integer wakeLockId) @@ -360,6 +367,56 @@ public class K9 extends Application } + /** + * Register BroadcastReceivers programmaticaly because doing it from manifest + * would make K-9 auto-start. We don't want auto-start because the initialization + * sequence isn't safe while some events occur (SD card unmount). + */ + protected void registerReceivers() + { + final StorageGoneReceiver receiver = new StorageGoneReceiver(); + final IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_MEDIA_EJECT); + filter.addAction(Intent.ACTION_MEDIA_UNMOUNTED); + filter.addDataScheme("file"); + + final BlockingQueue queue = new SynchronousQueue(); + + // starting a new thread to handle unmount events + new Thread(new Runnable() + { + @Override + public void run() + { + Looper.prepare(); + try + { + queue.put(new Handler()); + } + catch (InterruptedException e) + { + Log.e(K9.LOG_TAG, "", e); + } + Looper.loop(); + } + + }, "Unmount-thread").start(); + + try + { + final Handler storageGoneHandler = queue.take(); + registerReceiver(receiver, filter, null, storageGoneHandler); + Log.i(K9.LOG_TAG, "Registered: unmount receiver"); + } + catch (InterruptedException e) + { + Log.e(K9.LOG_TAG, "Unable to register unmount receiver", e); + } + + registerReceiver(new ShutdownReceiver(), new IntentFilter(Intent.ACTION_SHUTDOWN)); + Log.i(K9.LOG_TAG, "Registered: shutdown receiver"); + } + public static void save(SharedPreferences.Editor editor) { editor.putBoolean("enableDebugLogging", K9.DEBUG); @@ -452,7 +509,7 @@ public class K9 extends Application K9.setK9Language(sprefs.getString("language", "")); K9.setK9Theme(sprefs.getInt("theme", android.R.style.Theme_Light)); - MessagingController.getInstance(this).resetVisibleLimits(prefs.getAccounts()); + MessagingController.getInstance(this).resetVisibleLimits(prefs.getAvailableAccounts()); /* * We have to give MimeMessage a temp directory because File.createTempFile(String, String) @@ -465,6 +522,7 @@ public class K9 extends Application */ setServicesEnabled(this); + registerReceivers(); MessagingController.getInstance(this).addListener(new MessagingListener() { diff --git a/src/com/fsck/k9/Preferences.java b/src/com/fsck/k9/Preferences.java index e4fd5dcbe..0de70115f 100644 --- a/src/com/fsck/k9/Preferences.java +++ b/src/com/fsck/k9/Preferences.java @@ -2,6 +2,8 @@ package com.fsck.k9; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; import android.content.Context; import android.content.SharedPreferences; @@ -33,10 +35,12 @@ public class Preferences private Storage mStorage; private List accounts; private Account newAccount; + private Context mContext; private Preferences(Context context) { mStorage = Storage.getStorage(context); + mContext = context; if (mStorage.size() == 0) { Log.i(K9.LOG_TAG, "Preferences storage is zero-size, importing from Android-style preferences"); @@ -67,6 +71,7 @@ public class Preferences /** * Returns an array of the accounts on the system. If no accounts are * registered the method returns an empty array. + * @return all accounts */ public synchronized Account[] getAccounts() { @@ -84,6 +89,36 @@ public class Preferences return accounts.toArray(EMPTY_ACCOUNT_ARRAY); } + /** + * Returns an array of the accounts on the system. If no accounts are + * registered the method returns an empty array. + * @param context + * @return all accounts with {@link Account#isAvailable(Context)} + */ + public synchronized Collection getAvailableAccounts() + { + if (accounts == null) + { + loadAccounts(); + } + + if ((newAccount != null) && newAccount.getAccountNumber() != -1) + { + accounts.add(newAccount); + newAccount = null; + } + Collection retval = new ArrayList(accounts.size()); + for (Account account : accounts) + { + if (account.isAvailable(mContext)) + { + retval.add(account); + } + } + + return retval; + } + public synchronized Account getAccount(String uuid) { if (accounts == null) @@ -137,10 +172,10 @@ public class Preferences if (defaultAccount == null) { - Account[] accounts = getAccounts(); - if (accounts.length > 0) + Collection accounts = getAvailableAccounts(); + if (accounts.size() > 0) { - defaultAccount = accounts[0]; + defaultAccount = accounts.iterator().next(); setDefaultAccount(defaultAccount); } } diff --git a/src/com/fsck/k9/activity/Accounts.java b/src/com/fsck/k9/activity/Accounts.java index 854e70445..35ff78380 100644 --- a/src/com/fsck/k9/activity/Accounts.java +++ b/src/com/fsck/k9/activity/Accounts.java @@ -164,7 +164,14 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC try { AccountStats stats = account.getStats(Accounts.this); - accountStatusChanged(account, stats); + if (stats == null) + { + Log.w(K9.LOG_TAG, "Unable to get account stats"); + } + else + { + accountStatusChanged(account, stats); + } } catch (Exception e) { @@ -180,6 +187,10 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC { oldUnreadMessageCount = oldStats.unreadMessageCount; } + if (stats == null) + { + stats = new AccountStats(); // empty stats for unavailable accounts + } accountStats.put(account.getUuid(), stats); if (account instanceof Account) { @@ -343,8 +354,10 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC } else if (startup && accounts.length == 1) { - onOpenAccount(accounts[0]); - finish(); + if (onOpenAccount(accounts[0])) + { + finish(); + } } else { @@ -513,7 +526,13 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC } } - private void onOpenAccount(BaseAccount account) + /** + * Show that account's inbox or folder-list + * or return false if the account is not available. + * @param account the account to open ({@link SearchAccount} or {@link Account}) + * @return false if unsuccessfull + */ + private boolean onOpenAccount(BaseAccount account) { if (account instanceof SearchAccount) { @@ -523,6 +542,11 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC else { Account realAccount = (Account)account; + if (!realAccount.isAvailable(this)) + { + Log.i(K9.LOG_TAG, "refusing to open account that is not available"); + return false; + } if (K9.FOLDER_NONE.equals(realAccount.getAutoExpandFolderName())) { FolderList.actionHandleAccount(this, realAccount); @@ -532,6 +556,7 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC MessageList.actionHandleFolder(this, realAccount, realAccount.getAutoExpandFolderName()); } } + return true; } public void onClick(View view) @@ -609,7 +634,7 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC } catch (Exception e) { - // Ignore + // Ignore, this may lead to localStores on sd-cards that are currently not inserted to be left } MessagingController.getInstance(getApplication()).notifyAccountCancel(Accounts.this, realAccount); Preferences.getPreferences(Accounts.this).deleteAccount(realAccount); @@ -914,6 +939,24 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC } AccountStats stats = accountStats.get(account.getUuid()); + /* + // 20101024/fiouzy: the following code throws NullPointerException because Background is null + + // display unavailable accounts translucent + if (account instanceof Account) { + Account realAccount = (Account) account; + if (realAccount.isAvailable(Accounts.this)) { + holder.email.getBackground().setAlpha(255); + holder.description.getBackground().setAlpha(255); + } else { + holder.email.getBackground().setAlpha(127); + holder.description.getBackground().setAlpha(127); + } + } else { + holder.email.getBackground().setAlpha(255); + holder.description.getBackground().setAlpha(255); + } + */ if (stats != null && account instanceof Account && stats.size >= 0) { holder.email.setText(SizeFormatter.formatSize(Accounts.this, stats.size)); diff --git a/src/com/fsck/k9/activity/ChooseAccount.java b/src/com/fsck/k9/activity/ChooseAccount.java index c4636ccb2..75a186432 100644 --- a/src/com/fsck/k9/activity/ChooseAccount.java +++ b/src/com/fsck/k9/activity/ChooseAccount.java @@ -3,6 +3,7 @@ package com.fsck.k9.activity; import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.util.Log; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; @@ -63,6 +64,11 @@ public class ChooseAccount extends K9ExpandableListActivity final Identity identity = (Identity) adapter.getChild(groupPosition, childPosition); final Account account = (Account) adapter.getGroup(groupPosition); + if (!account.isAvailable(v.getContext())) + { + Log.i(K9.LOG_TAG, "Refusing selection of unavailable account"); + return true; + } final Intent intent = new Intent(); intent.putExtra(EXTRA_ACCOUNT, account.getUuid()); intent.putExtra(EXTRA_IDENTITY, identity); @@ -180,7 +186,6 @@ public class ChooseAccount extends K9ExpandableListActivity final View v; if (convertView == null) { - // is it okay to reuse? v = mLayoutInflater.inflate(R.layout.choose_account_item, parent, false); } else @@ -193,6 +198,22 @@ public class ChooseAccount extends K9ExpandableListActivity description.setText(account.getDescription()); description.setTextSize(TypedValue.COMPLEX_UNIT_DIP, K9.getFontSizes().getAccountName()); + // display unavailable accounts translucent + /* + * 20101030/fiouzy: NullPointerException on null getBackground() + * + if (account.isAvailable(parent.getContext())) + { + description.getBackground().setAlpha(255); + description.getBackground().setAlpha(255); + } + else + { + description.getBackground().setAlpha(127); + description.getBackground().setAlpha(127); + } + */ + v.findViewById(R.id.chip).setBackgroundColor(account.getChipColor()); return v; diff --git a/src/com/fsck/k9/activity/FolderInfoHolder.java b/src/com/fsck/k9/activity/FolderInfoHolder.java index 5e4bbde53..649f79ab9 100644 --- a/src/com/fsck/k9/activity/FolderInfoHolder.java +++ b/src/com/fsck/k9/activity/FolderInfoHolder.java @@ -67,6 +67,10 @@ public class FolderInfoHolder implements Comparable public FolderInfoHolder(Context context, Folder folder, Account account) { + if (context == null) + { + throw new IllegalArgumentException("null context given"); + } populate(context, folder, account); } diff --git a/src/com/fsck/k9/activity/FolderList.java b/src/com/fsck/k9/activity/FolderList.java index 3caabb9ff..5685f9050 100644 --- a/src/com/fsck/k9/activity/FolderList.java +++ b/src/com/fsck/k9/activity/FolderList.java @@ -382,6 +382,13 @@ public class FolderList extends K9ListActivity { super.onResume(); + if (!mAccount.isAvailable(this)) + { + Log.i(K9.LOG_TAG, "account unavaliabale, not showing folder-list but account-list"); + startActivity(new Intent(this, Accounts.class)); + finish(); + return; + } if (mAdapter == null) initializeActivityView(); @@ -828,6 +835,10 @@ public class FolderList extends K9ListActivity { return; } + if (stats == null) + { + return; + } mUnreadMessageCount = stats.unreadMessageCount; mHandler.refreshTitle(); } @@ -1028,6 +1039,11 @@ public class FolderList extends K9ListActivity { if (account != null && folderName != null) { + if (!account.isAvailable(FolderList.this)) + { + Log.i(K9.LOG_TAG, "not refreshing folder of unavailable account"); + return; + } localFolder = account.getLocalStore().getFolder(folderName); int unreadMessageCount = localFolder.getUnreadMessageCount(); if (localFolder != null) diff --git a/src/com/fsck/k9/activity/LauncherShortcuts.java b/src/com/fsck/k9/activity/LauncherShortcuts.java index f92a47443..a20f5ce8f 100644 --- a/src/com/fsck/k9/activity/LauncherShortcuts.java +++ b/src/com/fsck/k9/activity/LauncherShortcuts.java @@ -4,6 +4,7 @@ package com.fsck.k9.activity; import android.content.Intent; import android.os.Bundle; import android.os.Parcelable; +import android.util.Log; import android.util.TypedValue; import android.view.View; import android.view.View.OnClickListener; @@ -143,6 +144,18 @@ public class LauncherShortcuts extends K9ListActivity implements OnItemClickList holder.chip.setBackgroundColor(realAccount.getChipColor()); holder.chip.getBackground().setAlpha(255); + + // show unavailable accounts as translucent + if (realAccount.isAvailable(getContext())) + { + holder.email.getBackground().setAlpha(255); + holder.description.getBackground().setAlpha(255); + } + else + { + holder.email.getBackground().setAlpha(127); + holder.description.getBackground().setAlpha(127); + } } else { @@ -158,7 +171,13 @@ public class LauncherShortcuts extends K9ListActivity implements OnItemClickList { public void onClick(View v) { - FolderList.actionHandleAccount(LauncherShortcuts.this, (Account)account); + Account account2 = (Account)account; + if (!account2.isAvailable(getContext())) + { + Log.i(K9.LOG_TAG, "refusing selection of unavailable account"); + return ; + } + FolderList.actionHandleAccount(LauncherShortcuts.this, account2); } }); diff --git a/src/com/fsck/k9/activity/MessageCompose.java b/src/com/fsck/k9/activity/MessageCompose.java index a37634e2d..99af534e7 100644 --- a/src/com/fsck/k9/activity/MessageCompose.java +++ b/src/com/fsck/k9/activity/MessageCompose.java @@ -75,6 +75,7 @@ import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.TextBody; import com.fsck.k9.mail.store.LocalStore; import com.fsck.k9.mail.store.LocalStore.LocalAttachmentBody; +import com.fsck.k9.mail.store.UnavailableStorageException; public class MessageCompose extends K9Activity implements OnClickListener, OnFocusChangeListener { @@ -1525,7 +1526,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, OnFoc { // keep things simple: trigger account choice only if there are more // than 1 account - if (Preferences.getPreferences(this).getAccounts().length > 1) + if (Preferences.getPreferences(this).getAvailableAccounts().size() > 1) { final Intent intent = new Intent(this, ChooseAccount.class); intent.putExtra(ChooseAccount.EXTRA_ACCOUNT, mAccount.getUuid()); @@ -2335,11 +2336,22 @@ public class MessageCompose extends K9Activity implements OnClickListener, OnFoc } } - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "Saving identity: " + k9identity); - message.addHeader(K9.K9MAIL_IDENTITY, k9identity); + final MessagingController messagingController = MessagingController.getInstance(getApplication()); - Message draftMessage = MessagingController.getInstance(getApplication()).saveDraft(mAccount, message); + if (K9.DEBUG) + { + Log.d(K9.LOG_TAG, "Saving identity: " + k9identity); + } + try + { + message.addHeader(K9.K9MAIL_IDENTITY, k9identity); + } + catch (UnavailableStorageException e) + { + messagingController.addErrorMessage(mAccount, "Unable to save identity", e); + } + + Message draftMessage = messagingController.saveDraft(mAccount, message); mDraftUid = draftMessage.getUid(); // Don't display the toast if the user is just changing the orientation diff --git a/src/com/fsck/k9/activity/MessageList.java b/src/com/fsck/k9/activity/MessageList.java index c6b6f95c2..85a226d1d 100644 --- a/src/com/fsck/k9/activity/MessageList.java +++ b/src/com/fsck/k9/activity/MessageList.java @@ -70,6 +70,7 @@ import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.store.LocalStore; +import com.fsck.k9.mail.store.StorageManager; import com.fsck.k9.mail.store.LocalStore.LocalFolder; /** @@ -327,6 +328,33 @@ public class MessageList /* package visibility for faster inner class access */ MessageHelper mMessageHelper = MessageHelper.getInstance(this); + private StorageManager.StorageListener mStorageListener = new StorageListenerImplementation(); + + private final class StorageListenerImplementation implements StorageManager.StorageListener + { + @Override + public void onUnmount(String providerId) + { + if (mAccount != null && providerId.equals(mAccount.getLocalStorageProviderId())) + { + runOnUiThread(new Runnable() + { + @Override + public void run() + { + onAccountUnavailable(); + } + }); + } + } + + @Override + public void onMount(String providerId) + { + // no-op + } + } + class MessageListHandler { public void removeMessage(final List messages) @@ -671,6 +699,13 @@ public class MessageList mFolderName = intent.getStringExtra(EXTRA_FOLDER); mQueryString = intent.getStringExtra(EXTRA_QUERY); + if (mAccount != null && !mAccount.isAvailable(this)) + { + Log.i(K9.LOG_TAG, "not opening MessageList of unavailable account"); + onAccountUnavailable(); + return; + } + String queryFlags = intent.getStringExtra(EXTRA_QUERY_FLAGS); if (queryFlags != null) { @@ -727,6 +762,8 @@ public class MessageList super.onPause(); mController.removeListener(mAdapter.mListener); saveListState(); + + StorageManager.getInstance(getApplication()).removeListener(mStorageListener); } public void saveListState() @@ -769,6 +806,13 @@ public class MessageList { super.onResume(); + if (mAccount != null && !mAccount.isAvailable(this)) + { + onAccountUnavailable(); + return; + } + StorageManager.getInstance(getApplication()).addListener(mStorageListener); + mStars = K9.messageListStars(); mCheckboxes = K9.messageListCheckboxes(); @@ -3363,4 +3407,12 @@ public class MessageList } mController.copyMessages(mAccount, mCurrentFolder.name, messageList.toArray(EMPTY_MESSAGE_ARRAY), folderName, null); } + + protected void onAccountUnavailable() + { + finish(); + // TODO inform user about account unavailability using Toast + Accounts.listAccounts(getApplicationContext()); + } + } diff --git a/src/com/fsck/k9/activity/MessageView.java b/src/com/fsck/k9/activity/MessageView.java index dcbe893a8..678a6dd7f 100644 --- a/src/com/fsck/k9/activity/MessageView.java +++ b/src/com/fsck/k9/activity/MessageView.java @@ -7,6 +7,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.Serializable; import java.util.ArrayList; +import java.util.Collection; import java.util.HashSet; import java.util.LinkedList; import java.util.List; @@ -81,6 +82,7 @@ import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Multipart; import com.fsck.k9.mail.Part; import com.fsck.k9.mail.internet.MimeUtility; +import com.fsck.k9.mail.store.StorageManager; import com.fsck.k9.mail.store.LocalStore.LocalAttachmentBodyPart; import com.fsck.k9.mail.store.LocalStore.LocalMessage; import com.fsck.k9.mail.store.LocalStore.LocalTextBody; @@ -170,6 +172,33 @@ public class MessageView extends K9Activity implements OnClickListener private Contacts mContacts; + private StorageManager.StorageListener mStorageListener = new StorageListenerImplementation(); + + private final class StorageListenerImplementation implements StorageManager.StorageListener + { + @Override + public void onUnmount(String providerId) + { + if (providerId.equals(mAccount.getLocalStorageProviderId())) + { + runOnUiThread(new Runnable() + { + @Override + public void run() + { + onAccountUnavailable(); + } + }); + } + } + + @Override + public void onMount(String providerId) + { + // no-op + } + } + /** * Pair class is only available since API Level 5, so we need * this helper class unfortunately @@ -970,7 +999,7 @@ public class MessageView extends K9Activity implements OnClickListener if (segmentList.size() == 3) { String accountId = segmentList.get(0); - Account[] accounts = Preferences.getPreferences(this).getAccounts(); + Collection accounts = Preferences.getPreferences(this).getAvailableAccounts(); boolean found = false; for (Account account : accounts) { @@ -1310,6 +1339,26 @@ public class MessageView extends K9Activity implements OnClickListener public void onResume() { super.onResume(); + if (!mAccount.isAvailable(this)) + { + onAccountUnavailable(); + return; + } + StorageManager.getInstance(getApplication()).addListener(mStorageListener); + } + + @Override + protected void onPause() + { + StorageManager.getInstance(getApplication()).removeListener(mStorageListener); + super.onPause(); + } + + protected void onAccountUnavailable() + { + finish(); + // TODO inform user about account unavailability using Toast + Accounts.listAccounts(this); } /** diff --git a/src/com/fsck/k9/activity/setup/AccountSettings.java b/src/com/fsck/k9/activity/setup/AccountSettings.java index 014f6dff5..b2a22be09 100644 --- a/src/com/fsck/k9/activity/setup/AccountSettings.java +++ b/src/com/fsck/k9/activity/setup/AccountSettings.java @@ -14,6 +14,8 @@ import android.preference.RingtonePreference; import android.util.Log; import android.view.KeyEvent; +import java.util.Map; + import com.fsck.k9.Account; import com.fsck.k9.Account.FolderMode; import com.fsck.k9.K9; @@ -29,6 +31,12 @@ import com.fsck.k9.crypto.Apg; import com.fsck.k9.mail.Store; import com.fsck.k9.service.MailService; +import com.fsck.k9.mail.store.LocalStore; +import com.fsck.k9.mail.store.StorageManager; +import com.fsck.k9.mail.store.LocalStore.LocalFolder; +import com.fsck.k9.mail.store.StorageManager.StorageProvider; + + public class AccountSettings extends K9PreferenceActivity { private static final String EXTRA_ACCOUNT = "account"; @@ -80,6 +88,9 @@ public class AccountSettings extends K9PreferenceActivity private static final String PREFERENCE_CRYPTO_APP = "crypto_app"; private static final String PREFERENCE_CRYPTO_AUTO_SIGNATURE = "crypto_auto_signature"; + private static final String PREFERENCE_LOCAL_STORAGE_PROVIDER = "local_storage_provider"; + + private Account mAccount; private boolean mIsPushCapable = false; private boolean mIsExpungeCapable = false; @@ -124,6 +135,7 @@ public class AccountSettings extends K9PreferenceActivity private ListPreference mCryptoApp; private CheckBoxPreference mCryptoAutoSignature; + private ListPreference mLocalStorageProvider; public static void actionSettings(Context context, Account account) { @@ -411,6 +423,33 @@ public class AccountSettings extends K9PreferenceActivity }); + mLocalStorageProvider = (ListPreference) findPreference(PREFERENCE_LOCAL_STORAGE_PROVIDER); + { + final Map providers; + providers = StorageManager.getInstance(K9.app).getAvailableProviders(); + int i = 0; + final String[] providerLabels = new String[providers.size()]; + final String[] providerIds = new String[providers.size()]; + for (final Map.Entry entry : providers.entrySet()) + { + providerIds[i] = entry.getKey(); + providerLabels[i] = entry.getValue(); + i++; + } + mLocalStorageProvider.setEntryValues(providerIds); + mLocalStorageProvider.setEntries(providerLabels); + mLocalStorageProvider.setValue(mAccount.getLocalStorageProviderId()); + mLocalStorageProvider.setSummary(providers.get((Object)mAccount.getLocalStorageProviderId())); + + mLocalStorageProvider.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() + { + public boolean onPreferenceChange(Preference preference, Object newValue) + { + mLocalStorageProvider.setSummary(providers.get(newValue)); + return true; + } + }); + } // IMAP-specific preferences mPushPollOnConnect = (CheckBoxPreference) findPreference(PREFERENCE_PUSH_POLL_ON_CONNECT); @@ -668,6 +707,8 @@ public class AccountSettings extends K9PreferenceActivity mAccount.setReplyAfterQuote(mReplyAfterQuote.isChecked()); mAccount.setCryptoApp(mCryptoApp.getValue()); mAccount.setCryptoAutoSignature(mCryptoAutoSignature.isChecked()); + mAccount.setLocalStorageProviderId(mLocalStorageProvider.getValue()); + if (mIsPushCapable) { diff --git a/src/com/fsck/k9/activity/setup/AccountSetupIncoming.java b/src/com/fsck/k9/activity/setup/AccountSetupIncoming.java index ceaa5f189..bb6637069 100644 --- a/src/com/fsck/k9/activity/setup/AccountSetupIncoming.java +++ b/src/com/fsck/k9/activity/setup/AccountSetupIncoming.java @@ -16,11 +16,14 @@ import com.fsck.k9.*; import com.fsck.k9.activity.ChooseFolder; import com.fsck.k9.activity.K9Activity; import com.fsck.k9.helper.Utility; +import com.fsck.k9.mail.store.StorageManager; + import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URLDecoder; import java.net.URLEncoder; +import java.util.Map; public class AccountSetupIncoming extends K9Activity implements OnClickListener { diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index 9a0a2ece4..3736eeae9 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -66,7 +66,9 @@ import com.fsck.k9.mail.Transport; import com.fsck.k9.mail.internet.MimeMessage; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.TextBody; +import com.fsck.k9.mail.store.UnavailableAccountException; import com.fsck.k9.mail.store.LocalStore; +import com.fsck.k9.mail.store.UnavailableStorageException; import com.fsck.k9.mail.store.LocalStore.LocalFolder; import com.fsck.k9.mail.store.LocalStore.LocalMessage; import com.fsck.k9.mail.store.LocalStore.PendingCommand; @@ -279,7 +281,7 @@ public class MessagingController implements Runnable String commandDescription = null; try { - Command command = mCommands.take(); + final Command command = mCommands.take(); if (command != null) { @@ -289,7 +291,32 @@ public class MessagingController implements Runnable Log.i(K9.LOG_TAG, "Running " + (command.isForeground ? "Foreground" : "Background") + " command '" + command.description + "', seq = " + command.sequence); mBusy = true; - command.runnable.run(); + try + { + command.runnable.run(); + } + catch (UnavailableAccountException e) + { + // retry later + new Thread() + { + @Override + public void run() + { + try + { + sleep(30 * 1000); + mCommands.put(command); + } + catch (InterruptedException e) + { + Log.e(K9.LOG_TAG, "interrupted while putting a pending command for" + + " an unavailable account back into the queue." + + " THIS SHOULD NEVER HAPPEN."); + } + } + } .start(); + } if (K9.DEBUG) Log.i(K9.LOG_TAG, (command.isForeground ? "Foreground" : "Background") + @@ -434,43 +461,50 @@ public class MessagingController implements Runnable l.listFoldersStarted(account); } List localFolders = null; - try + if (!account.isAvailable(mApplication)) { - Store localStore = account.getLocalStore(); - localFolders = localStore.getPersonalNamespaces(false); - - Folder[] folderArray = localFolders.toArray(EMPTY_FOLDER_ARRAY); - - if (refreshRemote || localFolders == null || localFolders.size() == 0) + Log.i(K9.LOG_TAG, "not listing folders of unavailable account"); + } + else + { + try { - doRefreshRemote(account, listener); + Store localStore = account.getLocalStore(); + localFolders = localStore.getPersonalNamespaces(false); + + Folder[] folderArray = localFolders.toArray(EMPTY_FOLDER_ARRAY); + + if (refreshRemote || localFolders == null || localFolders.size() == 0) + { + doRefreshRemote(account, listener); + return; + } + + for (MessagingListener l : getListeners(listener)) + { + l.listFolders(account, folderArray); + } + } + catch (Exception e) + { + for (MessagingListener l : getListeners(listener)) + { + l.listFoldersFailed(account, e.getMessage()); + } + + addErrorMessage(account, null, e); return; } - - for (MessagingListener l : getListeners(listener)) + finally { - l.listFolders(account, folderArray); - } - } - catch (Exception e) - { - for (MessagingListener l : getListeners(listener)) - { - l.listFoldersFailed(account, e.getMessage()); - } - - addErrorMessage(account, null, e); - return; - } - finally - { - if (localFolders != null) - { - for (Folder localFolder : localFolders) + if (localFolders != null) { - if (localFolder != null) + for (Folder localFolder : localFolders) { - localFolder.close(); + if (localFolder != null) + { + localFolder.close(); + } } } } @@ -748,6 +782,11 @@ public class MessagingController implements Runnable boolean noSpecialFolders = true; for (final Account account : accounts) { + if (!account.isAvailable(mApplication)) + { + Log.d(K9.LOG_TAG, "searchLocalMessagesSynchronous() ignores account that is not available"); + continue; + } if (accountUuids != null && !accountUuidsSet.contains(account.getUuid())) { continue; @@ -939,7 +978,7 @@ public class MessagingController implements Runnable } } - public void resetVisibleLimits(Account[] accounts) + public void resetVisibleLimits(Collection accounts) { for (Account account : accounts) { @@ -2060,6 +2099,11 @@ public class MessagingController implements Runnable { processPendingCommandsSynchronous(account); } + catch (UnavailableStorageException e) + { + Log.i(K9.LOG_TAG, "Failed to process pending command because storage is not available - trying again later."); + throw new UnavailableAccountException(e); + } catch (MessagingException me) { Log.e(K9.LOG_TAG, "processPendingCommands", me); @@ -3385,6 +3429,10 @@ public class MessagingController implements Runnable { public void run() { + if (!account.isAvailable(mApplication)) + { + throw new UnavailableAccountException(); + } if (messagesPendingSend(account)) { NotificationManager notifMgr = @@ -3611,6 +3659,11 @@ public class MessagingController implements Runnable notifMgr.notify(-1500 - account.getAccountNumber(), notif); } } + catch (UnavailableStorageException e) + { + Log.i(K9.LOG_TAG, "Failed to send pending messages because storage is not available - trying again later."); + throw new UnavailableAccountException(e); + } catch (Exception e) { for (MessagingListener l : getListeners()) @@ -3834,6 +3887,11 @@ public class MessagingController implements Runnable processPendingCommands(account); } + catch (UnavailableStorageException e) + { + Log.i(K9.LOG_TAG, "Failed to move/copy message because storage is not available - trying again later."); + throw new UnavailableAccountException(e); + } catch (MessagingException me) { addErrorMessage(account, null, me); @@ -4009,6 +4067,11 @@ public class MessagingController implements Runnable unsuppressMessage(account, folder, uid); } } + catch (UnavailableStorageException e) + { + Log.i(K9.LOG_TAG, "Failed to delete message because storage is not available - trying again later."); + throw new UnavailableAccountException(e); + } catch (MessagingException me) { addErrorMessage(account, null, me); @@ -4090,6 +4153,11 @@ public class MessagingController implements Runnable queuePendingCommand(account, command); processPendingCommands(account); } + catch (UnavailableStorageException e) + { + Log.i(K9.LOG_TAG, "Failed to empty trash because storage is not available - trying again later."); + throw new UnavailableAccountException(e); + } catch (Exception e) { Log.e(K9.LOG_TAG, "emptyTrash failed", e); @@ -4212,6 +4280,14 @@ public class MessagingController implements Runnable for (final Account account : accounts) { + if (!account.isAvailable(context)) + { + if (K9.DEBUG) + { + Log.i(K9.LOG_TAG, "Skipping synchronizing unavailable account " + account.getDescription()); + } + continue; + } final long accountInterval = account.getAutomaticCheckIntervalMinutes() * 60 * 1000; if (!ignoreLastCheckedTime && accountInterval <= 0) { @@ -4369,7 +4445,8 @@ public class MessagingController implements Runnable account.setRingNotified(false); try { - if (account.getStats(context).unreadMessageCount == 0) + AccountStats stats = account.getStats(context); + if (stats == null || stats.unreadMessageCount == 0) { notifyAccountCancel(context, account); } @@ -4436,6 +4513,11 @@ public class MessagingController implements Runnable l.accountSizeChanged(account, oldSize, newSize); } } + catch (UnavailableStorageException e) + { + Log.i(K9.LOG_TAG, "Failed to compact account because storage is not available - trying again later."); + throw new UnavailableAccountException(e); + } catch (Exception e) { Log.e(K9.LOG_TAG, "Failed to compact account " + account.getDescription(), e); @@ -4472,6 +4554,11 @@ public class MessagingController implements Runnable l.accountStatusChanged(account, stats); } } + catch (UnavailableStorageException e) + { + Log.i(K9.LOG_TAG, "Failed to clear account because storage is not available - trying again later."); + throw new UnavailableAccountException(e); + } catch (Exception e) { Log.e(K9.LOG_TAG, "Failed to clear account " + account.getDescription(), e); @@ -4508,6 +4595,11 @@ public class MessagingController implements Runnable l.accountStatusChanged(account, stats); } } + catch (UnavailableStorageException e) + { + Log.i(K9.LOG_TAG, "Failed to recreate an account because storage is not available - trying again later."); + throw new UnavailableAccountException(e); + } catch (Exception e) { Log.e(K9.LOG_TAG, "Failed to recreate account " + account.getDescription(), e); diff --git a/src/com/fsck/k9/mail/Message.java b/src/com/fsck/k9/mail/Message.java index 0644e23e0..684104c13 100644 --- a/src/com/fsck/k9/mail/Message.java +++ b/src/com/fsck/k9/mail/Message.java @@ -5,6 +5,7 @@ import java.util.Date; import java.util.HashSet; import java.util.Set; import com.fsck.k9.activity.MessageReference; +import com.fsck.k9.mail.store.UnavailableStorageException; public abstract class Message implements Part, Body { @@ -140,7 +141,7 @@ public abstract class Message implements Part, Body public abstract String[] getHeader(String name) throws MessagingException; - public abstract Set getHeaderNames(); + public abstract Set getHeaderNames() throws UnavailableStorageException; public abstract void removeHeader(String name) throws MessagingException; @@ -204,7 +205,7 @@ public abstract class Message implements Part, Body public abstract void saveChanges() throws MessagingException; - public abstract void setEncoding(String encoding); + public abstract void setEncoding(String encoding) throws UnavailableStorageException; public MessageReference makeMessageReference() { diff --git a/src/com/fsck/k9/mail/Store.java b/src/com/fsck/k9/mail/Store.java index 3dae092d0..59b13203f 100644 --- a/src/com/fsck/k9/mail/Store.java +++ b/src/com/fsck/k9/mail/Store.java @@ -2,12 +2,14 @@ package com.fsck.k9.mail; import android.app.Application; +import android.content.Context; import com.fsck.k9.Account; import com.fsck.k9.mail.store.ImapStore; import com.fsck.k9.mail.store.LocalStore; import com.fsck.k9.mail.store.Pop3Store; import com.fsck.k9.mail.store.WebDavStore; +import com.fsck.k9.mail.store.StorageManager.StorageProvider; import java.util.HashMap; import java.util.List; @@ -25,7 +27,14 @@ public abstract class Store protected static final int SOCKET_CONNECT_TIMEOUT = 30000; protected static final int SOCKET_READ_TIMEOUT = 60000; + /** + * Remote stores indexed by Uri. + */ private static HashMap mStores = new HashMap(); + /** + * Local stores indexed by UUid because the Uri may change due to migration to/from SD-card. + */ + private static HashMap mLocalStores = new HashMap(); protected final Account mAccount; @@ -78,33 +87,18 @@ public abstract class Store /** * Get an instance of a local mail store. + * @throws UnavailableStorageException if not {@link StorageProvider#isReady(Context)} */ public synchronized static LocalStore getLocalInstance(Account account, Application application) throws MessagingException { - String uri = account.getLocalStoreUri(); - - if (!uri.startsWith("local")) - { - throw new RuntimeException("LocalStore URI doesn't start with 'local'"); - } - - Store store = mStores.get(uri); + Store store = mLocalStores.get(account.getUuid()); if (store == null) { store = new LocalStore(account, application); - - if (store != null) - { - mStores.put(uri, store); - } + mLocalStores.put(account.getUuid(), store); } - if (store == null) - { - throw new MessagingException("Unable to locate an applicable Store for " + uri); - } - - return (LocalStore)store; + return (LocalStore) store; } public abstract Folder getFolder(String name) throws MessagingException; diff --git a/src/com/fsck/k9/mail/internet/MimeMessage.java b/src/com/fsck/k9/mail/internet/MimeMessage.java index a0ba8b444..1072b9b7a 100644 --- a/src/com/fsck/k9/mail/internet/MimeMessage.java +++ b/src/com/fsck/k9/mail/internet/MimeMessage.java @@ -2,6 +2,8 @@ package com.fsck.k9.mail.internet; import com.fsck.k9.mail.*; +import com.fsck.k9.mail.store.UnavailableStorageException; + import org.apache.james.mime4j.BodyDescriptor; import org.apache.james.mime4j.ContentHandler; import org.apache.james.mime4j.EOLConvertingInputStream; @@ -340,7 +342,7 @@ public class MimeMessage extends Message return "<"+UUID.randomUUID().toString()+"@email.android.com>"; } - public void setMessageId(String messageId) + public void setMessageId(String messageId) throws UnavailableStorageException { setHeader("Message-ID", messageId); mMessageId = messageId; @@ -438,31 +440,31 @@ public class MimeMessage extends Message } @Override - public void addHeader(String name, String value) + public void addHeader(String name, String value) throws UnavailableStorageException { mHeader.addHeader(name, value); } @Override - public void setHeader(String name, String value) + public void setHeader(String name, String value) throws UnavailableStorageException { mHeader.setHeader(name, value); } @Override - public String[] getHeader(String name) + public String[] getHeader(String name) throws UnavailableStorageException { return mHeader.getHeader(name); } @Override - public void removeHeader(String name) + public void removeHeader(String name) throws UnavailableStorageException { mHeader.removeHeader(name); } @Override - public Set getHeaderNames() + public Set getHeaderNames() throws UnavailableStorageException { return mHeader.getHeaderNames(); } @@ -486,7 +488,7 @@ public class MimeMessage extends Message } @Override - public void setEncoding(String encoding) + public void setEncoding(String encoding) throws UnavailableStorageException { if (mBody instanceof Multipart) { diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java index 1fcdb2884..74ca947b8 100644 --- a/src/com/fsck/k9/mail/store/LocalStore.java +++ b/src/com/fsck/k9/mail/store/LocalStore.java @@ -1,43 +1,121 @@ package com.fsck.k9.mail.store; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.io.StringReader; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.regex.Matcher; + +import org.apache.commons.io.IOUtils; + import android.app.Application; import android.content.ContentValues; +import android.content.Context; import android.content.SharedPreferences; import android.database.Cursor; +import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.net.Uri; import android.text.Html; import android.util.Log; + import com.fsck.k9.Account; import com.fsck.k9.K9; import com.fsck.k9.Preferences; +import com.fsck.k9.Account.LocalStoreMigrationListener; import com.fsck.k9.controller.MessageRemovalListener; import com.fsck.k9.controller.MessageRetrievalListener; import com.fsck.k9.helper.Regex; import com.fsck.k9.helper.Utility; -import com.fsck.k9.mail.*; +import com.fsck.k9.mail.Address; +import com.fsck.k9.mail.Body; +import com.fsck.k9.mail.BodyPart; +import com.fsck.k9.mail.FetchProfile; +import com.fsck.k9.mail.Flag; +import com.fsck.k9.mail.Folder; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Part; +import com.fsck.k9.mail.Store; import com.fsck.k9.mail.Message.RecipientType; import com.fsck.k9.mail.filter.Base64OutputStream; -import com.fsck.k9.mail.internet.*; +import com.fsck.k9.mail.internet.MimeBodyPart; +import com.fsck.k9.mail.internet.MimeHeader; +import com.fsck.k9.mail.internet.MimeMessage; +import com.fsck.k9.mail.internet.MimeMultipart; +import com.fsck.k9.mail.internet.MimeUtility; +import com.fsck.k9.mail.internet.TextBody; +import com.fsck.k9.mail.store.StorageManager.StorageProvider; import com.fsck.k9.provider.AttachmentProvider; -import org.apache.commons.io.IOUtils; - -import java.io.*; -import java.net.URI; -import java.net.URLEncoder; -import java.util.*; -import java.util.regex.Matcher; /** *
  * Implements a SQLite database backed local store for Messages.
  * 
*/ -public class LocalStore extends Store implements Serializable +public class LocalStore extends Store implements Serializable, LocalStoreMigrationListener { + /** + * Callback interface for DB operations. Concept is similar to Spring + * HibernateCallback. + * + * @param + * Return value type for {@link #doDbWork(SQLiteDatabase)} + */ + public static interface DbCallback + { + /** + * @param db + * The locked database on which the work should occur. Never + * null. + * @return Any relevant data. Can be null. + * @throws WrappedException + * @throws UnavailableStorageException + */ + T doDbWork(SQLiteDatabase db) throws WrappedException, UnavailableStorageException; + } + + /** + * Workaround exception wrapper used to keep the inner exception generated + * in a {@link DbCallback}. + */ + protected static class WrappedException extends RuntimeException + { + /** + * + */ + private static final long serialVersionUID = 8184421232587399369L; + + public WrappedException(final Exception cause) + { + super(cause); + } + } + private static final Message[] EMPTY_MESSAGE_ARRAY = new Message[0]; /** @@ -50,9 +128,27 @@ public class LocalStore extends Store implements Serializable private static final int MAX_SMART_HTMLIFY_MESSAGE_LENGTH = 1024 * 256 ; - private String mPath; + private String mStorageProviderId; + private SQLiteDatabase mDb; - private File mAttachmentsDir; + + { + final ReadWriteLock lock = new ReentrantReadWriteLock(true); + mReadLock = lock.readLock(); + mWriteLock = lock.writeLock(); + } + + /** + * Reentrant read lock + */ + protected final Lock mReadLock; + + /** + * Reentrant write lock (if you lock it 2x from the same thread, you have to + * unlock it 2x to release it) + */ + protected final Lock mWriteLock; + private Application mApplication; private String uUid = null; @@ -77,76 +173,455 @@ public class LocalStore extends Store implements Serializable "subject, sender_list, date, uid, flags, id, to_list, cc_list, " + "bcc_list, reply_to_list, attachment_count, internal_date, message_id, folder_id, preview "; + private final StorageListener mStorageListener = new StorageListener(); + + /** + * {@link ThreadLocal} to check whether a DB transaction is occuring in the + * current {@link Thread}. + * + * @see #execute(boolean, DbCallback) + */ + private ThreadLocal inTransaction = new ThreadLocal(); + /** * local://localhost/path/to/database/uuid.db + * This constructor is only used by {@link Store#getLocalInstance(Account, Application)} + * @throws UnavailableStorageException if not {@link StorageProvider#isReady(Context)} */ public LocalStore(Account account, Application application) throws MessagingException { super(account); mApplication = application; - URI uri = null; + mStorageProviderId = account.getLocalStorageProviderId(); + account.setLocalStoreMigrationListener(this); + uUid = account.getUuid(); + + lockWrite(); try { - uri = new URI(mAccount.getLocalStoreUri()); + openOrCreateDataspace(application); + } + finally + { + unlockWrite(); + } + + StorageManager.getInstance(application).addListener(mStorageListener); + } + + /** + * Lock the storage for shared operations (concurrent threads are allowed to + * run simultaneously). + * + *

+ * You have to invoke {@link #unlockRead()} when you're + * done with the storage. + *

+ * + * @throws UnavailableStorageException + * If storage can't be locked because it is not available + */ + protected void lockRead() throws UnavailableStorageException + { + mReadLock.lock(); + try + { + StorageManager.getInstance(mApplication).lockProvider(mStorageProviderId); + } + catch (UnavailableStorageException e) + { + mReadLock.unlock(); + throw e; + } + catch (RuntimeException e) + { + mReadLock.unlock(); + throw e; + } + } + + protected void unlockRead() + { + StorageManager.getInstance(mApplication).unlockProvider(mStorageProviderId); + mReadLock.unlock(); + } + + /** + * Lock the storage for exclusive access (other threads aren't allowed to + * run simultaneously) + * + *

+ * You have to invoke {@link #unlockWrite()} when you're + * done with the storage. + *

+ * + * @throws UnavailableStorageException + * If storage can't be locked because it is not available. + */ + protected void lockWrite() throws UnavailableStorageException + { + lockWrite(mStorageProviderId); + } + + /** + * Lock the storage for exclusive access (other threads aren't allowed to + * run simultaneously) + * + *

+ * You have to invoke {@link #unlockWrite()} when you're + * done with the storage. + *

+ * + * @param providerId + * Never null. + * + * @throws UnavailableStorageException + * If storage can't be locked because it is not available. + */ + protected void lockWrite(final String providerId) throws UnavailableStorageException + { + mWriteLock.lock(); + try + { + StorageManager.getInstance(mApplication).lockProvider(providerId); + } + catch (UnavailableStorageException e) + { + mWriteLock.unlock(); + throw e; + } + catch (RuntimeException e) + { + mWriteLock.unlock(); + throw e; + } + } + + protected void unlockWrite() + { + unlockWrite(mStorageProviderId); + } + + protected void unlockWrite(final String providerId) + { + StorageManager.getInstance(mApplication).unlockProvider(providerId); + mWriteLock.unlock(); + } + + /** + * Execute a DB callback in a shared context (doesn't prevent concurrent + * shared executions), taking care of locking the DB storage. + * + *

+ * Can be instructed to start a transaction if none is currently active in + * the current thread. Callback will participe in any active transaction (no + * inner transaction created). + *

+ * + * @param transactional + * true the callback must be executed in a + * transactional context. + * @param callback + * Never null. + * + * @param + * @return Whatever {@link DbCallback#doDbWork(SQLiteDatabase)} returns. + * @throws UnavailableStorageException + */ + protected T execute(final boolean transactional, final DbCallback callback) throws UnavailableStorageException + { + lockRead(); + final boolean doTransaction = transactional && inTransaction.get() == null; + try + { + + final boolean debug = K9.DEBUG; + if (doTransaction) + { + inTransaction.set(Boolean.TRUE); + mDb.beginTransaction(); + } + try + { + final T result = callback.doDbWork(mDb); + if (doTransaction) + { + mDb.setTransactionSuccessful(); + } + return result; + } + finally + { + if (doTransaction) + { + final long begin; + if (debug) + { + begin = System.currentTimeMillis(); + } + else + { + begin = 0l; + } + // not doing endTransaction in the same 'finally' block of unlockRead() because endTransaction() may throw an exception + mDb.endTransaction(); + if (debug) + { + Log.v(K9.LOG_TAG, "LocalStore: Transaction ended, took " + Long.toString(System.currentTimeMillis() - begin) + "ms"); + } + } + } + } + finally + { + if (doTransaction) + { + inTransaction.set(null); + } + unlockRead(); + } + } + + public void onLocalStoreMigration(final String oldProviderId, + final String newProviderId) throws MessagingException + { + lockWrite(oldProviderId); + try + { + lockWrite(newProviderId); + try + { + try + { + mDb.close(); + } + catch (Exception e) + { + Log.i(K9.LOG_TAG, "Unable to close DB on local store migration", e); + } + + final StorageManager storageManager = StorageManager.getInstance(mApplication); + + // create new path + prepareStorage(newProviderId); + + // move all database files + moveRecursive(storageManager.getDatabase(uUid, oldProviderId), storageManager.getDatabase(uUid, newProviderId)); + // move all attachment files + moveRecursive(storageManager.getAttachmentDirectory(uUid, oldProviderId), storageManager.getAttachmentDirectory(uUid, newProviderId)); + + mStorageProviderId = newProviderId; + + // re-initialize this class with the new Uri + openOrCreateDataspace(mApplication); + } + finally + { + unlockWrite(newProviderId); + } + } + finally + { + unlockWrite(oldProviderId); + } + } + + private void moveRecursive(File fromDir, File toDir) + { + if (!fromDir.exists()) + { + return; + } + if (!fromDir.isDirectory()) + { + if (toDir.exists()) + { + if (!toDir.delete()) + { + Log.w(K9.LOG_TAG, "cannot delete already existing file/directory " + toDir.getAbsolutePath() + " during migration to/from SD-card"); + } + } + if (!fromDir.renameTo(toDir)) + { + Log.w(K9.LOG_TAG, "cannot rename " + fromDir.getAbsolutePath() + " to " + toDir.getAbsolutePath() + " - moving instead"); + move(fromDir, toDir); + } + return; + } + if (!toDir.exists() || !toDir.isDirectory()) + { + if (toDir.exists() ) + { + toDir.delete(); + } + if (!toDir.mkdirs()) + { + Log.w(K9.LOG_TAG, "cannot create directory " + toDir.getAbsolutePath() + " during migration to/from SD-card"); + } + } + File[] files = fromDir.listFiles(); + for (File file : files) + { + if (file.isDirectory()) + { + moveRecursive(file, new File(toDir, file.getName())); + file.delete(); + } + else + { + File target = new File(toDir, file.getName()); + if (!file.renameTo(target)) + { + Log.w(K9.LOG_TAG, "cannot rename " + file.getAbsolutePath() + " to " + target.getAbsolutePath() + " - moving instead"); + move(file, target); + } + } + } + if (!fromDir.delete()) + { + Log.w(K9.LOG_TAG, "cannot delete " + fromDir.getAbsolutePath() + " after migration to/from SD-card"); + } + } + private boolean move(File from, File to) + { + if (to.exists()) + { + to.delete(); + } + to.getParentFile().mkdirs(); + + try + { + FileInputStream in = new FileInputStream(from); + FileOutputStream out = new FileOutputStream(to); + byte[] buffer = new byte[1024]; + int count = -1; + while ((count = in.read(buffer)) > 0) + { + out.write(buffer, 0, count); + } + out.close(); + in.close(); + from.delete(); + return true; } catch (Exception e) { - throw new MessagingException("Invalid uri for LocalStore"); + Log.w(K9.LOG_TAG, "cannot move " + from.getAbsolutePath() + " to " + to.getAbsolutePath(), e); + return false; } - if (!uri.getScheme().equals("local")) - { - throw new MessagingException("Invalid scheme"); - } - mPath = uri.getPath(); - - - // We need to associate the localstore with the account. Since we don't have the account - // handy here, we'll take the filename from the DB and use the basename of the filename - // Folders probably should have references to their containing accounts - //TODO: We do have an account object now - File dbFile = new File(mPath); - String[] tokens = dbFile.getName().split("\\."); - uUid = tokens[0]; - - openOrCreateDataspace(application); } - private void openOrCreateDataspace(Application application) + /** + * + * @param application + * @throws UnavailableStorageException + */ + void openOrCreateDataspace(final Application application) throws UnavailableStorageException { - File parentDir = new File(mPath).getParentFile(); - if (!parentDir.exists()) - { - parentDir.mkdirs(); - } - - mAttachmentsDir = new File(mPath + "_att"); - if (!mAttachmentsDir.exists()) - { - mAttachmentsDir.mkdirs(); - } + lockWrite(); try { - mDb = SQLiteDatabase.openOrCreateDatabase(mPath, null); + final File databaseFile = prepareStorage(mStorageProviderId); + try + { + mDb = SQLiteDatabase.openOrCreateDatabase(databaseFile, null); + } + catch (SQLiteException e) + { + // try to gracefully handle DB corruption - see issue 2537 + Log.w(K9.LOG_TAG, "Unable to open DB " + databaseFile + " - removing file and retrying", e); + databaseFile.delete(); + mDb = SQLiteDatabase.openOrCreateDatabase(databaseFile, null); + } + if (mDb.getVersion() != DB_VERSION) + { + doDbUpgrade(mDb, application); + } } - catch (SQLiteException e) + finally { - // try to gracefully handle DB corruption - see issue 2537 - Log.w(K9.LOG_TAG, "Unable to open DB " + mPath + " - removing file and retrying", e); - new File(mPath).delete(); - mDb = SQLiteDatabase.openOrCreateDatabase(mPath, null); - } - if (mDb.getVersion() != DB_VERSION) - { - doDbUpgrade(mDb, application); + unlockWrite(); } } - private void doDbUpgrade(SQLiteDatabase mDb, Application application) + /** + * @param providerId + * Never null. + * @return DB file. + * @throws UnavailableStorageException + */ + protected File prepareStorage(final String providerId) throws UnavailableStorageException + { + final StorageManager storageManager = StorageManager.getInstance(mApplication); + + final File databaseFile; + final File databaseParentDir; + databaseFile = storageManager.getDatabase(uUid, providerId); + databaseParentDir = databaseFile.getParentFile(); + if (databaseParentDir.isFile()) + { + // should be safe to inconditionally delete clashing file: user is not supposed to mess with our directory + databaseParentDir.delete(); + } + if (!databaseParentDir.exists()) + { + if (!databaseParentDir.mkdirs()) + { + // Android seems to be unmounting the storage... + throw new UnavailableStorageException("Unable to access: " + databaseParentDir); + } + touchFile(databaseParentDir, ".nomedia"); + } + + final File attachmentDir; + final File attachmentParentDir; + attachmentDir = storageManager + .getAttachmentDirectory(uUid, providerId); + attachmentParentDir = attachmentDir.getParentFile(); + if (!attachmentParentDir.exists()) + { + attachmentParentDir.mkdirs(); + touchFile(attachmentParentDir, ".nomedia"); + } + if (!attachmentDir.exists()) + { + attachmentDir.mkdirs(); + } + return databaseFile; + } + + /** + * @param parentDir + * @param name + * Never null. + */ + protected void touchFile(final File parentDir, String name) + { + final File file = new File(parentDir, name); + try + { + if (!file.exists()) + { + file.createNewFile(); + } + else + { + file.setLastModified(System.currentTimeMillis()); + } + } + catch (Exception e) + { + Log.d(K9.LOG_TAG, "Unable to touch file: " + file.getAbsolutePath(), e); + } + } + + private void doDbUpgrade(final SQLiteDatabase db, final Application application) { Log.i(K9.LOG_TAG, String.format("Upgrading database from version %d to version %d", - mDb.getVersion(), DB_VERSION)); + db.getVersion(), DB_VERSION)); AttachmentProvider.clear(application); @@ -155,52 +630,52 @@ public class LocalStore extends Store implements Serializable { // schema version 29 was when we moved to incremental updates // in the case of a new db or a < v29 db, we blow away and start from scratch - if (mDb.getVersion() < 29) + if (db.getVersion() < 29) { - mDb.execSQL("DROP TABLE IF EXISTS folders"); - mDb.execSQL("CREATE TABLE folders (id INTEGER PRIMARY KEY, name TEXT, " - + "last_updated INTEGER, unread_count INTEGER, visible_limit INTEGER, status TEXT, push_state TEXT, last_pushed INTEGER, flagged_count INTEGER default 0)"); + db.execSQL("DROP TABLE IF EXISTS folders"); + db.execSQL("CREATE TABLE folders (id INTEGER PRIMARY KEY, name TEXT, " + + "last_updated INTEGER, unread_count INTEGER, visible_limit INTEGER, status TEXT, push_state TEXT, last_pushed INTEGER, flagged_count INTEGER default 0)"); - mDb.execSQL("CREATE INDEX IF NOT EXISTS folder_name ON folders (name)"); - mDb.execSQL("DROP TABLE IF EXISTS messages"); - mDb.execSQL("CREATE TABLE messages (id INTEGER PRIMARY KEY, deleted INTEGER default 0, folder_id INTEGER, uid TEXT, subject TEXT, " - + "date INTEGER, flags TEXT, sender_list TEXT, to_list TEXT, cc_list TEXT, bcc_list TEXT, reply_to_list TEXT, " - + "html_content TEXT, text_content TEXT, attachment_count INTEGER, internal_date INTEGER, message_id TEXT, preview TEXT)"); + db.execSQL("CREATE INDEX IF NOT EXISTS folder_name ON folders (name)"); + db.execSQL("DROP TABLE IF EXISTS messages"); + db.execSQL("CREATE TABLE messages (id INTEGER PRIMARY KEY, deleted INTEGER default 0, folder_id INTEGER, uid TEXT, subject TEXT, " + + "date INTEGER, flags TEXT, sender_list TEXT, to_list TEXT, cc_list TEXT, bcc_list TEXT, reply_to_list TEXT, " + + "html_content TEXT, text_content TEXT, attachment_count INTEGER, internal_date INTEGER, message_id TEXT, preview TEXT)"); - mDb.execSQL("DROP TABLE IF EXISTS headers"); - mDb.execSQL("CREATE TABLE headers (id INTEGER PRIMARY KEY, message_id INTEGER, name TEXT, value TEXT)"); - mDb.execSQL("CREATE INDEX IF NOT EXISTS header_folder ON headers (message_id)"); + db.execSQL("DROP TABLE IF EXISTS headers"); + db.execSQL("CREATE TABLE headers (id INTEGER PRIMARY KEY, message_id INTEGER, name TEXT, value TEXT)"); + db.execSQL("CREATE INDEX IF NOT EXISTS header_folder ON headers (message_id)"); - mDb.execSQL("CREATE INDEX IF NOT EXISTS msg_uid ON messages (uid, folder_id)"); - mDb.execSQL("DROP INDEX IF EXISTS msg_folder_id"); - mDb.execSQL("DROP INDEX IF EXISTS msg_folder_id_date"); - mDb.execSQL("CREATE INDEX IF NOT EXISTS msg_folder_id_deleted_date ON messages (folder_id,deleted,internal_date)"); - mDb.execSQL("DROP TABLE IF EXISTS attachments"); - mDb.execSQL("CREATE TABLE attachments (id INTEGER PRIMARY KEY, message_id INTEGER," - + "store_data TEXT, content_uri TEXT, size INTEGER, name TEXT," - + "mime_type TEXT, content_id TEXT, content_disposition TEXT)"); + db.execSQL("CREATE INDEX IF NOT EXISTS msg_uid ON messages (uid, folder_id)"); + db.execSQL("DROP INDEX IF EXISTS msg_folder_id"); + db.execSQL("DROP INDEX IF EXISTS msg_folder_id_date"); + db.execSQL("CREATE INDEX IF NOT EXISTS msg_folder_id_deleted_date ON messages (folder_id,deleted,internal_date)"); + db.execSQL("DROP TABLE IF EXISTS attachments"); + db.execSQL("CREATE TABLE attachments (id INTEGER PRIMARY KEY, message_id INTEGER," + + "store_data TEXT, content_uri TEXT, size INTEGER, name TEXT," + + "mime_type TEXT, content_id TEXT, content_disposition TEXT)"); - mDb.execSQL("DROP TABLE IF EXISTS pending_commands"); - mDb.execSQL("CREATE TABLE pending_commands " + - "(id INTEGER PRIMARY KEY, command TEXT, arguments TEXT)"); + db.execSQL("DROP TABLE IF EXISTS pending_commands"); + db.execSQL("CREATE TABLE pending_commands " + + "(id INTEGER PRIMARY KEY, command TEXT, arguments TEXT)"); - mDb.execSQL("DROP TRIGGER IF EXISTS delete_folder"); - mDb.execSQL("CREATE TRIGGER delete_folder BEFORE DELETE ON folders BEGIN DELETE FROM messages WHERE old.id = folder_id; END;"); + db.execSQL("DROP TRIGGER IF EXISTS delete_folder"); + db.execSQL("CREATE TRIGGER delete_folder BEFORE DELETE ON folders BEGIN DELETE FROM messages WHERE old.id = folder_id; END;"); - mDb.execSQL("DROP TRIGGER IF EXISTS delete_message"); - mDb.execSQL("CREATE TRIGGER delete_message BEFORE DELETE ON messages BEGIN DELETE FROM attachments WHERE old.id = message_id; " - + "DELETE FROM headers where old.id = message_id; END;"); + db.execSQL("DROP TRIGGER IF EXISTS delete_message"); + db.execSQL("CREATE TRIGGER delete_message BEFORE DELETE ON messages BEGIN DELETE FROM attachments WHERE old.id = message_id; " + + "DELETE FROM headers where old.id = message_id; END;"); } else { // in the case that we're starting out at 29 or newer, run all the needed updates - if (mDb.getVersion() < 30) + if (db.getVersion() < 30) { try { - mDb.execSQL("ALTER TABLE messages ADD deleted INTEGER default 0"); + db.execSQL("ALTER TABLE messages ADD deleted INTEGER default 0"); } catch (SQLiteException e) { @@ -210,21 +685,21 @@ public class LocalStore extends Store implements Serializable } } } - if (mDb.getVersion() < 31) + if (db.getVersion() < 31) { - mDb.execSQL("DROP INDEX IF EXISTS msg_folder_id_date"); - mDb.execSQL("CREATE INDEX IF NOT EXISTS msg_folder_id_deleted_date ON messages (folder_id,deleted,internal_date)"); + db.execSQL("DROP INDEX IF EXISTS msg_folder_id_date"); + db.execSQL("CREATE INDEX IF NOT EXISTS msg_folder_id_deleted_date ON messages (folder_id,deleted,internal_date)"); } - if (mDb.getVersion() < 32) + if (db.getVersion() < 32) { - mDb.execSQL("UPDATE messages SET deleted = 1 WHERE flags LIKE '%DELETED%'"); + db.execSQL("UPDATE messages SET deleted = 1 WHERE flags LIKE '%DELETED%'"); } - if (mDb.getVersion() < 33) + if (db.getVersion() < 33) { try { - mDb.execSQL("ALTER TABLE messages ADD preview TEXT"); + db.execSQL("ALTER TABLE messages ADD preview TEXT"); } catch (SQLiteException e) { @@ -235,11 +710,11 @@ public class LocalStore extends Store implements Serializable } } - if (mDb.getVersion() < 34) + if (db.getVersion() < 34) { try { - mDb.execSQL("ALTER TABLE folders ADD flagged_count INTEGER default 0"); + db.execSQL("ALTER TABLE folders ADD flagged_count INTEGER default 0"); } catch (SQLiteException e) { @@ -249,33 +724,33 @@ public class LocalStore extends Store implements Serializable } } } - if (mDb.getVersion() < 35) + if (db.getVersion() < 35) { try { - mDb.execSQL("update messages set flags = replace(flags, 'X_NO_SEEN_INFO', 'X_BAD_FLAG')"); + db.execSQL("update messages set flags = replace(flags, 'X_NO_SEEN_INFO', 'X_BAD_FLAG')"); } catch (SQLiteException e) { Log.e(K9.LOG_TAG, "Unable to get rid of obsolete flag X_NO_SEEN_INFO", e); } } - if (mDb.getVersion() < 36) + if (db.getVersion() < 36) { try { - mDb.execSQL("ALTER TABLE attachments ADD content_id TEXT"); + db.execSQL("ALTER TABLE attachments ADD content_id TEXT"); } catch (SQLiteException e) { Log.e(K9.LOG_TAG, "Unable to add content_id column to attachments"); } } - if (mDb.getVersion() < 37) + if (db.getVersion() < 37) { try { - mDb.execSQL("ALTER TABLE attachments ADD content_disposition TEXT"); + db.execSQL("ALTER TABLE attachments ADD content_disposition TEXT"); } catch (SQLiteException e) { @@ -285,11 +760,11 @@ public class LocalStore extends Store implements Serializable // Database version 38 is solely to prune cached attachments now that we clear them better - if (mDb.getVersion() < 39) + if (db.getVersion() < 39) { try { - mDb.execSQL("DELETE FROM headers WHERE id in (SELECT headers.id FROM headers LEFT JOIN messages ON headers.message_id = messages.id WHERE messages.id IS NULL)"); + db.execSQL("DELETE FROM headers WHERE id in (SELECT headers.id FROM headers LEFT JOIN messages ON headers.message_id = messages.id WHERE messages.id IS NULL)"); } catch (SQLiteException e) { @@ -305,15 +780,15 @@ public class LocalStore extends Store implements Serializable catch (SQLiteException e) { Log.e(K9.LOG_TAG, "Exception while upgrading database. Resetting the DB to v0"); - mDb.setVersion(0); + db.setVersion(0); throw new Error("Database upgrade failed! Resetting your DB version to 0 to force a full schema recreation."); } - mDb.setVersion(DB_VERSION); + db.setVersion(DB_VERSION); - if (mDb.getVersion() != DB_VERSION) + if (db.getVersion() != DB_VERSION) { throw new Error("Database upgrade failed!"); } @@ -328,22 +803,33 @@ public class LocalStore extends Store implements Serializable } } - public long getSize() + public long getSize() throws UnavailableStorageException { - long attachmentLength = 0; - File[] files = mAttachmentsDir.listFiles(); - for (File file : files) + final StorageManager storageManager = StorageManager.getInstance(mApplication); + + final File attachmentDirectory = storageManager.getAttachmentDirectory(uUid, + mStorageProviderId); + + return execute(false, new DbCallback() { - if (file.exists()) + @Override + public Long doDbWork(final SQLiteDatabase db) { - attachmentLength += file.length(); + final File[] files = attachmentDirectory.listFiles(); + long attachmentLength = 0; + for (File file : files) + { + if (file.exists()) + { + attachmentLength += file.length(); + } + } + + final File dbFile = storageManager.getDatabase(uUid, mStorageProviderId); + return dbFile.length() + attachmentLength; } - } - - - File dbFile = new File(mPath); - return dbFile.length() + attachmentLength; + }); } public void compact() throws MessagingException @@ -355,7 +841,15 @@ public class LocalStore extends Store implements Serializable if (K9.DEBUG) Log.i(K9.LOG_TAG, "After prune / before compaction size = " + getSize()); - mDb.execSQL("VACUUM"); + execute(false, new DbCallback() + { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException + { + db.execSQL("VACUUM"); + return null; + } + }); if (K9.DEBUG) Log.i(K9.LOG_TAG, "After compaction size = " + getSize()); } @@ -379,10 +873,19 @@ public class LocalStore extends Store implements Serializable // don't delete messages that are Local, since there is no copy on the server. // Don't delete deleted messages. They are essentially placeholders for UIDs of messages that have // been deleted locally. They take up insignificant space - mDb.execSQL("DELETE FROM messages WHERE deleted = 0 and uid not like 'Local%'"); - mDb.execSQL("update folders set flagged_count = 0, unread_count = 0"); + execute(false, new DbCallback() + { + @Override + public Void doDbWork(final SQLiteDatabase db) + { + db.execSQL("DELETE FROM messages WHERE deleted = 0 and uid not like 'Local%'"); + db.execSQL("update folders set flagged_count = 0, unread_count = 0"); + return null; + } + }); compact(); + if (K9.DEBUG) { Log.i(K9.LOG_TAG, "After clear message count = " + getMessageCount()); @@ -393,40 +896,54 @@ public class LocalStore extends Store implements Serializable public int getMessageCount() throws MessagingException { - Cursor cursor = null; - try + return execute(false, new DbCallback() { - cursor = mDb.rawQuery("SELECT COUNT(*) FROM messages", null); - cursor.moveToFirst(); - int messageCount = cursor.getInt(0); - return messageCount; - } - finally - { - if (cursor != null) + @Override + public Integer doDbWork(final SQLiteDatabase db) { - cursor.close(); + Cursor cursor = null; + try + { + cursor = db.rawQuery("SELECT COUNT(*) FROM messages", null); + cursor.moveToFirst(); + int messageCount = cursor.getInt(0); + return messageCount; + } + finally + { + if (cursor != null) + { + cursor.close(); + } + } } - } + }); } public int getFolderCount() throws MessagingException { - Cursor cursor = null; - try + return execute(false, new DbCallback() { - cursor = mDb.rawQuery("SELECT COUNT(*) FROM folders", null); - cursor.moveToFirst(); - int messageCount = cursor.getInt(0); - return messageCount; - } - finally - { - if (cursor != null) + @Override + public Integer doDbWork(final SQLiteDatabase db) { - cursor.close(); + Cursor cursor = null; + try + { + cursor = db.rawQuery("SELECT COUNT(*) FROM folders", null); + cursor.moveToFirst(); + int messageCount = cursor.getInt(0); + return messageCount; + } + finally + { + if (cursor != null) + { + cursor.close(); + } + } } - } + }); } @Override @@ -439,26 +956,45 @@ public class LocalStore extends Store implements Serializable @Override public List getPersonalNamespaces(boolean forceListAll) throws MessagingException { - LinkedList folders = new LinkedList(); - Cursor cursor = null; - + final List folders = new LinkedList(); try { - cursor = mDb.rawQuery("SELECT id, name, unread_count, visible_limit, last_updated, status, push_state, last_pushed, flagged_count FROM folders", null); - while (cursor.moveToNext()) + execute(false, new DbCallback>() { - LocalFolder folder = new LocalFolder(cursor.getString(1)); - folder.open(cursor.getInt(0), cursor.getString(1), cursor.getInt(2), cursor.getInt(3), cursor.getLong(4), cursor.getString(5), cursor.getString(6), cursor.getLong(7), cursor.getInt(8)); + @Override + public List doDbWork(final SQLiteDatabase db) throws WrappedException + { + Cursor cursor = null; - folders.add(folder); - } + try + { + cursor = db.rawQuery("SELECT id, name, unread_count, visible_limit, last_updated, status, push_state, last_pushed, flagged_count FROM folders", null); + while (cursor.moveToNext()) + { + LocalFolder folder = new LocalFolder(cursor.getString(1)); + folder.open(cursor.getInt(0), cursor.getString(1), cursor.getInt(2), cursor.getInt(3), cursor.getLong(4), cursor.getString(5), cursor.getString(6), cursor.getLong(7), cursor.getInt(8)); + + folders.add(folder); + } + return folders; + } + catch (MessagingException e) + { + throw new WrappedException(e); + } + finally + { + if (cursor != null) + { + cursor.close(); + } + } + } + }); } - finally + catch (WrappedException e) { - if (cursor != null) - { - cursor.close(); - } + throw (MessagingException) e.getCause(); } return folders; } @@ -470,49 +1006,106 @@ public class LocalStore extends Store implements Serializable /** * Delete the entire Store and it's backing database. + * @throws UnavailableStorageException */ - public void delete() + public void delete() throws UnavailableStorageException { + lockWrite(); try { - mDb.close(); - } - catch (Exception e) - { - - } - try - { - File[] attachments = mAttachmentsDir.listFiles(); - for (File attachment : attachments) + try { - if (attachment.exists()) + mDb.close(); + } + catch (Exception e) + { + + } + final StorageManager storageManager = StorageManager.getInstance(mApplication); + try + { + final File attachmentDirectory = storageManager.getAttachmentDirectory(uUid, mStorageProviderId); + final File[] attachments = attachmentDirectory.listFiles(); + for (File attachment : attachments) { - attachment.delete(); + if (attachment.exists()) + { + attachment.delete(); + } + } + if (attachmentDirectory.exists()) + { + attachmentDirectory.delete(); } } - if (mAttachmentsDir.exists()) + catch (Exception e) { - mAttachmentsDir.delete(); } - } - catch (Exception e) - { - } - try - { - new File(mPath).delete(); - } - catch (Exception e) - { + try + { + storageManager.getDatabase(uUid, mStorageProviderId).delete(); + } + catch (Exception e) + { + Log.i(K9.LOG_TAG, "LocalStore: delete(): Unable to delete backing DB file", e); + } + // stop waiting for mount/unmount events + StorageManager.getInstance(mApplication).removeListener(mStorageListener); + } + finally + { + unlockWrite(); } } - public void recreate() + public void recreate() throws UnavailableStorageException { - delete(); - openOrCreateDataspace(mApplication); + lockWrite(); + try + { + try + { + mDb.close(); + } + catch (Exception e) + { + + } + final StorageManager storageManager = StorageManager.getInstance(mApplication); + try + { + final File attachmentDirectory = storageManager.getAttachmentDirectory(uUid, mStorageProviderId); + final File[] attachments = attachmentDirectory.listFiles(); + for (File attachment : attachments) + { + if (attachment.exists()) + { + attachment.delete(); + } + } + if (attachmentDirectory.exists()) + { + attachmentDirectory.delete(); + } + } + catch (Exception e) + { + } + try + { + storageManager.getDatabase(uUid, mStorageProviderId).delete(); + } + catch (Exception e) + { + + } + openOrCreateDataspace(mApplication); + } + finally + { + unlockWrite(); + } } public void pruneCachedAttachments() throws MessagingException @@ -523,129 +1116,152 @@ public class LocalStore extends Store implements Serializable /** * Deletes all cached attachments for the entire store. */ - public void pruneCachedAttachments(boolean force) throws MessagingException + public void pruneCachedAttachments(final boolean force) throws MessagingException { - - if (force) + execute(false, new DbCallback() { - ContentValues cv = new ContentValues(); - cv.putNull("content_uri"); - mDb.update("attachments", cv, null, null); - } - File[] files = mAttachmentsDir.listFiles(); - for (File file : files) - { - if (file.exists()) + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException { - if (!force) + if (force) { - Cursor cursor = null; - try + ContentValues cv = new ContentValues(); + cv.putNull("content_uri"); + db.update("attachments", cv, null, null); + } + final StorageManager storageManager = StorageManager.getInstance(mApplication); + File[] files = storageManager.getAttachmentDirectory(uUid, mStorageProviderId).listFiles(); + for (File file : files) + { + if (file.exists()) { - cursor = mDb.query( - "attachments", - new String[] { "store_data" }, - "id = ?", - new String[] { file.getName() }, - null, - null, - null); - if (cursor.moveToNext()) + if (!force) { - if (cursor.getString(0) == null) + Cursor cursor = null; + try { - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "Attachment " + file.getAbsolutePath() + " has no store data, not deleting"); - /* - * If the attachment has no store data it is not recoverable, so - * we won't delete it. - */ - continue; + cursor = db.query( + "attachments", + new String[] { "store_data" }, + "id = ?", + new String[] { file.getName() }, + null, + null, + null); + if (cursor.moveToNext()) + { + if (cursor.getString(0) == null) + { + if (K9.DEBUG) + Log.d(K9.LOG_TAG, "Attachment " + file.getAbsolutePath() + " has no store data, not deleting"); + /* + * If the attachment has no store data it is not recoverable, so + * we won't delete it. + */ + continue; + } + } + } + finally + { + if (cursor != null) + { + cursor.close(); + } } } - } - finally - { - if (cursor != null) + if (!force) { - cursor.close(); + try + { + ContentValues cv = new ContentValues(); + cv.putNull("content_uri"); + db.update("attachments", cv, "id = ?", new String[] { file.getName() }); + } + catch (Exception e) + { + /* + * If the row has gone away before we got to mark it not-downloaded that's + * okay. + */ + } + } + if (!file.delete()) + { + file.deleteOnExit(); } } } - if (!force) - { - try - { - ContentValues cv = new ContentValues(); - cv.putNull("content_uri"); - mDb.update("attachments", cv, "id = ?", new String[] { file.getName() }); - } - catch (Exception e) - { - /* - * If the row has gone away before we got to mark it not-downloaded that's - * okay. - */ - } - } - if (!file.delete()) - { - file.deleteOnExit(); - } + return null; } - } + }); } - public void resetVisibleLimits() + public void resetVisibleLimits() throws UnavailableStorageException { resetVisibleLimits(mAccount.getDisplayCount()); } - public void resetVisibleLimits(int visibleLimit) + public void resetVisibleLimits(int visibleLimit) throws UnavailableStorageException { - ContentValues cv = new ContentValues(); + final ContentValues cv = new ContentValues(); cv.put("visible_limit", Integer.toString(visibleLimit)); - mDb.update("folders", cv, null, null); + execute(false, new DbCallback() + { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException + { + db.update("folders", cv, null, null); + return null; + } + }); } - public ArrayList getPendingCommands() + public ArrayList getPendingCommands() throws UnavailableStorageException { - Cursor cursor = null; - try + return execute(false, new DbCallback>() { - cursor = mDb.query("pending_commands", - new String[] { "id", "command", "arguments" }, - null, - null, - null, - null, - "id ASC"); - ArrayList commands = new ArrayList(); - while (cursor.moveToNext()) + @Override + public ArrayList doDbWork(final SQLiteDatabase db) throws WrappedException { - PendingCommand command = new PendingCommand(); - command.mId = cursor.getLong(0); - command.command = cursor.getString(1); - String arguments = cursor.getString(2); - command.arguments = arguments.split(","); - for (int i = 0; i < command.arguments.length; i++) + Cursor cursor = null; + try { - command.arguments[i] = Utility.fastUrlDecode(command.arguments[i]); + cursor = db.query("pending_commands", + new String[] { "id", "command", "arguments" }, + null, + null, + null, + null, + "id ASC"); + ArrayList commands = new ArrayList(); + while (cursor.moveToNext()) + { + PendingCommand command = new PendingCommand(); + command.mId = cursor.getLong(0); + command.command = cursor.getString(1); + String arguments = cursor.getString(2); + command.arguments = arguments.split(","); + for (int i = 0; i < command.arguments.length; i++) + { + command.arguments[i] = Utility.fastUrlDecode(command.arguments[i]); + } + commands.add(command); + } + return commands; + } + finally + { + if (cursor != null) + { + cursor.close(); + } } - commands.add(command); } - return commands; - } - finally - { - if (cursor != null) - { - cursor.close(); - } - } + }); } - public void addPendingCommand(PendingCommand command) + public void addPendingCommand(PendingCommand command) throws UnavailableStorageException { try { @@ -653,10 +1269,18 @@ public class LocalStore extends Store implements Serializable { command.arguments[i] = URLEncoder.encode(command.arguments[i], "UTF-8"); } - ContentValues cv = new ContentValues(); + final ContentValues cv = new ContentValues(); cv.put("command", command.command); cv.put("arguments", Utility.combine(command.arguments, ',')); - mDb.insert("pending_commands", "command", cv); + execute(false, new DbCallback() + { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException + { + db.insert("pending_commands", "command", cv); + return null; + } + }); } catch (UnsupportedEncodingException usee) { @@ -664,14 +1288,90 @@ public class LocalStore extends Store implements Serializable } } - public void removePendingCommand(PendingCommand command) + public void removePendingCommand(final PendingCommand command) throws UnavailableStorageException { - mDb.delete("pending_commands", "id = ?", new String[] { Long.toString(command.mId) }); + execute(false, new DbCallback() + { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException + { + db.delete("pending_commands", "id = ?", new String[] { Long.toString(command.mId) }); + return null; + } + }); } - public void removePendingCommands() + public void removePendingCommands() throws UnavailableStorageException { - mDb.delete("pending_commands", null, null); + execute(false, new DbCallback() + { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException + { + db.delete("pending_commands", null, null); + return null; + } + }); + } + + /** + * Open the DB on mount and close the DB on unmount + */ + private class StorageListener implements StorageManager.StorageListener + { + @Override + public void onUnmount(final String providerId) + { + if (!providerId.equals(mStorageProviderId)) + { + return; + } + + if (K9.DEBUG) + { + Log.d(K9.LOG_TAG, "LocalStore: Closing DB " + uUid + " due to unmount event on StorageProvider: " + providerId); + } + + try + { + lockWrite(); + try + { + mDb.close(); + } + finally + { + unlockWrite(); + } + } + catch (UnavailableStorageException e) + { + Log.w(K9.LOG_TAG, "Unable to writelock on unmount", e); + } + } + + @Override + public void onMount(String providerId) + { + if (!providerId.equals(mStorageProviderId)) + { + return; + } + + if (K9.DEBUG) + { + Log.d(K9.LOG_TAG, "LocalStore: Opening DB " + uUid + " due to mount event on StorageProvider: " + providerId); + } + + try + { + openOrCreateDataspace(mApplication); + } + catch (UnavailableStorageException e) + { + Log.e(K9.LOG_TAG, "Unable to open DB on mount", e); + } + } } public static class PendingCommand @@ -821,66 +1521,161 @@ public class LocalStore extends Store implements Serializable * call the MessageRetrievalListener for each one */ private Message[] getMessages( - MessageRetrievalListener listener, - LocalFolder folder, - String queryString, String[] placeHolders + final MessageRetrievalListener listener, + final LocalFolder folder, + final String queryString, final String[] placeHolders ) throws MessagingException { - ArrayList messages = new ArrayList(); - Cursor cursor = null; - int i = 0; - try + final ArrayList messages = new ArrayList(); + final int j = execute(false, new DbCallback() { - cursor = mDb.rawQuery(queryString + " LIMIT 10", placeHolders); - - while (cursor.moveToNext()) + @Override + public Integer doDbWork(final SQLiteDatabase db) throws WrappedException { - LocalMessage message = new LocalMessage(null, folder); - message.populateFromGetMessageCursor(cursor); - - messages.add(message); - if (listener != null) + Cursor cursor = null; + int i = 0; + try { - listener.messageFinished(message, i, -1); + cursor = db.rawQuery(queryString + " LIMIT 10", placeHolders); + + while (cursor.moveToNext()) + { + LocalMessage message = new LocalMessage(null, folder); + message.populateFromGetMessageCursor(cursor); + + messages.add(message); + if (listener != null) + { + listener.messageFinished(message, i, -1); + } + i++; + } + cursor.close(); + cursor = db.rawQuery(queryString + " LIMIT -1 OFFSET 10", placeHolders); + + while (cursor.moveToNext()) + { + LocalMessage message = new LocalMessage(null, folder); + message.populateFromGetMessageCursor(cursor); + + messages.add(message); + if (listener != null) + { + listener.messageFinished(message, i, -1); + } + i++; + } } - i++; - } - cursor.close(); - cursor = mDb.rawQuery(queryString + " LIMIT -1 OFFSET 10", placeHolders); - - while (cursor.moveToNext()) - { - LocalMessage message = new LocalMessage(null, folder); - message.populateFromGetMessageCursor(cursor); - - messages.add(message); - if (listener != null) + catch (Exception e) { - listener.messageFinished(message, i, -1); + Log.d(K9.LOG_TAG,"Got an exception "+e); } - i++; + finally + { + if (cursor != null) + { + cursor.close(); + } + } + return i; } - } - catch (Exception e) - { - Log.d(K9.LOG_TAG,"Got an exception "+e); - } - finally - { - if (cursor != null) - { - cursor.close(); - } - } + }); if (listener != null) { - listener.messagesFinished(i); + listener.messagesFinished(j); } return messages.toArray(EMPTY_MESSAGE_ARRAY); } + public String getAttachmentType(final String attachmentId) throws UnavailableStorageException + { + return execute(false, new DbCallback() + { + @Override + public String doDbWork(final SQLiteDatabase db) throws WrappedException + { + Cursor cursor = null; + try + { + cursor = db.query( + "attachments", + new String[] { "mime_type", "name" }, + "id = ?", + new String[] { attachmentId }, + null, + null, + null); + cursor.moveToFirst(); + String type = cursor.getString(0); + String name = cursor.getString(1); + cursor.close(); + + if (MimeUtility.DEFAULT_ATTACHMENT_MIME_TYPE.equals(type)) + { + type = MimeUtility.getMimeTypeByExtension(name); + } + return type; + } + finally + { + if (cursor != null) + { + cursor.close(); + } + } + } + }); + } + + public AttachmentInfo getAttachmentInfo(final String attachmentId) throws UnavailableStorageException + { + return execute(false, new DbCallback() + { + @Override + public AttachmentInfo doDbWork(final SQLiteDatabase db) throws WrappedException + { + String name = null; + int size = -1; + Cursor cursor = null; + try + { + cursor = db.query( + "attachments", + new String[] { "name", "size" }, + "id = ?", + new String[] { attachmentId }, + null, + null, + null); + if (!cursor.moveToFirst()) + { + return null; + } + name = cursor.getString(0); + size = cursor.getInt(1); + final AttachmentInfo attachmentInfo = new AttachmentInfo(); + attachmentInfo.name = name; + attachmentInfo.size = size; + return attachmentInfo; + } + finally + { + if (cursor != null) + { + cursor.close(); + } + } + } + }); + } + + public static class AttachmentInfo + { + public String name; + public int size; + } public class LocalFolder extends Folder implements Serializable { @@ -925,49 +1720,66 @@ public class LocalStore extends Store implements Serializable } @Override - public void open(OpenMode mode) throws MessagingException + public void open(final OpenMode mode) throws MessagingException { if (isOpen()) { return; } - Cursor cursor = null; try { - String baseQuery = - "SELECT id, name,unread_count, visible_limit, last_updated, status, push_state, last_pushed, flagged_count FROM folders "; - if (mName != null) + execute(false, new DbCallback() { - cursor = mDb.rawQuery(baseQuery + "where folders.name = ?", new String[] { mName }); - } - else - { - cursor = mDb.rawQuery(baseQuery + "where folders.id = ?", new String[] { Long.toString(mFolderId) }); - - - } - - if (cursor.moveToFirst()) - { - int folderId = cursor.getInt(0); - if (folderId > 0) + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException { - open(folderId, cursor.getString(1), cursor.getInt(2), cursor.getInt(3), cursor.getLong(4), cursor.getString(5), cursor.getString(6), cursor.getLong(7), cursor.getInt(8)); + Cursor cursor = null; + try + { + String baseQuery = + "SELECT id, name,unread_count, visible_limit, last_updated, status, push_state, last_pushed, flagged_count FROM folders "; + if (mName != null) + { + cursor = db.rawQuery(baseQuery + "where folders.name = ?", new String[] { mName }); + } + else + { + cursor = db.rawQuery(baseQuery + "where folders.id = ?", new String[] { Long.toString(mFolderId) }); + } + + if (cursor.moveToFirst()) + { + int folderId = cursor.getInt(0); + if (folderId > 0) + { + open(folderId, cursor.getString(1), cursor.getInt(2), cursor.getInt(3), cursor.getLong(4), cursor.getString(5), cursor.getString(6), cursor.getLong(7), cursor.getInt(8)); + } + } + else + { + Log.w(K9.LOG_TAG, "Creating folder " + getName() + " with existing id " + getId()); + create(FolderType.HOLDS_MESSAGES); + open(mode); + } + } + catch (MessagingException e) + { + throw new WrappedException(e); + } + finally + { + if (cursor != null) + { + cursor.close(); + } + } + return null; } - } - else - { - Log.w(K9.LOG_TAG, "Creating folder " + getName() + " with existing id " + getId()); - create(FolderType.HOLDS_MESSAGES); - open(mode); - } + }); } - finally + catch (WrappedException e) { - if (cursor != null) - { - cursor.close(); - } + throw (MessagingException) e.getCause(); } } @@ -1007,30 +1819,37 @@ public class LocalStore extends Store implements Serializable @Override public boolean exists() throws MessagingException { - Cursor cursor = null; - try + return execute(false, new DbCallback() { - cursor = mDb.rawQuery("SELECT id FROM folders " - + "where folders.name = ?", new String[] { this - .getName() - }); - if (cursor.moveToFirst()) + @Override + public Boolean doDbWork(final SQLiteDatabase db) throws WrappedException { - int folderId = cursor.getInt(0); - return (folderId > 0); + Cursor cursor = null; + try + { + cursor = db.rawQuery("SELECT id FROM folders " + + "where folders.name = ?", new String[] { LocalFolder.this + .getName() + }); + if (cursor.moveToFirst()) + { + int folderId = cursor.getInt(0); + return (folderId > 0); + } + else + { + return false; + } + } + finally + { + if (cursor != null) + { + cursor.close(); + } + } } - else - { - return false; - } - } - finally - { - if (cursor != null) - { - cursor.close(); - } - } + }); } @Override @@ -1040,26 +1859,42 @@ public class LocalStore extends Store implements Serializable { throw new MessagingException("Folder " + mName + " already exists."); } - mDb.execSQL("INSERT INTO folders (name, visible_limit) VALUES (?, ?)", new Object[] - { - mName, - mAccount.getDisplayCount() - }); + execute(false, new DbCallback() + { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException + { + db.execSQL("INSERT INTO folders (name, visible_limit) VALUES (?, ?)", new Object[] + { + mName, + mAccount.getDisplayCount() + }); + return null; + } + }); return true; } @Override - public boolean create(FolderType type, int visibleLimit) throws MessagingException + public boolean create(FolderType type, final int visibleLimit) throws MessagingException { if (exists()) { throw new MessagingException("Folder " + mName + " already exists."); } - mDb.execSQL("INSERT INTO folders (name, visible_limit) VALUES (?, ?)", new Object[] - { - mName, - visibleLimit - }); + execute(false, new DbCallback() + { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException + { + db.execSQL("INSERT INTO folders (name, visible_limit) VALUES (?, ?)", new Object[] + { + mName, + visibleLimit + }); + return null; + } + }); return true; } @@ -1072,25 +1907,46 @@ public class LocalStore extends Store implements Serializable @Override public int getMessageCount() throws MessagingException { - open(OpenMode.READ_WRITE); - Cursor cursor = null; try { - cursor = mDb.rawQuery("SELECT COUNT(*) FROM messages WHERE messages.folder_id = ?", - new String[] - { - Long.toString(mFolderId) - }); - cursor.moveToFirst(); - int messageCount = cursor.getInt(0); - return messageCount; - } - finally - { - if (cursor != null) + return execute(false, new DbCallback() { - cursor.close(); - } + @Override + public Integer doDbWork(final SQLiteDatabase db) throws WrappedException + { + try + { + open(OpenMode.READ_WRITE); + } + catch (MessagingException e) + { + throw new WrappedException(e); + } + Cursor cursor = null; + try + { + cursor = db.rawQuery("SELECT COUNT(*) FROM messages WHERE messages.folder_id = ?", + new String[] + { + Long.toString(mFolderId) + }); + cursor.moveToFirst(); + int messageCount = cursor.getInt(0); + return messageCount; + } + finally + { + if (cursor != null) + { + cursor.close(); + } + } + } + }); + } + catch (WrappedException e) + { + throw (MessagingException) e.getCause(); } } @@ -1108,38 +1964,126 @@ public class LocalStore extends Store implements Serializable return mFlaggedMessageCount; } - public void setUnreadMessageCount(int unreadMessageCount) throws MessagingException + public void setUnreadMessageCount(final int unreadMessageCount) throws MessagingException { - open(OpenMode.READ_WRITE); - mUnreadMessageCount = Math.max(0, unreadMessageCount); - mDb.execSQL("UPDATE folders SET unread_count = ? WHERE id = ?", - new Object[] { mUnreadMessageCount, mFolderId }); + try + { + execute(false, new DbCallback() + { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException + { + try + { + open(OpenMode.READ_WRITE); + } + catch (MessagingException e) + { + throw new WrappedException(e); + } + mUnreadMessageCount = Math.max(0, unreadMessageCount); + db.execSQL("UPDATE folders SET unread_count = ? WHERE id = ?", + new Object[] { mUnreadMessageCount, mFolderId }); + return null; + } + }); + } + catch (WrappedException e) + { + throw (MessagingException) e.getCause(); + } } - public void setFlaggedMessageCount(int flaggedMessageCount) throws MessagingException + public void setFlaggedMessageCount(final int flaggedMessageCount) throws MessagingException { - open(OpenMode.READ_WRITE); - mFlaggedMessageCount = Math.max(0, flaggedMessageCount); - mDb.execSQL("UPDATE folders SET flagged_count = ? WHERE id = ?", - new Object[] { mFlaggedMessageCount, mFolderId }); + try + { + execute(false, new DbCallback() + { + @Override + public Integer doDbWork(final SQLiteDatabase db) throws WrappedException + { + try + { + open(OpenMode.READ_WRITE); + } + catch (MessagingException e) + { + throw new WrappedException(e); + } + mFlaggedMessageCount = Math.max(0, flaggedMessageCount); + db.execSQL("UPDATE folders SET flagged_count = ? WHERE id = ?", new Object[] + { mFlaggedMessageCount, mFolderId }); + return null; + } + }); + } + catch (WrappedException e) + { + throw (MessagingException) e.getCause(); + } } @Override - public void setLastChecked(long lastChecked) throws MessagingException + public void setLastChecked(final long lastChecked) throws MessagingException { - open(OpenMode.READ_WRITE); - super.setLastChecked(lastChecked); - mDb.execSQL("UPDATE folders SET last_updated = ? WHERE id = ?", - new Object[] { lastChecked, mFolderId }); + try + { + execute(false, new DbCallback() + { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException + { + try + { + open(OpenMode.READ_WRITE); + LocalFolder.super.setLastChecked(lastChecked); + } + catch (MessagingException e) + { + throw new WrappedException(e); + } + db.execSQL("UPDATE folders SET last_updated = ? WHERE id = ?", new Object[] + { lastChecked, mFolderId }); + return null; + } + }); + } + catch (WrappedException e) + { + throw (MessagingException) e.getCause(); + } } @Override - public void setLastPush(long lastChecked) throws MessagingException + public void setLastPush(final long lastChecked) throws MessagingException { - open(OpenMode.READ_WRITE); - super.setLastPush(lastChecked); - mDb.execSQL("UPDATE folders SET last_pushed = ? WHERE id = ?", - new Object[] { lastChecked, mFolderId }); + try + { + execute(false, new DbCallback() + { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException + { + try + { + open(OpenMode.READ_WRITE); + LocalFolder.super.setLastPush(lastChecked); + } + catch (MessagingException e) + { + throw new WrappedException(e); + } + db.execSQL("UPDATE folders SET last_pushed = ? WHERE id = ?", new Object[] + { lastChecked, mFolderId }); + return null; + } + }); + } + catch (WrappedException e) + { + throw (MessagingException) e.getCause(); + } } public int getVisibleLimit() throws MessagingException @@ -1168,28 +2112,87 @@ public class LocalStore extends Store implements Serializable } - public void setVisibleLimit(int visibleLimit) throws MessagingException + public void setVisibleLimit(final int visibleLimit) throws MessagingException { - open(OpenMode.READ_WRITE); - mVisibleLimit = visibleLimit; - mDb.execSQL("UPDATE folders SET visible_limit = ? WHERE id = ?", - new Object[] { mVisibleLimit, mFolderId }); + execute(false, new DbCallback() + { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException + { + try + { + open(OpenMode.READ_WRITE); + } + catch (MessagingException e) + { + throw new WrappedException(e); + } + mVisibleLimit = visibleLimit; + db.execSQL("UPDATE folders SET visible_limit = ? WHERE id = ?", + new Object[] { mVisibleLimit, mFolderId }); + return null; + } + }); } @Override - public void setStatus(String status) throws MessagingException + public void setStatus(final String status) throws MessagingException { - open(OpenMode.READ_WRITE); - super.setStatus(status); - mDb.execSQL("UPDATE folders SET status = ? WHERE id = ?", - new Object[] { status, mFolderId }); + try + { + execute(false, new DbCallback() + { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException + { + try + { + open(OpenMode.READ_WRITE); + LocalFolder.super.setStatus(status); + } + catch (MessagingException e) + { + throw new WrappedException(e); + } + db.execSQL("UPDATE folders SET status = ? WHERE id = ?", new Object[] + { status, mFolderId }); + return null; + } + }); + } + catch (WrappedException e) + { + throw (MessagingException) e.getCause(); + } } - public void setPushState(String pushState) throws MessagingException + public void setPushState(final String pushState) throws MessagingException { - open(OpenMode.READ_WRITE); - mPushState = pushState; - mDb.execSQL("UPDATE folders SET push_state = ? WHERE id = ?", - new Object[] { pushState, mFolderId }); + try + { + execute(false, new DbCallback() + { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException + { + try + { + open(OpenMode.READ_WRITE); + } + catch (MessagingException e) + { + throw new WrappedException(e); + } + mPushState = pushState; + db.execSQL("UPDATE folders SET push_state = ? WHERE id = ?", new Object[] + { pushState, mFolderId }); + return null; + } + }); + } + catch (WrappedException e) + { + throw (MessagingException) e.getCause(); + } } public String getPushState() { @@ -1412,132 +2415,154 @@ public class LocalStore extends Store implements Serializable } @Override - public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener) + public void fetch(final Message[] messages, final FetchProfile fp, final MessageRetrievalListener listener) throws MessagingException { - open(OpenMode.READ_WRITE); - if (fp.contains(FetchProfile.Item.BODY)) + try { - for (Message message : messages) + execute(false, new DbCallback() { - LocalMessage localMessage = (LocalMessage)message; - Cursor cursor = null; - MimeMultipart mp = new MimeMultipart(); - mp.setSubType("mixed"); - try + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException { - cursor = mDb.rawQuery("SELECT html_content, text_content FROM messages " - + "WHERE id = ?", - new String[] { Long.toString(localMessage.mId) }); - cursor.moveToNext(); - String htmlContent = cursor.getString(0); - String textContent = cursor.getString(1); - - if (textContent != null) + try { - LocalTextBody body = new LocalTextBody(textContent, htmlContent); - MimeBodyPart bp = new MimeBodyPart(body, "text/plain"); - mp.addBodyPart(bp); - } - else - { - TextBody body = new TextBody(htmlContent); - MimeBodyPart bp = new MimeBodyPart(body, "text/html"); - mp.addBodyPart(bp); - } - } - finally - { - if (cursor != null) - { - cursor.close(); - } - } - - try - { - cursor = mDb.query( - "attachments", - new String[] - { - "id", - "size", - "name", - "mime_type", - "store_data", - "content_uri", - "content_id", - "content_disposition" - }, - "message_id = ?", - new String[] { Long.toString(localMessage.mId) }, - null, - null, - null); - - while (cursor.moveToNext()) - { - long id = cursor.getLong(0); - int size = cursor.getInt(1); - String name = cursor.getString(2); - String type = cursor.getString(3); - String storeData = cursor.getString(4); - String contentUri = cursor.getString(5); - String contentId = cursor.getString(6); - String contentDisposition = cursor.getString(7); - Body body = null; - - if (contentDisposition == null) + open(OpenMode.READ_WRITE); + if (fp.contains(FetchProfile.Item.BODY)) { - contentDisposition = "attachment"; + for (Message message : messages) + { + LocalMessage localMessage = (LocalMessage)message; + Cursor cursor = null; + MimeMultipart mp = new MimeMultipart(); + mp.setSubType("mixed"); + try + { + cursor = db.rawQuery("SELECT html_content, text_content FROM messages " + + "WHERE id = ?", + new String[] { Long.toString(localMessage.mId) }); + cursor.moveToNext(); + String htmlContent = cursor.getString(0); + String textContent = cursor.getString(1); + + if (textContent != null) + { + LocalTextBody body = new LocalTextBody(textContent, htmlContent); + MimeBodyPart bp = new MimeBodyPart(body, "text/plain"); + mp.addBodyPart(bp); + } + else + { + TextBody body = new TextBody(htmlContent); + MimeBodyPart bp = new MimeBodyPart(body, "text/html"); + mp.addBodyPart(bp); + } + } + finally + { + if (cursor != null) + { + cursor.close(); + } + } + + try + { + cursor = db.query( + "attachments", + new String[] + { + "id", + "size", + "name", + "mime_type", + "store_data", + "content_uri", + "content_id", + "content_disposition" + }, + "message_id = ?", + new String[] { Long.toString(localMessage.mId) }, + null, + null, + null); + + while (cursor.moveToNext()) + { + long id = cursor.getLong(0); + int size = cursor.getInt(1); + String name = cursor.getString(2); + String type = cursor.getString(3); + String storeData = cursor.getString(4); + String contentUri = cursor.getString(5); + String contentId = cursor.getString(6); + String contentDisposition = cursor.getString(7); + Body body = null; + + if (contentDisposition == null) + { + contentDisposition = "attachment"; + } + + if (contentUri != null) + { + body = new LocalAttachmentBody(Uri.parse(contentUri), mApplication); + } + MimeBodyPart bp = new LocalAttachmentBodyPart(body, id); + bp.setHeader(MimeHeader.HEADER_CONTENT_TYPE, + String.format("%s;\n name=\"%s\"", + type, + name)); + bp.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64"); + bp.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, + String.format("%s;\n filename=\"%s\";\n size=%d", + contentDisposition, + name, + size)); + + bp.setHeader(MimeHeader.HEADER_CONTENT_ID, contentId); + /* + * HEADER_ANDROID_ATTACHMENT_STORE_DATA is a custom header we add to that + * we can later pull the attachment from the remote store if neccesary. + */ + bp.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, storeData); + + mp.addBodyPart(bp); + } + } + finally + { + if (cursor != null) + { + cursor.close(); + } + } + + if (mp.getCount() == 1) + { + BodyPart part = mp.getBodyPart(0); + localMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, part.getContentType()); + localMessage.setBody(part.getBody()); + } + else + { + localMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "multipart/mixed"); + localMessage.setBody(mp); + } + } } - - if (contentUri != null) - { - body = new LocalAttachmentBody(Uri.parse(contentUri), mApplication); - } - MimeBodyPart bp = new LocalAttachmentBodyPart(body, id); - bp.setHeader(MimeHeader.HEADER_CONTENT_TYPE, - String.format("%s;\n name=\"%s\"", - type, - name)); - bp.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64"); - bp.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, - String.format("%s;\n filename=\"%s\";\n size=%d", - contentDisposition, - name, - size)); - - bp.setHeader(MimeHeader.HEADER_CONTENT_ID, contentId); - /* - * HEADER_ANDROID_ATTACHMENT_STORE_DATA is a custom header we add to that - * we can later pull the attachment from the remote store if neccesary. - */ - bp.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, storeData); - - mp.addBodyPart(bp); } - } - finally - { - if (cursor != null) + catch (MessagingException e) { - cursor.close(); + throw new WrappedException(e); } + return null; } - - if (mp.getCount() == 1) - { - BodyPart part = mp.getBodyPart(0); - localMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, part.getContentType()); - localMessage.setBody(part.getBody()); - } - else - { - localMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "multipart/mixed"); - localMessage.setBody(mp); - } - } + }); + } + catch (WrappedException e) + { + throw (MessagingException) e.getCause(); } } @@ -1556,88 +2581,118 @@ public class LocalStore extends Store implements Serializable * * @param messages * The messages whose headers should be loaded. + * @throws UnavailableStorageException */ - private void populateHeaders(List messages) + private void populateHeaders(final List messages) throws UnavailableStorageException { - Cursor cursor = null; - if (messages.size() == 0) + execute(false, new DbCallback() { - return; - } - try - { - Map popMessages = new HashMap(); - List ids = new ArrayList(); - StringBuffer questions = new StringBuffer(); - - for (int i = 0; i < messages.size(); i++) + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { - if (i != 0) + Cursor cursor = null; + if (messages.size() == 0) { - questions.append(", "); + return null; } - questions.append("?"); - LocalMessage message = messages.get(i); - Long id = message.getId(); - ids.add(Long.toString(id)); - popMessages.put(id, message); + try + { + Map popMessages = new HashMap(); + List ids = new ArrayList(); + StringBuffer questions = new StringBuffer(); + for (int i = 0; i < messages.size(); i++) + { + if (i != 0) + { + questions.append(", "); + } + questions.append("?"); + LocalMessage message = messages.get(i); + Long id = message.getId(); + ids.add(Long.toString(id)); + popMessages.put(id, message); + + } + + cursor = db.rawQuery( + "SELECT message_id, name, value FROM headers " + "WHERE message_id in ( " + questions + ") ", + ids.toArray(EMPTY_STRING_ARRAY)); + + + while (cursor.moveToNext()) + { + Long id = cursor.getLong(0); + String name = cursor.getString(1); + String value = cursor.getString(2); + //Log.i(K9.LOG_TAG, "Retrieved header name= " + name + ", value = " + value + " for message " + id); + popMessages.get(id).addHeader(name, value); + } + } + finally + { + if (cursor != null) + { + cursor.close(); + } + } + return null; } - - cursor = mDb.rawQuery( - "SELECT message_id, name, value FROM headers " + "WHERE message_id in ( " + questions + ") ", - ids.toArray(EMPTY_STRING_ARRAY)); - - - while (cursor.moveToNext()) - { - Long id = cursor.getLong(0); - String name = cursor.getString(1); - String value = cursor.getString(2); - //Log.i(K9.LOG_TAG, "Retrieved header name= " + name + ", value = " + value + " for message " + id); - popMessages.get(id).addHeader(name, value); - } - } - finally - { - if (cursor != null) - { - cursor.close(); - } - } + }); } @Override - public Message getMessage(String uid) throws MessagingException + public Message getMessage(final String uid) throws MessagingException { - open(OpenMode.READ_WRITE); - LocalMessage message = new LocalMessage(uid, this); - Cursor cursor = null; - try { - cursor = mDb.rawQuery( - "SELECT " - + GET_MESSAGES_COLS - + "FROM messages WHERE uid = ? AND folder_id = ?", - new String[] - { - message.getUid(), Long.toString(mFolderId) - }); - if (!cursor.moveToNext()) + return execute(false, new DbCallback() { - return null; - } - message.populateFromGetMessageCursor(cursor); + @Override + public Message doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException + { + try + { + open(OpenMode.READ_WRITE); + LocalMessage message = new LocalMessage(uid, LocalFolder.this); + Cursor cursor = null; + + try + { + cursor = db.rawQuery( + "SELECT " + + GET_MESSAGES_COLS + + "FROM messages WHERE uid = ? AND folder_id = ?", + new String[] + { + message.getUid(), Long.toString(mFolderId) + }); + if (!cursor.moveToNext()) + { + return null; + } + message.populateFromGetMessageCursor(cursor); + } + finally + { + if (cursor != null) + { + cursor.close(); + } + } + return message; + } + catch (MessagingException e) + { + throw new WrappedException(e); + } + } + }); } - finally + catch (WrappedException e) { - if (cursor != null) - { - cursor.close(); - } + throw (MessagingException) e.getCause(); } - return message; } @Override @@ -1647,22 +2702,42 @@ public class LocalStore extends Store implements Serializable } @Override - public Message[] getMessages(MessageRetrievalListener listener, boolean includeDeleted) throws MessagingException + public Message[] getMessages(final MessageRetrievalListener listener, final boolean includeDeleted) throws MessagingException { - open(OpenMode.READ_WRITE); - return LocalStore.this.getMessages( - listener, - this, - "SELECT " + GET_MESSAGES_COLS - + "FROM messages WHERE " - + (includeDeleted ? "" : "deleted = 0 AND ") - + " folder_id = ? ORDER BY date DESC" - , new String[] - { - Long.toString(mFolderId) - } - ); - + try + { + return execute(false, new DbCallback() + { + @Override + public Message[] doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException + { + try + { + open(OpenMode.READ_WRITE); + return LocalStore.this.getMessages( + listener, + LocalFolder.this, + "SELECT " + GET_MESSAGES_COLS + + "FROM messages WHERE " + + (includeDeleted ? "" : "deleted = 0 AND ") + + " folder_id = ? ORDER BY date DESC" + , new String[] + { + Long.toString(mFolderId) + } + ); + } + catch (MessagingException e) + { + throw new WrappedException(e); + } + } + }); + } + catch (WrappedException e) + { + throw (MessagingException) e.getCause(); + } } @@ -1698,50 +2773,73 @@ public class LocalStore extends Store implements Serializable } @Override - public void moveMessages(Message[] msgs, Folder destFolder) throws MessagingException + public void moveMessages(final Message[] msgs, final Folder destFolder) throws MessagingException { if (!(destFolder instanceof LocalFolder)) { throw new MessagingException("moveMessages called with non-LocalFolder"); } - LocalFolder lDestFolder = (LocalFolder)destFolder; - lDestFolder.open(OpenMode.READ_WRITE); - for (Message message : msgs) + final LocalFolder lDestFolder = (LocalFolder)destFolder; + + try { - LocalMessage lMessage = (LocalMessage)message; - - if (!message.isSet(Flag.SEEN)) + execute(false, new DbCallback() { - setUnreadMessageCount(getUnreadMessageCount() - 1); - lDestFolder.setUnreadMessageCount(lDestFolder.getUnreadMessageCount() + 1); - } - - if (message.isSet(Flag.FLAGGED)) - { - setFlaggedMessageCount(getFlaggedMessageCount() - 1); - lDestFolder.setFlaggedMessageCount(lDestFolder.getFlaggedMessageCount() + 1); - } - - String oldUID = message.getUid(); - - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "Updating folder_id to " + lDestFolder.getId() + " for message with UID " - + message.getUid() + ", id " + lMessage.getId() + " currently in folder " + getName()); - - message.setUid(K9.LOCAL_UID_PREFIX + UUID.randomUUID().toString()); - - mDb.execSQL("UPDATE messages " + "SET folder_id = ?, uid = ? " + "WHERE id = ?", new Object[] + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException + { + try + { + lDestFolder.open(OpenMode.READ_WRITE); + for (Message message : msgs) { - lDestFolder.getId(), - message.getUid(), - lMessage.getId() - }); + LocalMessage lMessage = (LocalMessage)message; - LocalMessage placeHolder = new LocalMessage(oldUID, this); - placeHolder.setFlagInternal(Flag.DELETED, true); - placeHolder.setFlagInternal(Flag.SEEN, true); - appendMessages(new Message[] { placeHolder }); + if (!message.isSet(Flag.SEEN)) + { + setUnreadMessageCount(getUnreadMessageCount() - 1); + lDestFolder.setUnreadMessageCount(lDestFolder.getUnreadMessageCount() + 1); + } + + if (message.isSet(Flag.FLAGGED)) + { + setFlaggedMessageCount(getFlaggedMessageCount() - 1); + lDestFolder.setFlaggedMessageCount(lDestFolder.getFlaggedMessageCount() + 1); + } + + String oldUID = message.getUid(); + + if (K9.DEBUG) + Log.d(K9.LOG_TAG, "Updating folder_id to " + lDestFolder.getId() + " for message with UID " + + message.getUid() + ", id " + lMessage.getId() + " currently in folder " + getName()); + + message.setUid(K9.LOCAL_UID_PREFIX + UUID.randomUUID().toString()); + + db.execSQL("UPDATE messages " + "SET folder_id = ?, uid = ? " + "WHERE id = ?", new Object[] + { + lDestFolder.getId(), + message.getUid(), + lMessage.getId() + }); + + LocalMessage placeHolder = new LocalMessage(oldUID, LocalFolder.this); + placeHolder.setFlagInternal(Flag.DELETED, true); + placeHolder.setFlagInternal(Flag.SEEN, true); + appendMessages(new Message[] { placeHolder }); + } + } + catch (MessagingException e) + { + throw new WrappedException(e); + } + return null; + } + }); + } + catch (WrappedException e) + { + throw (MessagingException) e.getCause(); } } @@ -1774,127 +2872,148 @@ public class LocalStore extends Store implements Serializable * fact, in most cases, they are not). Therefore, if you want to make local changes only to a * message, retrieve the appropriate local message instance first (if it already exists). */ - private void appendMessages(Message[] messages, boolean copy) throws MessagingException + private void appendMessages(final Message[] messages, final boolean copy) throws MessagingException { open(OpenMode.READ_WRITE); - for (Message message : messages) + try { - if (!(message instanceof MimeMessage)) + execute(true, new DbCallback() { - throw new Error("LocalStore can only store Messages that extend MimeMessage"); - } - - String uid = message.getUid(); - if (uid == null || copy) - { - uid = K9.LOCAL_UID_PREFIX + UUID.randomUUID().toString(); - if (!copy) + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { - message.setUid(uid); - } - } - else - { - Message oldMessage = getMessage(uid); - if (oldMessage != null && !oldMessage.isSet(Flag.SEEN)) - { - setUnreadMessageCount(getUnreadMessageCount() - 1); - } - if (oldMessage != null && oldMessage.isSet(Flag.FLAGGED)) - { - setFlaggedMessageCount(getFlaggedMessageCount() - 1); - } - /* - * The message may already exist in this Folder, so delete it first. - */ - deleteAttachments(message.getUid()); - mDb.execSQL("DELETE FROM messages WHERE folder_id = ? AND uid = ?", - new Object[] { mFolderId, message.getUid() }); - } - - ArrayList viewables = new ArrayList(); - ArrayList attachments = new ArrayList(); - MimeUtility.collectParts(message, viewables, attachments); - - StringBuffer sbHtml = new StringBuffer(); - StringBuffer sbText = new StringBuffer(); - for (Part viewable : viewables) - { - try - { - String text = MimeUtility.getTextFromPart(viewable); - /* - * Anything with MIME type text/html will be stored as such. Anything - * else will be stored as text/plain. - */ - if (viewable.getMimeType().equalsIgnoreCase("text/html")) + try { - sbHtml.append(text); + for (Message message : messages) + { + if (!(message instanceof MimeMessage)) + { + throw new Error("LocalStore can only store Messages that extend MimeMessage"); + } + + String uid = message.getUid(); + if (uid == null || copy) + { + uid = K9.LOCAL_UID_PREFIX + UUID.randomUUID().toString(); + if (!copy) + { + message.setUid(uid); + } + } + else + { + Message oldMessage = getMessage(uid); + if (oldMessage != null && !oldMessage.isSet(Flag.SEEN)) + { + setUnreadMessageCount(getUnreadMessageCount() - 1); + } + if (oldMessage != null && oldMessage.isSet(Flag.FLAGGED)) + { + setFlaggedMessageCount(getFlaggedMessageCount() - 1); + } + /* + * The message may already exist in this Folder, so delete it first. + */ + deleteAttachments(message.getUid()); + db.execSQL("DELETE FROM messages WHERE folder_id = ? AND uid = ?", + new Object[] + { mFolderId, message.getUid() }); + } + + ArrayList viewables = new ArrayList(); + ArrayList attachments = new ArrayList(); + MimeUtility.collectParts(message, viewables, attachments); + + StringBuffer sbHtml = new StringBuffer(); + StringBuffer sbText = new StringBuffer(); + for (Part viewable : viewables) + { + try + { + String text = MimeUtility.getTextFromPart(viewable); + /* + * Anything with MIME type text/html will be stored as such. Anything + * else will be stored as text/plain. + */ + if (viewable.getMimeType().equalsIgnoreCase("text/html")) + { + sbHtml.append(text); + } + else + { + sbText.append(text); + } + } + catch (Exception e) + { + throw new MessagingException("Unable to get text for message part", e); + } + } + + String text = sbText.toString(); + String html = markupContent(text, sbHtml.toString()); + String preview = calculateContentPreview(text); + + try + { + ContentValues cv = new ContentValues(); + cv.put("uid", uid); + cv.put("subject", message.getSubject()); + cv.put("sender_list", Address.pack(message.getFrom())); + cv.put("date", message.getSentDate() == null + ? System.currentTimeMillis() : message.getSentDate().getTime()); + cv.put("flags", Utility.combine(message.getFlags(), ',').toUpperCase()); + cv.put("deleted", message.isSet(Flag.DELETED) ? 1 : 0); + cv.put("folder_id", mFolderId); + cv.put("to_list", Address.pack(message.getRecipients(RecipientType.TO))); + cv.put("cc_list", Address.pack(message.getRecipients(RecipientType.CC))); + cv.put("bcc_list", Address.pack(message.getRecipients(RecipientType.BCC))); + cv.put("html_content", html.length() > 0 ? html : null); + cv.put("text_content", text.length() > 0 ? text : null); + cv.put("preview", preview.length() > 0 ? preview : null); + cv.put("reply_to_list", Address.pack(message.getReplyTo())); + cv.put("attachment_count", attachments.size()); + cv.put("internal_date", message.getInternalDate() == null + ? System.currentTimeMillis() : message.getInternalDate().getTime()); + + String messageId = message.getMessageId(); + if (messageId != null) + { + cv.put("message_id", messageId); + } + long messageUid; + messageUid = db.insert("messages", "uid", cv); + for (Part attachment : attachments) + { + saveAttachment(messageUid, attachment, copy); + } + saveHeaders(messageUid, (MimeMessage)message); + if (!message.isSet(Flag.SEEN)) + { + setUnreadMessageCount(getUnreadMessageCount() + 1); + } + if (message.isSet(Flag.FLAGGED)) + { + setFlaggedMessageCount(getFlaggedMessageCount() + 1); + } + } + catch (Exception e) + { + throw new MessagingException("Error appending message", e); + } + } } - else + catch (MessagingException e) { - sbText.append(text); + throw new WrappedException(e); } + return null; } - catch (Exception e) - { - throw new MessagingException("Unable to get text for message part", e); - } - } - - String text = sbText.toString(); - String html = markupContent(text, sbHtml.toString()); - String preview = calculateContentPreview(text); - if (preview == null || preview.length() == 0) - { - preview = calculateContentPreview(Html.fromHtml(html).toString()); - } - try - { - ContentValues cv = new ContentValues(); - cv.put("uid", uid); - cv.put("subject", message.getSubject()); - cv.put("sender_list", Address.pack(message.getFrom())); - cv.put("date", message.getSentDate() == null - ? System.currentTimeMillis() : message.getSentDate().getTime()); - cv.put("flags", Utility.combine(message.getFlags(), ',').toUpperCase()); - cv.put("deleted", message.isSet(Flag.DELETED) ? 1 : 0); - cv.put("folder_id", mFolderId); - cv.put("to_list", Address.pack(message.getRecipients(RecipientType.TO))); - cv.put("cc_list", Address.pack(message.getRecipients(RecipientType.CC))); - cv.put("bcc_list", Address.pack(message.getRecipients(RecipientType.BCC))); - cv.put("html_content", html.length() > 0 ? html : null); - cv.put("text_content", text.length() > 0 ? text : null); - cv.put("preview", preview.length() > 0 ? preview : null); - cv.put("reply_to_list", Address.pack(message.getReplyTo())); - cv.put("attachment_count", attachments.size()); - cv.put("internal_date", message.getInternalDate() == null - ? System.currentTimeMillis() : message.getInternalDate().getTime()); - - String messageId = message.getMessageId(); - if (messageId != null) - { - cv.put("message_id", messageId); - } - long messageUid = mDb.insert("messages", "uid", cv); - for (Part attachment : attachments) - { - saveAttachment(messageUid, attachment, copy); - } - saveHeaders(messageUid, (MimeMessage)message); - if (!message.isSet(Flag.SEEN)) - { - setUnreadMessageCount(getUnreadMessageCount() + 1); - } - if (message.isSet(Flag.FLAGGED)) - { - setFlaggedMessageCount(getFlaggedMessageCount() + 1); - } - } - catch (Exception e) - { - throw new MessagingException("Error appending message", e); - } + }); + } + catch (WrappedException e) + { + throw (MessagingException) e.getCause(); } } @@ -1908,88 +3027,113 @@ public class LocalStore extends Store implements Serializable * @param message * @throws MessagingException */ - public void updateMessage(LocalMessage message) throws MessagingException + public void updateMessage(final LocalMessage message) throws MessagingException { open(OpenMode.READ_WRITE); - ArrayList viewables = new ArrayList(); - ArrayList attachments = new ArrayList(); - - message.buildMimeRepresentation(); - - MimeUtility.collectParts(message, viewables, attachments); - - StringBuffer sbHtml = new StringBuffer(); - StringBuffer sbText = new StringBuffer(); - for (int i = 0, count = viewables.size(); i < count; i++) - { - Part viewable = viewables.get(i); - try - { - String text = MimeUtility.getTextFromPart(viewable); - /* - * Anything with MIME type text/html will be stored as such. Anything - * else will be stored as text/plain. - */ - if (viewable.getMimeType().equalsIgnoreCase("text/html")) - { - sbHtml.append(text); - } - else - { - sbText.append(text); - } - } - catch (Exception e) - { - throw new MessagingException("Unable to get text for message part", e); - } - } - - String text = sbText.toString(); - String html = markupContent(text, sbHtml.toString()); - String preview = calculateContentPreview(text); - try { - mDb.execSQL("UPDATE messages SET " - + "uid = ?, subject = ?, sender_list = ?, date = ?, flags = ?, " - + "folder_id = ?, to_list = ?, cc_list = ?, bcc_list = ?, " - + "html_content = ?, text_content = ?, preview = ?, reply_to_list = ?, " - + "attachment_count = ? WHERE id = ?", - new Object[] - { - message.getUid(), - message.getSubject(), - Address.pack(message.getFrom()), - message.getSentDate() == null ? System - .currentTimeMillis() : message.getSentDate() - .getTime(), - Utility.combine(message.getFlags(), ',').toUpperCase(), - mFolderId, - Address.pack(message - .getRecipients(RecipientType.TO)), - Address.pack(message - .getRecipients(RecipientType.CC)), - Address.pack(message - .getRecipients(RecipientType.BCC)), - html.length() > 0 ? html : null, - text.length() > 0 ? text : null, - preview.length() > 0 ? preview : null, - Address.pack(message.getReplyTo()), - attachments.size(), - message.mId - }); - - for (int i = 0, count = attachments.size(); i < count; i++) + execute(false, new DbCallback() { - Part attachment = attachments.get(i); - saveAttachment(message.mId, attachment, false); - } - saveHeaders(message.getId(), message); + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException + { + try + { + ArrayList viewables = new ArrayList(); + ArrayList attachments = new ArrayList(); + + message.buildMimeRepresentation(); + + MimeUtility.collectParts(message, viewables, attachments); + + StringBuffer sbHtml = new StringBuffer(); + StringBuffer sbText = new StringBuffer(); + for (int i = 0, count = viewables.size(); i < count; i++) + { + Part viewable = viewables.get(i); + try + { + String text = MimeUtility.getTextFromPart(viewable); + /* + * Anything with MIME type text/html will be stored as such. Anything + * else will be stored as text/plain. + */ + if (viewable.getMimeType().equalsIgnoreCase("text/html")) + { + sbHtml.append(text); + } + else + { + sbText.append(text); + } + } + catch (Exception e) + { + throw new MessagingException("Unable to get text for message part", e); + } + } + + String text = sbText.toString(); + String html = markupContent(text, sbHtml.toString()); + String preview = calculateContentPreview(text); + if (preview == null || preview.length() == 0) + { + preview = calculateContentPreview(Html.fromHtml(html).toString()); + } + try + { + db.execSQL("UPDATE messages SET " + + "uid = ?, subject = ?, sender_list = ?, date = ?, flags = ?, " + + "folder_id = ?, to_list = ?, cc_list = ?, bcc_list = ?, " + + "html_content = ?, text_content = ?, preview = ?, reply_to_list = ?, " + + "attachment_count = ? WHERE id = ?", + new Object[] + { + message.getUid(), + message.getSubject(), + Address.pack(message.getFrom()), + message.getSentDate() == null ? System + .currentTimeMillis() : message.getSentDate() + .getTime(), + Utility.combine(message.getFlags(), ',').toUpperCase(), + mFolderId, + Address.pack(message + .getRecipients(RecipientType.TO)), + Address.pack(message + .getRecipients(RecipientType.CC)), + Address.pack(message + .getRecipients(RecipientType.BCC)), + html.length() > 0 ? html : null, + text.length() > 0 ? text : null, + preview.length() > 0 ? preview : null, + Address.pack(message.getReplyTo()), + attachments.size(), + message.mId + }); + + for (int i = 0, count = attachments.size(); i < count; i++) + { + Part attachment = attachments.get(i); + saveAttachment(message.mId, attachment, false); + } + saveHeaders(message.getId(), message); + } + catch (Exception e) + { + throw new MessagingException("Error appending message", e); + } + } + catch (MessagingException e) + { + throw new WrappedException(e); + } + return null; + } + }); } - catch (Exception e) + catch (WrappedException e) { - throw new MessagingException("Error appending message", e); + throw (MessagingException) e.getCause(); } } @@ -1997,52 +3141,66 @@ public class LocalStore extends Store implements Serializable * Save the headers of the given message. Note that the message is not * necessarily a {@link LocalMessage} instance. */ - private void saveHeaders(long id, MimeMessage message) throws MessagingException + private void saveHeaders(final long id, final MimeMessage message) throws MessagingException { - boolean saveAllHeaders = mAccount.saveAllHeaders(); - boolean gotAdditionalHeaders = false; - - deleteHeaders(id); - for (String name : message.getHeaderNames()) + execute(true, new DbCallback() { - if (saveAllHeaders || HEADERS_TO_SAVE.contains(name)) + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { - String[] values = message.getHeader(name); - for (String value : values) + boolean saveAllHeaders = mAccount.saveAllHeaders(); + boolean gotAdditionalHeaders = false; + + deleteHeaders(id); + for (String name : message.getHeaderNames()) { - ContentValues cv = new ContentValues(); - cv.put("message_id", id); - cv.put("name", name); - cv.put("value", value); - mDb.insert("headers", "name", cv); + if (saveAllHeaders || HEADERS_TO_SAVE.contains(name)) + { + String[] values = message.getHeader(name); + for (String value : values) + { + ContentValues cv = new ContentValues(); + cv.put("message_id", id); + cv.put("name", name); + cv.put("value", value); + db.insert("headers", "name", cv); + } + } + else + { + gotAdditionalHeaders = true; + } } - } - else - { - gotAdditionalHeaders = true; - } - } - if (!gotAdditionalHeaders) - { - // Remember that all headers for this message have been saved, so it is - // not necessary to download them again in case the user wants to see all headers. - List appendedFlags = new ArrayList(); - appendedFlags.addAll(Arrays.asList(message.getFlags())); - appendedFlags.add(Flag.X_GOT_ALL_HEADERS); + if (!gotAdditionalHeaders) + { + // Remember that all headers for this message have been saved, so it is + // not necessary to download them again in case the user wants to see all headers. + List appendedFlags = new ArrayList(); + appendedFlags.addAll(Arrays.asList(message.getFlags())); + appendedFlags.add(Flag.X_GOT_ALL_HEADERS); - mDb.execSQL("UPDATE messages " + "SET flags = ? " + " WHERE id = ?", new Object[] - { Utility.combine(appendedFlags.toArray(), ',').toUpperCase(), id }); - } + db.execSQL("UPDATE messages " + "SET flags = ? " + " WHERE id = ?", + new Object[] + { Utility.combine(appendedFlags.toArray(), ',').toUpperCase(), id }); + } + return null; + } + }); } - private void deleteHeaders(long id) + private void deleteHeaders(final long id) throws UnavailableStorageException { - mDb.execSQL("DELETE FROM headers WHERE message_id = ?", - new Object[] - { - id - }); + execute(false, new DbCallback() + { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException + { + db.execSQL("DELETE FROM headers WHERE message_id = ?", new Object[] + { id }); + return null; + } + }); } /** @@ -2052,149 +3210,182 @@ public class LocalStore extends Store implements Serializable * @throws IOException * @throws MessagingException */ - private void saveAttachment(long messageId, Part attachment, boolean saveAsNew) + private void saveAttachment(final long messageId, final Part attachment, final boolean saveAsNew) throws IOException, MessagingException { - long attachmentId = -1; - Uri contentUri = null; - int size = -1; - File tempAttachmentFile = null; - - if ((!saveAsNew) && (attachment instanceof LocalAttachmentBodyPart)) + try { - attachmentId = ((LocalAttachmentBodyPart) attachment).getAttachmentId(); - } - - if (attachment.getBody() != null) - { - Body body = attachment.getBody(); - if (body instanceof LocalAttachmentBody) + execute(true, new DbCallback() { - contentUri = ((LocalAttachmentBody) body).getContentUri(); + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException + { + try + { + long attachmentId = -1; + Uri contentUri = null; + int size = -1; + File tempAttachmentFile = null; + + if ((!saveAsNew) && (attachment instanceof LocalAttachmentBodyPart)) + { + attachmentId = ((LocalAttachmentBodyPart) attachment).getAttachmentId(); + } + + final File attachmentDirectory = StorageManager.getInstance(mApplication).getAttachmentDirectory(uUid, mStorageProviderId); + if (attachment.getBody() != null) + { + Body body = attachment.getBody(); + if (body instanceof LocalAttachmentBody) + { + contentUri = ((LocalAttachmentBody) body).getContentUri(); + } + else + { + /* + * If the attachment has a body we're expected to save it into the local store + * so we copy the data into a cached attachment file. + */ + InputStream in = attachment.getBody().getInputStream(); + tempAttachmentFile = File.createTempFile("att", null, attachmentDirectory); + FileOutputStream out = new FileOutputStream(tempAttachmentFile); + size = IOUtils.copy(in, out); + in.close(); + out.close(); + } + } + + if (size == -1) + { + /* + * If the attachment is not yet downloaded see if we can pull a size + * off the Content-Disposition. + */ + String disposition = attachment.getDisposition(); + if (disposition != null) + { + String s = MimeUtility.getHeaderParameter(disposition, "size"); + if (s != null) + { + size = Integer.parseInt(s); + } + } + } + if (size == -1) + { + size = 0; + } + + String storeData = + Utility.combine(attachment.getHeader( + MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA), ','); + + String name = MimeUtility.getHeaderParameter(attachment.getContentType(), "name"); + String contentId = MimeUtility.getHeaderParameter(attachment.getContentId(), null); + + String contentDisposition = MimeUtility.unfoldAndDecode(attachment.getDisposition()); + if (name == null && contentDisposition != null) + { + name = MimeUtility.getHeaderParameter(contentDisposition, "filename"); + } + if (attachmentId == -1) + { + ContentValues cv = new ContentValues(); + cv.put("message_id", messageId); + cv.put("content_uri", contentUri != null ? contentUri.toString() : null); + cv.put("store_data", storeData); + cv.put("size", size); + cv.put("name", name); + cv.put("mime_type", attachment.getMimeType()); + cv.put("content_id", contentId); + cv.put("content_disposition", contentDisposition); + + attachmentId = db.insert("attachments", "message_id", cv); + } + else + { + ContentValues cv = new ContentValues(); + cv.put("content_uri", contentUri != null ? contentUri.toString() : null); + cv.put("size", size); + db.update("attachments", cv, "id = ?", new String[] + { Long.toString(attachmentId) }); + } + + if (attachmentId != -1 && tempAttachmentFile != null) + { + File attachmentFile = new File(attachmentDirectory, Long.toString(attachmentId)); + tempAttachmentFile.renameTo(attachmentFile); + contentUri = AttachmentProvider.getAttachmentUri( + mAccount, + attachmentId); + attachment.setBody(new LocalAttachmentBody(contentUri, mApplication)); + ContentValues cv = new ContentValues(); + cv.put("content_uri", contentUri != null ? contentUri.toString() : null); + db.update("attachments", cv, "id = ?", new String[] + { Long.toString(attachmentId) }); + } + + /* The message has attachment with Content-ID */ + if (contentId != null && contentUri != null) + { + Cursor cursor = null; + cursor = db.query("messages", new String[] + { "html_content" }, "id = ?", new String[] + { Long.toString(messageId) }, null, null, null); + try + { + if (cursor.moveToNext()) + { + String new_html; + + new_html = cursor.getString(0); + new_html = new_html.replaceAll("cid:" + contentId, + contentUri.toString()); + + ContentValues cv = new ContentValues(); + cv.put("html_content", new_html); + db.update("messages", cv, "id = ?", new String[] + { Long.toString(messageId) }); + } + } + finally + { + if (cursor != null) + { + cursor.close(); + } + } + } + + if (attachmentId != -1 && attachment instanceof LocalAttachmentBodyPart) + { + ((LocalAttachmentBodyPart) attachment).setAttachmentId(attachmentId); + } + return null; + } + catch (MessagingException e) + { + throw new WrappedException(e); + } + catch (IOException e) + { + throw new WrappedException(e); + } + } + }); + } + catch (WrappedException e) + { + final Throwable cause = e.getCause(); + if (cause instanceof IOException) + { + throw (IOException) cause; } else { - /* - * If the attachment has a body we're expected to save it into the local store - * so we copy the data into a cached attachment file. - */ - InputStream in = attachment.getBody().getInputStream(); - tempAttachmentFile = File.createTempFile("att", null, mAttachmentsDir); - FileOutputStream out = new FileOutputStream(tempAttachmentFile); - size = IOUtils.copy(in, out); - in.close(); - out.close(); + throw (MessagingException) cause; } } - - if (size == -1) - { - /* - * If the attachment is not yet downloaded see if we can pull a size - * off the Content-Disposition. - */ - String disposition = attachment.getDisposition(); - if (disposition != null) - { - String s = MimeUtility.getHeaderParameter(disposition, "size"); - if (s != null) - { - size = Integer.parseInt(s); - } - } - } - if (size == -1) - { - size = 0; - } - - String storeData = - Utility.combine(attachment.getHeader( - MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA), ','); - - String name = MimeUtility.getHeaderParameter(attachment.getContentType(), "name"); - String contentId = MimeUtility.getHeaderParameter(attachment.getContentId(), null); - - String contentDisposition = MimeUtility.unfoldAndDecode(attachment.getDisposition()); - if (name == null && contentDisposition != null) - { - name = MimeUtility.getHeaderParameter(contentDisposition, "filename"); - } - if (attachmentId == -1) - { - ContentValues cv = new ContentValues(); - cv.put("message_id", messageId); - cv.put("content_uri", contentUri != null ? contentUri.toString() : null); - cv.put("store_data", storeData); - cv.put("size", size); - cv.put("name", name); - cv.put("mime_type", attachment.getMimeType()); - cv.put("content_id", contentId); - cv.put("content_disposition", contentDisposition); - - attachmentId = mDb.insert("attachments", "message_id", cv); - } - else - { - ContentValues cv = new ContentValues(); - cv.put("content_uri", contentUri != null ? contentUri.toString() : null); - cv.put("size", size); - mDb.update( - "attachments", - cv, - "id = ?", - new String[] { Long.toString(attachmentId) }); - } - - if (attachmentId != -1 && tempAttachmentFile != null) - { - File attachmentFile = new File(mAttachmentsDir, Long.toString(attachmentId)); - tempAttachmentFile.renameTo(attachmentFile); - contentUri = AttachmentProvider.getAttachmentUri( - new File(mPath).getName(), - attachmentId); - attachment.setBody(new LocalAttachmentBody(contentUri, mApplication)); - ContentValues cv = new ContentValues(); - cv.put("content_uri", contentUri != null ? contentUri.toString() : null); - mDb.update( - "attachments", - cv, - "id = ?", - new String[] { Long.toString(attachmentId) }); - } - - /* The message has attachment with Content-ID */ - if (contentId != null && contentUri != null) - { - Cursor cursor = null; - cursor = mDb.query("messages", new String[] { "html_content" }, "id = ?", new String[] { Long.toString(messageId) }, null, null, null); - try - { - if (cursor.moveToNext()) - { - String new_html; - - new_html = cursor.getString(0); - new_html = new_html.replaceAll("cid:" + contentId, contentUri.toString()); - - ContentValues cv = new ContentValues(); - cv.put("html_content", new_html); - mDb.update("messages", cv, "id = ?", new String[] { Long.toString(messageId) }); - } - } - finally - { - if (cursor != null) - { - cursor.close(); - } - } - } - - if (attachmentId != -1 && attachment instanceof LocalAttachmentBodyPart) - { - ((LocalAttachmentBodyPart) attachment).setAttachmentId(attachmentId); - } } /** @@ -2202,12 +3393,21 @@ public class LocalStore extends Store implements Serializable * the uid in the message. * @param message */ - public void changeUid(LocalMessage message) throws MessagingException + public void changeUid(final LocalMessage message) throws MessagingException { open(OpenMode.READ_WRITE); - ContentValues cv = new ContentValues(); + final ContentValues cv = new ContentValues(); cv.put("uid", message.getUid()); - mDb.update("messages", cv, "id = ?", new String[] { Long.toString(message.mId) }); + execute(false, new DbCallback() + { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException + { + db.update("messages", cv, "id = ?", new String[] + { Long.toString(message.mId) }); + return null; + } + }); } @Override @@ -2257,7 +3457,15 @@ public class LocalStore extends Store implements Serializable { deleteAttachments(message.getUid()); } - mDb.execSQL("DELETE FROM messages WHERE " + where, params); + execute(false, new DbCallback() + { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException + { + db.execSQL("DELETE FROM messages WHERE " + where, params); + return null; + } + }); resetUnreadAndFlaggedCounts(); } @@ -2290,19 +3498,39 @@ public class LocalStore extends Store implements Serializable @Override - public void delete(boolean recurse) throws MessagingException + public void delete(final boolean recurse) throws MessagingException { - // We need to open the folder first to make sure we've got it's id - open(OpenMode.READ_ONLY); - Message[] messages = getMessages(null); - for (Message message : messages) + try { - deleteAttachments(message.getUid()); - } - mDb.execSQL("DELETE FROM folders WHERE id = ?", new Object[] + execute(false, new DbCallback() + { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException + { + try { - Long.toString(mFolderId), - }); + // We need to open the folder first to make sure we've got it's id + open(OpenMode.READ_ONLY); + Message[] messages = getMessages(null); + for (Message message : messages) + { + deleteAttachments(message.getUid()); + } + } + catch (MessagingException e) + { + throw new WrappedException(e); + } + db.execSQL("DELETE FROM folders WHERE id = ?", new Object[] + { Long.toString(mFolderId), }); + return null; + } + }); + } + catch (WrappedException e) + { + throw (MessagingException) e.getCause(); + } } @Override @@ -2328,73 +3556,92 @@ public class LocalStore extends Store implements Serializable } - private void deleteAttachments(long messageId) throws MessagingException + private void deleteAttachments(final long messageId) throws MessagingException { open(OpenMode.READ_WRITE); - Cursor attachmentsCursor = null; - try + execute(false, new DbCallback() { - attachmentsCursor = mDb.query( - "attachments", - new String[] { "id" }, - "message_id = ?", - new String[] { Long.toString(messageId) }, - null, - null, - null); - while (attachmentsCursor.moveToNext()) + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { - long attachmentId = attachmentsCursor.getLong(0); + Cursor attachmentsCursor = null; try { - File file = new File(mAttachmentsDir, Long.toString(attachmentId)); - if (file.exists()) + attachmentsCursor = db.query("attachments", new String[] + { "id" }, "message_id = ?", new String[] + { Long.toString(messageId) }, null, null, null); + final File attachmentDirectory = StorageManager.getInstance(mApplication) + .getAttachmentDirectory(uUid, mStorageProviderId); + while (attachmentsCursor.moveToNext()) { - file.delete(); + long attachmentId = attachmentsCursor.getLong(0); + try + { + File file = new File(attachmentDirectory, Long.toString(attachmentId)); + if (file.exists()) + { + file.delete(); + } + } + catch (Exception e) + { + + } } } - catch (Exception e) + finally { - + if (attachmentsCursor != null) + { + attachmentsCursor.close(); + } } + return null; } - } - finally - { - if (attachmentsCursor != null) - { - attachmentsCursor.close(); - } - } + }); } - private void deleteAttachments(String uid) throws MessagingException + private void deleteAttachments(final String uid) throws MessagingException { open(OpenMode.READ_WRITE); - Cursor messagesCursor = null; try { - messagesCursor = mDb.query( - "messages", - new String[] { "id" }, - "folder_id = ? AND uid = ?", - new String[] { Long.toString(mFolderId), uid }, - null, - null, - null); - while (messagesCursor.moveToNext()) + execute(false, new DbCallback() { - long messageId = messagesCursor.getLong(0); - deleteAttachments(messageId); + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException + { + Cursor messagesCursor = null; + try + { + messagesCursor = db.query("messages", new String[] + { "id" }, "folder_id = ? AND uid = ?", new String[] + { Long.toString(mFolderId), uid }, null, null, null); + while (messagesCursor.moveToNext()) + { + long messageId = messagesCursor.getLong(0); + deleteAttachments(messageId); - } + } + } + catch (MessagingException e) + { + throw new WrappedException(e); + } + finally + { + if (messagesCursor != null) + { + messagesCursor.close(); + } + } + return null; + } + }); } - finally + catch (WrappedException e) { - if (messagesCursor != null) - { - messagesCursor.close(); - } + throw (MessagingException) e.getCause(); } } @@ -4869,25 +6116,45 @@ public class LocalStore extends Store implements Serializable } @Override - public void setFlag(Flag flag, boolean set) throws MessagingException + public void setFlag(final Flag flag, final boolean set) throws MessagingException { - if (flag == Flag.DELETED && set) + try { - delete(); - } - - updateFolderCountsOnFlag(flag, set); - - - super.setFlag(flag, set); - /* - * Set the flags on the message. - */ - mDb.execSQL("UPDATE messages " + "SET flags = ? " + " WHERE id = ?", new Object[] + execute(true, new DbCallback() + { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException + { + try { - Utility.combine(getFlags(), ',').toUpperCase(), mId - }); + if (flag == Flag.DELETED && set) + { + delete(); + } + + updateFolderCountsOnFlag(flag, set); + + + LocalMessage.super.setFlag(flag, set); + } + catch (MessagingException e) + { + throw new WrappedException(e); + } + /* + * Set the flags on the message. + */ + db.execSQL("UPDATE messages " + "SET flags = ? " + " WHERE id = ?", new Object[] + { Utility.combine(getFlags(), ',').toUpperCase(), mId }); + return null; + } + }); + } + catch (WrappedException e) + { + throw (MessagingException) e.getCause(); + } } @@ -4903,38 +6170,42 @@ public class LocalStore extends Store implements Serializable /* * Delete all of the message's content to save space. */ - - mDb.execSQL( - "UPDATE messages SET " + - "deleted = 1," + - "subject = NULL, " + - "sender_list = NULL, " + - "date = NULL, " + - "to_list = NULL, " + - "cc_list = NULL, " + - "bcc_list = NULL, " + - "preview = NULL, " + - "html_content = NULL, " + - "text_content = NULL, " + - "reply_to_list = NULL " + - "WHERE id = ?", - new Object[] + try + { + execute(true, new DbCallback() { - mId - }); - - /* - * Delete all of the message's attachments to save space. - * We do this explicit deletion here because we're not deleting the record - * in messages, which means our ON DELETE trigger for messages won't cascade - */ - ((LocalFolder)mFolder).deleteAttachments(mId); - mDb.execSQL("DELETE FROM attachments WHERE message_id = ?", - new Object[] + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException + { + db.execSQL("UPDATE messages SET " + "deleted = 1," + "subject = NULL, " + + "sender_list = NULL, " + "date = NULL, " + "to_list = NULL, " + + "cc_list = NULL, " + "bcc_list = NULL, " + "preview = NULL, " + + "html_content = NULL, " + "text_content = NULL, " + + "reply_to_list = NULL " + "WHERE id = ?", new Object[] + { mId }); + /* + * Delete all of the message's attachments to save space. + * We do this explicit deletion here because we're not deleting the record + * in messages, which means our ON DELETE trigger for messages won't cascade + */ + try { - mId - }); - + ((LocalFolder) mFolder).deleteAttachments(mId); + } + catch (MessagingException e) + { + throw new WrappedException(e); + } + db.execSQL("DELETE FROM attachments WHERE message_id = ?", new Object[] + { mId }); + return null; + } + }); + } + catch (WrappedException e) + { + throw (MessagingException) e.getCause(); + } ((LocalFolder)mFolder).deleteHeaders(mId); @@ -4946,9 +6217,32 @@ public class LocalStore extends Store implements Serializable @Override public void destroy() throws MessagingException { - ((LocalFolder) mFolder).deleteAttachments(mId); - mDb.execSQL("DELETE FROM messages WHERE id = ?", new Object[] { mId }); - updateFolderCountsOnFlag(Flag.X_DESTROYED, true); + try + { + execute(false, new DbCallback() + { + @Override + public Void doDbWork(final SQLiteDatabase db) throws WrappedException, + UnavailableStorageException + { + try + { + ((LocalFolder) mFolder).deleteAttachments(mId); + mDb.execSQL("DELETE FROM messages WHERE id = ?", new Object[] { mId }); + updateFolderCountsOnFlag(Flag.X_DESTROYED, true); + } + catch (MessagingException e) + { + throw new WrappedException(e); + } + return null; + } + }); + } + catch (WrappedException e) + { + throw (MessagingException) e.getCause(); + } } private void updateFolderCountsOnFlag(Flag flag, boolean set) throws MessagingException @@ -4993,7 +6287,7 @@ public class LocalStore extends Store implements Serializable } } - private void loadHeaders() + private void loadHeaders() throws UnavailableStorageException { ArrayList messages = new ArrayList(); messages.add(this); @@ -5003,7 +6297,7 @@ public class LocalStore extends Store implements Serializable } @Override - public void addHeader(String name, String value) + public void addHeader(String name, String value) throws UnavailableStorageException { if (!mHeadersLoaded) loadHeaders(); @@ -5011,7 +6305,7 @@ public class LocalStore extends Store implements Serializable } @Override - public void setHeader(String name, String value) + public void setHeader(String name, String value) throws UnavailableStorageException { if (!mHeadersLoaded) loadHeaders(); @@ -5019,7 +6313,7 @@ public class LocalStore extends Store implements Serializable } @Override - public String[] getHeader(String name) + public String[] getHeader(String name) throws UnavailableStorageException { if (!mHeadersLoaded) loadHeaders(); @@ -5027,7 +6321,7 @@ public class LocalStore extends Store implements Serializable } @Override - public void removeHeader(String name) + public void removeHeader(String name) throws UnavailableStorageException { if (!mHeadersLoaded) loadHeaders(); @@ -5035,7 +6329,7 @@ public class LocalStore extends Store implements Serializable } @Override - public Set getHeaderNames() + public Set getHeaderNames() throws UnavailableStorageException { if (!mHeadersLoaded) loadHeaders(); diff --git a/src/com/fsck/k9/mail/store/StorageManager.java b/src/com/fsck/k9/mail/store/StorageManager.java new file mode 100644 index 000000000..0d69333d5 --- /dev/null +++ b/src/com/fsck/k9/mail/store/StorageManager.java @@ -0,0 +1,830 @@ +package com.fsck.k9.mail.store; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import android.app.Application; +import android.content.Context; +import android.os.Build; +import android.os.Environment; +import android.util.Log; + +import com.fsck.k9.K9; +import com.fsck.k9.R; +import com.fsck.k9.service.MailService; + +/** + * Manager for different {@link StorageProvider} -classes that abstract access + * to sd-cards, additional internal memory and other storage-locations. + */ +public class StorageManager +{ + + /** + * Provides entry points (File objects) to an underlying storage, + * alleviating the caller from having to know where that storage is located. + * + *

+ * Allow checking for the denoted storage availability since its lifecycle + * can evolving (a storage might become unavailable at some time and be back + * online later). + *

+ */ + public static interface StorageProvider + { + + /** + * Retrieve the uniquely identifier for the current implementation. + * + *

+ * It is expected that the identifier doesn't change over reboots since + * it'll be used to save settings and retrieve the provider at a later + * time. + *

+ * + *

+ * The returned identifier doesn't have to be user friendly. + *

+ * + * @return Never null. + */ + String getId(); + + /** + * Hook point for provider initialization. + * + * @param context + * Never null. + */ + void init(Context context); + + /** + * @param context + * Never null. + * @return A user displayable, localized name for this provider. Never + * null. + */ + String getName(Context context); + + /** + * Some implementations may not be able to return valid File handles + * because the device doesn't provide the denoted storage. You can check + * the provider compatibility with this method to prevent from having to + * invoke this provider ever again. + * + * @param context + * TODO + * @return Whether this provider supports the current device. + * @see StorageManager#getAvailableProviders() + */ + boolean isSupported(Context context); + + /** + * Return the {@link File} to the choosen email database file. The + * resulting {@link File} doesn't necessarily match an existing file on + * the filesystem. + * + * @param context + * Never null. + * @param id + * Never null. + * @return Never null. + */ + File getDatabase(Context context, String id); + + /** + * Return the {@link File} to the choosen attachment directory. The + * resulting {@link File} doesn't necessarily match an existing + * directory on the filesystem. + * + * @param context + * Never null. + * @param id + * Never null. + * @return Never null. + */ + File getAttachmentDirectory(Context context, String id); + + /** + * Check for the underlying storage availability. + * + * @param context + * Never null. + * @return Whether the underlying storage returned by this provider is + * ready for read/write operations at the time of invokation. + */ + boolean isReady(Context context); + + /** + * Retrieve the root of the underlying storage. + * + * @param context + * Never null. + * @return The root directory of the denoted storage. Never + * null. + */ + File getRoot(Context context); + } + + /** + * Interface for components wanting to be notified of storage availability + * events. + */ + public static interface StorageListener + { + /** + * Invoked on storage mount (with read/write access). + * + * @param providerId + * Identifier (as returned by {@link StorageProvider#getId()} + * of the newly mounted storage. Never null. + */ + void onMount(String providerId); + + /** + * Invoked when a storage is about to be unmounted. + * + * @param providerId + * Identifier (as returned by {@link StorageProvider#getId()} + * of the to-be-unmounted storage. Never null. + */ + void onUnmount(String providerId); + } + + /** + * Base provider class for providers that rely on well-known path to check + * for storage availability. + * + *

+ * Since solely checking for paths can be unsafe, this class allows to check + * for device compatibility using {@link #supportsVendor()}. If the vendor + * specific check fails, the provider won't be able to provide any valid + * File handle, regardless of the path existence. + *

+ * + *

+ * Moreover, this class validates the denoted storage path matches against + * mount points using {@link StorageManager#isMountPoint(File)}. + *

+ */ + public abstract static class FixedStorageProviderBase implements StorageProvider + { + /** + * The root of the denoted storage. Used for mount points checking. + */ + protected File mRoot; + + /** + * Choosen base directory + */ + protected File mApplicationDir; + + @Override + public void init(final Context context) + { + mRoot = computeRoot(context); + // use /k9 + mApplicationDir = new File(mRoot, "k9"); + } + + /** + * Vendor specific checks + * + * @return Whether this provider supports the underlying vendor specific + * storage + */ + protected abstract boolean supportsVendor(); + + @Override + public boolean isReady(Context context) + { + try + { + final File root = mRoot.getCanonicalFile(); + return isMountPoint(root) + && Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()); + } + catch (IOException e) + { + Log.w(K9.LOG_TAG, "Specified root isn't ready: " + mRoot, e); + return false; + } + } + + @Override + public final boolean isSupported(Context context) + { + return mRoot.isDirectory() && supportsVendor(); + } + + @Override + public File getDatabase(Context context, String id) + { + return new File(mApplicationDir, id + ".db"); + } + + @Override + public File getAttachmentDirectory(Context context, String id) + { + return new File(mApplicationDir, id + ".db_att"); + } + + @Override + public final File getRoot(Context context) + { + return mRoot; + } + + /** + * Retrieve the well-known storage root directory from the actual + * implementation. + * + * @param context + * Never null. + * @return Never null. + */ + protected abstract File computeRoot(Context context); + } + + /** + * Strategy to access the always available internal storage. + * + *

+ * This implementation is expected to work on every device since it's based + * on the regular Android API {@link Context#getDatabasePath(String)} and + * uses the resul to retrieve the DB path and the attachment directory path. + *

+ * + *

+ * The underlying storage has always been used by K-9. + *

+ */ + public static class InternalStorageProvider implements StorageProvider + { + + public static final String ID = "InternalStorage"; + + protected File mRoot; + + @Override + public String getId() + { + return ID; + } + + @Override + public void init(Context context) + { + // XXX + mRoot = new File("/"); + } + + @Override + public String getName(Context context) + { + return context.getString(R.string.local_storage_provider_internal_label); + } + + @Override + public boolean isSupported(Context context) + { + return true; + } + + @Override + public File getDatabase(Context context, String id) + { + return context.getDatabasePath(id + ".db"); + } + + @Override + public File getAttachmentDirectory(Context context, String id) + { + // we store attachments in the database directory + return context.getDatabasePath(id + ".db_att"); + } + + @Override + public boolean isReady(Context context) + { + return true; + } + + @Override + public File getRoot(Context context) + { + return mRoot; + } + } + + /** + * Strategy for accessing the storage as returned by + * {@link Environment#getExternalStorageDirectory()}. In order to be + * compliant with Android recommendation regarding application uninstalling + * and to prevent from cluttering the storage root, the choosen directory + * will be + * <STORAGE_ROOT>/Android/data/<APPLICATION_PACKAGE_NAME>/files/ + * + *

+ * The denoted storage is usually a SD card. + *

+ * + *

+ * This provider is expected to work on all devices but the returned + * underlying storage might not be always available, due to + * mount/unmount/USB share events. + *

+ */ + public static class ExternalStorageProvider implements StorageProvider + { + + public static final String ID = "ExternalStorage"; + + /** + * Root of the denoted storage. + */ + protected File mRoot; + + /** + * Choosen base directory. + */ + protected File mApplicationDirectory; + + public String getId() + { + return ID; + } + + @Override + public void init(Context context) + { + mRoot = Environment.getExternalStorageDirectory(); + mApplicationDirectory = new File(new File(new File(new File(mRoot, "Android"), "data"), + context.getPackageName()), "files"); + } + + @Override + public String getName(Context context) + { + return context.getString(R.string.local_storage_provider_external_label); + } + + @Override + public boolean isSupported(Context context) + { + return true; + } + + @Override + public File getDatabase(Context context, String id) + { + return new File(mApplicationDirectory, id + ".db"); + } + + @Override + public File getAttachmentDirectory(Context context, String id) + { + return new File(mApplicationDirectory, id + ".db_att"); + } + + @Override + public boolean isReady(Context context) + { + return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()); + } + + @Override + public File getRoot(Context context) + { + return mRoot; + } + } + + /** + * Storage provider to allow access the /emmc directory on a HTC Incredible. + * + *

+ * This implementation is experimental and untested. + *

+ * + * See http://groups.google.com/group/android-developers/browse_frm/thread/96f15e57150ed173 + * + * @see FixedStorageProviderBase + */ + public static class HtcIncredibleStorageProvider extends FixedStorageProviderBase + { + + public static final String ID = "HtcIncredibleStorage"; + + public String getId() + { + return ID; + } + + @Override + public String getName(Context context) + { + return context.getString(R.string.local_storage_provider_samsunggalaxy_label, + Build.MODEL); + } + + @Override + protected boolean supportsVendor() + { + return "inc".equals(Build.DEVICE); + } + + @Override + protected File computeRoot(Context context) + { + return new File("/emmc"); + } + } + + /** + * Storage provider to allow access the /emmc directory on a Samsung Galaxy S. + * + *

+ * This implementation is experimental and untested. + *

+ * + * See http://groups.google.com/group/android-developers/browse_frm/thread/a1adf7122a75a657 + * + * @see FixedStorageProviderBase + */ + public static class SamsungGalaxySStorageProvider extends FixedStorageProviderBase + { + + public static final String ID = "SamsungGalaxySStorage"; + + public String getId() + { + return ID; + } + + @Override + public String getName(Context context) + { + return context.getString(R.string.local_storage_provider_samsunggalaxy_label, + Build.MODEL); + } + + @Override + protected boolean supportsVendor() + { + // FIXME + return "GT-I5800".equals(Build.DEVICE) || "GT-I9000".equals(Build.DEVICE) + || "SGH-T959".equals(Build.DEVICE) || "SGH-I897".equals(Build.DEVICE); + } + + @Override + protected File computeRoot(Context context) + { + return Environment.getExternalStorageDirectory(); // was: new + // File("/sdcard") + } + } + + /** + * Stores storage provider locking informations + */ + public static class SynchronizationAid + { + /** + * {@link Lock} has a thread semantic so it can't be released from + * another thread - this flags act as a holder for the unmount state + */ + public boolean unmounting = false; + + public final Lock readLock; + + public final Lock writeLock; + + { + final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true); + readLock = readWriteLock.readLock(); + writeLock = readWriteLock.writeLock(); + } + } + + /** + * The active storage providers. + */ + private final Map mProviders = new LinkedHashMap(); + + /** + * Locking data for the active storage providers. + */ + private final Map mProviderLocks = new IdentityHashMap(); + + protected final Application mApplication; + + /** + * Listener to be notified for storage related events. + */ + private List mListeners = new ArrayList(); + + private static transient StorageManager instance; + + public static synchronized StorageManager getInstance(final Application application) + { + if (instance == null) + { + instance = new StorageManager(application); + } + return instance; + } + + /** + * @param file + * Canonical file to match. Never null. + * @return Whether the specified file matches a filesystem root. + * @throws IOException + */ + public static boolean isMountPoint(final File file) throws IOException + { + for (final File root : File.listRoots()) + { + if (root.equals(file)) + { + return true; + } + } + return false; + } + + /** + * @param application + * Never null. + * @throws NullPointerException + * If application is null. + */ + protected StorageManager(final Application application) throws NullPointerException + { + if (application == null) + { + throw new NullPointerException("No application instance given"); + } + + mApplication = application; + + /* + * 20101113/fiouzy: + * + * Here is where we define which providers are used, currently we only + * allow the internal storage and the regular external storage. + * + * HTC Incredible storage and Samsung Galaxy S are omitted on purpose + * (they're experimental and I don't have those devices to test). + */ + final List allProviders = Arrays.asList(new InternalStorageProvider(), + new ExternalStorageProvider()); + for (final StorageProvider provider : allProviders) + { + // check for provider compatibility + if (provider.isSupported(mApplication)) + { + // provider is compatible! proceeding + + provider.init(application); + mProviders.put(provider.getId(), provider); + mProviderLocks.put(provider, new SynchronizationAid()); + } + } + + } + + /** + * @return Never null. + */ + public String getDefaultProviderId() + { + // assume there is at least 1 provider defined + return mProviders.entrySet().iterator().next().getKey(); + } + + /** + * @param providerId + * Never null. + * @return null if not found. + */ + protected StorageProvider getProvider(final String providerId) + { + return mProviders.get(providerId); + } + + /** + * @param dbName + * Never null. + * @param providerId + * Never null. + * @return The resolved database file for the given provider ID. + */ + public File getDatabase(final String dbName, final String providerId) + { + StorageProvider provider = getProvider(providerId); + // TODO fallback to internal storage if no provider + return provider.getDatabase(mApplication, dbName); + } + + /** + * @param dbName + * Never null. + * @param providerId + * Never null. + * @return The resolved attachement directory for the given provider ID. + */ + public File getAttachmentDirectory(final String dbName, final String providerId) + { + StorageProvider provider = getProvider(providerId); + // TODO fallback to internal storage if no provider + return provider.getAttachmentDirectory(mApplication, dbName); + } + + /** + * @param providerId + * Never null. + * @return Whether the specified provider is ready for read/write operations + */ + public boolean isReady(final String providerId) + { + StorageProvider provider = getProvider(providerId); + if (provider == null) + { + Log.w(K9.LOG_TAG, "Storage-Provider \"" + providerId + "\" does not exist"); + return false; + } + return provider.isReady(mApplication); + } + + /** + * @return A map of available providers names, indexed by their ID. Never + * null. + * @see StorageManager + * @see StorageProvider#isSupported(Context) + */ + public Map getAvailableProviders() + { + final Map result = new LinkedHashMap(); + for (final Map.Entry entry : mProviders.entrySet()) + { + result.put(entry.getKey(), entry.getValue().getName(mApplication)); + } + return result; + } + + /** + * @param path + */ + public void onBeforeUnmount(final String path) + { + Log.i(K9.LOG_TAG, "storage path \"" + path + "\" unmounting"); + final StorageProvider provider = resolveProvider(path); + if (provider == null) + { + return; + } + for (final StorageListener listener : mListeners) + { + try + { + listener.onUnmount(provider.getId()); + } + catch (Exception e) + { + Log.w(K9.LOG_TAG, "Error while notifying StorageListener", e); + } + } + final SynchronizationAid sync = mProviderLocks.get(resolveProvider(path)); + sync.writeLock.lock(); + sync.unmounting = true; + sync.writeLock.unlock(); + } + + public void onAfterUnmount(final String path) + { + Log.i(K9.LOG_TAG, "storage path \"" + path + "\" unmounted"); + final StorageProvider provider = resolveProvider(path); + if (provider == null) + { + return; + } + final SynchronizationAid sync = mProviderLocks.get(resolveProvider(path)); + sync.writeLock.lock(); + sync.unmounting = false; + sync.writeLock.unlock(); + } + + /** + * @param path + * @param readOnly + */ + public void onMount(final String path, final boolean readOnly) + { + Log.i(K9.LOG_TAG, "storage path \"" + path + "\" mounted readOnly=" + readOnly); + if (readOnly) + { + return; + } + + final StorageProvider provider = resolveProvider(path); + if (provider == null) + { + return; + } + for (final StorageListener listener : mListeners) + { + try + { + listener.onMount(provider.getId()); + } + catch (Exception e) + { + Log.w(K9.LOG_TAG, "Error while notifying StorageListener", e); + } + } + + // XXX we should reset mail service ONLY if there are accounts using the storage (this is not done in a regular listener because it has to be invoked afterward) + MailService.actionReset(mApplication, null); + } + + /** + * @param path + * Never null. + * @return The corresponding provider. null if no match. + */ + protected StorageProvider resolveProvider(final String path) + { + for (final StorageProvider provider : mProviders.values()) + { + if (path.equals(provider.getRoot(mApplication).getAbsolutePath())) + { + return provider; + } + } + return null; + } + + public void addListener(final StorageListener listener) + { + mListeners.add(listener); + } + + public void removeListener(final StorageListener listener) + { + mListeners.remove(listener); + } + + /** + * Try to lock the underlying storage to prevent concurrent unmount. + * + *

+ * You must invoke {@link #unlockProvider(String)} when you're done with the + * storage. + *

+ * + * @param providerId + * @throws UnavailableStorageException + * If the storage can't be locked. + */ + public void lockProvider(final String providerId) throws UnavailableStorageException + { + final StorageProvider provider = getProvider(providerId); + if (provider == null) + { + throw new UnavailableStorageException("StorageProvider not found: " + providerId); + } + // lock provider + final SynchronizationAid sync = mProviderLocks.get(provider); + final boolean locked = sync.readLock.tryLock(); + if (!locked || (locked && sync.unmounting)) + { + if (locked) + { + sync.readLock.unlock(); + } + throw new UnavailableStorageException("StorageProvider is unmounting"); + } + else if (locked && !provider.isReady(mApplication)) + { + sync.readLock.unlock(); + throw new UnavailableStorageException("StorageProvider not ready"); + } + } + + public void unlockProvider(final String providerId) + { + final StorageProvider provider = getProvider(providerId); + final SynchronizationAid sync = mProviderLocks.get(provider); + sync.readLock.unlock(); + } +} diff --git a/src/com/fsck/k9/mail/store/UnavailableAccountException.java b/src/com/fsck/k9/mail/store/UnavailableAccountException.java new file mode 100644 index 000000000..64c336ee6 --- /dev/null +++ b/src/com/fsck/k9/mail/store/UnavailableAccountException.java @@ -0,0 +1,47 @@ +package com.fsck.k9.mail.store; + +import com.fsck.k9.Account; + +/** + * An {@link Account} is not + * {@link Account#isAvailable(android.content.Context)}.
+ * The operation may be retried later. + */ +public class UnavailableAccountException extends RuntimeException +{ + + /** + * + */ + private static final long serialVersionUID = -1827283277120501465L; + + public UnavailableAccountException() + { + super("please try again later"); + } + + /** + * @param detailMessage + * @param throwable + */ + public UnavailableAccountException(String detailMessage, Throwable throwable) + { + super(detailMessage, throwable); + } + + /** + * @param detailMessage + */ + public UnavailableAccountException(String detailMessage) + { + super(detailMessage); + } + + /** + * @param throwable + */ + public UnavailableAccountException(Throwable throwable) + { + super(throwable); + } +} diff --git a/src/com/fsck/k9/mail/store/UnavailableStorageException.java b/src/com/fsck/k9/mail/store/UnavailableStorageException.java new file mode 100644 index 000000000..7a22a49f7 --- /dev/null +++ b/src/com/fsck/k9/mail/store/UnavailableStorageException.java @@ -0,0 +1,32 @@ +package com.fsck.k9.mail.store; + +import com.fsck.k9.mail.MessagingException; + +public class UnavailableStorageException extends MessagingException +{ + + private static final long serialVersionUID = 1348267375054620792L; + + public UnavailableStorageException(String message) + { + // consider this exception as a permanent failure by default + this(message, true); + } + + public UnavailableStorageException(String message, boolean perm) + { + super(message, perm); + } + + public UnavailableStorageException(String message, Throwable throwable) + { + // consider this exception as permanent failure by default + this(message, true, throwable); + } + + public UnavailableStorageException(String message, boolean perm, Throwable throwable) + { + super(message, perm, throwable); + } + +} diff --git a/src/com/fsck/k9/provider/AttachmentProvider.java b/src/com/fsck/k9/provider/AttachmentProvider.java index 9b28d9f7b..13e6cbf23 100644 --- a/src/com/fsck/k9/provider/AttachmentProvider.java +++ b/src/com/fsck/k9/provider/AttachmentProvider.java @@ -10,17 +10,22 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.ParcelFileDescriptor; -import android.os.Environment; import android.util.Log; import com.fsck.k9.Account; import com.fsck.k9.K9; +import com.fsck.k9.Preferences; +import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.internet.MimeUtility; +import com.fsck.k9.mail.store.LocalStore; +import com.fsck.k9.mail.store.LocalStore.AttachmentInfo; +import com.fsck.k9.mail.store.StorageManager; import java.io.*; import java.util.List; -/* - * A simple ContentProvider that allows file access to Email's attachments. +/** + * A simple ContentProvider that allows file access to Email's attachments.
+ * Warning! We make heavy assumptions about the Uris used by the {@link LocalStore} for an {@link Account} here. */ public class AttachmentProvider extends ContentProvider { @@ -39,13 +44,13 @@ public class AttachmentProvider extends ContentProvider public static Uri getAttachmentUri(Account account, long id) { - return getAttachmentUri(account.getUuid() + ".db" , id); + return getAttachmentUri(account.getUuid(), id); } public static Uri getAttachmentThumbnailUri(Account account, long id, int width, int height) { return CONTENT_URI.buildUpon() - .appendPath(account.getUuid() + ".db") + .appendPath(account.getUuid()) .appendPath(Long.toString(id)) .appendPath(FORMAT_THUMBNAIL) .appendPath(Integer.toString(width)) @@ -53,7 +58,7 @@ public class AttachmentProvider extends ContentProvider .build(); } - public static Uri getAttachmentUri(String db, long id) + private static Uri getAttachmentUri(String db, long id) { return CONTENT_URI.buildUpon() .appendPath(db) @@ -113,43 +118,17 @@ public class AttachmentProvider extends ContentProvider } else { - String path = getContext().getDatabasePath(dbName).getAbsolutePath(); - SQLiteDatabase db = null; - Cursor cursor = null; + final Account account = Preferences.getPreferences(getContext()).getAccount(dbName); + try { - db = SQLiteDatabase.openDatabase(path, null, 0); - cursor = db.query( - "attachments", - new String[] { "mime_type", "name" }, - "id = ?", - new String[] { id }, - null, - null, - null); - cursor.moveToFirst(); - String type = cursor.getString(0); - String name = cursor.getString(1); - cursor.close(); - db.close(); - - if (MimeUtility.DEFAULT_ATTACHMENT_MIME_TYPE.equals(type)) - { - type = MimeUtility.getMimeTypeByExtension(name); - } - return type; + final LocalStore localStore = LocalStore.getLocalInstance(account, K9.app); + return localStore.getAttachmentType(id); } - finally + catch (MessagingException e) { - if (cursor != null) - { - cursor.close(); - } - if (db != null) - { - db.close(); - } - + Log.e(K9.LOG_TAG, "Unable to retrieve LocalStore for " + account, e); + return null; } } } @@ -159,15 +138,14 @@ public class AttachmentProvider extends ContentProvider { try { - File attachmentsDir = getContext().getDatabasePath(dbName + "_att"); - File file = new File(attachmentsDir, id); + final Account account = Preferences.getPreferences(getContext()).getAccount(dbName); + final File attachmentsDir; + attachmentsDir = StorageManager.getInstance(K9.app).getAttachmentDirectory(dbName, + account.getLocalStorageProviderId()); + final File file = new File(attachmentsDir, id); if (!file.exists()) { - file = new File(Environment.getExternalStorageDirectory() + attachmentsDir.getCanonicalPath().substring("/data".length()), id); - if (!file.exists()) - { - throw new FileNotFoundException(); - } + throw new FileNotFoundException(file.getAbsolutePath()); } return file; } @@ -182,7 +160,7 @@ public class AttachmentProvider extends ContentProvider public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { List segments = uri.getPathSegments(); - String dbName = segments.get(0); + String dbName = segments.get(0); // "/sdcard/..." is URL-encoded and makes up only 1 segment String id = segments.get(1); String format = segments.get(2); if (FORMAT_THUMBNAIL.equals(format)) @@ -190,6 +168,11 @@ public class AttachmentProvider extends ContentProvider int width = Integer.parseInt(segments.get(3)); int height = Integer.parseInt(segments.get(4)); String filename = "thmb_" + dbName + "_" + id + ".tmp"; + int index = dbName.lastIndexOf('/'); + if (index >= 0) + { + filename = /*dbName.substring(0, index + 1) + */"thmb_" + dbName.substring(index + 1) + "_" + id + ".tmp"; + } File dir = getContext().getCacheDir(); File file = new File(dir, filename); if (!file.exists()) @@ -199,12 +182,21 @@ public class AttachmentProvider extends ContentProvider try { FileInputStream in = new FileInputStream(getFile(dbName, id)); - Bitmap thumbnail = createThumbnail(type, in); - thumbnail = Bitmap.createScaledBitmap(thumbnail, width, height, true); - FileOutputStream out = new FileOutputStream(file); - thumbnail.compress(Bitmap.CompressFormat.PNG, 100, out); - out.close(); - in.close(); + try + { + Bitmap thumbnail = createThumbnail(type, in); + if (thumbnail != null) + { + thumbnail = Bitmap.createScaledBitmap(thumbnail, width, height, true); + FileOutputStream out = new FileOutputStream(file); + thumbnail.compress(Bitmap.CompressFormat.PNG, 100, out); + out.close(); + } + } + finally + { + in.close(); + } } catch (IOException ioe) { @@ -251,39 +243,16 @@ public class AttachmentProvider extends ContentProvider String dbName = segments.get(0); String id = segments.get(1); //String format = segments.get(2); - String path = getContext().getDatabasePath(dbName).getAbsolutePath(); - String name = null; - int size = -1; - SQLiteDatabase db = null; - Cursor cursor = null; + final AttachmentInfo attachmentInfo; try { - db = SQLiteDatabase.openDatabase(path, null, 0); - cursor = db.query( - "attachments", - new String[] { "name", "size" }, - "id = ?", - new String[] { id }, - null, - null, - null); - if (!cursor.moveToFirst()) - { - return null; - } - name = cursor.getString(0); - size = cursor.getInt(1); + final Account account = Preferences.getPreferences(getContext()).getAccount(dbName); + attachmentInfo = LocalStore.getLocalInstance(account, K9.app).getAttachmentInfo(id); } - finally + catch (MessagingException e) { - if (cursor != null) - { - cursor.close(); - } - if (db != null) - { - db.close(); - } + Log.e(K9.LOG_TAG, "Uname to retrieve attachment info from local store for ID: " + id, e); + return null; } MatrixCursor ret = new MatrixCursor(projection); @@ -301,11 +270,11 @@ public class AttachmentProvider extends ContentProvider } else if (AttachmentProviderColumns.DISPLAY_NAME.equals(column)) { - values[i] = name; + values[i] = attachmentInfo.name; } else if (AttachmentProviderColumns.SIZE.equals(column)) { - values[i] = size; + values[i] = attachmentInfo.size; } } ret.addRow(values); diff --git a/src/com/fsck/k9/provider/MessageProvider.java b/src/com/fsck/k9/provider/MessageProvider.java index 668d79ec8..01ffb3ed4 100644 --- a/src/com/fsck/k9/provider/MessageProvider.java +++ b/src/com/fsck/k9/provider/MessageProvider.java @@ -426,7 +426,7 @@ public class MessageProvider extends ContentProvider Object[] values = new Object[2]; - for (Account account : Preferences.getPreferences(getContext()).getAccounts()) + for (Account account : Preferences.getPreferences(getContext()).getAvailableAccounts()) { if (account.getAccountNumber()==accountNumber) { @@ -435,7 +435,15 @@ public class MessageProvider extends ContentProvider { myAccountStats = account.getStats(getContext()); values[0] = myAccount.getDescription(); - values[1] = myAccountStats.unreadMessageCount; + if (myAccountStats == null) + { + values[1] = 0; + } + else + { + values[1] = myAccountStats.unreadMessageCount; + } + ret.addRow(values); } catch (MessagingException e) @@ -1024,6 +1032,11 @@ public class MessageProvider extends ContentProvider if (account.getAccountNumber() == accountId) { myAccount = account; + if (!account.isAvailable(getContext())) + { + Log.w(K9.LOG_TAG, "not deleting messages because account is unavailable at the moment"); + return 0; + } } } diff --git a/src/com/fsck/k9/service/BootReceiver.java b/src/com/fsck/k9/service/BootReceiver.java index 9f4d42a6b..23ea5214a 100644 --- a/src/com/fsck/k9/service/BootReceiver.java +++ b/src/com/fsck/k9/service/BootReceiver.java @@ -30,27 +30,28 @@ public class BootReceiver extends CoreReceiver if (K9.DEBUG) Log.i(K9.LOG_TAG, "BootReceiver.onReceive" + intent); - if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) + final String action = intent.getAction(); + if (Intent.ACTION_BOOT_COMPLETED.equals(action)) { //K9.setServicesEnabled(context, tmpWakeLockId); //tmpWakeLockId = null; } - else if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(intent.getAction())) + else if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(action)) { MailService.actionCancel(context, tmpWakeLockId); tmpWakeLockId = null; } - else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(intent.getAction())) + else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) { MailService.actionReset(context, tmpWakeLockId); tmpWakeLockId = null; } - else if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) + else if (ConnectivityManager.CONNECTIVITY_ACTION.equals(action)) { MailService.connectivityChange(context, tmpWakeLockId); tmpWakeLockId = null; } - else if (AutoSyncHelper.SYNC_CONN_STATUS_CHANGE.equals(intent.getAction())) + else if (AutoSyncHelper.SYNC_CONN_STATUS_CHANGE.equals(action)) { K9.BACKGROUND_OPS bOps = K9.getBackgroundOps(); if (bOps == K9.BACKGROUND_OPS.WHEN_CHECKED_AUTO_SYNC) @@ -59,7 +60,7 @@ public class BootReceiver extends CoreReceiver tmpWakeLockId = null; } } - else if (ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED.equals(intent.getAction())) + else if (ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED.equals(action)) { K9.BACKGROUND_OPS bOps = K9.getBackgroundOps(); if (bOps == K9.BACKGROUND_OPS.WHEN_CHECKED || bOps == K9.BACKGROUND_OPS.WHEN_CHECKED_AUTO_SYNC) @@ -68,7 +69,7 @@ public class BootReceiver extends CoreReceiver tmpWakeLockId = null; } } - else if (FIRE_INTENT.equals(intent.getAction())) + else if (FIRE_INTENT.equals(action)) { Intent alarmedIntent = intent.getParcelableExtra(ALARMED_INTENT); String alarmedAction = alarmedIntent.getAction(); @@ -81,7 +82,7 @@ public class BootReceiver extends CoreReceiver context.startService(alarmedIntent); } } - else if (SCHEDULE_INTENT.equals(intent.getAction())) + else if (SCHEDULE_INTENT.equals(action)) { long atTime = intent.getLongExtra(AT_TIME, -1); Intent alarmedIntent = intent.getParcelableExtra(ALARMED_INTENT); @@ -93,7 +94,7 @@ public class BootReceiver extends CoreReceiver alarmMgr.set(AlarmManager.RTC_WAKEUP, atTime, pi); } - else if (CANCEL_INTENT.equals(intent.getAction())) + else if (CANCEL_INTENT.equals(action)) { Intent alarmedIntent = intent.getParcelableExtra(ALARMED_INTENT); if (K9.DEBUG) @@ -146,4 +147,24 @@ public class BootReceiver extends CoreReceiver context.sendBroadcast(i); } + /** + * Cancel any scheduled alarm. + * + * @param context + */ + public static void purgeSchedule(final Context context) + { + final AlarmManager alarmService = (AlarmManager) context + .getSystemService(Context.ALARM_SERVICE); + alarmService.cancel(PendingIntent.getBroadcast(context, 0, new Intent() + { + @Override + public boolean filterEquals(final Intent other) + { + // we want to match all intents + return true; + } + }, 0)); + } + } diff --git a/src/com/fsck/k9/service/MailService.java b/src/com/fsck/k9/service/MailService.java index 7d2a42aa8..f1994238f 100644 --- a/src/com/fsck/k9/service/MailService.java +++ b/src/com/fsck/k9/service/MailService.java @@ -432,7 +432,14 @@ public class MailService extends CoreService { if (K9.DEBUG) Log.i(K9.LOG_TAG, "Setting up pushers for account " + account.getDescription()); - pushing |= MessagingController.getInstance(getApplication()).setupPushing(account); + if (account.isAvailable(getApplicationContext())) + { + pushing |= MessagingController.getInstance(getApplication()).setupPushing(account); + } + else + { + //TODO: setupPushing of unavailable accounts when they become available (sd-card inserted) + } } if (pushing) { diff --git a/src/com/fsck/k9/service/RemoteControlReceiver.java b/src/com/fsck/k9/service/RemoteControlReceiver.java index b3704b3ee..1ea3293ef 100644 --- a/src/com/fsck/k9/service/RemoteControlReceiver.java +++ b/src/com/fsck/k9/service/RemoteControlReceiver.java @@ -36,6 +36,7 @@ public class RemoteControlReceiver extends CoreReceiver String[] descriptions = new String[accounts.length]; for (int i = 0; i < accounts.length; i++) { + //warning: account may not be isAvailable() Account account = accounts[i]; uuids[i] = account.getUuid(); diff --git a/src/com/fsck/k9/service/RemoteControlService.java b/src/com/fsck/k9/service/RemoteControlService.java index 1b131bc76..2c43d92b7 100644 --- a/src/com/fsck/k9/service/RemoteControlService.java +++ b/src/com/fsck/k9/service/RemoteControlService.java @@ -86,6 +86,7 @@ public class RemoteControlService extends CoreService Account[] accounts = preferences.getAccounts(); for (Account account : accounts) { + //warning: account may not be isAvailable() if (allAccounts || account.getUuid().equals(uuid)) { diff --git a/src/com/fsck/k9/service/ShutdownReceiver.java b/src/com/fsck/k9/service/ShutdownReceiver.java new file mode 100644 index 000000000..d82d7b5a3 --- /dev/null +++ b/src/com/fsck/k9/service/ShutdownReceiver.java @@ -0,0 +1,44 @@ +package com.fsck.k9.service; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import com.fsck.k9.K9; + +/** + * Capture the system shutdown event in order to properly free resources. + * + *

+ * It is advised not to statically register (from AndroidManifest.xml) this + * receiver in order to avoid unecessary K-9 launch (which would defeat the + * purpose of that receiver). Using AndroidManifest.xml instructs Android to + * launch K-9 if not running, defeating the purpose of this receiver.
+ * The recommended way is to register this receiver using + * {@link Context#registerReceiver(BroadcastReceiver, android.content.IntentFilter)} + *

+ */ +public class ShutdownReceiver extends BroadcastReceiver +{ + + @Override + public void onReceive(final Context context, final Intent intent) + { + if (Intent.ACTION_SHUTDOWN.equals(intent.getAction())) + { + Log.i(K9.LOG_TAG, "System is shutting down, releasing resources"); + + // prevent any scheduled intent from waking up K-9 + BootReceiver.purgeSchedule(context); + + /* + * TODO invoke proper shutdown methods (stop any running thread) + * + * 20101111: this can't be done now as we don't have proper + * startup/shutdown sequences + */ + } + } + +} diff --git a/src/com/fsck/k9/service/StorageGoneReceiver.java b/src/com/fsck/k9/service/StorageGoneReceiver.java new file mode 100644 index 000000000..ab7021e29 --- /dev/null +++ b/src/com/fsck/k9/service/StorageGoneReceiver.java @@ -0,0 +1,51 @@ +package com.fsck.k9.service; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.util.Log; + +import com.fsck.k9.K9; +import com.fsck.k9.mail.store.StorageManager; + +/** + * That BroadcastReceiver is only interested in UNMOUNT events. + * + *

+ * Code was separated from {@link StorageReceiver} because we don't want that + * receiver to be statically defined in manifest. + *

+ */ +public class StorageGoneReceiver extends BroadcastReceiver +{ + + @Override + public void onReceive(final Context context, final Intent intent) + { + final String action = intent.getAction(); + final Uri uri = intent.getData(); + + if (uri == null || uri.getPath() == null) + { + return; + } + + if (K9.DEBUG) + { + Log.v(K9.LOG_TAG, "StorageGoneReceiver: " + intent.toString()); + } + + final String path = uri.getPath(); + + if (Intent.ACTION_MEDIA_EJECT.equals(action)) + { + StorageManager.getInstance(K9.app).onBeforeUnmount(path); + } + else if (Intent.ACTION_MEDIA_UNMOUNTED.equals(action)) + { + StorageManager.getInstance(K9.app).onAfterUnmount(path); + } + } + +} diff --git a/src/com/fsck/k9/service/StorageReceiver.java b/src/com/fsck/k9/service/StorageReceiver.java new file mode 100644 index 000000000..10676cf46 --- /dev/null +++ b/src/com/fsck/k9/service/StorageReceiver.java @@ -0,0 +1,43 @@ +package com.fsck.k9.service; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.util.Log; + +import com.fsck.k9.K9; +import com.fsck.k9.mail.store.StorageManager; + +/** + * That BroadcastReceiver is only interested in MOUNT events. + */ +public class StorageReceiver extends BroadcastReceiver +{ + + @Override + public void onReceive(final Context context, final Intent intent) + { + final String action = intent.getAction(); + final Uri uri = intent.getData(); + + if (uri == null || uri.getPath() == null) + { + return; + } + + if (K9.DEBUG) + { + Log.v(K9.LOG_TAG, "StorageReceiver: " + intent.toString()); + } + + final String path = uri.getPath(); + + if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) + { + StorageManager.getInstance(K9.app).onMount(path, + intent.getBooleanExtra("read-only", true)); + } + } + +}