diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 405cedaa8..67654233c 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -396,6 +396,11 @@ otherwise it would make K-9 start at the wrong time android:readPermission="com.fsck.k9.permission.READ_MESSAGES" android:writePermission="com.fsck.k9.permission.DELETE_MESSAGES" /> + + + + + - - - diff --git a/res/menu/message_list_option.xml b/res/menu/message_list_option.xml index 4fef1630c..18bdd61c1 100644 --- a/res/menu/message_list_option.xml +++ b/res/menu/message_list_option.xml @@ -34,9 +34,11 @@ + diff --git a/res/values/strings.xml b/res/values/strings.xml index 7b23bf410..5c55ee35d 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1138,6 +1138,7 @@ http://k9mail.googlecode.com/ Sending query to server Fetching %d results Fetching %1$d of %2$d results + Remote search failed Search Enable server search @@ -1149,4 +1150,7 @@ http://k9mail.googlecode.com/ Use background as (un)read indicator Show read and unread messages with different background colors + + Threaded view + Collapse messages belonging to the same thread diff --git a/res/xml/global_preferences.xml b/res/xml/global_preferences.xml index caaf7f190..51bf29284 100644 --- a/res/xml/global_preferences.xml +++ b/res/xml/global_preferences.xml @@ -150,6 +150,12 @@ android:summary="@string/global_settings_background_as_unread_indicator_summary" /> + + mSortAscending = new HashMap(); private static boolean sUseBackgroundAsUnreadIndicator = true; + private static boolean sThreadedViewEnabled = true; + /** * The MIME type(s) of attachments we're willing to view. @@ -473,6 +475,7 @@ public class K9 extends Application { editor.putString("attachmentdefaultpath", mAttachmentDefaultPath); editor.putBoolean("useBackgroundAsUnreadIndicator", sUseBackgroundAsUnreadIndicator); + editor.putBoolean("threadedView", sThreadedViewEnabled); fontSizes.save(editor); } @@ -643,6 +646,7 @@ public class K9 extends Application { mAttachmentDefaultPath = sprefs.getString("attachmentdefaultpath", Environment.getExternalStorageDirectory().toString()); sUseBackgroundAsUnreadIndicator = sprefs.getBoolean("useBackgroundAsUnreadIndicator", true); + sThreadedViewEnabled = sprefs.getBoolean("threadedView", true); fontSizes.load(sprefs); try { @@ -1138,4 +1142,12 @@ public class K9 extends Application { public static synchronized void setUseBackgroundAsUnreadIndicator(boolean enabled) { sUseBackgroundAsUnreadIndicator = enabled; } + + public static synchronized boolean isThreadedViewEnabled() { + return sThreadedViewEnabled; + } + + public static synchronized void setThreadedViewEnabled(boolean enable) { + sThreadedViewEnabled = enable; + } } diff --git a/src/com/fsck/k9/SearchAccount.java b/src/com/fsck/k9/SearchAccount.java deleted file mode 100644 index bf2f25570..000000000 --- a/src/com/fsck/k9/SearchAccount.java +++ /dev/null @@ -1,152 +0,0 @@ -package com.fsck.k9; - -import java.io.Serializable; -import java.util.UUID; - -import android.content.Context; - -import com.fsck.k9.mail.Flag; - -/** - * This is a meta-Account that represents one or more accounts with filters on them. The filter specification - * is defined by {@link com.fsck.k9.activity.SearchModifier}. - */ -public class SearchAccount implements BaseAccount, SearchSpecification, Serializable { - /** - * Create a {@code SearchAccount} instance for the Unified Inbox. - * - * @param context - * A {@link Context} instance that will be used to get localized strings and will be - * passed on to the {@code SearchAccount} instance. - * - * @return The {@link SearchAccount} instance for the Unified Inbox. - */ - public static SearchAccount createUnifiedInboxAccount(Context context) { - SearchAccount unifiedInbox = new SearchAccount(context, true, null, null); - unifiedInbox.setDescription(context.getString(R.string.integrated_inbox_title)); - unifiedInbox.setEmail(context.getString(R.string.integrated_inbox_detail)); - return unifiedInbox; - } - - /** - * Create a {@code SearchAccount} instance for the special account "All messages". - * - * @param context - * A {@link Context} instance that will be used to get localized strings and will be - * passed on to the {@code SearchAccount} instance. - * - * @return The {@link SearchAccount} instance for the Unified Inbox. - */ - public static SearchAccount createAllMessagesAccount(Context context) { - SearchAccount allMessages = new SearchAccount(context, false, null, null); - allMessages.setDescription(context.getString(R.string.search_all_messages_title)); - allMessages.setEmail(context.getString(R.string.search_all_messages_detail)); - return allMessages; - } - - - private static final long serialVersionUID = -4388420303235543976L; - private Flag[] mRequiredFlags = null; - private Flag[] mForbiddenFlags = null; - private String email = null; - private String description = null; - private String query = ""; - private boolean integrate = false; - private String mUuid = null; - private boolean builtin = false; - private String[] accountUuids = null; - private String[] folderNames = null; - - public SearchAccount(Preferences preferences) { - } - - protected synchronized void delete(Preferences preferences) { - } - - public synchronized void save(Preferences preferences) { - } - - public SearchAccount(Context context, boolean nintegrate, Flag[] requiredFlags, Flag[] forbiddenFlags) { - mRequiredFlags = requiredFlags; - mForbiddenFlags = forbiddenFlags; - integrate = nintegrate; - } - - @Override - public synchronized String getEmail() { - return email; - } - - @Override - public synchronized void setEmail(String email) { - this.email = email; - } - - public Flag[] getRequiredFlags() { - return mRequiredFlags; - } - - public Flag[] getForbiddenFlags() { - return mForbiddenFlags; - } - - public boolean isIntegrate() { - return integrate; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - public String getQuery() { - return query; - } - - public void setQuery(String query) { - this.query = query; - } - - public String getUuid() { - if (mUuid == null) { - setUuid(UUID.randomUUID().toString()); - } - return mUuid; - } - - public void setUuid(String nUuid) { - mUuid = nUuid; - } - - public void setIntegrate(boolean integrate) { - this.integrate = integrate; - } - - public boolean isBuiltin() { - return builtin; - } - - public void setBuiltin(boolean builtin) { - this.builtin = builtin; - } - - public String[] getAccountUuids() { - return accountUuids; - } - - public void setAccountUuids(String[] accountUuids) { - this.accountUuids = accountUuids; - } - - @Override - public String[] getFolderNames() { - return folderNames; - } - - public void setFolderNames(String[] folderNames) { - this.folderNames = folderNames; - } -} diff --git a/src/com/fsck/k9/SearchSpecification.java b/src/com/fsck/k9/SearchSpecification.java deleted file mode 100644 index a1f1de0a8..000000000 --- a/src/com/fsck/k9/SearchSpecification.java +++ /dev/null @@ -1,19 +0,0 @@ - -package com.fsck.k9; - -import com.fsck.k9.mail.Flag; - -public interface SearchSpecification { - - public Flag[] getRequiredFlags(); - - public Flag[] getForbiddenFlags(); - - public boolean isIntegrate(); - - public String getQuery(); - - public String[] getAccountUuids(); - - public String[] getFolderNames(); -} \ No newline at end of file diff --git a/src/com/fsck/k9/activity/AccountList.java b/src/com/fsck/k9/activity/AccountList.java index 52c64dbe3..9c17ffd42 100644 --- a/src/com/fsck/k9/activity/AccountList.java +++ b/src/com/fsck/k9/activity/AccountList.java @@ -21,7 +21,7 @@ import com.fsck.k9.FontSizes; import com.fsck.k9.K9; import com.fsck.k9.Preferences; import com.fsck.k9.R; -import com.fsck.k9.SearchAccount; +import com.fsck.k9.search.SearchAccount; /** diff --git a/src/com/fsck/k9/activity/Accounts.java b/src/com/fsck/k9/activity/Accounts.java index 131d8fb6a..5e04a6ede 100644 --- a/src/com/fsck/k9/activity/Accounts.java +++ b/src/com/fsck/k9/activity/Accounts.java @@ -62,11 +62,8 @@ import com.fsck.k9.FontSizes; import com.fsck.k9.K9; import com.fsck.k9.Preferences; import com.fsck.k9.R; -import com.fsck.k9.SearchAccount; -import com.fsck.k9.SearchSpecification; import com.fsck.k9.activity.misc.ExtendedAsyncTask; import com.fsck.k9.activity.misc.NonConfigurationInstance; -import com.fsck.k9.activity.setup.AccountSettings; import com.fsck.k9.activity.setup.AccountSetupBasics; import com.fsck.k9.activity.setup.Prefs; import com.fsck.k9.activity.setup.WelcomeMessage; @@ -80,6 +77,9 @@ import com.fsck.k9.mail.Transport; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.store.StorageManager; import com.fsck.k9.mail.store.WebDavStore; +import com.fsck.k9.search.LocalSearch; +import com.fsck.k9.search.SearchAccount; +import com.fsck.k9.search.SearchModifier; import com.fsck.k9.view.ColorChip; import com.fsck.k9.preferences.SettingsExporter; import com.fsck.k9.preferences.SettingsImportExportException; @@ -126,8 +126,8 @@ public class Accounts extends K9ListActivity implements OnItemClickListener { private AccountsHandler mHandler = new AccountsHandler(); private AccountsAdapter mAdapter; - private SearchAccount unreadAccount = null; - private SearchAccount integratedInboxAccount = null; + private SearchAccount mAllMessagesAccount = null; + private SearchAccount mUnifiedInboxAccount = null; private FontSizes mFontSizes = K9.getFontSizes(); private MenuItem mRefreshMenuItem; @@ -375,7 +375,7 @@ public class Accounts extends K9ListActivity implements OnItemClickListener { boolean startup = intent.getBooleanExtra(EXTRA_STARTUP, true); if (startup && K9.startIntegratedInbox() && !K9.isHideSpecialAccounts()) { - onOpenAccount(integratedInboxAccount); + onOpenAccount(mUnifiedInboxAccount); finish(); return; } else if (startup && accounts.length == 1 && onOpenAccount(accounts[0])) { @@ -424,8 +424,8 @@ public class Accounts extends K9ListActivity implements OnItemClickListener { * Creates and initializes the special accounts ('Unified Inbox' and 'All Messages') */ private void createSpecialAccounts() { - integratedInboxAccount = SearchAccount.createUnifiedInboxAccount(this); - unreadAccount = SearchAccount.createAllMessagesAccount(this); + mUnifiedInboxAccount = SearchAccount.createUnifiedInboxAccount(this); + mAllMessagesAccount = SearchAccount.createAllMessagesAccount(this); } @SuppressWarnings("unchecked") @@ -519,14 +519,14 @@ public class Accounts extends K9ListActivity implements OnItemClickListener { List newAccounts; if (!K9.isHideSpecialAccounts() && accounts.length > 0) { - if (integratedInboxAccount == null || unreadAccount == null) { + if (mUnifiedInboxAccount == null || mAllMessagesAccount == null) { createSpecialAccounts(); } newAccounts = new ArrayList(accounts.length + SPECIAL_ACCOUNTS_COUNT); - newAccounts.add(integratedInboxAccount); - newAccounts.add(unreadAccount); + newAccounts.add(mUnifiedInboxAccount); + newAccounts.add(mAllMessagesAccount); } else { newAccounts = new ArrayList(accounts.length); } @@ -550,7 +550,8 @@ public class Accounts extends K9ListActivity implements OnItemClickListener { pendingWork.put(account, "true"); final SearchAccount searchAccount = (SearchAccount)account; - MessagingController.getInstance(getApplication()).searchLocalMessages(searchAccount, null, new MessagingListener() { + MessagingController.getInstance(getApplication()) + .searchLocalMessages(searchAccount.getRelatedSearch(), new MessagingListener() { @Override public void searchStats(AccountStats stats) { mListener.accountStatusChanged(searchAccount, stats); @@ -607,7 +608,7 @@ public class Accounts extends K9ListActivity implements OnItemClickListener { private boolean onOpenAccount(BaseAccount account) { if (account instanceof SearchAccount) { SearchAccount searchAccount = (SearchAccount)account; - MessageList.actionHandle(this, searchAccount.getDescription(), searchAccount); + MessageList.actionDisplaySearch(this, searchAccount.getRelatedSearch(), false, false); } else { Account realAccount = (Account)account; if (!realAccount.isEnabled()) { @@ -624,8 +625,10 @@ public class Accounts extends K9ListActivity implements OnItemClickListener { if (K9.FOLDER_NONE.equals(realAccount.getAutoExpandFolderName())) { FolderList.actionHandleAccount(this, realAccount); } else { - MessageList.actionHandleFolder(this, realAccount, realAccount.getAutoExpandFolderName()); - } + LocalSearch search = new LocalSearch(realAccount.getAutoExpandFolderName()); + search.addAllowedFolder(realAccount.getAutoExpandFolderName()); + search.addAccountUuid(realAccount.getUuid()); + MessageList.actionDisplaySearch(this, search, false, true);} } return true; } @@ -1782,49 +1785,20 @@ public class Accounts extends K9ListActivity implements OnItemClickListener { } @Override public void onClick(View v) { - String description = getString(R.string.search_title, account.getDescription(), getString(searchModifier.resId)); + final String description = getString(R.string.search_title, account.getDescription(), getString(searchModifier.resId)); + LocalSearch search = null; + if (account instanceof SearchAccount) { - SearchAccount searchAccount = (SearchAccount)account; - - MessageList.actionHandle(Accounts.this, - description, "", searchAccount.isIntegrate(), - combine(searchAccount.getRequiredFlags(), searchModifier.requiredFlags), - combine(searchAccount.getForbiddenFlags(), searchModifier.forbiddenFlags)); + search = ((SearchAccount) account).getRelatedSearch().clone(); + search.setName(description); } else { - SearchSpecification searchSpec = new SearchSpecification() { - @Override - public String[] getAccountUuids() { - return new String[] { account.getUuid() }; - } - - @Override - public Flag[] getForbiddenFlags() { - return searchModifier.forbiddenFlags; - } - - @Override - public String getQuery() { - return ""; - } - - @Override - public Flag[] getRequiredFlags() { - return searchModifier.requiredFlags; - } - - @Override - public boolean isIntegrate() { - return false; - } - - @Override - public String[] getFolderNames() { - return null; - } - - }; - MessageList.actionHandle(Accounts.this, description, searchSpec); + search = new LocalSearch(description); + search.addAccountUuid(account.getUuid()); } + + search.allRequiredFlags(searchModifier.requiredFlags); + search.allForbiddenFlags(searchModifier.forbiddenFlags); + MessageList.actionDisplaySearch(Accounts.this, search, true, false); } } diff --git a/src/com/fsck/k9/activity/FolderList.java b/src/com/fsck/k9/activity/FolderList.java index 2e761c8a0..d72edfbc6 100644 --- a/src/com/fsck/k9/activity/FolderList.java +++ b/src/com/fsck/k9/activity/FolderList.java @@ -52,7 +52,6 @@ import com.fsck.k9.FontSizes; import com.fsck.k9.K9; import com.fsck.k9.Preferences; import com.fsck.k9.R; -import com.fsck.k9.SearchSpecification; import com.fsck.k9.activity.FolderList.FolderListAdapter.FolderListFilter; import com.fsck.k9.activity.misc.ActionBarNavigationSpinner; import com.fsck.k9.activity.setup.AccountSettings; @@ -68,6 +67,9 @@ import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.store.LocalStore.LocalFolder; +import com.fsck.k9.search.LocalSearch; +import com.fsck.k9.search.SearchModifier; +import com.fsck.k9.search.SearchSpecification; import com.fsck.k9.service.MailService; /** @@ -620,7 +622,10 @@ public class FolderList extends K9ListActivity implements OnNavigationListener { } private void onOpenFolder(String folder) { - MessageList.actionHandleFolder(this, mAccount, folder); + LocalSearch search = new LocalSearch(folder); + search.addAccountUuid(mAccount.getUuid()); + search.addAllowedFolder(folder); + MessageList.actionDisplaySearch(this, search, false, false); } private void onCompact(Account account) { @@ -1267,86 +1272,34 @@ public class FolderList extends K9ListActivity implements OnNavigationListener { } @Override public void onClick(View v) { - String description = getString(R.string.search_title, + final String description = getString(R.string.search_title, getString(R.string.message_list_title, account.getDescription(), displayName), getString(searchModifier.resId)); - SearchSpecification searchSpec = new SearchSpecification() { - @Override - public String[] getAccountUuids() { - return new String[] { account.getUuid() }; - } - - @Override - public Flag[] getForbiddenFlags() { - return searchModifier.forbiddenFlags; - } - - @Override - public String getQuery() { - return ""; - } - - @Override - public Flag[] getRequiredFlags() { - return searchModifier.requiredFlags; - } - - @Override - public boolean isIntegrate() { - return false; - } - - @Override - public String[] getFolderNames() { - return new String[] { folderName }; - } - - }; - MessageList.actionHandle(FolderList.this, description, searchSpec); - + LocalSearch search = new LocalSearch(description); + try { + search.allRequiredFlags(searchModifier.requiredFlags); + search.allForbiddenFlags(searchModifier.forbiddenFlags); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + search.addAllowedFolder(folderName); + search.addAccountUuid(account.getUuid()); + MessageList.actionDisplaySearch(FolderList.this, search, true, false); } - } - private static Flag[] UNREAD_FLAG_ARRAY = { Flag.SEEN }; - private void openUnreadSearch(Context context, final Account account) { String description = getString(R.string.search_title, mAccount.getDescription(), getString(R.string.unread_modifier)); - - SearchSpecification searchSpec = new SearchSpecification() { - //interface has no override @Override - public String[] getAccountUuids() { - return new String[] { account.getUuid() }; - } - - //interface has no override @Override - public Flag[] getForbiddenFlags() { - return UNREAD_FLAG_ARRAY; - } - - //interface has no override @Override - public String getQuery() { - return ""; - } - - @Override - public Flag[] getRequiredFlags() { - return null; - } - - @Override - public boolean isIntegrate() { - return false; - } - - @Override - public String[] getFolderNames() { - return null; - } - - }; - MessageList.actionHandle(context, description, searchSpec); + LocalSearch search = new LocalSearch(description); + search.addAccountUuid(account.getUuid()); + try { + search.allRequiredFlags(new Flag[]{Flag.SEEN}); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } } } diff --git a/src/com/fsck/k9/activity/LauncherShortcuts.java b/src/com/fsck/k9/activity/LauncherShortcuts.java index 415689057..94e6a4ff8 100644 --- a/src/com/fsck/k9/activity/LauncherShortcuts.java +++ b/src/com/fsck/k9/activity/LauncherShortcuts.java @@ -7,7 +7,7 @@ import android.os.Parcelable; import com.fsck.k9.Account; import com.fsck.k9.BaseAccount; import com.fsck.k9.R; -import com.fsck.k9.SearchSpecification; +import com.fsck.k9.search.SearchSpecification; public class LauncherShortcuts extends AccountList { @Override @@ -31,8 +31,8 @@ public class LauncherShortcuts extends AccountList { Intent shortcutIntent = null; if (account instanceof SearchSpecification) { - shortcutIntent = MessageList.actionHandleAccountIntent(this, account.getDescription(), - (SearchSpecification) account); + shortcutIntent = MessageList.intentDisplaySearch(this, (SearchSpecification) account, + false, true, true); } else { shortcutIntent = FolderList.actionHandleAccountIntent(this, (Account) account, null, true); diff --git a/src/com/fsck/k9/activity/MessageList.java b/src/com/fsck/k9/activity/MessageList.java index 49211c2c1..c5ad31281 100644 --- a/src/com/fsck/k9/activity/MessageList.java +++ b/src/com/fsck/k9/activity/MessageList.java @@ -24,17 +24,19 @@ import com.fsck.k9.Account.SortType; import com.fsck.k9.K9; import com.fsck.k9.Preferences; import com.fsck.k9.R; -import com.fsck.k9.SearchSpecification; import com.fsck.k9.activity.misc.SwipeGestureDetector.OnSwipeGestureListener; import com.fsck.k9.activity.setup.AccountSettings; import com.fsck.k9.activity.setup.FolderSettings; import com.fsck.k9.activity.setup.Prefs; import com.fsck.k9.fragment.MessageListFragment; import com.fsck.k9.fragment.MessageListFragment.MessageListFragmentListener; -import com.fsck.k9.helper.Utility; -import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.store.StorageManager; +import com.fsck.k9.search.LocalSearch; +import com.fsck.k9.search.SearchSpecification; +import com.fsck.k9.search.SearchSpecification.Attribute; +import com.fsck.k9.search.SearchSpecification.Searchfield; +import com.fsck.k9.search.SearchSpecification.SearchCondition; /** @@ -44,86 +46,42 @@ import com.fsck.k9.mail.store.StorageManager; */ public class MessageList extends K9FragmentActivity implements MessageListFragmentListener, OnBackStackChangedListener, OnSwipeGestureListener { - private static final String EXTRA_ACCOUNT = "account"; - private static final String EXTRA_FOLDER = "folder"; + + // for this activity + private static final String EXTRA_SEARCH = "search"; + private static final String EXTRA_NO_THREADING = "no_threading"; + + // used for remote search private static final String EXTRA_SEARCH_ACCOUNT = "com.fsck.k9.search_account"; private static final String EXTRA_SEARCH_FOLDER = "com.fsck.k9.search_folder"; - private static final String EXTRA_QUERY_FLAGS = "queryFlags"; - private static final String EXTRA_FORBIDDEN_FLAGS = "forbiddenFlags"; - private static final String EXTRA_INTEGRATE = "integrate"; - private static final String EXTRA_ACCOUNT_UUIDS = "accountUuids"; - private static final String EXTRA_FOLDER_NAMES = "folderNames"; - private static final String EXTRA_TITLE = "title"; - - public static void actionHandleFolder(Context context, Account account, String folder) { - Intent intent = new Intent(context, MessageList.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); - intent.putExtra(EXTRA_ACCOUNT, account.getUuid()); - - if (folder != null) { - intent.putExtra(EXTRA_FOLDER, folder); - } - context.startActivity(intent); + public static void actionDisplaySearch(Context context, SearchSpecification search, + boolean noThreading, boolean newTask) { + actionDisplaySearch(context, search, noThreading, newTask, true); } - public static Intent actionHandleFolderIntent(Context context, Account account, String folder) { - Intent intent = new Intent(context, MessageList.class); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP | - Intent.FLAG_ACTIVITY_SINGLE_TOP); - intent.putExtra(EXTRA_ACCOUNT, account.getUuid()); - - if (folder != null) { - intent.putExtra(EXTRA_FOLDER, folder); - } - return intent; + public static void actionDisplaySearch(Context context, SearchSpecification search, + boolean noThreading, boolean newTask, boolean clearTop) { + context.startActivity( + intentDisplaySearch(context, search, noThreading, newTask, clearTop)); } - public static void actionHandle(Context context, String title, String queryString, boolean integrate, Flag[] flags, Flag[] forbiddenFlags) { + public static Intent intentDisplaySearch(Context context, SearchSpecification search, + boolean noThreading, boolean newTask, boolean clearTop) { Intent intent = new Intent(context, MessageList.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); - intent.putExtra(SearchManager.QUERY, queryString); - if (flags != null) { - intent.putExtra(EXTRA_QUERY_FLAGS, Utility.combine(flags, ',')); - } - if (forbiddenFlags != null) { - intent.putExtra(EXTRA_FORBIDDEN_FLAGS, Utility.combine(forbiddenFlags, ',')); - } - intent.putExtra(EXTRA_INTEGRATE, integrate); - intent.putExtra(EXTRA_TITLE, title); - context.startActivity(intent); - } + intent.putExtra(EXTRA_SEARCH, search); + intent.putExtra(EXTRA_NO_THREADING, noThreading); - /** - * Creates and returns an intent that opens Unified Inbox or All Messages screen. - */ - public static Intent actionHandleAccountIntent(Context context, String title, - SearchSpecification searchSpecification) { - Intent intent = new Intent(context, MessageList.class); - intent.putExtra(SearchManager.QUERY, searchSpecification.getQuery()); - if (searchSpecification.getRequiredFlags() != null) { - intent.putExtra(EXTRA_QUERY_FLAGS, Utility.combine(searchSpecification.getRequiredFlags(), ',')); + if (clearTop) { + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); } - if (searchSpecification.getForbiddenFlags() != null) { - intent.putExtra(EXTRA_FORBIDDEN_FLAGS, Utility.combine(searchSpecification.getForbiddenFlags(), ',')); + if (newTask) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } - intent.putExtra(EXTRA_INTEGRATE, searchSpecification.isIntegrate()); - intent.putExtra(EXTRA_ACCOUNT_UUIDS, searchSpecification.getAccountUuids()); - intent.putExtra(EXTRA_FOLDER_NAMES, searchSpecification.getFolderNames()); - intent.putExtra(EXTRA_TITLE, title); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); return intent; } - public static void actionHandle(Context context, String title, - SearchSpecification searchSpecification) { - Intent intent = actionHandleAccountIntent(context, title, searchSpecification); - context.startActivity(intent); - } - private StorageManager.StorageListener mStorageListener = new StorageListenerImplementation(); @@ -131,31 +89,28 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme private TextView mActionBarTitle; private TextView mActionBarSubTitle; private TextView mActionBarUnread; - private String mTitle; private Menu mMenu; private MessageListFragment mMessageListFragment; private Account mAccount; - private String mQueryString; private String mFolderName; - private Flag[] mQueryFlags; - private Flag[] mForbiddenFlags; - private String mSearchAccount = null; - private String mSearchFolder = null; - private boolean mIntegrate; - private String[] mAccountUuids; - private String[] mFolderNames; + private LocalSearch mSearch; + private boolean mSingleFolderMode; + private boolean mSingleAccountMode; + /** + * {@code true} if the message list should be displayed as flat list (i.e. no threading) + * regardless whether or not message threading was enabled in the settings. This is used for + * filtered views, e.g. when only displaying the unread messages in a folder. + */ + private boolean mNoThreading; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.message_list); - // need this for actionbar initialization - mQueryString = getIntent().getStringExtra(SearchManager.QUERY); - mActionBar = getSupportActionBar(); initializeActionBar(); @@ -171,76 +126,63 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme if (mMessageListFragment == null) { FragmentTransaction ft = fragmentManager.beginTransaction(); - if (mQueryString == null) { - mMessageListFragment = MessageListFragment.newInstance(mAccount, mFolderName); - } else if (mSearchAccount != null) { - mMessageListFragment = MessageListFragment.newInstance(mSearchAccount, - mSearchFolder, mQueryString, false); - } else { - mMessageListFragment = MessageListFragment.newInstance(mTitle, mAccountUuids, - mFolderNames, mQueryString, mQueryFlags, mForbiddenFlags, mIntegrate); - } + mMessageListFragment = MessageListFragment.newInstance(mSearch, + (K9.isThreadedViewEnabled() && !mNoThreading)); ft.add(R.id.message_list_container, mMessageListFragment); ft.commit(); } } private void decodeExtras(Intent intent) { - mQueryString = intent.getStringExtra(SearchManager.QUERY); - mFolderName = null; - mSearchAccount = null; - mSearchFolder = null; - if (mQueryString != null) { + // check if this intent comes from the system search ( remote ) + if (intent.getStringExtra(SearchManager.QUERY) != null) { if (Intent.ACTION_SEARCH.equals(intent.getAction())) { //Query was received from Search Dialog + String query = intent.getStringExtra(SearchManager.QUERY); + + mSearch = new LocalSearch(getString(R.string.search_results)); + mSearch.setManualSearch(true); + mNoThreading = true; + + mSearch.or(new SearchCondition(Searchfield.SENDER, Attribute.CONTAINS, query)); + mSearch.or(new SearchCondition(Searchfield.SUBJECT, Attribute.CONTAINS, query)); + mSearch.or(new SearchCondition(Searchfield.MESSAGE_CONTENTS, Attribute.CONTAINS, query)); + Bundle appData = getIntent().getBundleExtra(SearchManager.APP_DATA); if (appData != null) { - mSearchAccount = appData.getString(EXTRA_SEARCH_ACCOUNT); - mSearchFolder = appData.getString(EXTRA_SEARCH_FOLDER); + mSearch.addAccountUuid(appData.getString(EXTRA_SEARCH_ACCOUNT)); + mSearch.addAllowedFolder(appData.getString(EXTRA_SEARCH_FOLDER)); + } else { + mSearch.addAccountUuid(LocalSearch.ALL_ACCOUNTS); } - } else { - mSearchAccount = intent.getStringExtra(EXTRA_SEARCH_ACCOUNT); - mSearchFolder = intent.getStringExtra(EXTRA_SEARCH_FOLDER); + } + } else { + // regular LocalSearch object was passed + mSearch = intent.getParcelableExtra(EXTRA_SEARCH); + mNoThreading = intent.getBooleanExtra(EXTRA_NO_THREADING, false); + } + + String[] accountUuids = mSearch.getAccountUuids(); + mSingleAccountMode = (accountUuids.length == 1 && !mSearch.searchAllAccounts()); + mSingleFolderMode = mSingleAccountMode && (mSearch.getFolderNames().size() == 1); + + if (mSingleAccountMode) { + Preferences prefs = Preferences.getPreferences(getApplicationContext()); + mAccount = prefs.getAccount(accountUuids[0]); + + if (mAccount != null && !mAccount.isAvailable(this)) { + Log.i(K9.LOG_TAG, "not opening MessageList of unavailable account"); + onAccountUnavailable(); + return; } } - String accountUuid = intent.getStringExtra(EXTRA_ACCOUNT); - mFolderName = intent.getStringExtra(EXTRA_FOLDER); - - mAccount = Preferences.getPreferences(this).getAccount(accountUuid); - - if (mAccount != null && !mAccount.isAvailable(this)) { - Log.i(K9.LOG_TAG, "not opening MessageList of unavailable account"); - onAccountUnavailable(); - return; + if (mSingleFolderMode) { + mFolderName = mSearch.getFolderNames().get(0); } - String queryFlags = intent.getStringExtra(EXTRA_QUERY_FLAGS); - if (queryFlags != null) { - String[] flagStrings = queryFlags.split(","); - mQueryFlags = new Flag[flagStrings.length]; - for (int i = 0; i < flagStrings.length; i++) { - mQueryFlags[i] = Flag.valueOf(flagStrings[i]); - } - } - String forbiddenFlags = intent.getStringExtra(EXTRA_FORBIDDEN_FLAGS); - if (forbiddenFlags != null) { - String[] flagStrings = forbiddenFlags.split(","); - mForbiddenFlags = new Flag[flagStrings.length]; - for (int i = 0; i < flagStrings.length; i++) { - mForbiddenFlags[i] = Flag.valueOf(flagStrings[i]); - } - } - mIntegrate = intent.getBooleanExtra(EXTRA_INTEGRATE, false); - mAccountUuids = intent.getStringArrayExtra(EXTRA_ACCOUNT_UUIDS); - mFolderNames = intent.getStringArrayExtra(EXTRA_FOLDER_NAMES); - mTitle = intent.getStringExtra(EXTRA_TITLE); - - // Take the initial folder into account only if we are *not* restoring - // the activity already. - if (mFolderName == null && mQueryString == null) { - mFolderName = mAccount.getAutoExpandFolderName(); - } + // now we know if we are in single account mode and need a subtitle + mActionBarSubTitle.setVisibility((!mSingleFolderMode) ? View.GONE : View.VISIBLE); } @Override @@ -276,10 +218,6 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme mActionBarSubTitle = (TextView) customView.findViewById(R.id.actionbar_title_sub); mActionBarUnread = (TextView) customView.findViewById(R.id.actionbar_unread_count); - if (mQueryString != null) { - mActionBarSubTitle.setVisibility(View.GONE); - } - mActionBar.setDisplayHomeAsUpEnabled(true); } @@ -407,15 +345,9 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme FragmentManager fragmentManager = getSupportFragmentManager(); if (fragmentManager.getBackStackEntryCount() > 0) { fragmentManager.popBackStack(); - } else if (mIntegrate) { - // If we were in one of the integrated mailboxes (think All Mail or Integrated Inbox), then - // go to accounts. - onAccounts(); - } else if (mQueryString != null) { - // We did a search of some sort. Go back to wherever the user searched from. + } else if (!mSingleFolderMode || mMessageListFragment.isManualSearch()) { onBackPressed(); } else { - // In a standard message list of a folder. Go to folder list. onShowFolderList(); } return true; @@ -440,10 +372,10 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme mMessageListFragment.changeSort(SortType.SORT_SUBJECT); return true; } - case R.id.set_sort_sender: { - mMessageListFragment.changeSort(SortType.SORT_SENDER); - return true; - } +// case R.id.set_sort_sender: { +// mMessageListFragment.changeSort(SortType.SORT_SENDER); +// return true; +// } case R.id.set_sort_flag: { mMessageListFragment.changeSort(SortType.SORT_FLAGGED); return true; @@ -464,6 +396,10 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme onEditPrefs(); return true; } + case R.id.account_settings: { + onEditAccount(); + return true; + } case R.id.search: { mMessageListFragment.onSearchRequested(); return true; @@ -474,7 +410,7 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme } } - if (mQueryString != null) { + if (!mSingleFolderMode) { // None of the options after this point are "safe" for search results //TODO: This is not true for "unread" and "starred" searches in regular folders return false; @@ -491,10 +427,6 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme } return true; } - case R.id.account_settings: { - onEditAccount(); - return true; - } case R.id.expunge: { mMessageListFragment.onExpunge(); return true; @@ -539,22 +471,14 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme menu.findItem(R.id.select_all).setVisible(true); menu.findItem(R.id.settings).setVisible(true); - if (mMessageListFragment.isSearchQuery()) { + if (!mSingleAccountMode) { menu.findItem(R.id.expunge).setVisible(false); menu.findItem(R.id.check_mail).setVisible(false); menu.findItem(R.id.send_messages).setVisible(false); menu.findItem(R.id.folder_settings).setVisible(false); menu.findItem(R.id.account_settings).setVisible(false); - - // If this is an explicit local search, show the option to search the cloud. - if (!mMessageListFragment.isRemoteSearch() && - mMessageListFragment.isRemoteSearchAllowed()) { - menu.findItem(R.id.search_remote).setVisible(true); - } - } else { - menu.findItem(R.id.search).setVisible(true); - menu.findItem(R.id.folder_settings).setVisible(true); + menu.findItem(R.id.folder_settings).setVisible(mSingleFolderMode); menu.findItem(R.id.account_settings).setVisible(true); if (mMessageListFragment.isOutbox()) { @@ -571,6 +495,14 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme menu.findItem(R.id.expunge).setVisible(false); } } + + // If this is an explicit local search, show the option to search the cloud. + if (!mMessageListFragment.isRemoteSearch() && + mMessageListFragment.isRemoteSearchAllowed()) { + menu.findItem(R.id.search_remote).setVisible(true); + } else if (!mMessageListFragment.isManualSearch()) { + menu.findItem(R.id.search).setVisible(true); + } } } @@ -672,8 +604,11 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme @Override public void showMoreFromSameSender(String senderAddress) { - MessageListFragment fragment = MessageListFragment.newInstance("From " + senderAddress, - null, null, senderAddress, null, null, false); + LocalSearch tmpSearch = new LocalSearch("From " + senderAddress); + tmpSearch.addAccountUuids(mSearch.getAccountUuids()); + tmpSearch.and(Searchfield.SENDER, senderAddress, Attribute.CONTAINS); + + MessageListFragment fragment = MessageListFragment.newInstance(tmpSearch, false); addMessageListFragment(fragment, true); } @@ -720,13 +655,6 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme } } - public void remoteSearch(String searchAccount, String searchFolder, String queryString) { - MessageListFragment fragment = MessageListFragment.newInstance(searchAccount, searchFolder, - queryString, true); - mMenu.findItem(R.id.search_remote).setVisible(false); - addMessageListFragment(fragment, false); - } - private void addMessageListFragment(MessageListFragment fragment, boolean addToBackStack) { FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); @@ -754,4 +682,21 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme return true; } + + @Override + public void showThread(Account account, String folderName, long threadRootId) { + LocalSearch tmpSearch = new LocalSearch(); + tmpSearch.addAccountUuid(account.getUuid()); + tmpSearch.and(Searchfield.THREAD_ROOT, String.valueOf(threadRootId), Attribute.EQUALS); + tmpSearch.or(new SearchCondition(Searchfield.ID, Attribute.EQUALS, String.valueOf(threadRootId))); + + MessageListFragment fragment = MessageListFragment.newInstance(tmpSearch, false); + addMessageListFragment(fragment, true); + } + + @Override + public void remoteSearchStarted() { + // Remove action button for remote search + configureMenu(mMenu); + } } diff --git a/src/com/fsck/k9/activity/SearchModifier.java b/src/com/fsck/k9/activity/SearchModifier.java deleted file mode 100644 index 8b610ff6e..000000000 --- a/src/com/fsck/k9/activity/SearchModifier.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.fsck.k9.activity; - -import com.fsck.k9.R; -import com.fsck.k9.mail.Flag; - -/** - * This enum represents filtering parameters used by {@link com.fsck.k9.SearchAccount}. - */ -enum SearchModifier { - FLAGGED(R.string.flagged_modifier, new Flag[]{Flag.FLAGGED}, null), - UNREAD(R.string.unread_modifier, null, new Flag[]{Flag.SEEN}); - - final int resId; - final Flag[] requiredFlags; - final Flag[] forbiddenFlags; - - SearchModifier(int nResId, Flag[] nRequiredFlags, Flag[] nForbiddenFlags) { - resId = nResId; - requiredFlags = nRequiredFlags; - forbiddenFlags = nForbiddenFlags; - } - -} \ No newline at end of file diff --git a/src/com/fsck/k9/activity/setup/AccountSettings.java b/src/com/fsck/k9/activity/setup/AccountSettings.java index c50836d8d..8c29fc2e8 100644 --- a/src/com/fsck/k9/activity/setup/AccountSettings.java +++ b/src/com/fsck/k9/activity/setup/AccountSettings.java @@ -499,6 +499,7 @@ public class AccountSettings extends K9PreferenceActivity { // IMAP-specific preferences mSearchScreen = (PreferenceScreen) findPreference(PREFERENCE_SCREEN_SEARCH); + mCloudSearchEnabled = (CheckBoxPreference) findPreference(PREFERENCE_CLOUD_SEARCH_ENABLED); mRemoteSearchNumResults = (ListPreference) findPreference(PREFERENCE_REMOTE_SEARCH_NUM_RESULTS); mRemoteSearchNumResults.setOnPreferenceChangeListener( diff --git a/src/com/fsck/k9/activity/setup/Prefs.java b/src/com/fsck/k9/activity/setup/Prefs.java index c878d3f62..0175765e1 100644 --- a/src/com/fsck/k9/activity/setup/Prefs.java +++ b/src/com/fsck/k9/activity/setup/Prefs.java @@ -87,8 +87,11 @@ public class Prefs extends K9PreferenceActivity { private static final String PREFERENCE_ATTACHMENT_DEF_PATH = "attachment_default_path"; private static final String PREFERENCE_BACKGROUND_AS_UNREAD_INDICATOR = "messagelist_background_as_unread_indicator"; + private static final String PREFERENCE_THREADED_VIEW = "threaded_view"; private static final int ACTIVITY_CHOOSE_FOLDER = 1; + + private ListPreference mLanguage; private ListPreference mTheme; private ListPreference mDateFormat; @@ -128,6 +131,8 @@ public class Prefs extends K9PreferenceActivity { private CheckBoxPreference mBatchButtonsFlag; private CheckBoxPreference mBatchButtonsUnselect; private CheckBoxPreference mBackgroundAsUnreadIndicator; + private CheckBoxPreference mThreadedView; + public static void actionPrefs(Context context) { Intent i = new Intent(context, Prefs.class); @@ -235,6 +240,10 @@ public class Prefs extends K9PreferenceActivity { mChangeContactNameColor = (CheckBoxPreference)findPreference(PREFERENCE_MESSAGELIST_CONTACT_NAME_COLOR); mChangeContactNameColor.setChecked(K9.changeContactNameColor()); + + mThreadedView = (CheckBoxPreference) findPreference(PREFERENCE_THREADED_VIEW); + mThreadedView.setChecked(K9.isThreadedViewEnabled()); + if (K9.changeContactNameColor()) { mChangeContactNameColor.setSummary(R.string.global_settings_registered_name_color_changed); } else { @@ -418,6 +427,7 @@ public class Prefs extends K9PreferenceActivity { K9.setMessageListSenderAboveSubject(mSenderAboveSubject.isChecked()); K9.setShowContactName(mShowContactName.isChecked()); K9.setUseBackgroundAsUnreadIndicator(mBackgroundAsUnreadIndicator.isChecked()); + K9.setThreadedViewEnabled(mThreadedView.isChecked()); K9.setChangeContactNameColor(mChangeContactNameColor.isChecked()); K9.setMessageViewFixedWidthFont(mFixedWidth.isChecked()); K9.setMessageViewReturnToList(mReturnToList.isChecked()); diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index d5860a4c3..d23d5799c 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -40,11 +40,9 @@ import com.fsck.k9.K9.Intents; import com.fsck.k9.NotificationSetting; import com.fsck.k9.Preferences; import com.fsck.k9.R; -import com.fsck.k9.SearchSpecification; import com.fsck.k9.activity.FolderList; import com.fsck.k9.activity.MessageList; import com.fsck.k9.helper.NotificationBuilder; -import com.fsck.k9.helper.Utility; import com.fsck.k9.helper.power.TracingPowerManager; import com.fsck.k9.helper.power.TracingPowerManager.TracingWakeLock; import com.fsck.k9.mail.Address; @@ -70,6 +68,8 @@ import com.fsck.k9.mail.store.LocalStore.LocalMessage; import com.fsck.k9.mail.store.LocalStore.PendingCommand; import com.fsck.k9.mail.store.UnavailableAccountException; import com.fsck.k9.mail.store.UnavailableStorageException; +import com.fsck.k9.search.LocalSearch; +import com.fsck.k9.search.SearchSpecification; /** @@ -509,235 +509,39 @@ public class MessagingController implements Runnable { }); } - - - /** - * List the messages in the local message store for the given folder asynchronously. - * - * @param account - * @param folder - * @param listener - * @throws MessagingException - */ - public void listLocalMessages(final Account account, final String folder, final MessagingListener listener) { - threadPool.execute(new Runnable() { - @Override - public void run() { - listLocalMessagesSynchronous(account, folder, listener); - } - }); - } - - - /** - * List the messages in the local message store for the given folder synchronously. - * - * @param account - * @param folder - * @param listener - * @throws MessagingException - */ - public void listLocalMessagesSynchronous(final Account account, final String folder, final MessagingListener listener) { - - for (MessagingListener l : getListeners(listener)) { - l.listLocalMessagesStarted(account, folder); - } - - LocalFolder localFolder = null; - MessageRetrievalListener retrievalListener = - new MessageRetrievalListener() { - List pendingMessages = new ArrayList(); - - - @Override - public void messageStarted(String message, int number, int ofTotal) {} - @Override - public void messageFinished(Message message, int number, int ofTotal) { - - if (!isMessageSuppressed(account, folder, message)) { - pendingMessages.add(message); - if (pendingMessages.size() > 10) { - addPendingMessages(); - } - - } else { - for (MessagingListener l : getListeners(listener)) { - l.listLocalMessagesRemoveMessage(account, folder, message); - } - } - } - @Override - public void messagesFinished(int number) { - addPendingMessages(); - } - private void addPendingMessages() { - for (MessagingListener l : getListeners(listener)) { - l.listLocalMessagesAddMessages(account, folder, pendingMessages); - } - pendingMessages.clear(); - } - }; - - - - try { - LocalStore localStore = account.getLocalStore(); - localFolder = localStore.getFolder(folder); - localFolder.open(OpenMode.READ_WRITE); - - //Purging followed by getting requires 2 DB queries. - //TODO: Fix getMessages to allow auto-pruning at visible limit? - localFolder.purgeToVisibleLimit(null); - localFolder.getMessages( - retrievalListener, - false // Skip deleted messages - ); - if (K9.DEBUG) - Log.v(K9.LOG_TAG, "Got ack that callbackRunner finished"); - - for (MessagingListener l : getListeners(listener)) { - l.listLocalMessagesFinished(account, folder); - } - } catch (Exception e) { - for (MessagingListener l : getListeners(listener)) { - l.listLocalMessagesFailed(account, folder, e.getMessage()); - } - addErrorMessage(account, null, e); - } finally { - closeFolder(localFolder); - } - } - - public void searchLocalMessages(SearchSpecification searchSpecification, final Message[] messages, final MessagingListener listener) { - searchLocalMessages(searchSpecification.getAccountUuids(), searchSpecification.getFolderNames(), messages, - searchSpecification.getQuery(), searchSpecification.isIntegrate(), searchSpecification.getRequiredFlags(), searchSpecification.getForbiddenFlags(), listener); - } - - /** * Find all messages in any local account which match the query 'query' * @throws MessagingException */ - public void searchLocalMessages(final String[] accountUuids, final String[] folderNames, final Message[] messages, final String query, final boolean integrate, - final Flag[] requiredFlags, final Flag[] forbiddenFlags, final MessagingListener listener) { - if (K9.DEBUG) { - Log.i(K9.LOG_TAG, "searchLocalMessages (" - + "accountUuids=" + Utility.combine(accountUuids, ',') - + ", folderNames = " + Utility.combine(folderNames, ',') - + ", messages.size() = " + (messages != null ? messages.length : -1) - + ", query = " + query - + ", integrate = " + integrate - + ", requiredFlags = " + Utility.combine(requiredFlags, ',') - + ", forbiddenFlags = " + Utility.combine(forbiddenFlags, ',') - + ")"); - } - + public void searchLocalMessages(final LocalSearch search, final MessagingListener listener) { threadPool.execute(new Runnable() { @Override public void run() { - searchLocalMessagesSynchronous(accountUuids, folderNames, messages, query, integrate, requiredFlags, forbiddenFlags, listener); + searchLocalMessagesSynchronous(search, listener); } }); } - public void searchLocalMessagesSynchronous(final String[] accountUuids, final String[] folderNames, final Message[] messages, final String query, final boolean integrate, final Flag[] requiredFlags, final Flag[] forbiddenFlags, final MessagingListener listener) { + public void searchLocalMessagesSynchronous(final LocalSearch search, final MessagingListener listener) { final AccountStats stats = new AccountStats(); - final Set accountUuidsSet = new HashSet(); - if (accountUuids != null) { - accountUuidsSet.addAll(Arrays.asList(accountUuids)); - } - final Preferences prefs = Preferences.getPreferences(mApplication.getApplicationContext()); - List foldersToSearch = null; - boolean displayableOnly = false; - boolean noSpecialFolders = true; - for (final Account account : prefs.getAvailableAccounts()) { - if (accountUuids != null && !accountUuidsSet.contains(account.getUuid())) { + final HashSet uuidSet = new HashSet(Arrays.asList(search.getAccountUuids())); + Account[] accounts = Preferences.getPreferences(mApplication.getApplicationContext()).getAccounts(); + boolean allAccounts = uuidSet.contains(SearchSpecification.ALL_ACCOUNTS); + + // for every account we want to search do the query in the localstore + for (final Account account : accounts) { + + if (!allAccounts && !uuidSet.contains(account.getUuid())) { continue; } - if (accountUuids != null && accountUuidsSet.contains(account.getUuid())) { - displayableOnly = true; - noSpecialFolders = true; - } else if (!integrate && folderNames == null) { - Account.Searchable searchableFolders = account.getSearchableFolders(); - switch (searchableFolders) { - case NONE: - continue; - case DISPLAYABLE: - displayableOnly = true; - break; - - } - } - List messagesToSearch = null; - if (messages != null) { - messagesToSearch = new LinkedList(); - for (Message message : messages) { - if (message.getFolder().getAccount().getUuid().equals(account.getUuid())) { - messagesToSearch.add(message); - } - } - if (messagesToSearch.isEmpty()) { - continue; - } - } - if (listener != null) { - listener.listLocalMessagesStarted(account, null); - } - - if (integrate || displayableOnly || folderNames != null || noSpecialFolders) { - List tmpFoldersToSearch = new LinkedList(); - try { - LocalStore store = account.getLocalStore(); - List folders = store.getPersonalNamespaces(false); - Set folderNameSet = null; - if (folderNames != null) { - folderNameSet = new HashSet(); - folderNameSet.addAll(Arrays.asList(folderNames)); - } - for (Folder folder : folders) { - LocalFolder localFolder = (LocalFolder)folder; - boolean include = true; - folder.refresh(prefs); - String localFolderName = localFolder.getName(); - if (integrate) { - include = localFolder.isIntegrate(); - } else { - if (folderNameSet != null) { - if (!folderNameSet.contains(localFolderName)) - - { - include = false; - } - } - // Never exclude the INBOX (see issue 1817) - else if (noSpecialFolders && !localFolderName.equalsIgnoreCase(account.getInboxFolderName()) && - !localFolderName.equals(account.getArchiveFolderName()) && account.isSpecialFolder(localFolderName)) { - include = false; - } else if (displayableOnly && modeMismatch(account.getFolderDisplayMode(), folder.getDisplayClass())) { - include = false; - } - } - - if (include) { - tmpFoldersToSearch.add(localFolder); - } - } - if (tmpFoldersToSearch.size() < 1) { - continue; - } - foldersToSearch = tmpFoldersToSearch; - } catch (MessagingException me) { - Log.e(K9.LOG_TAG, "Unable to restrict search folders in Account " + account.getDescription() + ", searching all", me); - addErrorMessage(account, null, me); - } - - } - + // Collecting statistics of the search result MessageRetrievalListener retrievalListener = new MessageRetrievalListener() { @Override public void messageStarted(String message, int number, int ofTotal) {} @Override + public void messagesFinished(int number) {} + @Override public void messageFinished(Message message, int number, int ofTotal) { if (!isMessageSuppressed(message.getFolder().getAccount(), message.getFolder().getName(), message)) { List messages = new ArrayList(); @@ -749,22 +553,18 @@ public class MessagingController implements Runnable { listener.listLocalMessagesAddMessages(account, null, messages); } } - - } - @Override - public void messagesFinished(int number) { - } }; - try { - String[] queryFields = {"html_content", "subject", "sender_list"}; - LocalStore localStore = account.getLocalStore(); - localStore.searchForMessages(retrievalListener, queryFields - , query, foldersToSearch, - messagesToSearch == null ? null : messagesToSearch.toArray(EMPTY_MESSAGE_ARRAY), - requiredFlags, forbiddenFlags); + // alert everyone the search has started + if (listener != null) { + listener.listLocalMessagesStarted(account, null); + } + // build and do the query in the localstore + try { + LocalStore localStore = account.getLocalStore(); + localStore.searchForMessages(retrievalListener, search); } catch (Exception e) { if (listener != null) { listener.listLocalMessagesFailed(account, null, e.getMessage()); @@ -776,6 +576,8 @@ public class MessagingController implements Runnable { } } } + + // publish the total search statistics if (listener != null) { listener.searchStats(stats); } @@ -824,24 +626,28 @@ public class MessagingController implements Runnable { } List messages = remoteFolder.search(query, requiredFlags, forbiddenFlags); - if (listener != null) { - listener.remoteSearchServerQueryComplete(acct, folderName, messages.size()); - } if (K9.DEBUG) { Log.i("Remote Search", "Remote search got " + messages.size() + " results"); } - Collections.sort(messages, new UidReverseComparator()); + // There's no need to fetch messages already completely downloaded + List remoteMessages = localFolder.extractNewMessages(messages); + messages.clear(); - - int resultLimit = acct.getRemoteSearchNumResults(); - if (resultLimit > 0 && messages.size() > resultLimit) { - extraResults = messages.subList(resultLimit, messages.size()); - messages = messages.subList(0, resultLimit); + if (listener != null) { + listener.remoteSearchServerQueryComplete(acct, folderName, remoteMessages.size()); } - loadSearchResultsSynchronous(messages, localFolder, remoteFolder, listener); + Collections.sort(remoteMessages, new UidReverseComparator()); + + int resultLimit = acct.getRemoteSearchNumResults(); + if (resultLimit > 0 && remoteMessages.size() > resultLimit) { + extraResults = remoteMessages.subList(resultLimit, remoteMessages.size()); + remoteMessages = remoteMessages.subList(0, resultLimit); + } + + loadSearchResultsSynchronous(remoteMessages, localFolder, remoteFolder, listener); } catch (Exception e) { @@ -2753,20 +2559,39 @@ public class MessagingController implements Runnable { processPendingCommands(account); } - public void setFlag( - final Message[] messages, - final Flag flag, - final boolean newState) { + public void setFlag(final List messages, final Flag flag, final boolean newState) { + actOnMessages(messages, new MessageActor() { @Override public void act(final Account account, final Folder folder, - final List messages) { - setFlag(account, folder.getName(), messages.toArray(EMPTY_MESSAGE_ARRAY), flag, + final List accountMessages) { + + setFlag(account, folder.getName(), accountMessages.toArray(EMPTY_MESSAGE_ARRAY), flag, newState); } - }); + } + public void setFlagForThreads(final List messages, final Flag flag, + final boolean newState) { + + actOnMessages(messages, new MessageActor() { + @Override + public void act(final Account account, final Folder folder, + final List accountMessages) { + + try { + List messagesInThreads = collectMessagesInThreads(account, + accountMessages); + + setFlag(account, folder.getName(), + messagesInThreads.toArray(EMPTY_MESSAGE_ARRAY), flag, newState); + + } catch (MessagingException e) { + addErrorMessage(account, "Something went wrong in setFlagForThreads()", e); + } + } + }); } /** @@ -3023,14 +2848,15 @@ public class MessagingController implements Runnable { false, true)) { if (account.isMarkMessageAsReadOnView() && !message.isSet(Flag.SEEN)) { message.setFlag(Flag.SEEN, true); - setFlag(new Message[] { message }, Flag.SEEN, true); + setFlag(Collections.singletonList((Message) message), + Flag.SEEN, true); } } return; } if (!message.isSet(Flag.SEEN)) { message.setFlag(Flag.SEEN, true); - setFlag(new Message[] { message }, Flag.SEEN, true); + setFlag(Collections.singletonList((Message) message), Flag.SEEN, true); } for (MessagingListener l : getListeners(listener)) { @@ -3247,8 +3073,11 @@ public class MessagingController implements Runnable { builder.setContentTitle(mApplication.getString(R.string.notification_bg_send_title)); builder.setContentText(account.getDescription()); - Intent intent = MessageList.actionHandleFolderIntent(mApplication, account, - account.getInboxFolderName()); + LocalSearch search = new LocalSearch(account.getInboxFolderName()); + search.addAllowedFolder(account.getInboxFolderName()); + search.addAccountUuid(account.getUuid()); + Intent intent = MessageList.intentDisplaySearch(mApplication, search, false, true, true); + PendingIntent pi = PendingIntent.getActivity(mApplication, 0, intent, 0); builder.setContentIntent(pi); @@ -3330,8 +3159,11 @@ public class MessagingController implements Runnable { mApplication.getString(R.string.notification_bg_title_separator) + folder.getName()); - Intent intent = MessageList.actionHandleFolderIntent(mApplication, account, - account.getInboxFolderName()); + LocalSearch search = new LocalSearch(account.getInboxFolderName()); + search.addAllowedFolder(account.getInboxFolderName()); + search.addAccountUuid(account.getUuid()); + Intent intent = MessageList.intentDisplaySearch(mApplication, search, false, true, true); + PendingIntent pi = PendingIntent.getActivity(mApplication, 0, intent, 0); builder.setContentIntent(pi); @@ -3597,40 +3429,90 @@ public class MessagingController implements Runnable { return false; } } - public void moveMessages(final Account account, final String srcFolder, final Message[] messages, final String destFolder, - final MessagingListener listener) { + public void moveMessages(final Account account, final String srcFolder, + final List messages, final String destFolder, + final MessagingListener listener) { + for (Message message : messages) { suppressMessage(account, srcFolder, message); } + putBackground("moveMessages", null, new Runnable() { @Override public void run() { - moveOrCopyMessageSynchronous(account, srcFolder, messages, destFolder, false, listener); + moveOrCopyMessageSynchronous(account, srcFolder, messages, destFolder, false, + listener); } }); } - public void moveMessage(final Account account, final String srcFolder, final Message message, final String destFolder, - final MessagingListener listener) { - moveMessages(account, srcFolder, new Message[] { message }, destFolder, listener); + public void moveMessagesInThread(final Account account, final String srcFolder, + final List messages, final String destFolder) { + + for (Message message : messages) { + suppressMessage(account, srcFolder, message); + } + + putBackground("moveMessagesInThread", null, new Runnable() { + @Override + public void run() { + try { + List messagesInThreads = collectMessagesInThreads(account, messages); + moveOrCopyMessageSynchronous(account, srcFolder, messagesInThreads, destFolder, + false, null); + } catch (MessagingException e) { + addErrorMessage(account, "Exception while moving messages", e); + } + } + }); } - public void copyMessages(final Account account, final String srcFolder, final Message[] messages, final String destFolder, - final MessagingListener listener) { + public void moveMessage(final Account account, final String srcFolder, final Message message, + final String destFolder, final MessagingListener listener) { + + moveMessages(account, srcFolder, Collections.singletonList(message), destFolder, listener); + } + + public void copyMessages(final Account account, final String srcFolder, + final List messages, final String destFolder, + final MessagingListener listener) { + putBackground("copyMessages", null, new Runnable() { @Override public void run() { - moveOrCopyMessageSynchronous(account, srcFolder, messages, destFolder, true, listener); + moveOrCopyMessageSynchronous(account, srcFolder, messages, destFolder, true, + listener); } }); } - public void copyMessage(final Account account, final String srcFolder, final Message message, final String destFolder, - final MessagingListener listener) { - copyMessages(account, srcFolder, new Message[] { message }, destFolder, listener); + + public void copyMessagesInThread(final Account account, final String srcFolder, + final List messages, final String destFolder) { + + putBackground("copyMessagesInThread", null, new Runnable() { + @Override + public void run() { + try { + List messagesInThreads = collectMessagesInThreads(account, messages); + moveOrCopyMessageSynchronous(account, srcFolder, messagesInThreads, destFolder, + true, null); + } catch (MessagingException e) { + addErrorMessage(account, "Exception while copying messages", e); + } + } + }); } - private void moveOrCopyMessageSynchronous(final Account account, final String srcFolder, final Message[] inMessages, - final String destFolder, final boolean isCopy, MessagingListener listener) { + public void copyMessage(final Account account, final String srcFolder, final Message message, + final String destFolder, final MessagingListener listener) { + + copyMessages(account, srcFolder, Collections.singletonList(message), destFolder, listener); + } + + private void moveOrCopyMessageSynchronous(final Account account, final String srcFolder, + final List inMessages, final String destFolder, final boolean isCopy, + MessagingListener listener) { + try { Map uidMap = new HashMap(); Store localStore = account.getLocalStore(); @@ -3741,7 +3623,7 @@ public class MessagingController implements Runnable { if (uid != null) { Message message = localFolder.getMessage(uid); if (message != null) { - deleteMessages(new Message[] { message }, null); + deleteMessages(Collections.singletonList(message), null); } } } catch (MessagingException me) { @@ -3751,20 +3633,72 @@ public class MessagingController implements Runnable { } } - public void deleteMessages(final Message[] messages, final MessagingListener listener) { + public void deleteThreads(final List messages) { actOnMessages(messages, new MessageActor() { @Override public void act(final Account account, final Folder folder, - final List messages) { - for (Message message : messages) { + final List accountMessages) { + + for (Message message : accountMessages) { + suppressMessage(account, folder.getName(), message); + } + + putBackground("deleteThreads", null, new Runnable() { + @Override + public void run() { + deleteThreadsSynchronous(account, folder.getName(), accountMessages); + } + }); + } + }); + } + + public void deleteThreadsSynchronous(Account account, String folderName, + List messages) { + + try { + List messagesToDelete = collectMessagesInThreads(account, messages); + + deleteMessagesSynchronous(account, folderName, + messagesToDelete.toArray(EMPTY_MESSAGE_ARRAY), null); + } catch (MessagingException e) { + Log.e(K9.LOG_TAG, "Something went wrong while deleting threads", e); + } + } + + public List collectMessagesInThreads(Account account, List messages) + throws MessagingException { + + LocalStore localStore = account.getLocalStore(); + + List messagesInThreads = new ArrayList(); + for (Message message : messages) { + long rootId = ((LocalMessage) message).getRootId(); + long threadId = (rootId == -1) ? message.getId() : rootId; + + Message[] messagesInThread = localStore.getMessagesInThread(threadId); + Collections.addAll(messagesInThreads, messagesInThread); + } + + return messagesInThreads; + } + + public void deleteMessages(final List messages, final MessagingListener listener) { + actOnMessages(messages, new MessageActor() { + + @Override + public void act(final Account account, final Folder folder, + final List accountMessages) { + for (Message message : accountMessages) { suppressMessage(account, folder.getName(), message); } putBackground("deleteMessages", null, new Runnable() { @Override public void run() { - deleteMessagesSynchronous(account, folder.getName(), messages.toArray(EMPTY_MESSAGE_ARRAY), listener); + deleteMessagesSynchronous(account, folder.getName(), + accountMessages.toArray(EMPTY_MESSAGE_ARRAY), listener); } }); } @@ -5024,7 +4958,7 @@ public class MessagingController implements Runnable { } - private void actOnMessages(Message[] messages, MessageActor actor) { + private void actOnMessages(List messages, MessageActor actor) { Map>> accountMap = new HashMap>>(); for (Message message : messages) { diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java index d87b8356e..d5c38e193 100644 --- a/src/com/fsck/k9/fragment/MessageListFragment.java +++ b/src/com/fsck/k9/fragment/MessageListFragment.java @@ -4,30 +4,39 @@ import java.text.DateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.Date; import java.util.EnumMap; -import java.util.Iterator; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; import java.util.concurrent.Future; import android.app.Activity; -import android.app.SearchManager; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences.Editor; +import android.database.Cursor; import android.graphics.Color; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.net.ConnectivityManager; import android.net.NetworkInfo; +import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.support.v4.app.DialogFragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.app.LoaderManager.LoaderCallbacks; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v4.widget.CursorAdapter; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.style.AbsoluteSizeSpan; import android.text.style.ForegroundColorSpan; -import android.text.style.StyleSpan; import android.util.Log; import android.util.TypedValue; import android.view.ContextMenu; @@ -39,7 +48,6 @@ import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.OnItemClickListener; -import android.widget.BaseAdapter; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; @@ -64,75 +72,85 @@ import com.fsck.k9.R; import com.fsck.k9.activity.ActivityListener; import com.fsck.k9.activity.ChooseFolder; import com.fsck.k9.activity.FolderInfoHolder; -import com.fsck.k9.activity.MessageInfoHolder; import com.fsck.k9.activity.MessageReference; import com.fsck.k9.controller.MessagingController; -import com.fsck.k9.controller.MessagingListener; +import com.fsck.k9.fragment.ConfirmationDialogFragment; import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener; import com.fsck.k9.helper.MessageHelper; +import com.fsck.k9.helper.MergeCursorWithUniqueId; +import com.fsck.k9.helper.StringUtils; import com.fsck.k9.helper.Utility; +import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.Message; -import com.fsck.k9.mail.Store; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Folder.OpenMode; import com.fsck.k9.mail.store.LocalStore; import com.fsck.k9.mail.store.LocalStore.LocalFolder; +import com.fsck.k9.provider.EmailProvider; +import com.fsck.k9.provider.EmailProvider.MessageColumns; +import com.fsck.k9.provider.EmailProvider.SpecialColumns; +import com.fsck.k9.search.LocalSearch; +import com.fsck.k9.search.SearchSpecification; +import com.fsck.k9.search.SqlQueryBuilder; import com.handmark.pulltorefresh.library.PullToRefreshBase; import com.handmark.pulltorefresh.library.PullToRefreshListView; public class MessageListFragment extends SherlockFragment implements OnItemClickListener, - ConfirmationDialogFragmentListener { + ConfirmationDialogFragmentListener, LoaderCallbacks { - public static MessageListFragment newInstance(Account account, String folderName) { + private static final String[] THREADED_PROJECTION = { + MessageColumns.ID, + MessageColumns.UID, + MessageColumns.INTERNAL_DATE, + MessageColumns.SUBJECT, + MessageColumns.DATE, + MessageColumns.SENDER_LIST, + MessageColumns.TO_LIST, + MessageColumns.CC_LIST, + MessageColumns.FLAGS, + MessageColumns.ATTACHMENT_COUNT, + MessageColumns.FOLDER_ID, + MessageColumns.PREVIEW, + MessageColumns.THREAD_ROOT, + SpecialColumns.ACCOUNT_UUID, + SpecialColumns.FOLDER_NAME, + + MessageColumns.THREAD_COUNT, + }; + + private static final int ID_COLUMN = 0; + private static final int UID_COLUMN = 1; + private static final int INTERNAL_DATE_COLUMN = 2; + private static final int SUBJECT_COLUMN = 3; + private static final int DATE_COLUMN = 4; + private static final int SENDER_LIST_COLUMN = 5; + private static final int TO_LIST_COLUMN = 6; + private static final int CC_LIST_COLUMN = 7; + private static final int FLAGS_COLUMN = 8; + private static final int ATTACHMENT_COUNT_COLUMN = 9; + private static final int FOLDER_ID_COLUMN = 10; + private static final int PREVIEW_COLUMN = 11; + private static final int THREAD_ROOT_COLUMN = 12; + private static final int ACCOUNT_UUID_COLUMN = 13; + private static final int FOLDER_NAME_COLUMN = 14; + private static final int THREAD_COUNT_COLUMN = 15; + + private static final String[] PROJECTION = Utility.copyOf(THREADED_PROJECTION, + THREAD_COUNT_COLUMN); + + + public static MessageListFragment newInstance(LocalSearch search, boolean threadedList) { MessageListFragment fragment = new MessageListFragment(); - Bundle args = new Bundle(); - args.putString(ARG_ACCOUNT, account.getUuid()); - args.putString(ARG_FOLDER, folderName); + args.putParcelable(ARG_SEARCH, search); + args.putBoolean(ARG_THREADED_LIST, threadedList); fragment.setArguments(args); - return fragment; } - public static MessageListFragment newInstance(String title, String[] accountUuids, - String[] folderNames, String queryString, Flag[] flags, - Flag[] forbiddenFlags, boolean integrate) { - - MessageListFragment fragment = new MessageListFragment(); - - Bundle args = new Bundle(); - args.putStringArray(ARG_ACCOUNT_UUIDS, accountUuids); - args.putStringArray(ARG_FOLDER_NAMES, folderNames); - args.putString(ARG_QUERY, queryString); - if (flags != null) { - args.putString(ARG_QUERY_FLAGS, Utility.combine(flags, ',')); - } - if (forbiddenFlags != null) { - args.putString(ARG_FORBIDDEN_FLAGS, Utility.combine(forbiddenFlags, ',')); - } - args.putBoolean(ARG_INTEGRATE, integrate); - args.putString(ARG_TITLE, title); - fragment.setArguments(args); - - return fragment; - } - - public static MessageListFragment newInstance(String searchAccount, String searchFolder, - String queryString, boolean remoteSearch) { - MessageListFragment fragment = new MessageListFragment(); - - Bundle args = new Bundle(); - args.putString(ARG_SEARCH_ACCOUNT, searchAccount); - args.putString(ARG_SEARCH_FOLDER, searchFolder); - args.putString(ARG_QUERY, queryString); - args.putBoolean(ARG_REMOTE_SEARCH, remoteSearch); - fragment.setArguments(args); - - return fragment; - } - - /** * Reverses the result of a {@link Comparator}. * @@ -143,7 +161,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick /** * @param delegate - * Never null. + * Never {@code null}. */ public ReverseComparator(final Comparator delegate) { mDelegate = delegate; @@ -154,7 +172,6 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick // arg1 & 2 are mixed up, this is done on purpose return mDelegate.compare(object2, object1); } - } /** @@ -163,12 +180,11 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick * @param */ public static class ComparatorChain implements Comparator { - private List> mChain; /** * @param chain - * Comparator chain. Never null. + * Comparator chain. Never {@code null}. */ public ComparatorChain(final List> chain) { mChain = chain; @@ -185,129 +201,117 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick } return result; } - } - public static class AttachmentComparator implements Comparator { + public static class ReverseIdComparator implements Comparator { + private int mIdColumn = -1; @Override - public int compare(MessageInfoHolder object1, MessageInfoHolder object2) { - return (object1.message.hasAttachments() ? 0 : 1) - (object2.message.hasAttachments() ? 0 : 1); + public int compare(Cursor cursor1, Cursor cursor2) { + if (mIdColumn == -1) { + mIdColumn = cursor1.getColumnIndex("_id"); + } + long o1Id = cursor1.getLong(mIdColumn); + long o2Id = cursor2.getLong(mIdColumn); + return (o1Id > o2Id) ? -1 : 1; } - } - public static class FlaggedComparator implements Comparator { + public static class AttachmentComparator implements Comparator { @Override - public int compare(MessageInfoHolder object1, MessageInfoHolder object2) { - return (object1.flagged ? 0 : 1) - (object2.flagged ? 0 : 1); + public int compare(Cursor cursor1, Cursor cursor2) { + int o1HasAttachment = (cursor1.getInt(ATTACHMENT_COUNT_COLUMN) > 0) ? 0 : 1; + int o2HasAttachment = (cursor2.getInt(ATTACHMENT_COUNT_COLUMN) > 0) ? 0 : 1; + return o1HasAttachment - o2HasAttachment; } - } - public static class UnreadComparator implements Comparator { + public static class FlaggedComparator implements Comparator { @Override - public int compare(MessageInfoHolder object1, MessageInfoHolder object2) { - return (object1.read ? 1 : 0) - (object2.read ? 1 : 0); + public int compare(Cursor cursor1, Cursor cursor2) { + int o1IsFlagged = (cursor1.getString(FLAGS_COLUMN).contains("FLAGGED")) ? 0 : 1; + int o2IsFlagged = (cursor2.getString(FLAGS_COLUMN).contains("FLAGGED")) ? 0 : 1; + return o1IsFlagged - o2IsFlagged; } - } - public static class SenderComparator implements Comparator { + public static class UnreadComparator implements Comparator { @Override - public int compare(MessageInfoHolder object1, MessageInfoHolder object2) { - if (object1.compareCounterparty == null) { - return (object2.compareCounterparty == null ? 0 : 1); - } else if (object2.compareCounterparty == null) { + public int compare(Cursor cursor1, Cursor cursor2) { + int o1IsUnread = (cursor1.getString(FLAGS_COLUMN).contains("SEEN")) ? 1 : 0; + int o2IsUnread = (cursor2.getString(FLAGS_COLUMN).contains("SEEN")) ? 1 : 0; + return o1IsUnread - o2IsUnread; + } + } + + public static class DateComparator implements Comparator { + + @Override + public int compare(Cursor cursor1, Cursor cursor2) { + long o1Date = cursor1.getLong(DATE_COLUMN); + long o2Date = cursor2.getLong(DATE_COLUMN); + if (o1Date < o2Date) { + return -1; + } else if (o1Date == o2Date) { + return 0; + } else { + return 1; + } + } + } + + public static class ArrivalComparator implements Comparator { + + @Override + public int compare(Cursor cursor1, Cursor cursor2) { + long o1Date = cursor1.getLong(INTERNAL_DATE_COLUMN); + long o2Date = cursor2.getLong(INTERNAL_DATE_COLUMN); + if (o1Date == o2Date) { + return 0; + } else if (o1Date < o2Date) { return -1; } else { - return object1.compareCounterparty.toLowerCase().compareTo(object2.compareCounterparty.toLowerCase()); + return 1; } } - } - public static class DateComparator implements Comparator { + public static class SubjectComparator implements Comparator { @Override - public int compare(MessageInfoHolder object1, MessageInfoHolder object2) { - if (object1.compareDate == null) { - return (object2.compareDate == null ? 0 : 1); - } else if (object2.compareDate == null) { - return -1; - } else { - return object1.compareDate.compareTo(object2.compareDate); - } + public int compare(Cursor cursor1, Cursor cursor2) { + String subject1 = cursor1.getString(SUBJECT_COLUMN); + String subject2 = cursor2.getString(SUBJECT_COLUMN); + + return subject1.compareToIgnoreCase(subject2); } - } - public static class ArrivalComparator implements Comparator { - - @Override - public int compare(MessageInfoHolder object1, MessageInfoHolder object2) { - return object1.compareArrival.compareTo(object2.compareArrival); - } - - } - - public static class SubjectComparator implements Comparator { - - @Override - public int compare(MessageInfoHolder arg0, MessageInfoHolder arg1) { - // XXX doesn't respect the Comparator contract since it alters the compared object - if (arg0.compareSubject == null) { - arg0.compareSubject = Utility.stripSubject(arg0.message.getSubject()); - } - if (arg1.compareSubject == null) { - arg1.compareSubject = Utility.stripSubject(arg1.message.getSubject()); - } - return arg0.compareSubject.compareToIgnoreCase(arg1.compareSubject); - } - - } - - /** - * Immutable empty {@link Message} array - */ - private static final Message[] EMPTY_MESSAGE_ARRAY = new Message[0]; - private static final int ACTIVITY_CHOOSE_FOLDER_MOVE = 1; private static final int ACTIVITY_CHOOSE_FOLDER_COPY = 2; - private static final String ARG_ACCOUNT = "account"; - private static final String ARG_FOLDER = "folder"; - private static final String ARG_REMOTE_SEARCH = "remote_search"; - private static final String ARG_QUERY = "query"; - private static final String ARG_SEARCH_ACCOUNT = "search_account"; - private static final String ARG_SEARCH_FOLDER = "search_folder"; - private static final String ARG_QUERY_FLAGS = "queryFlags"; - private static final String ARG_FORBIDDEN_FLAGS = "forbiddenFlags"; - private static final String ARG_INTEGRATE = "integrate"; - private static final String ARG_ACCOUNT_UUIDS = "accountUuids"; - private static final String ARG_FOLDER_NAMES = "folderNames"; - private static final String ARG_TITLE = "title"; - + private static final String ARG_SEARCH = "searchObject"; + private static final String ARG_THREADED_LIST = "threadedList"; private static final String STATE_LIST_POSITION = "listPosition"; /** * Maps a {@link SortType} to a {@link Comparator} implementation. */ - private static final Map> SORT_COMPARATORS; + private static final Map> SORT_COMPARATORS; static { // fill the mapping at class time loading - final Map> map = new EnumMap>(SortType.class); + final Map> map = + new EnumMap>(SortType.class); map.put(SortType.SORT_ATTACHMENT, new AttachmentComparator()); map.put(SortType.SORT_DATE, new DateComparator()); map.put(SortType.SORT_ARRIVAL, new ArrivalComparator()); map.put(SortType.SORT_FLAGGED, new FlaggedComparator()); - map.put(SortType.SORT_SENDER, new SenderComparator()); map.put(SortType.SORT_SUBJECT, new SubjectComparator()); map.put(SortType.SORT_UNREAD, new UnreadComparator()); @@ -331,28 +335,25 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick private MessagingController mController; private Account mAccount; + private String[] mAccountUuids; private int mUnreadMessageCount = 0; + private Cursor[] mCursors; + private int mUniqueIdColumn; + /** - * Stores the name of the folder that we want to open as soon as possible - * after load. + * Stores the name of the folder that we want to open as soon as possible after load. */ private String mFolderName; - /** - * If we're doing a search, this contains the query string. - */ - private String mQueryString; - private Flag[] mQueryFlags = null; - private Flag[] mForbiddenFlags = null; - private boolean mRemoteSearch = false; - private String mSearchAccount = null; - private String mSearchFolder = null; + private boolean mRemoteSearchPerformed = false; private Future mRemoteSearchFuture = null; - private boolean mIntegrate = false; - private String[] mAccountUuids = null; - private String[] mFolderNames = null; + public List mExtraSearchResults; + private String mTitle; + private LocalSearch mSearch = null; + private boolean mSingleAccountMode; + private boolean mSingleFolderMode; private MessageListHandler mHandler = new MessageListHandler(); @@ -363,6 +364,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick private boolean mCheckboxes = true; private int mSelectedCount = 0; + private Set mSelected = new HashSet(); private FontSizes mFontSizes = K9.getFontSizes(); @@ -374,11 +376,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick private Boolean mHasConnectivity; /** - * Relevant messages for the current context when we have to remember the - * chosen messages between user interactions (eg. Selecting a folder for - * move operation) + * Relevant messages for the current context when we have to remember the chosen messages + * between user interactions (e.g. selecting a folder for move operation). */ - private List mActiveMessages; + private List mActiveMessages; /* package visibility for faster inner class access */ MessageHelper mMessageHelper; @@ -391,6 +392,14 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick private DateFormat mTimeFormat; + private boolean mThreadedList; + + + private Context mContext; + + private final ActivityListener mListener = new MessageListActivityListener(); + + private Preferences mPreferences; /** * This class is used to run operations that modify UI elements in the UI thread. @@ -402,25 +411,12 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick * perform the operation in the calling thread.

*/ class MessageListHandler extends Handler { - private static final int ACTION_REMOVE_MESSAGE = 1; - private static final int ACTION_RESET_UNREAD_COUNT = 2; - private static final int ACTION_SORT_MESSAGES = 3; - private static final int ACTION_FOLDER_LOADING = 4; - private static final int ACTION_REFRESH_TITLE = 5; - private static final int ACTION_PROGRESS = 6; + private static final int ACTION_FOLDER_LOADING = 1; + private static final int ACTION_REFRESH_TITLE = 2; + private static final int ACTION_PROGRESS = 3; + private static final int ACTION_REMOTE_SEARCH_FINISHED = 4; - public void removeMessage(MessageReference messageReference) { - android.os.Message msg = android.os.Message.obtain(this, ACTION_REMOVE_MESSAGE, - messageReference); - sendMessage(msg); - } - - public void sortMessages() { - android.os.Message msg = android.os.Message.obtain(this, ACTION_SORT_MESSAGES); - sendMessage(msg); - } - public void folderLoading(String folder, boolean loading) { android.os.Message msg = android.os.Message.obtain(this, ACTION_FOLDER_LOADING, (loading) ? 1 : 0, 0, folder); @@ -438,6 +434,11 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick sendMessage(msg); } + public void remoteSearchFinished() { + android.os.Message msg = android.os.Message.obtain(this, ACTION_REMOTE_SEARCH_FINISHED); + sendMessage(msg); + } + public void updateFooter(final String message, final boolean showProgress) { post(new Runnable() { @Override @@ -447,47 +448,23 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick }); } - public void changeMessageUid(final MessageReference ref, final String newUid) { - // Instead of explicitly creating a container to be able to pass both arguments in a - // Message we post a Runnable to the message queue. - post(new Runnable() { - @Override - public void run() { - mAdapter.changeMessageUid(ref, newUid); - } - }); - } - - public void addOrUpdateMessages(final Account account, final String folderName, - final List providedMessages, final boolean verifyAgainstSearch) { - // We copy the message list because it's later modified by MessagingController - final List messages = new ArrayList(providedMessages); - - post(new Runnable() { - @Override - public void run() { - mAdapter.addOrUpdateMessages(account, folderName, messages, - verifyAgainstSearch); - } - }); - } - @Override public void handleMessage(android.os.Message msg) { + // The following messages don't need an attached activity. switch (msg.what) { - case ACTION_REMOVE_MESSAGE: { - MessageReference messageReference = (MessageReference) msg.obj; - mAdapter.removeMessage(messageReference); - break; - } - case ACTION_RESET_UNREAD_COUNT: { - mAdapter.resetUnreadCount(); - break; - } - case ACTION_SORT_MESSAGES: { - mAdapter.sortMessages(); - break; + case ACTION_REMOTE_SEARCH_FINISHED: { + MessageListFragment.this.remoteSearchFinished(); + return; } + } + + // Discard messages if the fragment isn't attached to an activity anymore. + Activity activity = getActivity(); + if (activity == null) { + return; + } + + switch (msg.what) { case ACTION_FOLDER_LOADING: { String folder = (String) msg.obj; boolean loading = (msg.arg1 == 1); @@ -509,37 +486,35 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick /** * @return The comparator to use to display messages in an ordered - * fashion. Never null. + * fashion. Never {@code null}. */ - protected Comparator getComparator() { - final List> chain = new ArrayList>(2 /* we add 2 comparators at most */); + protected Comparator getComparator() { + final List> chain = + new ArrayList>(3 /* we add 3 comparators at most */); - { - // add the specified comparator - final Comparator comparator = SORT_COMPARATORS.get(mSortType); - if (mSortAscending) { - chain.add(comparator); + // Add the specified comparator + final Comparator comparator = SORT_COMPARATORS.get(mSortType); + if (mSortAscending) { + chain.add(comparator); + } else { + chain.add(new ReverseComparator(comparator)); + } + + // Add the date comparator if not already specified + if (mSortType != SortType.SORT_DATE && mSortType != SortType.SORT_ARRIVAL) { + final Comparator dateComparator = SORT_COMPARATORS.get(SortType.SORT_DATE); + if (mSortDateAscending) { + chain.add(dateComparator); } else { - chain.add(new ReverseComparator(comparator)); + chain.add(new ReverseComparator(dateComparator)); } } - { - // add the date comparator if not already specified - if (mSortType != SortType.SORT_DATE && mSortType != SortType.SORT_ARRIVAL) { - final Comparator comparator = SORT_COMPARATORS.get(SortType.SORT_DATE); - if (mSortDateAscending) { - chain.add(comparator); - } else { - chain.add(new ReverseComparator(comparator)); - } - } - } + // Add the id comparator + chain.add(new ReverseIdComparator()); - // build the comparator chain - final Comparator chainComparator = new ComparatorChain(chain); - - return chainComparator; + // Build the comparator chain + return new ComparatorChain(chain); } private void folderLoading(String folder, boolean loading) { @@ -551,7 +526,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick private void refreshTitle() { setWindowTitle(); - if (!mRemoteSearch) { + if (!mSearch.isManualSearch()) { setWindowProgress(); } } @@ -559,10 +534,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick private void setWindowProgress() { int level = Window.PROGRESS_END; - if (mCurrentFolder != null && mCurrentFolder.loading && mAdapter.mListener.getFolderTotal() > 0) { - int divisor = mAdapter.mListener.getFolderTotal(); + if (mCurrentFolder != null && mCurrentFolder.loading && mListener.getFolderTotal() > 0) { + int divisor = mListener.getFolderTotal(); if (divisor != 0) { - level = (Window.PROGRESS_END / divisor) * (mAdapter.mListener.getFolderCompleted()) ; + level = (Window.PROGRESS_END / divisor) * (mListener.getFolderCompleted()) ; if (level > Window.PROGRESS_END) { level = Window.PROGRESS_END; } @@ -574,20 +549,20 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick private void setWindowTitle() { // regular folder content display - if (mFolderName != null) { + if (!isManualSearch() && mSingleFolderMode) { Activity activity = getActivity(); String displayName = FolderInfoHolder.getDisplayName(activity, mAccount, mFolderName); mFragmentListener.setMessageListTitle(displayName); - String operation = mAdapter.mListener.getOperation(activity, getTimeFormat()).trim(); + String operation = mListener.getOperation(activity, getTimeFormat()).trim(); if (operation.length() < 1) { mFragmentListener.setMessageListSubTitle(mAccount.getEmail()); } else { mFragmentListener.setMessageListSubTitle(operation); } - } else if (mQueryString != null) { + } else { // query result display. This may be for a search folder as opposed to a user-initiated search. if (mTitle != null) { // This was a search folder; the search folder has overridden our title. @@ -604,8 +579,8 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick if (mUnreadMessageCount == 0) { mFragmentListener.setUnreadCount(0); } else { - if (mQueryString != null && mTitle == null) { - // This is a search result. The unread message count is easily confused + if (!mSingleFolderMode && mTitle == null) { + // The unread message count is easily confused // with total number of messages in the search result, so let's hide it. mFragmentListener.setUnreadCount(0); } else { @@ -615,7 +590,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick } private void setupFormats() { - mTimeFormat = android.text.format.DateFormat.getTimeFormat(getActivity()); + mTimeFormat = android.text.format.DateFormat.getTimeFormat(mContext); } private DateFormat getTimeFormat() { @@ -641,35 +616,52 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick @Override public void onItemClick(AdapterView parent, View view, int position, long id) { if (view == mFooterView) { - if (mCurrentFolder != null && !mRemoteSearch) { - mController.loadMoreMessages(mAccount, mFolderName, mAdapter.mListener); - } else if (mRemoteSearch && mAdapter.mExtraSearchResults != null && mAdapter.mExtraSearchResults.size() > 0 && mSearchAccount != null) { - int numResults = mAdapter.mExtraSearchResults.size(); - Context appContext = getActivity().getApplicationContext(); - Account account = Preferences.getPreferences(appContext).getAccount(mSearchAccount); - if (account == null) { - mHandler.updateFooter("", false); - return; - } - int limit = account.getRemoteSearchNumResults(); - List toProcess = mAdapter.mExtraSearchResults; + if (mCurrentFolder != null && !mSearch.isManualSearch()) { + + mController.loadMoreMessages(mAccount, mFolderName, null); + + } else if (mCurrentFolder != null && isRemoteSearch() && + mExtraSearchResults != null && mExtraSearchResults.size() > 0) { + + int numResults = mExtraSearchResults.size(); + int limit = mAccount.getRemoteSearchNumResults(); + + List toProcess = mExtraSearchResults; + if (limit > 0 && numResults > limit) { toProcess = toProcess.subList(0, limit); - mAdapter.mExtraSearchResults = mAdapter.mExtraSearchResults.subList(limit, mAdapter.mExtraSearchResults.size()); + mExtraSearchResults = mExtraSearchResults.subList(limit, + mExtraSearchResults.size()); } else { - mAdapter.mExtraSearchResults = null; - mHandler.updateFooter("", false); + mExtraSearchResults = null; + updateFooter("", false); } - mController.loadSearchResults(account, mSearchFolder, toProcess, mAdapter.mListener); + + mController.loadSearchResults(mAccount, mCurrentFolder.name, toProcess, mListener); } + return; } - final MessageInfoHolder message = (MessageInfoHolder) parent.getItemAtPosition(position); + Cursor cursor = (Cursor) parent.getItemAtPosition(position); if (mSelectedCount > 0) { - toggleMessageSelect(message); + toggleMessageSelect(position); } else { - onOpenMessage(message); + Account account = getAccountFromCursor(cursor); + + long folderId = cursor.getLong(FOLDER_ID_COLUMN); + String folderName = getFolderNameById(account, folderId); + + if (mThreadedList && cursor.getInt(THREAD_COUNT_COLUMN) > 1) { + long rootId = cursor.getLong(THREAD_ROOT_COLUMN); + mFragmentListener.showThread(account, folderName, rootId); + } else { + MessageReference ref = new MessageReference(); + ref.accountUuid = account.getUuid(); + ref.folderName = folderName; + ref.uid = cursor.getString(UID_COLUMN); + onOpenMessage(ref); + } } } @@ -677,6 +669,8 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick public void onAttach(Activity activity) { super.onAttach(activity); + mContext = activity.getApplicationContext(); + try { mFragmentListener = (MessageListFragmentListener) activity; } catch (ClassCastException e) { @@ -689,6 +683,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + mPreferences = Preferences.getPreferences(getActivity().getApplicationContext()); mController = MessagingController.getInstance(getActivity().getApplication()); mPreviewLines = K9.messageListPreviewLines(); @@ -722,64 +717,131 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick mMessageHelper = MessageHelper.getInstance(getActivity()); initializeMessageList(); + + // This needs to be done before initializing the cursor loader below + initializeSortSettings(); + + LoaderManager loaderManager = getLoaderManager(); + int len = mAccountUuids.length; + mCursors = new Cursor[len]; + for (int i = 0; i < len; i++) { + loaderManager.initLoader(i, null, this); + } + } + + private void initializeSortSettings() { + if (mSingleAccountMode) { + mSortType = mAccount.getSortType(); + mSortAscending = mAccount.isSortAscending(mSortType); + mSortDateAscending = mAccount.isSortAscending(SortType.SORT_DATE); + } else { + mSortType = K9.getSortType(); + mSortAscending = K9.isSortAscending(mSortType); + mSortDateAscending = K9.isSortAscending(SortType.SORT_DATE); + } } private void decodeArguments() { Bundle args = getArguments(); - mQueryString = args.getString(SearchManager.QUERY); - mFolderName = args.getString(ARG_FOLDER); - mRemoteSearch = args.getBoolean(ARG_REMOTE_SEARCH, false); - mSearchAccount = args.getString(ARG_SEARCH_ACCOUNT); - mSearchFolder = args.getString(ARG_SEARCH_FOLDER); + mThreadedList = args.getBoolean(ARG_THREADED_LIST, false); + mSearch = args.getParcelable(ARG_SEARCH); + mTitle = mSearch.getName(); - String accountUuid = args.getString(ARG_ACCOUNT); + String[] accountUuids = mSearch.getAccountUuids(); - Context appContext = getActivity().getApplicationContext(); - mAccount = Preferences.getPreferences(appContext).getAccount(accountUuid); - - String queryFlags = args.getString(ARG_QUERY_FLAGS); - if (queryFlags != null) { - String[] flagStrings = queryFlags.split(","); - mQueryFlags = new Flag[flagStrings.length]; - for (int i = 0; i < flagStrings.length; i++) { - mQueryFlags[i] = Flag.valueOf(flagStrings[i]); - } + mSingleAccountMode = false; + if (accountUuids.length == 1 && !mSearch.searchAllAccounts()) { + mSingleAccountMode = true; + mAccount = mPreferences.getAccount(accountUuids[0]); } - String forbiddenFlags = args.getString(ARG_FORBIDDEN_FLAGS); - if (forbiddenFlags != null) { - String[] flagStrings = forbiddenFlags.split(","); - mForbiddenFlags = new Flag[flagStrings.length]; - for (int i = 0; i < flagStrings.length; i++) { - mForbiddenFlags[i] = Flag.valueOf(flagStrings[i]); - } + mSingleFolderMode = false; + if (mSingleAccountMode && (mSearch.getFolderNames().size() == 1)) { + mSingleFolderMode = true; + mFolderName = mSearch.getFolderNames().get(0); + mCurrentFolder = getFolder(mFolderName, mAccount); } - mIntegrate = args.getBoolean(ARG_INTEGRATE, false); - mAccountUuids = args.getStringArray(ARG_ACCOUNT_UUIDS); - mFolderNames = args.getStringArray(ARG_FOLDER_NAMES); - mTitle = args.getString(ARG_TITLE); + if (mSingleAccountMode) { + mAccountUuids = new String[] { mAccount.getUuid() }; + } else { + if (accountUuids.length == 1 && + accountUuids[0].equals(SearchSpecification.ALL_ACCOUNTS)) { + + Account[] accounts = mPreferences.getAccounts(); + + mAccountUuids = new String[accounts.length]; + for (int i = 0, len = accounts.length; i < len; i++) { + mAccountUuids[i] = accounts[i].getUuid(); + } + } else { + mAccountUuids = accountUuids; + } + } } private void initializeMessageList() { mAdapter = new MessageListAdapter(); if (mFolderName != null) { - mCurrentFolder = mAdapter.getFolder(mFolderName, mAccount); + mCurrentFolder = getFolder(mFolderName, mAccount); } - // Hide "Load up to x more" footer for search views - mFooterView.setVisibility((mQueryString != null) ? View.GONE : View.VISIBLE); + if (mSingleFolderMode) { + mListView.addFooterView(getFooterView(mListView)); + updateFooterView(); + } mController = MessagingController.getInstance(getActivity().getApplication()); mListView.setAdapter(mAdapter); } + private FolderInfoHolder getFolder(String folder, Account account) { + LocalFolder local_folder = null; + try { + LocalStore localStore = account.getLocalStore(); + local_folder = localStore.getFolder(folder); + return new FolderInfoHolder(mContext, local_folder, account); + } catch (Exception e) { + Log.e(K9.LOG_TAG, "getFolder(" + folder + ") goes boom: ", e); + return null; + } finally { + if (local_folder != null) { + local_folder.close(); + } + } + } + + private String getFolderNameById(Account account, long folderId) { + try { + Folder folder = getFolderById(account, folderId); + if (folder != null) { + return folder.getName(); + } + } catch (Exception e) { + Log.e(K9.LOG_TAG, "getFolderNameById() failed.", e); + } + + return null; + } + + private Folder getFolderById(Account account, long folderId) { + try { + LocalStore localStore = account.getLocalStore(); + LocalFolder localFolder = localStore.getFolderById(folderId); + localFolder.open(OpenMode.READ_ONLY); + return localFolder; + } catch (Exception e) { + Log.e(K9.LOG_TAG, "getFolderNameById() failed.", e); + return null; + } + } + @Override public void onPause() { super.onPause(); - mController.removeListener(mAdapter.mListener); + mController.removeListener(mListener); saveListState(); } @@ -821,16 +883,6 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick mSenderAboveSubject = K9.messageListSenderAboveSubject(); - final Preferences prefs = Preferences.getPreferences(appContext); - - boolean allowRemoteSearch = false; - if (mSearchAccount != null) { - final Account searchAccount = prefs.getAccount(mSearchAccount); - if (searchAccount != null) { - allowRemoteSearch = searchAccount.allowRemoteSearch(); - } - } - // Check if we have connectivity. Cache the value. if (mHasConnectivity == null) { final ConnectivityManager connectivityManager = @@ -844,100 +896,47 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick } } - if (mQueryString == null) { - mPullToRefreshView.setOnRefreshListener(new PullToRefreshBase.OnRefreshListener() { - @Override - public void onRefresh(PullToRefreshBase refreshView) { - checkMail(); - } - }); - } else if (allowRemoteSearch && !mRemoteSearch && !mIntegrate && mHasConnectivity) { - // mQueryString != null is implied if we get this far. - mPullToRefreshView.setOnRefreshListener(new PullToRefreshBase.OnRefreshListener() { - @Override - public void onRefresh(PullToRefreshBase refreshView) { - mPullToRefreshView.onRefreshComplete(); - onRemoteSearchRequested(true); - } - }); - mPullToRefreshView.setPullLabel(getString(R.string.pull_to_refresh_remote_search_from_local_search_pull)); - mPullToRefreshView.setReleaseLabel(getString(R.string.pull_to_refresh_remote_search_from_local_search_release)); + if (mSingleFolderMode) { + if (mSearch.isManualSearch() && mAccount.allowRemoteSearch()) { + mPullToRefreshView.setOnRefreshListener(new PullToRefreshBase.OnRefreshListener() { + @Override + public void onRefresh(PullToRefreshBase refreshView) { + mPullToRefreshView.onRefreshComplete(); + onRemoteSearchRequested(); + } + }); + mPullToRefreshView.setPullLabel(getString(R.string.pull_to_refresh_remote_search_from_local_search_pull)); + mPullToRefreshView.setReleaseLabel(getString(R.string.pull_to_refresh_remote_search_from_local_search_release)); + } else { + mPullToRefreshView.setOnRefreshListener(new PullToRefreshBase.OnRefreshListener() { + @Override + public void onRefresh(PullToRefreshBase refreshView) { + checkMail(); + } + }); + } } else { mPullToRefreshView.setMode(PullToRefreshBase.Mode.DISABLED); } - mController.addListener(mAdapter.mListener); + mController.addListener(mListener); //Cancel pending new mail notifications when we open an account Account[] accountsWithNotification; - Account account = getCurrentAccount(prefs); - + Account account = mAccount; if (account != null) { accountsWithNotification = new Account[] { account }; - mSortType = account.getSortType(); - mSortAscending = account.isSortAscending(mSortType); - mSortDateAscending = account.isSortAscending(SortType.SORT_DATE); } else { - accountsWithNotification = prefs.getAccounts(); - mSortType = K9.getSortType(); - mSortAscending = K9.isSortAscending(mSortType); - mSortDateAscending = K9.isSortAscending(SortType.SORT_DATE); + accountsWithNotification = mPreferences.getAccounts(); } for (Account accountWithNotification : accountsWithNotification) { mController.notifyAccountCancel(appContext, accountWithNotification); } - if (mAdapter.isEmpty()) { - if (mRemoteSearch) { - //TODO: Support flag based search - mRemoteSearchFuture = mController.searchRemoteMessages(mSearchAccount, mSearchFolder, mQueryString, null, null, mAdapter.mListener); - } else if (mFolderName != null) { - mController.listLocalMessages(mAccount, mFolderName, mAdapter.mListener); - // Hide the archive button if we don't have an archive folder. - if (!mAccount.hasArchiveFolder()) { -// mBatchArchiveButton.setVisibility(View.GONE); - } - } else if (mQueryString != null) { - mController.searchLocalMessages(mAccountUuids, mFolderNames, null, mQueryString, mIntegrate, mQueryFlags, mForbiddenFlags, mAdapter.mListener); - // Don't show the archive button if this is a search. -// mBatchArchiveButton.setVisibility(View.GONE); - } - - } else { - // reread the selected date format preference in case it has changed - mMessageHelper.refresh(); - - mAdapter.markAllMessagesAsDirty(); - - if (!mRemoteSearch) { - new Thread() { - @Override - public void run() { - if (mFolderName != null) { - mController.listLocalMessagesSynchronous(mAccount, mFolderName, mAdapter.mListener); - } else if (mQueryString != null) { - mController.searchLocalMessagesSynchronous(mAccountUuids, mFolderNames, null, mQueryString, mIntegrate, mQueryFlags, mForbiddenFlags, mAdapter.mListener); - } - - mHandler.post(new Runnable() { - @Override - public void run() { - mAdapter.pruneDirtyMessages(); - mAdapter.notifyDataSetChanged(); - restoreListState(); - } - }); - } - - } - .start(); - } - } - - if (mAccount != null && mFolderName != null && !mRemoteSearch) { - mController.getFolderUnreadMessageCount(mAccount, mFolderName, mAdapter.mListener); + if (mAccount != null && mFolderName != null && !mSearch.isManualSearch()) { + mController.getFolderUnreadMessageCount(mAccount, mFolderName, mListener); } refreshTitle(); @@ -950,17 +949,16 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick mListView.setFastScrollEnabled(true); mListView.setScrollingCacheEnabled(false); mListView.setOnItemClickListener(this); - mListView.addFooterView(getFooterView(mListView)); registerForContextMenu(mListView); } - private void onOpenMessage(MessageInfoHolder message) { - mFragmentListener.openMessage(message.message.makeMessageReference()); + private void onOpenMessage(MessageReference reference) { + mFragmentListener.openMessage(reference); } public void onCompose() { - if (mQueryString != null) { + if (!mSingleAccountMode) { /* * If we have a query string, we don't have an account to let * compose start the default action. @@ -971,20 +969,20 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick } } - public void onReply(MessageInfoHolder holder) { - mFragmentListener.onReply(holder.message); + public void onReply(Message message) { + mFragmentListener.onReply(message); } - public void onReplyAll(MessageInfoHolder holder) { - mFragmentListener.onReplyAll(holder.message); + public void onReplyAll(Message message) { + mFragmentListener.onReplyAll(message); } - public void onForward(MessageInfoHolder holder) { - mFragmentListener.onForward(holder.message); + public void onForward(Message message) { + mFragmentListener.onForward(message); } - public void onResendMessage(MessageInfoHolder holder) { - mFragmentListener.onResendMessage(holder.message); + public void onResendMessage(Message message) { + mFragmentListener.onResendMessage(message); } public void changeSort(SortType sortType) { @@ -994,22 +992,21 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick /** * User has requested a remote search. Setup the bundle and start the intent. - * @param fromLocalSearch true if this is being called from a local search result screen. This affects - * where we pull the account and folder info used for the next search. */ - public void onRemoteSearchRequested(final boolean fromLocalSearch) { + public void onRemoteSearchRequested() { String searchAccount; String searchFolder; - if (fromLocalSearch) { - searchAccount = mSearchAccount; - searchFolder = mSearchFolder; - } else { - searchAccount = mAccount.getUuid(); - searchFolder = mCurrentFolder.name; - } + searchAccount = mAccount.getUuid(); + searchFolder = mCurrentFolder.name; - mFragmentListener.remoteSearch(searchAccount, searchFolder, mQueryString); + String queryString = mSearch.getRemoteSearchArguments(); + + mRemoteSearchPerformed = true; + mRemoteSearchFuture = mController.searchRemoteMessages(searchAccount, searchFolder, + queryString, null, null, mListener); + + mFragmentListener.remoteSearchStarted(); } /** @@ -1025,8 +1022,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick private void changeSort(SortType sortType, Boolean sortAscending) { mSortType = sortType; - Preferences prefs = Preferences.getPreferences(getActivity().getApplicationContext()); - Account account = getCurrentAccount(prefs); + Account account = mAccount; if (account != null) { account.setSortType(mSortType); @@ -1039,7 +1035,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick account.setSortAscending(mSortType, mSortAscending); mSortDateAscending = account.isSortAscending(SortType.SORT_DATE); - account.save(prefs); + account.save(mPreferences); } else { K9.setSortType(mSortType); @@ -1051,7 +1047,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick K9.setSortAscending(mSortType, mSortAscending); mSortDateAscending = K9.isSortAscending(SortType.SORT_DATE); - Editor editor = prefs.getPreferences().edit(); + Editor editor = mPreferences.getPreferences().edit(); K9.save(editor); editor.commit(); } @@ -1065,7 +1061,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick Toast toast = Toast.makeText(getActivity(), toastString, Toast.LENGTH_SHORT); toast.show(); - mAdapter.sortMessages(); + LoaderManager loaderManager = getLoaderManager(); + for (int i = 0, len = mAccountUuids.length; i < len; i++) { + loaderManager.restartLoader(i, null, this); + } } public void onCycleSort() { @@ -1088,17 +1087,16 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick changeSort(sorts[curIndex]); } - /** - * @param holders - * Never {@code null}. - */ - private void onDelete(final List holders) { - final List messagesToRemove = new ArrayList(); - for (MessageInfoHolder holder : holders) { - messagesToRemove.add(holder.message); + private void onDelete(Message message) { + onDelete(Collections.singletonList(message)); + } + + private void onDelete(List messages) { + if (mThreadedList) { + mController.deleteThreads(messages); + } else { + mController.deleteMessages(messages, null); } - mAdapter.removeMessages(holders); - mController.deleteMessages(messagesToRemove.toArray(EMPTY_MESSAGE_ARRAY), null); } @Override @@ -1115,22 +1113,22 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick } final String destFolderName = data.getStringExtra(ChooseFolder.EXTRA_NEW_FOLDER); - final List holders = mActiveMessages; + final List messages = mActiveMessages; if (destFolderName != null) { mActiveMessages = null; // don't need it any more - final Account account = holders.get(0).message.getFolder().getAccount(); + final Account account = messages.get(0).getFolder().getAccount(); account.setLastSelectedFolderName(destFolderName); switch (requestCode) { case ACTIVITY_CHOOSE_FOLDER_MOVE: - move(holders, destFolderName); + move(messages, destFolderName); break; case ACTIVITY_CHOOSE_FOLDER_COPY: - copy(holders, destFolderName); + copy(messages, destFolderName); break; } } @@ -1196,10 +1194,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick changeSort(SortType.SORT_SUBJECT); return true; } - case R.id.set_sort_sender: { - changeSort(SortType.SORT_SENDER); - return true; - } +// case R.id.set_sort_sender: { +// changeSort(SortType.SORT_SENDER); +// return true; +// } case R.id.set_sort_flag: { changeSort(SortType.SORT_FLAGGED); return true; @@ -1213,12 +1211,12 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick return true; } case R.id.select_all: { - setSelectionState(true); + selectAll(); return true; } } - if (mQueryString != null) { + if (!mSingleAccountMode) { // None of the options after this point are "safe" for search results //TODO: This is not true for "unread" and "starred" searches in regular folders return false; @@ -1242,110 +1240,143 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick } public void onSendPendingMessages() { - mController.sendPendingMessages(mAccount, mAdapter.mListener); + mController.sendPendingMessages(mAccount, null); } @Override public boolean onContextItemSelected(android.view.MenuItem item) { AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); - final MessageInfoHolder message = (MessageInfoHolder) mListView.getItemAtPosition(info.position); + int adapterPosition = listViewToAdapterPosition(info.position); + Message message = getMessageAtPosition(adapterPosition); - - final List selection = getSelectionFromMessage(message); - switch (item.getItemId()) { - case R.id.reply: { - onReply(message); - break; - } - case R.id.reply_all: { - onReplyAll(message); - break; - } - case R.id.forward: { - onForward(message); - break; - } - case R.id.send_again: { - onResendMessage(message); - mSelectedCount = 0; - break; - } - case R.id.same_sender: { - mFragmentListener.showMoreFromSameSender(message.senderAddress); - break; - } - case R.id.delete: { - onDelete(selection); - break; - } - case R.id.mark_as_read: { - setFlag(selection, Flag.SEEN, true); - break; - } - case R.id.mark_as_unread: { - setFlag(selection, Flag.SEEN, false); - break; - } - case R.id.flag: { - setFlag(selection, Flag.FLAGGED, true); - break; - } - case R.id.unflag: { - setFlag(selection, Flag.FLAGGED, false); - break; - } - - // only if the account supports this - case R.id.archive: { - onArchive(selection); - break; - } - case R.id.spam: { - onSpam(selection); - break; - } - case R.id.move: { - onMove(selection); - break; - } - case R.id.copy: { - onCopy(selection); - break; + switch (item.getItemId()) { + case R.id.reply: { + onReply(message); + break; + } + case R.id.reply_all: { + onReplyAll(message); + break; + } + case R.id.forward: { + onForward(message); + break; + } + case R.id.send_again: { + onResendMessage(message); + mSelectedCount = 0; + break; + } + case R.id.same_sender: { + Cursor cursor = (Cursor) mAdapter.getItem(adapterPosition); + String senderAddress = getSenderAddressFromCursor(cursor); + if (senderAddress != null) { + mFragmentListener.showMoreFromSameSender(senderAddress); } + break; + } + case R.id.delete: { + onDelete(message); + break; + } + case R.id.mark_as_read: { + setFlag(message, Flag.SEEN, true); + break; + } + case R.id.mark_as_unread: { + setFlag(message, Flag.SEEN, false); + break; + } + case R.id.flag: { + setFlag(message, Flag.FLAGGED, true); + break; + } + case R.id.unflag: { + setFlag(message, Flag.FLAGGED, false); + break; } - return true; + // only if the account supports this + case R.id.archive: { + onArchive(message); + break; + } + case R.id.spam: { + onSpam(message); + break; + } + case R.id.move: { + onMove(message); + break; + } + case R.id.copy: { + onCopy(message); + break; + } } + return true; + } + + + private String getSenderAddressFromCursor(Cursor cursor) { + String fromList = cursor.getString(SENDER_LIST_COLUMN); + Address[] fromAddrs = Address.unpack(fromList); + return (fromAddrs.length > 0) ? fromAddrs[0].getAddress() : null; + } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; - MessageInfoHolder message = (MessageInfoHolder) mListView.getItemAtPosition(info.position); + Cursor cursor = (Cursor) mListView.getItemAtPosition(info.position); - if (message == null) { + if (cursor == null) { return; } getActivity().getMenuInflater().inflate(R.menu.message_list_item_context, menu); - menu.setHeaderTitle(message.message.getSubject()); + Account account = getAccountFromCursor(cursor); - if (message.read) { + String subject = cursor.getString(SUBJECT_COLUMN); + String flagList = cursor.getString(FLAGS_COLUMN); + String[] flags = flagList.split(","); + boolean read = false; + boolean flagged = false; + for (int i = 0, len = flags.length; i < len; i++) { + try { + switch (Flag.valueOf(flags[i])) { + case SEEN: { + read = true; + break; + } + case FLAGGED: { + flagged = true; + break; + } + default: { + // We don't care about the other flags + } + } + } catch (Exception e) { /* ignore */ } + } + + menu.setHeaderTitle(subject); + + if (read) { menu.findItem(R.id.mark_as_read).setVisible(false); } else { menu.findItem(R.id.mark_as_unread).setVisible(false); } - if (message.flagged) { + if (flagged) { menu.findItem(R.id.flag).setVisible(false); } else { menu.findItem(R.id.unflag).setVisible(false); } - Account account = message.message.getFolder().getAccount(); if (!mController.isCopyCapable(account)) { menu.findItem(R.id.copy).setVisible(false); } @@ -1377,216 +1408,137 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick } /** - * Handle a select or unselect swipe event - * @param downMotion Event that started the swipe - * @param selected true if this was an attempt to select (i.e. left to right). + * Handle a select or unselect swipe event. + * + * @param downMotion + * Event that started the swipe + * @param selected + * {@code true} if this was an attempt to select (i.e. left to right). */ private void handleSwipe(final MotionEvent downMotion, final boolean selected) { int[] listPosition = new int[2]; mListView.getLocationOnScreen(listPosition); - int position = mListView.pointToPosition((int) downMotion.getRawX() - listPosition[0], (int) downMotion.getRawY() - listPosition[1]); - if (position != AdapterView.INVALID_POSITION) { - final MessageInfoHolder message = (MessageInfoHolder) mListView.getItemAtPosition(position); - toggleMessageSelect(message); + + int listX = (int) downMotion.getRawX() - listPosition[0]; + int listY = (int) downMotion.getRawY() - listPosition[1]; + + int listViewPosition = mListView.pointToPosition(listX, listY); + + toggleMessageSelect(listViewPosition); + } + + private int listViewToAdapterPosition(int position) { + if (position > 0 && position <= mAdapter.getCount()) { + return position - 1; + } + + return AdapterView.INVALID_POSITION; + } + + class MessageListActivityListener extends ActivityListener { + @Override + public void remoteSearchFailed(Account acct, String folder, final String err) { + mHandler.post(new Runnable() { + @Override + public void run() { + Activity activity = getActivity(); + if (activity != null) { + Toast.makeText(activity, R.string.remote_search_error, + Toast.LENGTH_LONG).show(); + } + } + }); + } + + @Override + public void remoteSearchStarted(Account acct, String folder) { + mHandler.progress(true); + mHandler.updateFooter(mContext.getString(R.string.remote_search_sending_query), true); + } + + + @Override + public void remoteSearchFinished(Account acct, String folder, int numResults, List extraResults) { + mHandler.progress(false); + mHandler.remoteSearchFinished(); + mExtraSearchResults = extraResults; + if (extraResults != null && extraResults.size() > 0) { + mHandler.updateFooter(String.format(mContext.getString(R.string.load_more_messages_fmt), acct.getRemoteSearchNumResults()), false); + } else { + mHandler.updateFooter("", false); + } + mFragmentListener.setMessageListProgress(Window.PROGRESS_END); + + } + + @Override + public void remoteSearchServerQueryComplete(Account account, String folderName, int numResults) { + mHandler.progress(true); + if (account != null && account.getRemoteSearchNumResults() != 0 && numResults > account.getRemoteSearchNumResults()) { + mHandler.updateFooter(mContext.getString(R.string.remote_search_downloading_limited, account.getRemoteSearchNumResults(), numResults), true); + } else { + mHandler.updateFooter(mContext.getString(R.string.remote_search_downloading, numResults), true); + } + mFragmentListener.setMessageListProgress(Window.PROGRESS_START); + } + + @Override + public void informUserOfStatus() { + mHandler.refreshTitle(); + } + + @Override + public void synchronizeMailboxStarted(Account account, String folder) { + if (updateForMe(account, folder)) { + mHandler.progress(true); + mHandler.folderLoading(folder, true); + } + super.synchronizeMailboxStarted(account, folder); + } + + @Override + public void synchronizeMailboxFinished(Account account, String folder, + int totalMessagesInMailbox, int numNewMessages) { + + if (updateForMe(account, folder)) { + mHandler.progress(false); + mHandler.folderLoading(folder, false); + } + super.synchronizeMailboxFinished(account, folder, totalMessagesInMailbox, numNewMessages); + } + + @Override + public void synchronizeMailboxFailed(Account account, String folder, String message) { + + if (updateForMe(account, folder)) { + mHandler.progress(false); + mHandler.folderLoading(folder, false); + } + super.synchronizeMailboxFailed(account, folder, message); + } + + @Override + public void searchStats(AccountStats stats) { + mUnreadMessageCount = stats.unreadMessageCount; + super.searchStats(stats); + } + + @Override + public void folderStatusChanged(Account account, String folder, int unreadMessageCount) { + if (updateForMe(account, folder)) { + mUnreadMessageCount = unreadMessageCount; + } + super.folderStatusChanged(account, folder, unreadMessageCount); + } + + private boolean updateForMe(Account account, String folder) { + //FIXME + return ((account.equals(mAccount) && folder.equals(mFolderName))); } } - class MessageListAdapter extends BaseAdapter { - private final List mMessages = - Collections.synchronizedList(new ArrayList()); - public List mExtraSearchResults; - - private final ActivityListener mListener = new ActivityListener() { - - @Override - public void remoteSearchAddMessage(Account account, String folderName, Message message, final int numDone, final int numTotal) { - - if (numTotal > 0 && numDone < numTotal) { - mFragmentListener.setMessageListProgress(Window.PROGRESS_END / numTotal * numDone); - } else { - mFragmentListener.setMessageListProgress(Window.PROGRESS_END); - } - - mHandler.addOrUpdateMessages(account, folderName, Collections.singletonList(message), false); - } - - @Override - public void remoteSearchFailed(Account acct, String folder, final String err) { - //TODO: Better error handling - mHandler.post(new Runnable() { - @Override - public void run() { - Toast.makeText(getActivity(), err, Toast.LENGTH_LONG).show(); - } - }); - } - - @Override - public void remoteSearchStarted(Account acct, String folder) { - mHandler.progress(true); - mHandler.updateFooter(getString(R.string.remote_search_sending_query), true); - } - - - @Override - public void remoteSearchFinished(Account acct, String folder, int numResults, List extraResults) { - mHandler.progress(false); - if (extraResults != null && extraResults.size() > 0) { - mExtraSearchResults = extraResults; - mHandler.updateFooter(String.format(getString(R.string.load_more_messages_fmt), acct.getRemoteSearchNumResults()), false); - } else { - mHandler.updateFooter("", false); - } - mFragmentListener.setMessageListProgress(Window.PROGRESS_END); - - } - - @Override - public void remoteSearchServerQueryComplete(Account account, String folderName, int numResults) { - mHandler.progress(true); - if (account != null && account.getRemoteSearchNumResults() != 0 && numResults > account.getRemoteSearchNumResults()) { - mHandler.updateFooter(getString(R.string.remote_search_downloading_limited, account.getRemoteSearchNumResults(), numResults), true); - } else { - mHandler.updateFooter(getString(R.string.remote_search_downloading, numResults), true); - } - mFragmentListener.setMessageListProgress(Window.PROGRESS_START); - } - - @Override - public void informUserOfStatus() { - mHandler.refreshTitle(); - } - - @Override - public void synchronizeMailboxStarted(Account account, String folder) { - if (updateForMe(account, folder)) { - mHandler.progress(true); - mHandler.folderLoading(folder, true); - } - super.synchronizeMailboxStarted(account, folder); - } - - @Override - public void synchronizeMailboxFinished(Account account, String folder, - int totalMessagesInMailbox, int numNewMessages) { - - if (updateForMe(account, folder)) { - mHandler.progress(false); - mHandler.folderLoading(folder, false); - mHandler.sortMessages(); - } - super.synchronizeMailboxFinished(account, folder, totalMessagesInMailbox, numNewMessages); - } - - @Override - public void synchronizeMailboxFailed(Account account, String folder, String message) { - - if (updateForMe(account, folder)) { - mHandler.progress(false); - mHandler.folderLoading(folder, false); - mHandler.sortMessages(); - } - super.synchronizeMailboxFailed(account, folder, message); - } - - @Override - public void synchronizeMailboxAddOrUpdateMessage(Account account, String folder, Message message) { - mHandler.addOrUpdateMessages(account, folder, Collections.singletonList(message), true); - } - - @Override - public void synchronizeMailboxRemovedMessage(Account account, String folder, Message message) { - mHandler.removeMessage(message.makeMessageReference()); - } - - @Override - public void listLocalMessagesStarted(Account account, String folder) { - if ((mQueryString != null && folder == null) || (account != null && account.equals(mAccount))) { - mHandler.progress(true); - if (folder != null) { - mHandler.folderLoading(folder, true); - } - } - } - - @Override - public void listLocalMessagesFailed(Account account, String folder, String message) { - if ((mQueryString != null && folder == null) || (account != null && account.equals(mAccount))) { - mHandler.sortMessages(); - mHandler.progress(false); - if (folder != null) { - mHandler.folderLoading(folder, false); - } - } - } - - @Override - public void listLocalMessagesFinished(Account account, String folder) { - if ((mQueryString != null && folder == null) || (account != null && account.equals(mAccount))) { - mHandler.sortMessages(); - mHandler.progress(false); - if (folder != null) { - mHandler.folderLoading(folder, false); - } - } - } - - @Override - public void listLocalMessagesRemoveMessage(Account account, String folder, Message message) { - mHandler.removeMessage(message.makeMessageReference()); - } - - @Override - public void listLocalMessagesAddMessages(Account account, String folder, List messages) { - mHandler.addOrUpdateMessages(account, folder, messages, false); - } - - @Override - public void listLocalMessagesUpdateMessage(Account account, String folder, Message message) { - mHandler.addOrUpdateMessages(account, folder, Collections.singletonList(message), false); - } - - @Override - public void searchStats(AccountStats stats) { - mUnreadMessageCount = stats.unreadMessageCount; - super.searchStats(stats); - } - - @Override - public void folderStatusChanged(Account account, String folder, int unreadMessageCount) { - if (updateForMe(account, folder)) { - mUnreadMessageCount = unreadMessageCount; - } - super.folderStatusChanged(account, folder, unreadMessageCount); - } - - @Override - public void messageUidChanged(Account account, String folder, String oldUid, String newUid) { - MessageReference ref = new MessageReference(); - ref.accountUuid = account.getUuid(); - ref.folderName = folder; - ref.uid = oldUid; - - mHandler.changeMessageUid(ref, newUid); - } - }; - - private boolean updateForMe(Account account, String folder) { - if ((account.equals(mAccount) && mFolderName != null && folder.equals(mFolderName))) { - return true; - } else { - return false; - } - } - - public List getMessages() { - return mMessages; - } - - public void restoreMessages(List messages) { - mMessages.addAll(messages); - } + class MessageListAdapter extends CursorAdapter { private Drawable mAttachmentIcon; private Drawable mForwardedIcon; @@ -1594,542 +1546,17 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick private Drawable mForwardedAnsweredIcon; MessageListAdapter() { + super(getActivity(), null, 0); mAttachmentIcon = getResources().getDrawable(R.drawable.ic_email_attachment_small); mAnsweredIcon = getResources().getDrawable(R.drawable.ic_email_answered_small); mForwardedIcon = getResources().getDrawable(R.drawable.ic_email_forwarded_small); mForwardedAnsweredIcon = getResources().getDrawable(R.drawable.ic_email_forwarded_answered_small); } - public void markAllMessagesAsDirty() { - for (MessageInfoHolder holder : mMessages) { - holder.dirty = true; - } - } - - public void pruneDirtyMessages() { - List messagesToRemove = new ArrayList(); - - for (MessageInfoHolder holder : mMessages) { - if (holder.dirty) { - messagesToRemove.add(holder); - } - } - removeMessages(messagesToRemove); - } - - public void removeMessage(MessageReference messageReference) { - MessageInfoHolder holder = getMessage(messageReference); - if (holder == null) { - Log.w(K9.LOG_TAG, "Got callback to remove non-existent message with UID " + - messageReference.uid); - } else { - removeMessages(Collections.singletonList(holder)); - } - } - - public void removeMessages(final List messages) { - if (messages.isEmpty()) { - return; - } - - for (MessageInfoHolder message : messages) { - if (message != null && (mFolderName == null || ( - message.folder != null && - message.folder.name.equals(mFolderName)))) { - if (message.selected && mSelectedCount > 0) { - mSelectedCount--; - } - mMessages.remove(message); - } - } - resetUnreadCount(); - - notifyDataSetChanged(); - computeSelectAllVisibility(); - } - - /** - * Set the selection state for all messages at once. - * @param selected Selection state to set. - */ - public void setSelectionForAllMesages(final boolean selected) { - for (MessageInfoHolder message : mMessages) { - message.selected = selected; - } - - notifyDataSetChanged(); - } - - public void addMessages(final List messages) { - if (messages.isEmpty()) { - return; - } - - final boolean wasEmpty = mMessages.isEmpty(); - - for (final MessageInfoHolder message : messages) { - if (mFolderName == null || (message.folder != null && message.folder.name.equals(mFolderName))) { - int index = Collections.binarySearch(mMessages, message, getComparator()); - - if (index < 0) { - index = (index * -1) - 1; - } - - mMessages.add(index, message); - } - } - - if (wasEmpty) { - mListView.setSelection(0); - } - resetUnreadCount(); - - notifyDataSetChanged(); - computeSelectAllVisibility(); - } - - public void changeMessageUid(MessageReference ref, String newUid) { - MessageInfoHolder holder = getMessage(ref); - if (holder != null) { - holder.uid = newUid; - holder.message.setUid(newUid); - } - } - - public void resetUnreadCount() { - if (mQueryString != null) { - int unreadCount = 0; - - for (MessageInfoHolder holder : mMessages) { - unreadCount += holder.read ? 0 : 1; - } - - mUnreadMessageCount = unreadCount; - refreshTitle(); - } - } - - public void sortMessages() { - final Comparator chainComparator = getComparator(); - - Collections.sort(mMessages, chainComparator); - - notifyDataSetChanged(); - } - - public void addOrUpdateMessages(final Account account, final String folderName, - final List messages, final boolean verifyAgainstSearch) { - - boolean needsSort = false; - final List messagesToAdd = new ArrayList(); - List messagesToRemove = new ArrayList(); - List messagesToSearch = new ArrayList(); - - // cache field into local variable for faster access for JVM without JIT - final MessageHelper messageHelper = mMessageHelper; - - for (Message message : messages) { - MessageInfoHolder m = getMessage(message); - if (message.isSet(Flag.DELETED)) { - if (m != null) { - messagesToRemove.add(m); - } - } else { - final Folder messageFolder = message.getFolder(); - final Account messageAccount = messageFolder.getAccount(); - if (m == null) { - if (updateForMe(account, folderName)) { - m = new MessageInfoHolder(); - FolderInfoHolder folderInfoHolder = new FolderInfoHolder( - getActivity(), messageFolder, messageAccount); - messageHelper.populate(m, message, folderInfoHolder, messageAccount); - messagesToAdd.add(m); - } else { - if (mQueryString != null) { - if (verifyAgainstSearch) { - messagesToSearch.add(message); - } else { - m = new MessageInfoHolder(); - FolderInfoHolder folderInfoHolder = new FolderInfoHolder( - getActivity(), messageFolder, messageAccount); - messageHelper.populate(m, message, folderInfoHolder, - messageAccount); - messagesToAdd.add(m); - } - } - } - } else { - m.dirty = false; // as we reload the message, unset its dirty flag - FolderInfoHolder folderInfoHolder = new FolderInfoHolder(getActivity(), - messageFolder, account); - messageHelper.populate(m, message, folderInfoHolder, account); - needsSort = true; - } - } - } - - if (!messagesToSearch.isEmpty()) { - mController.searchLocalMessages(mAccountUuids, mFolderNames, - messagesToSearch.toArray(EMPTY_MESSAGE_ARRAY), mQueryString, mIntegrate, - mQueryFlags, mForbiddenFlags, - new MessagingListener() { - @Override - public void listLocalMessagesAddMessages(Account account, String folder, - List messages) { - mHandler.addOrUpdateMessages(account, folder, messages, false); - } - }); - } - - if (!messagesToRemove.isEmpty()) { - removeMessages(messagesToRemove); - } - - if (!messagesToAdd.isEmpty()) { - addMessages(messagesToAdd); - } - - if (needsSort) { - sortMessages(); - resetUnreadCount(); - } - } - - /** - * Find a specific message in the message list. - * - *

Note: - * This method was optimized because it is called a lot. Don't change it unless you know - * what you are doing.

- * - * @param message - * A {@link Message} instance describing the message to look for. - * - * @return The corresponding {@link MessageInfoHolder} instance if the message was found in - * the message list. {@code null} otherwise. - */ - private MessageInfoHolder getMessage(Message message) { - String uid; - Folder folder; - for (MessageInfoHolder holder : mMessages) { - uid = message.getUid(); - if (uid != null && (holder.uid == uid || uid.equals(holder.uid))) { - folder = message.getFolder(); - if (holder.folder.name.equals(folder.getName()) && - holder.account.equals(folder.getAccount().getUuid())) { - return holder; - } - } - } - - return null; - } - - /** - * Find a specific message in the message list. - * - *

Note: - * This method was optimized because it is called a lot. Don't change it unless you know - * what you are doing.

- * - * @param messageReference - * A {@link MessageReference} instance describing the message to look for. - * - * @return The corresponding {@link MessageInfoHolder} instance if the message was found in - * the message list. {@code null} otherwise. - */ - private MessageInfoHolder getMessage(MessageReference messageReference) { - String uid; - for (MessageInfoHolder holder : mMessages) { - uid = messageReference.uid; - if ((holder.uid == uid || uid.equals(holder.uid)) && - holder.folder.name.equals(messageReference.folderName) && - holder.account.equals(messageReference.accountUuid)) { - return holder; - } - } - - return null; - } - - public FolderInfoHolder getFolder(String folder, Account account) { - LocalFolder local_folder = null; - try { - LocalStore localStore = account.getLocalStore(); - local_folder = localStore.getFolder(folder); - return new FolderInfoHolder(getActivity(), local_folder, account); - } catch (Exception e) { - Log.e(K9.LOG_TAG, "getFolder(" + folder + ") goes boom: ", e); - return null; - } finally { - if (local_folder != null) { - local_folder.close(); - } - } - } - - - @Override - public int getCount() { - return mMessages.size(); - } - - @Override - public long getItemId(int position) { - try { - MessageInfoHolder messageHolder = (MessageInfoHolder) getItem(position); - if (messageHolder != null) { - return messageHolder.message.getId(); - } - } catch (Exception e) { - Log.i(K9.LOG_TAG, "getItemId(" + position + ") ", e); - } - return -1; - } - - @Override - public Object getItem(int position) { - try { - if (position < mMessages.size()) { - return mMessages.get(position); - } - } catch (Exception e) { - Log.e(K9.LOG_TAG, "getItem(" + position + "), but folder.messages.size() = " + mMessages.size(), e); - } - return null; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - MessageInfoHolder message = (MessageInfoHolder) getItem(position); - View view; - - if ((convertView != null) && (convertView.getId() == R.layout.message_list_item)) { - view = convertView; - } else { - view = mInflater.inflate(R.layout.message_list_item, parent, false); - view.setId(R.layout.message_list_item); - } - - MessageViewHolder holder = (MessageViewHolder) view.getTag(); - - if (holder == null) { - holder = new MessageViewHolder(); - holder.date = (TextView) view.findViewById(R.id.date); - holder.chip = view.findViewById(R.id.chip); - holder.selected = (CheckBox) view.findViewById(R.id.selected_checkbox); - holder.preview = (TextView) view.findViewById(R.id.preview); - if (mCheckboxes) { - holder.selected.setVisibility(View.VISIBLE); - } - - if (holder.selected != null) { - holder.selected.setOnCheckedChangeListener(holder); - } - - - if (mSenderAboveSubject) { - holder.from = (TextView) view.findViewById(R.id.subject); - holder.from.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getMessageListSender()); - } else { - holder.subject = (TextView) view.findViewById(R.id.subject); - holder.subject.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getMessageListSubject()); - } - - holder.date.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getMessageListDate()); - - holder.preview.setLines(mPreviewLines); - holder.preview.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getMessageListPreview()); - - view.setTag(holder); - } - - if (message != null) { - bindView(position, view, holder, message); - } else { - // This branch code is triggered when the local store - // hands us an invalid message - - holder.chip.getBackground().setAlpha(0); - if (holder.subject != null) { - holder.subject.setText(getString(R.string.general_no_subject)); - holder.subject.setTypeface(null, Typeface.NORMAL); - } - - String noSender = getString(R.string.general_no_sender); - - if (holder.preview != null) { - holder.preview.setText(noSender, TextView.BufferType.SPANNABLE); - Spannable str = (Spannable) holder.preview.getText(); - - str.setSpan(new StyleSpan(Typeface.NORMAL), - 0, - noSender.length(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - str.setSpan(new AbsoluteSizeSpan(mFontSizes.getMessageListSender(), true), - 0, - noSender.length(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } else { - holder.from.setText(noSender); - holder.from.setTypeface(null, Typeface.NORMAL); - holder.from.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); - } - - holder.date.setText(getString(R.string.general_no_date)); - - //WARNING: Order of the next 2 lines matter - holder.position = -1; - holder.selected.setChecked(false); - - if (!mCheckboxes) { - holder.selected.setVisibility(View.GONE); - } - } - - - return view; - } - - /** - * Associate model data to view object. - * - * @param position - * The position of the item within the adapter's data set of - * the item whose view we want. - * @param view - * Main view component to alter. Never null. - * @param holder - * Convenience view holder - eases access to view - * child views. Never null. - * @param message - * Never null. - */ - - - - private void bindView(final int position, final View view, final MessageViewHolder holder, - final MessageInfoHolder message) { - - int maybeBoldTypeface = message.read ? Typeface.NORMAL : Typeface.BOLD; - - // So that the mSelectedCount is only incremented/decremented - // when a user checks the checkbox (vs code) - holder.position = -1; - - holder.selected.setChecked(message.selected); - - if (!mCheckboxes && message.selected) { - - holder.chip.setBackgroundDrawable(message.message.getFolder().getAccount().getCheckmarkChip().drawable()); - } - - else { - holder.chip.setBackgroundDrawable(message.message.getFolder().getAccount().generateColorChip(message.read,message.message.toMe(), message.message.ccMe(), message.message.fromMe(), message.flagged).drawable()); - - } - - if (K9.useBackgroundAsUnreadIndicator()) { - int res = (message.read) ? R.attr.messageListReadItemBackgroundColor : - R.attr.messageListUnreadItemBackgroundColor; - - TypedValue outValue = new TypedValue(); - getActivity().getTheme().resolveAttribute(res, outValue, true); - view.setBackgroundColor(outValue.data); - } - - String subject = null; - - if ((message.message.getSubject() == null) || message.message.getSubject().equals("")) { - subject = (String) getText(R.string.general_no_subject); - - } else { - subject = message.message.getSubject(); - } - - // We'll get badge support soon --jrv -// if (holder.badge != null) { -// String email = message.counterpartyAddress; -// holder.badge.assignContactFromEmail(email, true); -// if (email != null) { -// mContactsPictureLoader.loadContactPicture(email, holder.badge); -// } -// } - - if (holder.preview != null) { - /* - * In the touchable UI, we have previews. Otherwise, we - * have just a "from" line. - * Because text views can't wrap around each other(?) we - * compose a custom view containing the preview and the - * from. - */ - - CharSequence beforePreviewText = null; - if (mSenderAboveSubject) { - beforePreviewText = subject; - } else { - beforePreviewText = message.sender; - } - - holder.preview.setText(new SpannableStringBuilder(recipientSigil(message)) - .append(beforePreviewText).append(" ").append(message.message.getPreview()), - TextView.BufferType.SPANNABLE); - Spannable str = (Spannable)holder.preview.getText(); - - // Create a span section for the sender, and assign the correct font size and weight. - str.setSpan(new AbsoluteSizeSpan((mSenderAboveSubject ? mFontSizes.getMessageListSubject(): mFontSizes.getMessageListSender()), true), - 0, beforePreviewText.length() + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - - int color = (K9.getK9Theme() == K9.THEME_LIGHT) ? - Color.rgb(105, 105, 105) : - Color.rgb(160, 160, 160); - - // set span for preview message. - str.setSpan(new ForegroundColorSpan(color), // How do I can specify the android.R.attr.textColorTertiary - beforePreviewText.length() + 1, - str.length(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - - if (holder.from != null ) { - holder.from.setTypeface(null, maybeBoldTypeface); - if (mSenderAboveSubject) { - holder.from.setCompoundDrawablesWithIntrinsicBounds( - message.answered ? mAnsweredIcon : null, // left - null, // top - message.message.hasAttachments() ? mAttachmentIcon : null, // right - null); // bottom - - holder.from.setText(message.sender); - } else { - holder.from.setText(new SpannableStringBuilder(recipientSigil(message)).append(message.sender)); - } - } - - if (holder.subject != null ) { - if (!mSenderAboveSubject) { - holder.subject.setCompoundDrawablesWithIntrinsicBounds( - message.answered ? mAnsweredIcon : null, // left - null, // top - message.message.hasAttachments() ? mAttachmentIcon : null, // right - null); // bottom - } - - holder.subject.setTypeface(null, maybeBoldTypeface); - holder.subject.setText(subject); - } - - holder.date.setText(message.getDate(mMessageHelper)); - holder.position = position; - } - - - private String recipientSigil(MessageInfoHolder message) { - if (message.message.toMe()) { + private String recipientSigil(boolean toMe, boolean ccMe) { + if (toMe) { return getString(R.string.messagelist_sent_to_me_sigil); - } else if (message.message.ccMe()) { + } else if (ccMe) { return getString(R.string.messagelist_sent_cc_me_sigil); } else { return ""; @@ -2137,29 +1564,239 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick } @Override - public boolean hasStableIds() { - return true; + public View newView(Context context, Cursor cursor, ViewGroup parent) { + View view = mInflater.inflate(R.layout.message_list_item, parent, false); + view.setId(R.layout.message_list_item); + + MessageViewHolder holder = new MessageViewHolder(); + holder.date = (TextView) view.findViewById(R.id.date); + holder.chip = view.findViewById(R.id.chip); + holder.preview = (TextView) view.findViewById(R.id.preview); + + if (mSenderAboveSubject) { + holder.from = (TextView) view.findViewById(R.id.subject); + holder.from.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getMessageListSender()); + } else { + holder.subject = (TextView) view.findViewById(R.id.subject); + holder.subject.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getMessageListSubject()); + } + + holder.date.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getMessageListDate()); + + holder.preview.setLines(mPreviewLines); + holder.preview.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getMessageListPreview()); + holder.threadCount = (TextView) view.findViewById(R.id.thread_count); + + holder.selected = (CheckBox) view.findViewById(R.id.selected_checkbox); + if (mCheckboxes) { + holder.selected.setOnCheckedChangeListener(holder); + holder.selected.setVisibility(View.VISIBLE); + } else { + holder.selected.setVisibility(View.GONE); + } + + view.setTag(holder); + + return view; + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + Account account = getAccountFromCursor(cursor); + + String fromList = cursor.getString(SENDER_LIST_COLUMN); + String toList = cursor.getString(TO_LIST_COLUMN); + String ccList = cursor.getString(CC_LIST_COLUMN); + Address[] fromAddrs = Address.unpack(fromList); + Address[] toAddrs = Address.unpack(toList); + Address[] ccAddrs = Address.unpack(ccList); + + boolean fromMe = mMessageHelper.toMe(account, fromAddrs); + boolean toMe = mMessageHelper.toMe(account, toAddrs); + boolean ccMe = mMessageHelper.toMe(account, ccAddrs); + + CharSequence displayName = mMessageHelper.getDisplayName(account, fromAddrs, toAddrs); + + Date sentDate = new Date(cursor.getLong(DATE_COLUMN)); + String displayDate = mMessageHelper.formatDate(sentDate); + + String preview = cursor.getString(PREVIEW_COLUMN); + if (preview == null) { + preview = ""; + } + + String subject = cursor.getString(SUBJECT_COLUMN); + if (StringUtils.isNullOrEmpty(subject)) { + subject = getString(R.string.general_no_subject); + } + + int threadCount = (mThreadedList) ? cursor.getInt(THREAD_COUNT_COLUMN) : 0; + + String flagList = cursor.getString(FLAGS_COLUMN); + String[] flags = flagList.split(","); + boolean read = false; + boolean flagged = false; + boolean answered = false; + boolean forwarded = false; + for (int i = 0, len = flags.length; i < len; i++) { + try { + switch (Flag.valueOf(flags[i])) { + case SEEN: { + read = true; + break; + } + case FLAGGED: { + flagged = true; + break; + } + case ANSWERED: { + answered = true; + break; + } + case FORWARDED: { + forwarded = true; + break; + } + default: { + // We don't care about the other flags + } + } + } catch (Exception e) { /* ignore */ } + } + + boolean hasAttachments = (cursor.getInt(ATTACHMENT_COUNT_COLUMN) > 0); + + MessageViewHolder holder = (MessageViewHolder) view.getTag(); + + int maybeBoldTypeface = (read) ? Typeface.NORMAL : Typeface.BOLD; + + long uniqueId = cursor.getLong(mUniqueIdColumn); + boolean selected = mSelected.contains(uniqueId); + + if (!mCheckboxes && selected) { + holder.chip.setBackgroundDrawable(account.getCheckmarkChip().drawable()); + } else { + holder.chip.setBackgroundDrawable(account.generateColorChip(read, toMe, ccMe, + fromMe, flagged).drawable()); + } + + if (mCheckboxes) { + // Set holder.position to -1 to avoid MessageViewHolder.onCheckedChanged() toggling + // the selection state when setChecked() is called below. + holder.position = -1; + + // Only set the UI state, don't actually toggle the message selection. + holder.selected.setChecked(selected); + + // Now save the position so MessageViewHolder.onCheckedChanged() will know what + // message to (de)select. + holder.position = cursor.getPosition(); + } + + // Background indicator + if (K9.useBackgroundAsUnreadIndicator()) { + int res = (read) ? R.attr.messageListReadItemBackgroundColor : + R.attr.messageListUnreadItemBackgroundColor; + + TypedValue outValue = new TypedValue(); + getActivity().getTheme().resolveAttribute(res, outValue, true); + view.setBackgroundColor(outValue.data); + } + + // Thread count + if (threadCount > 1) { + holder.threadCount.setText(Integer.toString(threadCount)); + holder.threadCount.setVisibility(View.VISIBLE); + } else { + holder.threadCount.setVisibility(View.GONE); + } + + CharSequence beforePreviewText = (mSenderAboveSubject) ? subject : displayName; + + String sigil = recipientSigil(toMe, ccMe); + + holder.preview.setText( + new SpannableStringBuilder(sigil) + .append(beforePreviewText) + .append(" ") + .append(preview), TextView.BufferType.SPANNABLE); + + Spannable str = (Spannable)holder.preview.getText(); + + // Create a span section for the sender, and assign the correct font size and weight + int fontSize = (mSenderAboveSubject) ? + mFontSizes.getMessageListSubject(): + mFontSizes.getMessageListSender(); + + AbsoluteSizeSpan span = new AbsoluteSizeSpan(fontSize, true); + str.setSpan(span, 0, beforePreviewText.length() + 1, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + + //TODO: make this part of the theme + int color = (K9.getK9Theme() == K9.THEME_LIGHT) ? + Color.rgb(105, 105, 105) : + Color.rgb(160, 160, 160); + + // Set span (color) for preview message + str.setSpan(new ForegroundColorSpan(color), beforePreviewText.length() + 1, + str.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + + Drawable statusHolder = null; + if (forwarded && answered) { + statusHolder = mForwardedAnsweredIcon; + } else if (answered) { + statusHolder = mAnsweredIcon; + } else if (forwarded) { + statusHolder = mForwardedIcon; + } + + if (holder.from != null ) { + holder.from.setTypeface(null, maybeBoldTypeface); + if (mSenderAboveSubject) { + holder.from.setCompoundDrawablesWithIntrinsicBounds( + statusHolder, // left + null, // top + hasAttachments ? mAttachmentIcon : null, // right + null); // bottom + + holder.from.setText(displayName); + } else { + holder.from.setText(new SpannableStringBuilder(sigil).append(displayName)); + } + } + + if (holder.subject != null ) { + if (!mSenderAboveSubject) { + holder.subject.setCompoundDrawablesWithIntrinsicBounds( + statusHolder, // left + null, // top + hasAttachments ? mAttachmentIcon : null, // right + null); // bottom + } + + holder.subject.setTypeface(null, maybeBoldTypeface); + holder.subject.setText(subject); + } + + holder.date.setText(displayDate); } } - class MessageViewHolder - implements OnCheckedChangeListener { + class MessageViewHolder implements OnCheckedChangeListener { public TextView subject; public TextView preview; public TextView from; public TextView time; public TextView date; public View chip; + public TextView threadCount; public CheckBox selected; public int position = -1; @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (position != -1) { - MessageInfoHolder message = (MessageInfoHolder) mAdapter.getItem(position); - toggleMessageSelect(message); - - + toggleMessageSelectWithAdapterPosition(position); } } } @@ -2180,20 +1817,20 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick } private void updateFooterView() { - if (mCurrentFolder != null && mAccount != null) { + if (!mSearch.isManualSearch() && mCurrentFolder != null && mAccount != null) { if (mCurrentFolder.loading) { final boolean showProgress = true; - updateFooter(getString(R.string.status_loading_more), showProgress); + updateFooter(mContext.getString(R.string.status_loading_more), showProgress); } else { String message; if (!mCurrentFolder.lastCheckFailed) { if (mAccount.getDisplayCount() == 0) { - message = getString(R.string.message_list_load_more_messages_action); + message = mContext.getString(R.string.message_list_load_more_messages_action); } else { - message = String.format(getString(R.string.load_more_messages_fmt), mAccount.getDisplayCount()); + message = String.format(mContext.getString(R.string.load_more_messages_fmt), mAccount.getDisplayCount()); } } else { - message = getString(R.string.status_loading_more_failed); + message = mContext.getString(R.string.status_loading_more_failed); } final boolean showProgress = false; updateFooter(message, showProgress); @@ -2205,6 +1842,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick } public void updateFooter(final String text, final boolean progressVisible) { + if (mFooterView == null) { + return; + } + FooterViewHolder holder = (FooterViewHolder) mFooterView.getTag(); holder.progress.setVisibility(progressVisible ? ProgressBar.VISIBLE : ProgressBar.INVISIBLE); @@ -2223,23 +1864,6 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick public TextView main; } - private void setAllSelected(boolean isSelected) { - mSelectedCount = 0; - - for (MessageInfoHolder holder : mAdapter.getMessages()) { - holder.selected = isSelected; - mSelectedCount += (isSelected ? 1 : 0); - } - - computeBatchDirection(); - mAdapter.notifyDataSetChanged(); - - if (isSelected) { - updateActionModeTitle(); - computeSelectAllVisibility(); - } - } - /** * Set selection state for all messages. * @@ -2248,40 +1872,87 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick * action mode is finished. */ private void setSelectionState(boolean selected) { - mAdapter.setSelectionForAllMesages(selected); - if (selected) { - mSelectedCount = mAdapter.getCount(); - mActionMode = getSherlockActivity().startActionMode(mActionModeCallback); + if (mAdapter.getCount() == 0) { + // Nothing to do if there are no messages + return; + } + + mSelectedCount = 0; + for (int i = 0, end = mAdapter.getCount(); i < end; i++) { + Cursor cursor = (Cursor) mAdapter.getItem(i); + long uniqueId = cursor.getLong(mUniqueIdColumn); + mSelected.add(uniqueId); + + if (mThreadedList) { + int threadCount = cursor.getInt(THREAD_COUNT_COLUMN); + mSelectedCount += (threadCount > 1) ? threadCount : 1; + } else { + mSelectedCount++; + } + } + + if (mActionMode == null) { + mActionMode = getSherlockActivity().startActionMode(mActionModeCallback); + } + computeBatchDirection(); updateActionModeTitle(); computeSelectAllVisibility(); - computeBatchDirection(); } else { + mSelected.clear(); mSelectedCount = 0; if (mActionMode != null) { mActionMode.finish(); + mActionMode = null; } } + + mAdapter.notifyDataSetChanged(); } - private void toggleMessageSelect(final MessageInfoHolder holder){ + private void toggleMessageSelect(int listViewPosition) { + int adapterPosition = listViewToAdapterPosition(listViewPosition); + if (adapterPosition == AdapterView.INVALID_POSITION) { + return; + } + + toggleMessageSelectWithAdapterPosition(adapterPosition); + } + + private void toggleMessageSelectWithAdapterPosition(int adapterPosition) { + Cursor cursor = (Cursor) mAdapter.getItem(adapterPosition); + long uniqueId = cursor.getLong(mUniqueIdColumn); + + boolean selected = mSelected.contains(uniqueId); + if (!selected) { + mSelected.add(uniqueId); + } else { + mSelected.remove(uniqueId); + } + + int selectedCountDelta = 1; + if (mThreadedList) { + int threadCount = cursor.getInt(THREAD_COUNT_COLUMN); + if (threadCount > 1) { + selectedCountDelta = threadCount; + } + } + if (mActionMode != null) { - if (mSelectedCount == 1 && holder.selected) { + if (mSelectedCount == selectedCountDelta && selected) { mActionMode.finish(); + mActionMode = null; return; } } else { mActionMode = getSherlockActivity().startActionMode(mActionModeCallback); } - if (holder.selected) { - holder.selected = false; - mSelectedCount -= 1; + if (selected) { + mSelectedCount -= selectedCountDelta; } else { - holder.selected = true; - mSelectedCount += 1; + mSelectedCount += selectedCountDelta; } - mAdapter.notifyDataSetChanged(); computeBatchDirection(); updateActionModeTitle(); @@ -2290,6 +1961,8 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick mActionMode.invalidate(); computeSelectAllVisibility(); + + mAdapter.notifyDataSetChanged(); } private void updateActionModeTitle() { @@ -2297,19 +1970,24 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick } private void computeSelectAllVisibility() { - mActionModeCallback.showSelectAll(mSelectedCount != mAdapter.getCount()); + mActionModeCallback.showSelectAll(mSelected.size() != mAdapter.getCount()); } private void computeBatchDirection() { boolean isBatchFlag = false; boolean isBatchRead = false; - for (MessageInfoHolder holder : mAdapter.getMessages()) { - if (holder.selected) { - if (!holder.flagged) { + for (int i = 0, end = mAdapter.getCount(); i < end; i++) { + Cursor cursor = (Cursor) mAdapter.getItem(i); + long uniqueId = cursor.getLong(mUniqueIdColumn); + + if (mSelected.contains(uniqueId)) { + String flags = cursor.getString(FLAGS_COLUMN); + + if (!flags.contains(Flag.FLAGGED.name())) { isBatchFlag = true; } - if (!holder.read) { + if (!flags.contains(Flag.SEEN.name())) { isBatchRead = true; } @@ -2323,132 +2001,154 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick mActionModeCallback.showFlag(isBatchFlag); } - /** - * @param holders - * Messages to update. Never {@code null}. - * @param flag - * Flag to be updated on the specified messages. Never - * {@code null}. - * @param newState - * State to set for the given flag. - */ - private void setFlag(final List holders, final Flag flag, final boolean newState) { - if (holders.isEmpty()) { + private void setFlag(Message message, final Flag flag, final boolean newState) { + setFlag(Collections.singletonList(message), flag, newState); + } + + private void setFlag(List messages, final Flag flag, final boolean newState) { + if (messages.size() == 0) { return; } - final Message[] messageList = new Message[holders.size()]; - int i = 0; - for (final Iterator iterator = holders.iterator(); iterator.hasNext(); i++) { - final MessageInfoHolder holder = iterator.next(); - messageList[i] = holder.message; - if (flag == Flag.SEEN) { - holder.read = newState; - } else if (flag == Flag.FLAGGED) { - holder.flagged = newState; - } + + if (mThreadedList) { + mController.setFlagForThreads(messages, flag, newState); + } else { + mController.setFlag(messages, flag, newState); } - mController.setFlag(messageList, flag, newState); - mAdapter.sortMessages(); computeBatchDirection(); } + private void onMove(Message message) { + onMove(Collections.singletonList(message)); + } + /** * Display the message move activity. * - * @param holders - * Never {@code null}. + * @param messages + * Never {@code null}. */ - private void onMove(final List holders) { - if (!checkCopyOrMovePossible(holders, FolderOperation.MOVE)) { + private void onMove(List messages) { + if (!checkCopyOrMovePossible(messages, FolderOperation.MOVE)) { return; } - final Folder folder = holders.size() == 1 ? holders.get(0).message.getFolder() : mCurrentFolder.folder; - displayFolderChoice(ACTIVITY_CHOOSE_FOLDER_MOVE, folder, holders); + final Folder folder = (messages.size() == 1) ? + messages.get(0).getFolder() : mCurrentFolder.folder; + + displayFolderChoice(ACTIVITY_CHOOSE_FOLDER_MOVE, folder, messages); + } + + private void onCopy(Message message) { + onCopy(Collections.singletonList(message)); } /** * Display the message copy activity. * - * @param holders - * Never {@code null}. + * @param messages + * Never {@code null}. */ - private void onCopy(final List holders) { - if (!checkCopyOrMovePossible(holders, FolderOperation.COPY)) { + private void onCopy(List messages) { + if (!checkCopyOrMovePossible(messages, FolderOperation.COPY)) { return; } - final Folder folder = holders.size() == 1 ? holders.get(0).message.getFolder() : mCurrentFolder.folder; - displayFolderChoice(ACTIVITY_CHOOSE_FOLDER_COPY, folder, holders); + final Folder folder = (messages.size() == 1) ? + messages.get(0).getFolder() : mCurrentFolder.folder; + + displayFolderChoice(ACTIVITY_CHOOSE_FOLDER_COPY, folder, messages); } /** - * Helper method to manage the invocation of - * {@link #startActivityForResult(Intent, int)} for a folder operation - * ({@link ChooseFolder} activity), while saving a list of associated - * messages. + * Helper method to manage the invocation of {@link #startActivityForResult(Intent, int)} for a + * folder operation ({@link ChooseFolder} activity), while saving a list of associated messages. * * @param requestCode - * If >= 0, this code will be returned in onActivityResult() when - * the activity exits. + * If {@code >= 0}, this code will be returned in {@code onActivityResult()} when the + * activity exits. * @param folder - * Never {@code null}. - * @param holders - * Messages to be affected by the folder operation. Never - * {@code null}. + * The source folder. Never {@code null}. + * @param messages + * Messages to be affected by the folder operation. Never {@code null}. + * * @see #startActivityForResult(Intent, int) */ - private void displayFolderChoice(final int requestCode, final Folder folder, final List holders) { + private void displayFolderChoice(final int requestCode, final Folder folder, final List messages) { final Intent intent = new Intent(getActivity(), ChooseFolder.class); intent.putExtra(ChooseFolder.EXTRA_ACCOUNT, folder.getAccount().getUuid()); intent.putExtra(ChooseFolder.EXTRA_CUR_FOLDER, folder.getName()); intent.putExtra(ChooseFolder.EXTRA_SEL_FOLDER, folder.getAccount().getLastSelectedFolderName()); // remember the selected messages for #onActivityResult - mActiveMessages = holders; + mActiveMessages = messages; startActivityForResult(intent, requestCode); } - /** - * @param holders - * Never {@code null}. - */ - private void onArchive(final List holders) { - final String folderName = holders.get(0).message.getFolder().getAccount().getArchiveFolderName(); - if (K9.FOLDER_NONE.equalsIgnoreCase(folderName)) { - return; + private void onArchive(final Message message) { + onArchive(Collections.singletonList(message)); + } + + private void onArchive(final List messages) { + Map> messagesByAccount = groupMessagesByAccount(messages); + + for (Entry> entry : messagesByAccount.entrySet()) { + Account account = entry.getKey(); + String archiveFolder = account.getArchiveFolderName(); + + if (!K9.FOLDER_NONE.equals(archiveFolder)) { + move(entry.getValue(), archiveFolder); + } } - // TODO one should separate messages by account and call move afterwards - // (because each account might have a specific Archive folder name) - move(holders, folderName); + } + + private Map> groupMessagesByAccount(final List messages) { + Map> messagesByAccount = new HashMap>(); + for (Message message : messages) { + Account account = message.getFolder().getAccount(); + + List msgList = messagesByAccount.get(account); + if (msgList == null) { + msgList = new ArrayList(); + messagesByAccount.put(account, msgList); + } + + msgList.add(message); + } + return messagesByAccount; + } + + private void onSpam(Message message) { + onSpam(Collections.singletonList(message)); } /** - * @param holders - * Never {@code null}. + * Move messages to the spam folder. + * + * @param messages + * The messages to move to the spam folder. Never {@code null}. */ - private void onSpam(final List holders) { + private void onSpam(List messages) { if (K9.confirmSpam()) { // remember the message selection for #onCreateDialog(int) - mActiveMessages = holders; + mActiveMessages = messages; showDialog(R.id.dialog_confirm_spam); } else { - onSpamConfirmed(holders); + onSpamConfirmed(messages); } } - /** - * @param holders - * Never {@code null}. - */ - private void onSpamConfirmed(final List holders) { - final String folderName = holders.get(0).message.getFolder().getAccount().getSpamFolderName(); - if (K9.FOLDER_NONE.equalsIgnoreCase(folderName)) { - return; + private void onSpamConfirmed(List messages) { + Map> messagesByAccount = groupMessagesByAccount(messages); + + for (Entry> entry : messagesByAccount.entrySet()) { + Account account = entry.getKey(); + String spamFolder = account.getSpamFolderName(); + + if (!K9.FOLDER_NONE.equals(spamFolder)) { + move(entry.getValue(), spamFolder); + } } - // TODO one should separate messages by account and call move afterwards - // (because each account might have a specific Spam folder name) - move(holders, folderName); } private static enum FolderOperation { @@ -2456,32 +2156,36 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick } /** - * Display an Toast message if any message isn't synchronized + * Display a Toast message if any message isn't synchronized * - * @param holders - * Never null. + * @param messages + * The messages to copy or move. Never {@code null}. * @param operation - * Never {@code null}. + * The type of operation to perform. Never {@code null}. * - * @return true if operation is possible + * @return {@code true}, if operation is possible. */ - private boolean checkCopyOrMovePossible(final List holders, final FolderOperation operation) { - if (holders.isEmpty()) { + private boolean checkCopyOrMovePossible(final List messages, + final FolderOperation operation) { + + if (messages.size() == 0) { return false; } + boolean first = true; - for (final MessageInfoHolder holder : holders) { - final Message message = holder.message; + for (final Message message : messages) { if (first) { first = false; // account check final Account account = message.getFolder().getAccount(); - if ((operation == FolderOperation.MOVE && !mController.isMoveCapable(account)) || (operation == FolderOperation.COPY && !mController.isCopyCapable(account))) { + if ((operation == FolderOperation.MOVE && !mController.isMoveCapable(account)) || + (operation == FolderOperation.COPY && !mController.isCopyCapable(account))) { return false; } } // message check - if ((operation == FolderOperation.MOVE && !mController.isMoveCapable(message)) || (operation == FolderOperation.COPY && !mController.isCopyCapable(message))) { + if ((operation == FolderOperation.MOVE && !mController.isMoveCapable(message)) || + (operation == FolderOperation.COPY && !mController.isCopyCapable(message))) { final Toast toast = Toast.makeText(getActivity(), R.string.move_copy_cannot_copy_unsynced_message, Toast.LENGTH_LONG); toast.show(); @@ -2491,52 +2195,28 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick return true; } - /** - * Helper method to get a List of message ready to be processed. This implementation will return a list containing the sole argument. - * - * @param holder Never {@code null}. - * @return Never {@code null}. - */ - private List getSelectionFromMessage(final MessageInfoHolder holder) { - final List selection = Collections.singletonList(holder); - return selection; - } - - /** - * Helper method to get a List of message ready to be processed. This implementation will iterate over messages and choose the checked ones. - * - * @return Never {@code null}. - */ - private List getSelectionFromCheckboxes() { - final List selection = new ArrayList(); - - for (final MessageInfoHolder holder : mAdapter.getMessages()) { - if (holder.selected) { - selection.add(holder); - } - } - - return selection; - } - /** * Copy the specified messages to the specified folder. * - * @param holders Never {@code null}. - * @param destination Never {@code null}. + * @param messages + * List of messages to copy. Never {@code null}. + * @param destination + * The name of the destination folder. Never {@code null}. */ - private void copy(final List holders, final String destination) { - copyOrMove(holders, destination, FolderOperation.COPY); + private void copy(List messages, final String destination) { + copyOrMove(messages, destination, FolderOperation.COPY); } /** * Move the specified messages to the specified folder. * - * @param holders Never {@code null}. - * @param destination Never {@code null}. + * @param messages + * The list of messages to move. Never {@code null}. + * @param destination + * The name of the destination folder. Never {@code null}. */ - private void move(final List holders, final String destination) { - copyOrMove(holders, destination, FolderOperation.MOVE); + private void move(List messages, final String destination) { + copyOrMove(messages, destination, FolderOperation.MOVE); } /** @@ -2544,16 +2224,16 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick * {@link #move(List, String)}. This method was added mainly because those 2 * methods share common behavior. * - * Note: Must be called from the UI thread! - * - * @param holders - * Never {@code null}. + * @param messages + * The list of messages to copy or move. Never {@code null}. * @param destination - * Never {@code null}. + * The name of the destination folder. Never {@code null}. * @param operation - * Never {@code null}. + * Specifies what operation to perform. Never {@code null}. */ - private void copyOrMove(final List holders, final String destination, final FolderOperation operation) { + private void copyOrMove(List messages, final String destination, + final FolderOperation operation) { + if (K9.FOLDER_NONE.equalsIgnoreCase(destination)) { return; } @@ -2562,68 +2242,59 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick Account account = null; String folderName = null; - final List messages = new ArrayList(holders.size()); + List outMessages = new ArrayList(); - for (final MessageInfoHolder holder : holders) { - final Message message = holder.message; + for (Message message : messages) { if (first) { first = false; + folderName = message.getFolder().getName(); account = message.getFolder().getAccount(); - if ((operation == FolderOperation.MOVE && !mController.isMoveCapable(account)) || (operation == FolderOperation.COPY && !mController.isCopyCapable(account))) { - // account is not copy/move capable + + if ((operation == FolderOperation.MOVE && !mController.isMoveCapable(account)) || + (operation == FolderOperation.COPY && + !mController.isCopyCapable(account))) { + + // Account is not copy/move capable return; } - } else if (!account.equals(message.getFolder().getAccount()) - || !folderName.equals(message.getFolder().getName())) { - // make sure all messages come from the same account/folder? + } else if (!message.getFolder().getAccount().equals(account) || + !message.getFolder().getName().equals(folderName)) { + + // Make sure all messages come from the same account/folder return; } - if ((operation == FolderOperation.MOVE && !mController.isMoveCapable(message)) || (operation == FolderOperation.COPY && !mController.isCopyCapable(message))) { - final Toast toast = Toast.makeText(getActivity(), R.string.move_copy_cannot_copy_unsynced_message, - Toast.LENGTH_LONG); - toast.show(); + + if ((operation == FolderOperation.MOVE && !mController.isMoveCapable(message)) || + (operation == FolderOperation.COPY && !mController.isCopyCapable(message))) { + + Toast.makeText(getActivity(), R.string.move_copy_cannot_copy_unsynced_message, + Toast.LENGTH_LONG).show(); // XXX return meaningful error value? // message isn't synchronized return; } - messages.add(message); + + outMessages.add(message); } if (operation == FolderOperation.MOVE) { - mController.moveMessages(account, folderName, messages.toArray(new Message[messages.size()]), destination, - null); - mAdapter.removeMessages(holders); + if (mThreadedList) { + mController.moveMessagesInThread(account, folderName, outMessages, destination); + } else { + mController.moveMessages(account, folderName, outMessages, destination, null); + } } else { - mController.copyMessages(account, folderName, messages.toArray(new Message[messages.size()]), destination, - null); + if (mThreadedList) { + mController.copyMessagesInThread(account, folderName, outMessages, destination); + } else { + mController.copyMessages(account, folderName, outMessages, destination, null); + } } } - /** - * Return the currently "open" account if available. - * - * @param prefs - * A {@link Preferences} instance that might be used to retrieve the current - * {@link Account}. - * - * @return The {@code Account} all displayed messages belong to. - */ - private Account getCurrentAccount(Preferences prefs) { - Account account = null; - if (mQueryString != null && !mIntegrate && mAccountUuids != null && - mAccountUuids.length == 1) { - String uuid = mAccountUuids[0]; - account = prefs.getAccount(uuid); - } else if (mAccount != null) { - account = mAccount; - } - - return account; - } - class ActionModeCallback implements ActionMode.Callback { private MenuItem mSelectAll; @@ -2640,30 +2311,51 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick mFlag = menu.findItem(R.id.flag); mUnflag = menu.findItem(R.id.unflag); - if (mQueryString != null) { + // we don't support cross account actions atm + if (!mSingleAccountMode) { // show all menu.findItem(R.id.move).setVisible(true); menu.findItem(R.id.archive).setVisible(true); menu.findItem(R.id.spam).setVisible(true); menu.findItem(R.id.copy).setVisible(true); - // hide uncapable - /* - * TODO think of a better way then looping over all - * messages. - */ - final List selection = getSelectionFromCheckboxes(); - Account account; + Set accountUuids = getAccountUuidsForSelected(); - for (MessageInfoHolder holder : selection) { - account = holder.message.getFolder().getAccount(); - setContextCapabilities(account, menu); + for (String accountUuid : accountUuids) { + Account account = mPreferences.getAccount(accountUuid); + if (account != null) { + setContextCapabilities(account, menu); + } } } return true; } + /** + * Get the set of account UUIDs for the selected messages. + */ + private Set getAccountUuidsForSelected() { + int maxAccounts = mAccountUuids.length; + Set accountUuids = new HashSet(maxAccounts); + + for (int position = 0, end = mAdapter.getCount(); position < end; position++) { + Cursor cursor = (Cursor) mAdapter.getItem(position); + long uniqueId = cursor.getLong(mUniqueIdColumn); + + if (mSelected.contains(uniqueId)) { + String accountUuid = cursor.getString(ACCOUNT_UUID_COLUMN); + accountUuids.add(accountUuid); + + if (accountUuids.size() == mAccountUuids.length) { + break; + } + } + } + + return accountUuids; + } + @Override public void onDestroyActionMode(ActionMode mode) { mActionMode = null; @@ -2672,7 +2364,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick mMarkAsUnread = null; mFlag = null; mUnflag = null; - setAllSelected(false); + setSelectionState(false); } @Override @@ -2681,53 +2373,49 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick inflater.inflate(R.menu.message_list_context, menu); // check capabilities - if (mQueryString == null) { - setContextCapabilities(mAccount, menu); - } + setContextCapabilities(mAccount, menu); return true; } /** - * Disables menu options based on if the account supports it or not. - * It also checks the controller and for now the 'mode' the messagelist - * is operation in ( query or not ). + * Disables menu options not supported by the account type or current "search view". * - * @param mAccount Account to check capabilities of. - * @param menu Menu to adapt. + * @param account + * The account to query for its capabilities. + * @param menu + * The menu to adapt. */ - private void setContextCapabilities(Account mAccount, Menu menu) { - /* - * TODO get rid of this when we finally split the messagelist into - * a folder content display and a search result display - */ - if (mQueryString != null) { + private void setContextCapabilities(Account account, Menu menu) { + if (!mSingleAccountMode) { + // We don't support cross-account copy/move operations right now menu.findItem(R.id.move).setVisible(false); menu.findItem(R.id.copy).setVisible(false); + //TODO: we could support the archive and spam operations if all selected messages + // belong to non-POP3 accounts menu.findItem(R.id.archive).setVisible(false); menu.findItem(R.id.spam).setVisible(false); - return; - } + } else { + // hide unsupported + if (!mController.isCopyCapable(account)) { + menu.findItem(R.id.copy).setVisible(false); + } - // hide unsupported - if (!mController.isCopyCapable(mAccount)) { - menu.findItem(R.id.copy).setVisible(false); - } + if (!mController.isMoveCapable(account)) { + menu.findItem(R.id.move).setVisible(false); + menu.findItem(R.id.archive).setVisible(false); + menu.findItem(R.id.spam).setVisible(false); + } - if (!mController.isMoveCapable(mAccount)) { - menu.findItem(R.id.move).setVisible(false); - menu.findItem(R.id.archive).setVisible(false); - menu.findItem(R.id.spam).setVisible(false); - } + if (!account.hasArchiveFolder()) { + menu.findItem(R.id.archive).setVisible(false); + } - if (!mAccount.hasArchiveFolder()) { - menu.findItem(R.id.archive).setVisible(false); - } - - if (!mAccount.hasSpamFolder()) { - menu.findItem(R.id.spam).setVisible(false); + if (!account.hasSpamFolder()) { + menu.findItem(R.id.spam).setVisible(false); + } } } @@ -2753,7 +2441,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - final List selection = getSelectionFromCheckboxes(); + List messages = getCheckedMessages(); /* * In the following we assume that we can't move or copy @@ -2764,49 +2452,51 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick */ switch (item.getItemId()) { case R.id.delete: { - onDelete(selection); + onDelete(messages); + + //FIXME mSelectedCount = 0; break; } case R.id.mark_as_read: { - setFlag(selection, Flag.SEEN, true); + setFlag(messages, Flag.SEEN, true); break; } case R.id.mark_as_unread: { - setFlag(selection, Flag.SEEN, false); + setFlag(messages, Flag.SEEN, false); break; } case R.id.flag: { - setFlag(selection, Flag.FLAGGED, true); + setFlag(messages, Flag.FLAGGED, true); break; } case R.id.unflag: { - setFlag(selection, Flag.FLAGGED, false); + setFlag(messages, Flag.FLAGGED, false); break; } case R.id.select_all: { - setAllSelected(true); + selectAll(); break; } // only if the account supports this case R.id.archive: { - onArchive(selection); + onArchive(messages); mSelectedCount = 0; break; } case R.id.spam: { - onSpam(selection); + onSpam(messages); mSelectedCount = 0; break; } case R.id.move: { - onMove(selection); + onMove(messages); mSelectedCount = 0; break; } case R.id.copy: { - onCopy(selection); + onCopy(messages); mSelectedCount = 0; break; } @@ -2848,18 +2538,18 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick } public void checkMail() { - mController.synchronizeMailbox(mAccount, mFolderName, mAdapter.mListener, null); - mController.sendPendingMessages(mAccount, mAdapter.mListener); + mController.synchronizeMailbox(mAccount, mFolderName, mListener, null); + mController.sendPendingMessages(mAccount, mListener); } /** - * We need to do some special clean up when leaving a remote search result screen. If no remote search is - * in progress, this method does nothing special. + * We need to do some special clean up when leaving a remote search result screen. If no + * remote search is in progress, this method does nothing special. */ @Override public void onStop() { // If we represent a remote search, then kill that before going back. - if (mSearchAccount != null && mSearchFolder != null && mRemoteSearchFuture != null) { + if (isRemoteSearch() && mRemoteSearchFuture != null) { try { Log.i(K9.LOG_TAG, "Remote search in progress, attempting to abort..."); // Canceling the future stops any message fetches in progress. @@ -2868,13 +2558,11 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick Log.e(K9.LOG_TAG, "Could not cancel remote search future."); } // Closing the folder will kill off the connection if we're mid-search. - Context appContext = getActivity().getApplicationContext(); - final Account searchAccount = Preferences.getPreferences(appContext).getAccount(mSearchAccount); - final Store remoteStore = searchAccount.getRemoteStore(); - final Folder remoteFolder = remoteStore.getFolder(mSearchFolder); + final Account searchAccount = mAccount; + final Folder remoteFolder = mCurrentFolder.folder; remoteFolder.close(); // Send a remoteSearchFinished() message for good measure. - mAdapter.mListener.remoteSearchFinished(searchAccount, mSearchFolder, 0, null); + //mAdapter.mListener.remoteSearchFinished(searchAccount, mCurrentFolder.name, 0, null); } catch (Exception e) { // Since the user is going back, log and squash any exceptions. Log.e(K9.LOG_TAG, "Could not abort remote search before going back", e); @@ -2886,8 +2574,14 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick public ArrayList getMessageReferences() { ArrayList messageRefs = new ArrayList(); - for (MessageInfoHolder holder : mAdapter.getMessages()) { - MessageReference ref = holder.message.makeMessageReference(); + for (int i = 0, len = mAdapter.getCount(); i < len; i++) { + Cursor cursor = (Cursor) mAdapter.getItem(i); + + MessageReference ref = new MessageReference(); + ref.accountUuid = cursor.getString(ACCOUNT_UUID_COLUMN); + ref.folderName = cursor.getString(FOLDER_NAME_COLUMN); + ref.uid = cursor.getString(UID_COLUMN); + messageRefs.add(ref); } @@ -2921,7 +2615,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick public interface MessageListFragmentListener { void setMessageListProgress(int level); - void remoteSearch(String searchAccount, String searchFolder, String queryString); + void showThread(Account account, String folderName, long rootId); void showMoreFromSameSender(String senderAddress); void onResendMessage(Message message); void onForward(Message message); @@ -2933,69 +2627,101 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick void setUnreadCount(int unread); void onCompose(Account account); boolean startSearch(Account account, String folderName); + void remoteSearchStarted(); } public void onReverseSort() { changeSort(mSortType); } - private MessageInfoHolder getSelection() { - return (MessageInfoHolder) mListView.getSelectedItem(); + private Message getSelectedMessage() { + int listViewPosition = mListView.getSelectedItemPosition(); + int adapterPosition = listViewToAdapterPosition(listViewPosition); + + return getMessageAtPosition(adapterPosition); + } + + private Message getMessageAtPosition(int adapterPosition) { + if (adapterPosition == AdapterView.INVALID_POSITION) { + return null; + } + + Cursor cursor = (Cursor) mAdapter.getItem(adapterPosition); + String uid = cursor.getString(UID_COLUMN); + + Account account = getAccountFromCursor(cursor); + long folderId = cursor.getLong(FOLDER_ID_COLUMN); + Folder folder = getFolderById(account, folderId); + + try { + return folder.getMessage(uid); + } catch (MessagingException e) { + Log.e(K9.LOG_TAG, "Something went wrong while fetching a message", e); + } + + return null; + } + + private List getCheckedMessages() { + List messages = new ArrayList(mSelected.size()); + for (int position = 0, end = mAdapter.getCount(); position < end; position++) { + Cursor cursor = (Cursor) mAdapter.getItem(position); + long uniqueId = cursor.getLong(mUniqueIdColumn); + + if (mSelected.contains(uniqueId)) { + messages.add(getMessageAtPosition(position)); + } + } + + return messages; } public void onDelete() { - MessageInfoHolder message = getSelection(); + Message message = getSelectedMessage(); if (message != null) { onDelete(Collections.singletonList(message)); } } public void toggleMessageSelect() { - MessageInfoHolder message = getSelection(); - if (message != null) { - toggleMessageSelect(message); - } + toggleMessageSelect(mListView.getSelectedItemPosition()); } public void onToggleFlag() { - MessageInfoHolder message = getSelection(); + Message message = getSelectedMessage(); if (message != null) { - setFlag(Collections.singletonList(message), Flag.FLAGGED, !message.flagged); + setFlag(message, Flag.FLAGGED, !message.isSet(Flag.FLAGGED)); } } public void onMove() { - MessageInfoHolder message = getSelection(); + Message message = getSelectedMessage(); if (message != null) { - onMove(Collections.singletonList(message)); + onMove(message); } } public void onArchive() { - MessageInfoHolder message = getSelection(); + Message message = getSelectedMessage(); if (message != null) { - onArchive(Collections.singletonList(message)); + onArchive(message); } } public void onCopy() { - MessageInfoHolder message = getSelection(); + Message message = getSelectedMessage(); if (message != null) { - onCopy(Collections.singletonList(message)); + onCopy(message); } } public void onToggleRead() { - MessageInfoHolder message = getSelection(); + Message message = getSelectedMessage(); if (message != null) { - setFlag(Collections.singletonList(message), Flag.SEEN, !message.read); + setFlag(message, Flag.SEEN, !message.isSet(Flag.SEEN)); } } - public boolean isSearchQuery() { - return (mQueryString != null || mIntegrate); - } - public boolean isOutbox() { return (mFolderName != null && mFolderName.equals(mAccount.getOutboxFolderName())); } @@ -3005,7 +2731,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick } public boolean isRemoteFolder() { - if (isSearchQuery() || isOutbox() || isErrorFolder()) { + if (mSearch.isManualSearch() || isOutbox() || isErrorFolder()) { return false; } @@ -3017,6 +2743,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick return true; } + public boolean isManualSearch() { + return mSearch.isManualSearch(); + } + public boolean isAccountExpungeCapable() { try { return (mAccount != null && mAccount.getRemoteStore().isExpungeCapable()); @@ -3028,7 +2758,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick public void onRemoteSearch() { // Remote search is useless without the network. if (mHasConnectivity) { - onRemoteSearchRequested(true); + onRemoteSearchRequested(); } else { Toast.makeText(getActivity(), getText(R.string.remote_search_unavailable_no_network), Toast.LENGTH_SHORT).show(); @@ -3036,19 +2766,16 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick } public boolean isRemoteSearch() { - return mRemoteSearch; + return mRemoteSearchPerformed; } public boolean isRemoteSearchAllowed() { - if (!isSearchQuery() || mRemoteSearch || mSearchFolder == null || mSearchAccount == null) { + if (!mSearch.isManualSearch() || mRemoteSearchPerformed || !mSingleFolderMode) { return false; } - Context appContext = getActivity().getApplicationContext(); - final Preferences prefs = Preferences.getPreferences(appContext); - boolean allowRemoteSearch = false; - final Account searchAccount = prefs.getAccount(mSearchAccount); + final Account searchAccount = mAccount; if (searchAccount != null) { allowRemoteSearch = searchAccount.allowRemoteSearch(); } @@ -3060,4 +2787,127 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick String folderName = (mCurrentFolder != null) ? mCurrentFolder.name : null; return mFragmentListener.startSearch(mAccount, folderName); } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + String accountUuid = mAccountUuids[id]; + Account account = mPreferences.getAccount(accountUuid); + + Uri uri; + String[] projection; + if (mThreadedList) { + uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + accountUuid + "/messages/threaded"); + projection = THREADED_PROJECTION; + } else { + uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + accountUuid + "/messages"); + projection = PROJECTION; + } + + StringBuilder query = new StringBuilder(); + List queryArgs = new ArrayList(); + SqlQueryBuilder.buildWhereClause(account, mSearch.getConditions(), query, queryArgs); + + String selection = query.toString(); + String[] selectionArgs = queryArgs.toArray(new String[0]); + + String sortOrder = buildSortOrder(); + + return new CursorLoader(getActivity(), uri, projection, selection, selectionArgs, + sortOrder); + } + + private String buildSortOrder() { + String sortColumn = MessageColumns.ID; + switch (mSortType) { + case SORT_ARRIVAL: { + sortColumn = MessageColumns.INTERNAL_DATE; + break; + } + case SORT_ATTACHMENT: { + sortColumn = "(" + MessageColumns.ATTACHMENT_COUNT + " < 1)"; + break; + } + case SORT_FLAGGED: { + sortColumn = "(" + MessageColumns.FLAGS + " NOT LIKE '%FLAGGED%')"; + break; + } +// case SORT_SENDER: { +// //FIXME +// sortColumn = MessageColumns.SENDER_LIST; +// break; +// } + case SORT_SUBJECT: { + sortColumn = MessageColumns.SUBJECT + " COLLATE NOCASE"; + break; + } + case SORT_UNREAD: { + sortColumn = "(" + MessageColumns.FLAGS + " LIKE '%SEEN%')"; + break; + } + case SORT_DATE: + default: { + sortColumn = MessageColumns.DATE; + } + } + + String sortDirection = (mSortAscending) ? " ASC" : " DESC"; + String secondarySort; + if (mSortType == SortType.SORT_DATE || mSortType == SortType.SORT_ARRIVAL) { + secondarySort = ""; + } else { + secondarySort = MessageColumns.DATE + ((mSortDateAscending) ? " ASC, " : " DESC, "); + } + + String sortOrder = sortColumn + sortDirection + ", " + secondarySort + + MessageColumns.ID + " DESC"; + return sortOrder; + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + Cursor cursor; + if (mCursors.length > 1) { + mCursors[loader.getId()] = data; + cursor = new MergeCursorWithUniqueId(mCursors, getComparator()); + mUniqueIdColumn = cursor.getColumnIndex("_id"); + } else { + cursor = data; + mUniqueIdColumn = ID_COLUMN; + } + + cleanupSelected(cursor); + + mAdapter.swapCursor(cursor); + } + + private void cleanupSelected(Cursor cursor) { + if (mSelected.size() == 0) { + return; + } + + Set selected = new HashSet(); + for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { + long uniqueId = cursor.getLong(mUniqueIdColumn); + if (mSelected.contains(uniqueId)) { + selected.add(uniqueId); + } + } + + mSelected = selected; + } + + @Override + public void onLoaderReset(Loader loader) { + mSelected = null; + mAdapter.swapCursor(null); + } + + private Account getAccountFromCursor(Cursor cursor) { + String accountUuid = cursor.getString(ACCOUNT_UUID_COLUMN); + return mPreferences.getAccount(accountUuid); + } + + private void remoteSearchFinished() { + mRemoteSearchFuture = null; + } } diff --git a/src/com/fsck/k9/fragment/MessageViewFragment.java b/src/com/fsck/k9/fragment/MessageViewFragment.java index cc487042c..43681e756 100644 --- a/src/com/fsck/k9/fragment/MessageViewFragment.java +++ b/src/com/fsck/k9/fragment/MessageViewFragment.java @@ -1,6 +1,7 @@ package com.fsck.k9.fragment; import java.io.File; +import java.util.Collections; import android.app.Activity; import android.content.Context; @@ -274,7 +275,7 @@ public class MessageViewFragment extends SherlockFragment implements OnClickList mMenu.findItem(R.id.delete).setEnabled(false); Message messageToDelete = mMessage; mFragmentListener.showNextMessageOrReturn(); - mController.deleteMessages(new Message[] {messageToDelete}, null); + mController.deleteMessages(Collections.singletonList(messageToDelete), null); } } diff --git a/src/com/fsck/k9/helper/MergeCursor.java b/src/com/fsck/k9/helper/MergeCursor.java new file mode 100644 index 000000000..3325a6e36 --- /dev/null +++ b/src/com/fsck/k9/helper/MergeCursor.java @@ -0,0 +1,463 @@ +/* + * Copyright (C) 2012 The K-9 Dog Walkers + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fsck.k9.helper; + +import java.util.Comparator; + +import android.annotation.TargetApi; +import android.content.ContentResolver; +import android.database.CharArrayBuffer; +import android.database.ContentObserver; +import android.database.Cursor; +import android.database.DataSetObserver; +import android.net.Uri; +import android.os.Bundle; + + +/** + * This class can be used to combine multiple {@link Cursor}s into one. + */ +public class MergeCursor implements Cursor { + /** + * List of the cursors combined in this object. + */ + protected final Cursor[] mCursors; + + /** + * The currently active cursor. + */ + protected Cursor mActiveCursor; + + /** + * The index of the currently active cursor in {@link #mCursors}. + * + * @see #mActiveCursor + */ + protected int mActiveCursorIndex; + + /** + * The cursor's current position. + */ + protected int mPosition; + + /** + * Used to cache the value of {@link #getCount()}. + */ + private int mCount = -1; + + /** + * The comparator that is used to decide how the individual cursors are merged. + */ + private final Comparator mComparator; + + + /** + * Constructor + * + * @param cursors + * The list of cursors this {@code MultiCursor} should combine. + * @param comparator + * A comparator that is used to decide in what order the individual cursors are merged. + */ + public MergeCursor(Cursor[] cursors, Comparator comparator) { + mCursors = cursors.clone(); + mComparator = comparator; + + resetCursors(); + } + + private void resetCursors() { + mActiveCursorIndex = -1; + mActiveCursor = null; + mPosition = -1; + + for (int i = 0, len = mCursors.length; i < len; i++) { + Cursor cursor = mCursors[i]; + if (cursor != null) { + cursor.moveToPosition(-1); + + if (mActiveCursor == null) { + mActiveCursorIndex = i; + mActiveCursor = mCursors[mActiveCursorIndex]; + } + } + } + } + + @Override + public void close() { + for (Cursor cursor : mCursors) { + if (cursor != null) { + cursor.close(); + } + } + } + + @Override + public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { + mActiveCursor.copyStringToBuffer(columnIndex, buffer); + } + + @Override + public void deactivate() { + for (Cursor cursor : mCursors) { + if (cursor != null) { + cursor.deactivate(); + } + } + } + + @Override + public byte[] getBlob(int columnIndex) { + return mActiveCursor.getBlob(columnIndex); + } + + @Override + public int getColumnCount() { + return mActiveCursor.getColumnCount(); + } + + @Override + public int getColumnIndex(String columnName) { + return mActiveCursor.getColumnIndex(columnName); + } + + @Override + public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException { + return mActiveCursor.getColumnIndexOrThrow(columnName); + } + + @Override + public String getColumnName(int columnIndex) { + return mActiveCursor.getColumnName(columnIndex); + } + + @Override + public String[] getColumnNames() { + return mActiveCursor.getColumnNames(); + } + + @Override + public int getCount() { + // CursorLoaders seem to call getCount() a lot. So we're caching the aggregated count. + if (mCount == -1) { + int count = 0; + for (Cursor cursor : mCursors) { + if (cursor != null) { + count += cursor.getCount(); + } + } + + mCount = count; + } + + return mCount; + } + + @Override + public double getDouble(int columnIndex) { + return mActiveCursor.getDouble(columnIndex); + } + + @Override + public float getFloat(int columnIndex) { + return mActiveCursor.getFloat(columnIndex); + } + + @Override + public int getInt(int columnIndex) { + return mActiveCursor.getInt(columnIndex); + } + + @Override + public long getLong(int columnIndex) { + return mActiveCursor.getLong(columnIndex); + } + + @Override + public int getPosition() { + return mPosition; + } + + @Override + public short getShort(int columnIndex) { + return mActiveCursor.getShort(columnIndex); + } + + @Override + public String getString(int columnIndex) { + return mActiveCursor.getString(columnIndex); + } + + @TargetApi(11) + @Override + public int getType(int columnIndex) { + return mActiveCursor.getType(columnIndex); + } + + @Override + public boolean getWantsAllOnMoveCalls() { + return mActiveCursor.getWantsAllOnMoveCalls(); + } + + @Override + public boolean isAfterLast() { + int count = getCount(); + if (count == 0) { + return true; + } + + return (mPosition == count); + } + + @Override + public boolean isBeforeFirst() { + if (getCount() == 0) { + return true; + } + + return (mPosition == -1); + } + + @Override + public boolean isClosed() { + return mActiveCursor.isClosed(); + } + + @Override + public boolean isFirst() { + if (getCount() == 0) { + return false; + } + + return (mPosition == 0); + } + + @Override + public boolean isLast() { + int count = getCount(); + if (count == 0) { + return false; + } + + return (mPosition == (count - 1)); + } + + @Override + public boolean isNull(int columnIndex) { + return mActiveCursor.isNull(columnIndex); + } + + @Override + public boolean move(int offset) { + return moveToPosition(mPosition + offset); + } + + @Override + public boolean moveToFirst() { + return moveToPosition(0); + } + + @Override + public boolean moveToLast() { + return moveToPosition(getCount() - 1); + } + + @Override + public boolean moveToNext() { + int count = getCount(); + if (mPosition == count) { + return false; + } + + if (mPosition == count - 1) { + mActiveCursor.moveToNext(); + mPosition++; + return false; + } + + int smallest = -1; + for (int i = 0, len = mCursors.length; i < len; i++) { + if (mCursors[i] == null || mCursors[i].getCount() == 0 || mCursors[i].isLast()) { + continue; + } + + if (smallest == -1) { + smallest = i; + mCursors[smallest].moveToNext(); + continue; + } + + Cursor left = mCursors[smallest]; + Cursor right = mCursors[i]; + + right.moveToNext(); + + int result = mComparator.compare(left, right); + if (result > 0) { + smallest = i; + left.moveToPrevious(); + } else { + right.moveToPrevious(); + } + } + + mPosition++; + if (smallest != -1) { + mActiveCursorIndex = smallest; + mActiveCursor = mCursors[mActiveCursorIndex]; + } + + return true; + } + + @Override + public boolean moveToPosition(int position) { + // Make sure position isn't past the end of the cursor + final int count = getCount(); + if (position >= count) { + mPosition = count; + return false; + } + + // Make sure position isn't before the beginning of the cursor + if (position < 0) { + mPosition = -1; + return false; + } + + // Check for no-op moves, and skip the rest of the work for them + if (position == mPosition) { + return true; + } + + if (position > mPosition) { + for (int i = 0, end = position - mPosition; i < end; i++) { + if (!moveToNext()) { + return false; + } + } + } else { + for (int i = 0, end = mPosition - position; i < end; i++) { + if (!moveToPrevious()) { + return false; + } + } + } + + return true; + } + + @Override + public boolean moveToPrevious() { + if (mPosition < 0) { + return false; + } + + mActiveCursor.moveToPrevious(); + + if (mPosition == 0) { + mPosition = -1; + return false; + } + + int greatest = -1; + for (int i = 0, len = mCursors.length; i < len; i++) { + if (mCursors[i] == null || mCursors[i].isBeforeFirst()) { + continue; + } + + if (greatest == -1) { + greatest = i; + continue; + } + + Cursor left = mCursors[greatest]; + Cursor right = mCursors[i]; + + int result = mComparator.compare(left, right); + if (result <= 0) { + greatest = i; + } + } + + mPosition--; + if (greatest != -1) { + mActiveCursorIndex = greatest; + mActiveCursor = mCursors[mActiveCursorIndex]; + } + + return true; + } + + @Override + public void registerContentObserver(ContentObserver observer) { + for (Cursor cursor : mCursors) { + cursor.registerContentObserver(observer); + } + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + for (Cursor cursor : mCursors) { + cursor.registerDataSetObserver(observer); + } + } + + @Deprecated + @Override + public boolean requery() { + boolean success = true; + for (Cursor cursor : mCursors) { + success &= cursor.requery(); + } + + return success; + } + + @Override + public void setNotificationUri(ContentResolver cr, Uri uri) { + for (Cursor cursor : mCursors) { + cursor.setNotificationUri(cr, uri); + } + } + + @Override + public void unregisterContentObserver(ContentObserver observer) { + for (Cursor cursor : mCursors) { + cursor.unregisterContentObserver(observer); + } + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + for (Cursor cursor : mCursors) { + cursor.unregisterDataSetObserver(observer); + } + } + + @Override + public Bundle getExtras() { + throw new RuntimeException("Not implemented"); + } + + @Override + public Bundle respond(Bundle extras) { + throw new RuntimeException("Not implemented"); + } +} diff --git a/src/com/fsck/k9/helper/MergeCursorWithUniqueId.java b/src/com/fsck/k9/helper/MergeCursorWithUniqueId.java new file mode 100644 index 000000000..111d49fbb --- /dev/null +++ b/src/com/fsck/k9/helper/MergeCursorWithUniqueId.java @@ -0,0 +1,83 @@ +package com.fsck.k9.helper; + +import java.util.Comparator; + +import android.database.Cursor; + + +public class MergeCursorWithUniqueId extends MergeCursor { + private static final int SHIFT = 48; + private static final long MAX_ID = (1L << SHIFT) - 1; + private static final long MAX_CURSORS = 1L << (63 - SHIFT); + + private int mColumnCount = -1; + private int mIdColumnIndex = -1; + + + public MergeCursorWithUniqueId(Cursor[] cursors, Comparator comparator) { + super(cursors, comparator); + + if (cursors.length > MAX_CURSORS) { + throw new IllegalArgumentException("This class only supports up to " + + MAX_CURSORS + " cursors"); + } + } + + @Override + public int getColumnCount() { + if (mColumnCount == -1) { + mColumnCount = super.getColumnCount(); + } + + return mColumnCount + 1; + } + + @Override + public int getColumnIndex(String columnName) { + if ("_id".equals(columnName)) { + return getUniqueIdColumnIndex(); + } + + return super.getColumnIndexOrThrow(columnName); + } + + @Override + public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException { + if ("_id".equals(columnName)) { + return getUniqueIdColumnIndex(); + } + + return super.getColumnIndexOrThrow(columnName); + } + + @Override + public long getLong(int columnIndex) { + if (columnIndex == getUniqueIdColumnIndex()) { + long id = getPerCursorId(); + if (id > MAX_ID) { + throw new RuntimeException("Sorry, " + this.getClass().getName() + + " can only handle '_id' values up to " + SHIFT + " bits."); + } + + return (((long) mActiveCursorIndex) << SHIFT) + id; + } + + return super.getLong(columnIndex); + } + + protected int getUniqueIdColumnIndex() { + if (mColumnCount == -1) { + mColumnCount = super.getColumnCount(); + } + + return mColumnCount; + } + + protected long getPerCursorId() { + if (mIdColumnIndex == -1) { + mIdColumnIndex = super.getColumnIndexOrThrow("_id"); + } + + return super.getLong(mIdColumnIndex); + } +} diff --git a/src/com/fsck/k9/helper/MessageHelper.java b/src/com/fsck/k9/helper/MessageHelper.java index 9c2abb491..51ab08705 100644 --- a/src/com/fsck/k9/helper/MessageHelper.java +++ b/src/com/fsck/k9/helper/MessageHelper.java @@ -105,4 +105,28 @@ public class MessageHelper { mDateFormat = DateFormatter.getDateFormat(mContext); mTodayDateFormat = android.text.format.DateFormat.getTimeFormat(mContext); } + + public CharSequence getDisplayName(Account account, Address[] fromAddrs, Address[] toAddrs) { + final Contacts contactHelper = K9.showContactName() ? Contacts.getInstance(mContext) : null; + + CharSequence displayName; + if (fromAddrs.length > 0 && account.isAnIdentity(fromAddrs[0])) { + CharSequence to = Address.toFriendly(toAddrs, contactHelper); + displayName = new SpannableStringBuilder( + mContext.getString(R.string.message_to_label)).append(to); + } else { + displayName = Address.toFriendly(fromAddrs, contactHelper); + } + + return displayName; + } + + public boolean toMe(Account account, Address[] toAddrs) { + for (Address address : toAddrs) { + if (account.isAnIdentity(address)) { + return true; + } + } + return false; + } } diff --git a/src/com/fsck/k9/helper/Utility.java b/src/com/fsck/k9/helper/Utility.java index 113032d34..7d7754998 100644 --- a/src/com/fsck/k9/helper/Utility.java +++ b/src/com/fsck/k9/helper/Utility.java @@ -1,11 +1,13 @@ package com.fsck.k9.helper; +import android.annotation.SuppressLint; import android.app.Application; import android.content.Context; import android.database.Cursor; import android.net.ConnectivityManager; import android.net.NetworkInfo; +import android.os.Build; import android.text.Editable; import android.util.Log; import android.widget.EditText; @@ -18,7 +20,10 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; +import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -666,4 +671,57 @@ public class Utility { return false; } } + + private static final Pattern MESSAGE_ID = Pattern.compile("<" + + "(?:" + + "[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~]+" + + "(?:\\.[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~]+)*" + + "|" + + "\"(?:[^\\\\\"]|\\\\.)*\"" + + ")" + + "@" + + "(?:" + + "[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~]+" + + "(?:\\.[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~]+)*" + + "|" + + "\\[(?:[^\\\\\\]]|\\\\.)*\\]" + + ")" + + ">"); + + public static List extractMessageIds(final String text) { + List messageIds = new ArrayList(); + Matcher matcher = MESSAGE_ID.matcher(text); + + int start = 0; + while (matcher.find(start)) { + String messageId = text.substring(matcher.start(), matcher.end()); + messageIds.add(messageId); + start = matcher.end(); + } + + return messageIds; + } + + public static String extractMessageId(final String text) { + Matcher matcher = MESSAGE_ID.matcher(text); + + if (matcher.find()) { + return text.substring(matcher.start(), matcher.end()); + } + + return null; + } + + @SuppressLint("NewApi") + public static String[] copyOf(String[] original, int newLength) { + if (Build.VERSION.SDK_INT >= 9) { + return Arrays.copyOf(original, newLength); + } + + String[] newArray = new String[newLength]; + int copyLength = (original.length >= newLength) ? newLength : original.length; + System.arraycopy(original, 0, newArray, 0, copyLength); + + return newArray; + } } diff --git a/src/com/fsck/k9/mail/Message.java b/src/com/fsck/k9/mail/Message.java index 48e97d585..3d31f0839 100644 --- a/src/com/fsck/k9/mail/Message.java +++ b/src/com/fsck/k9/mail/Message.java @@ -143,10 +143,6 @@ public abstract class Message implements Part, Body { return getContentType().startsWith(mimeType); } - public abstract boolean toMe(); - public abstract boolean ccMe(); - public abstract boolean bccMe(); - public abstract boolean fromMe(); public abstract long getId(); public abstract String getPreview(); @@ -197,8 +193,6 @@ public abstract class Message implements Part, Body { public void destroy() throws MessagingException {} - public abstract void saveChanges() throws MessagingException; - public abstract void setEncoding(String encoding) throws UnavailableStorageException; public abstract void setCharset(String charset) throws MessagingException; @@ -213,11 +207,6 @@ public abstract class Message implements Part, Body { return mReference; } - public boolean equalsReference(MessageReference ref) { - MessageReference tmpReference = makeMessageReference(); - return tmpReference.equals(ref); - } - public long calculateSize() { try { diff --git a/src/com/fsck/k9/mail/internet/MimeMessage.java b/src/com/fsck/k9/mail/internet/MimeMessage.java index 93c80ba1c..2777420de 100644 --- a/src/com/fsck/k9/mail/internet/MimeMessage.java +++ b/src/com/fsck/k9/mail/internet/MimeMessage.java @@ -15,7 +15,6 @@ import java.util.UUID; import org.apache.james.mime4j.MimeException; import org.apache.james.mime4j.dom.field.DateTimeField; -import org.apache.james.mime4j.dom.field.ParsedField; import org.apache.james.mime4j.field.DefaultFieldParser; import org.apache.james.mime4j.io.EOLConvertingInputStream; import org.apache.james.mime4j.parser.ContentHandler; @@ -23,7 +22,6 @@ import org.apache.james.mime4j.parser.MimeStreamParser; import org.apache.james.mime4j.stream.BodyDescriptor; import org.apache.james.mime4j.stream.Field; import org.apache.james.mime4j.stream.MimeConfig; -import org.apache.james.mime4j.stream.RawField; import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Body; @@ -345,11 +343,6 @@ public class MimeMessage extends Message { setHeader("References", references); } - @Override - public void saveChanges() throws MessagingException { - throw new MessagingException("saveChanges not yet implemented"); - } - @Override public Body getBody() { return mBody; @@ -593,22 +586,6 @@ public class MimeMessage extends Message { return message; } - public boolean toMe() { - return false; - } - - public boolean ccMe() { - return false; - } - - public boolean bccMe() { - return false; - } - - public boolean fromMe() { - return false; - } - public long getId() { return Long.parseLong(mUid); //or maybe .mMessageId? } diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java index 906cf8b04..39fab2478 100644 --- a/src/com/fsck/k9/mail/store/ImapStore.java +++ b/src/com/fsck/k9/mail/store/ImapStore.java @@ -1478,7 +1478,7 @@ public class ImapStore extends Store { fetchFields.add("INTERNALDATE"); fetchFields.add("RFC822.SIZE"); fetchFields.add("BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc " + - "reply-to message-id " + K9.IDENTITY_HEADER + ")]"); + "reply-to message-id references " + K9.IDENTITY_HEADER + ")]"); } if (fp.contains(FetchProfile.Item.STRUCTURE)) { fetchFields.add("BODYSTRUCTURE"); diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java index 6e16c1cc7..21f03472f 100644 --- a/src/com/fsck/k9/mail/store/LocalStore.java +++ b/src/com/fsck/k9/mail/store/LocalStore.java @@ -13,7 +13,6 @@ import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -28,6 +27,7 @@ import java.util.regex.Pattern; import org.apache.commons.io.IOUtils; import android.app.Application; +import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; @@ -47,6 +47,7 @@ import com.fsck.k9.activity.Search; import com.fsck.k9.controller.MessageRemovalListener; import com.fsck.k9.controller.MessageRetrievalListener; import com.fsck.k9.helper.HtmlConverter; +import com.fsck.k9.helper.StringUtils; import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Body; @@ -71,6 +72,12 @@ import com.fsck.k9.mail.store.LockableDatabase.DbCallback; import com.fsck.k9.mail.store.LockableDatabase.WrappedException; import com.fsck.k9.mail.store.StorageManager.StorageProvider; import com.fsck.k9.provider.AttachmentProvider; +import com.fsck.k9.provider.EmailProvider; +import com.fsck.k9.search.LocalSearch; +import com.fsck.k9.search.SearchSpecification.Attribute; +import com.fsck.k9.search.SearchSpecification.SearchCondition; +import com.fsck.k9.search.SearchSpecification.Searchfield; +import com.fsck.k9.search.SqlQueryBuilder; /** *
@@ -82,27 +89,29 @@ public class LocalStore extends Store implements Serializable {
     private static final long serialVersionUID = -5142141896809423072L;
 
     private static final Message[] EMPTY_MESSAGE_ARRAY = new Message[0];
-
-    /**
-     * Immutable empty {@link String} array
-     */
     private static final String[] EMPTY_STRING_ARRAY = new String[0];
-
-    private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.X_DESTROYED, Flag.SEEN, Flag.FLAGGED };
+    private static final Flag[] EMPTY_FLAG_ARRAY = new Flag[0];
 
     /*
      * a String containing the columns getMessages expects to work with
      * in the correct order.
      */
     static private String GET_MESSAGES_COLS =
-        "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 ";
-
+        "subject, sender_list, date, uid, flags, messages.id, to_list, cc_list, "
+        + "bcc_list, reply_to_list, attachment_count, internal_date, message_id, folder_id, preview, thread_root, thread_parent ";
 
     static private String GET_FOLDER_COLS = "id, name, unread_count, visible_limit, last_updated, status, push_state, last_pushed, flagged_count, integrate, top_group, poll_class, push_class, display_class";
 
+    private static final String[] UID_CHECK_PROJECTION = { "uid" };
 
-    protected static final int DB_VERSION = 43;
+    /**
+     * Number of UIDs to check for existence at once.
+     *
+     * @see LocalFolder#extractNewMessages(List)
+     */
+    private static final int UID_CHECK_BATCH_SIZE = 500;
+
+    protected static final int DB_VERSION = 45;
 
     protected String uUid = null;
 
@@ -110,6 +119,8 @@ public class LocalStore extends Store implements Serializable {
 
     private LockableDatabase database;
 
+    private ContentResolver mContentResolver;
+
     /**
      * local://localhost/path/to/database/uuid.db
      * This constructor is only used by {@link Store#getLocalInstance(Account, Application)}
@@ -122,6 +133,7 @@ public class LocalStore extends Store implements Serializable {
         database = new LockableDatabase(application, account.getUuid(), new StoreSchemaDefinition());
 
         mApplication = application;
+        mContentResolver = application.getContentResolver();
         database.setStorageProviderId(account.getLocalStorageProviderId());
         uUid = account.getUuid();
 
@@ -166,10 +178,31 @@ public class LocalStore extends Store implements Serializable {
 
                         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, "
-                                   + "mime_type TEXT)");
+                        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, " +
+                                "mime_type TEXT, "+
+                                "thread_root INTEGER, " +
+                                "thread_parent INTEGER, " +
+                                "normalized_subject_hash INTEGER, " +
+                                "empty INTEGER" +
+                                ")");
 
                         db.execSQL("DROP TABLE IF EXISTS headers");
                         db.execSQL("CREATE TABLE headers (id INTEGER PRIMARY KEY, message_id INTEGER, name TEXT, value TEXT)");
@@ -179,6 +212,16 @@ public class LocalStore extends Store implements Serializable {
                         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 INDEX IF EXISTS msg_empty");
+                        db.execSQL("CREATE INDEX IF NOT EXISTS msg_empty ON messages (empty)");
+
+                        db.execSQL("DROP INDEX IF EXISTS msg_thread_root");
+                        db.execSQL("CREATE INDEX IF NOT EXISTS msg_thread_root ON messages (thread_root)");
+
+                        db.execSQL("DROP INDEX IF EXISTS msg_thread_parent");
+                        db.execSQL("CREATE INDEX IF NOT EXISTS msg_thread_parent ON messages (thread_parent)");
+
                         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,"
@@ -364,6 +407,34 @@ public class LocalStore extends Store implements Serializable {
                                 Log.e(K9.LOG_TAG, "Error trying to fix the outbox folders", e);
                             }
                         }
+                        if (db.getVersion() < 44) {
+                            try {
+                                db.execSQL("ALTER TABLE messages ADD thread_root INTEGER");
+                                db.execSQL("ALTER TABLE messages ADD thread_parent INTEGER");
+                                db.execSQL("ALTER TABLE messages ADD normalized_subject_hash INTEGER");
+                                db.execSQL("ALTER TABLE messages ADD empty INTEGER");
+                            } catch (SQLiteException e) {
+                                if (! e.getMessage().startsWith("duplicate column name:")) {
+                                    throw e;
+                                }
+                            }
+                        }
+                        if (db.getVersion() < 45) {
+                            try {
+                                db.execSQL("DROP INDEX IF EXISTS msg_empty");
+                                db.execSQL("CREATE INDEX IF NOT EXISTS msg_empty ON messages (empty)");
+
+                                db.execSQL("DROP INDEX IF EXISTS msg_thread_root");
+                                db.execSQL("CREATE INDEX IF NOT EXISTS msg_thread_root ON messages (thread_root)");
+
+                                db.execSQL("DROP INDEX IF EXISTS msg_thread_parent");
+                                db.execSQL("CREATE INDEX IF NOT EXISTS msg_thread_parent ON messages (thread_parent)");
+                            } catch (SQLiteException e) {
+                                if (! e.getMessage().startsWith("duplicate column name:")) {
+                                    throw e;
+                                }
+                            }
+                        }
                     }
                 } catch (SQLiteException e) {
                     Log.e(K9.LOG_TAG, "Exception while upgrading database. Resetting the DB to v0");
@@ -623,6 +694,26 @@ public class LocalStore extends Store implements Serializable {
         return new LocalFolder(name);
     }
 
+    public LocalFolder getFolderById(long folderId) {
+        return new LocalFolder(folderId);
+    }
+
+    private long getFolderId(final String name) throws MessagingException {
+        return database.execute(false, new DbCallback() {
+            @Override
+            public Long doDbWork(final SQLiteDatabase db) {
+                Cursor cursor = null;
+                try {
+                    cursor = db.rawQuery("SELECT id FROM folders WHERE name = '" + name + "'", null);
+                    cursor.moveToFirst();
+                    return cursor.getLong(0);
+                } finally {
+                    Utility.closeQuietly(cursor);
+                }
+            }
+        });
+    }
+
     // TODO this takes about 260-300ms, seems slow.
     @Override
     public List  getPersonalNamespaces(boolean forceListAll) throws MessagingException {
@@ -855,97 +946,54 @@ public class LocalStore extends Store implements Serializable {
         return true;
     }
 
-    public Message[] searchForMessages(MessageRetrievalListener listener, String[] queryFields, String queryString,
-                                       List folders, Message[] messages, final Flag[] requiredFlags, final Flag[] forbiddenFlags) throws MessagingException {
-        List args = new LinkedList();
-
-        StringBuilder whereClause = new StringBuilder();
-        if (queryString != null && queryString.length() > 0) {
-            boolean anyAdded = false;
-            String likeString = "%" + queryString + "%";
-            whereClause.append(" AND (");
-            for (String queryField : queryFields) {
-
-                if (anyAdded) {
-                    whereClause.append(" OR ");
-                }
-                whereClause.append(queryField).append(" LIKE ? ");
-                args.add(likeString);
-                anyAdded = true;
-            }
-
-
-            whereClause.append(" )");
+    // TODO find beter solution
+    private static boolean isFolderId(String str) {
+        if (str == null) {
+                return false;
         }
-        if (folders != null && !folders.isEmpty()) {
-            whereClause.append(" AND folder_id in (");
-            boolean anyAdded = false;
-            for (LocalFolder folder : folders) {
-                if (anyAdded) {
-                    whereClause.append(",");
-                }
-                anyAdded = true;
-                whereClause.append("?");
-                args.add(Long.toString(folder.getId()));
-            }
-            whereClause.append(" )");
+        int length = str.length();
+        if (length == 0) {
+                return false;
         }
-        if (messages != null && messages.length > 0) {
-            whereClause.append(" AND ( ");
-            boolean anyAdded = false;
-            for (Message message : messages) {
-                if (anyAdded) {
-                    whereClause.append(" OR ");
-                }
-                anyAdded = true;
-                whereClause.append(" ( uid = ? AND folder_id = ? ) ");
-                args.add(message.getUid());
-                args.add(Long.toString(((LocalFolder)message.getFolder()).getId()));
-            }
-            whereClause.append(" )");
+        int i = 0;
+        if (str.charAt(0) == '-') {
+            return false;
         }
-        if (forbiddenFlags != null && forbiddenFlags.length > 0) {
-            whereClause.append(" AND (");
-            boolean anyAdded = false;
-            for (Flag flag : forbiddenFlags) {
-                if (anyAdded) {
-                    whereClause.append(" AND ");
+        for (; i < length; i++) {
+                char c = str.charAt(i);
+                if (c <= '/' || c >= ':') {
+                        return false;
                 }
-                anyAdded = true;
-                whereClause.append(" flags NOT LIKE ?");
+        }
+        return true;
+    }
 
-                args.add("%" + flag.toString() + "%");
-            }
-            whereClause.append(" )");
-        }
-        if (requiredFlags != null && requiredFlags.length > 0) {
-            whereClause.append(" AND (");
-            boolean anyAdded = false;
-            for (Flag flag : requiredFlags) {
-                if (anyAdded) {
-                    whereClause.append(" OR ");
-                }
-                anyAdded = true;
-                whereClause.append(" flags LIKE ?");
+    public Message[] searchForMessages(MessageRetrievalListener retrievalListener,
+                                        LocalSearch search) throws MessagingException {
 
-                args.add("%" + flag.toString() + "%");
-            }
-            whereClause.append(" )");
-        }
+        StringBuilder query = new StringBuilder();
+        List queryArgs = new ArrayList();
+        SqlQueryBuilder.buildWhereClause(mAccount, search.getConditions(), query, queryArgs);
+
+        // Avoid "ambiguous column name" error by prefixing "id" with the message table name
+        String where = SqlQueryBuilder.addPrefixToSelection(new String[] { "id" },
+                "messages.", query.toString());
+
+        String[] selectionArgs = queryArgs.toArray(EMPTY_STRING_ARRAY);
+
+        String sqlQuery = "SELECT " + GET_MESSAGES_COLS + "FROM messages " +
+                "LEFT JOIN folders ON (folders.id = messages.folder_id) WHERE " +
+                "((empty IS NULL OR empty != 1) AND deleted = 0)" +
+                ((!StringUtils.isNullOrEmpty(where)) ? " AND (" + where + ")" : "") +
+                " ORDER BY date DESC";
 
         if (K9.DEBUG) {
-            Log.v(K9.LOG_TAG, "whereClause = " + whereClause.toString());
-            Log.v(K9.LOG_TAG, "args = " + args);
+            Log.d(K9.LOG_TAG, "Query = " + sqlQuery);
         }
-        return getMessages(
-                   listener,
-                   null,
-                   "SELECT "
-                   + GET_MESSAGES_COLS
-                   + "FROM messages WHERE deleted = 0 " + whereClause.toString() + " ORDER BY date DESC"
-                   , args.toArray(EMPTY_STRING_ARRAY)
-               );
+
+        return getMessages(retrievalListener, null, sqlQuery, selectionArgs);
     }
+
     /*
      * Given a query string, actually do the query for the messages and
      * call the MessageRetrievalListener for each one
@@ -1003,6 +1051,16 @@ public class LocalStore extends Store implements Serializable {
 
     }
 
+    public Message[] getMessagesInThread(final long rootId) throws MessagingException {
+        String rootIdString = Long.toString(rootId);
+
+        LocalSearch search = new LocalSearch();
+        search.and(Searchfield.THREAD_ROOT, rootIdString, Attribute.EQUALS);
+        search.or(new SearchCondition(Searchfield.ID, Attribute.EQUALS, rootIdString));
+
+        return searchForMessages(null, search);
+    }
+
     public AttachmentInfo getAttachmentInfo(final String attachmentId) throws UnavailableStorageException {
         return database.execute(false, new DbCallback() {
             @Override
@@ -1285,7 +1343,7 @@ public class LocalStore extends Store implements Serializable {
                         }
                         Cursor cursor = null;
                         try {
-                            cursor = db.rawQuery("SELECT COUNT(*) FROM messages WHERE deleted = 0 and folder_id = ?",
+                            cursor = db.rawQuery("SELECT COUNT(*) FROM messages WHERE (empty IS NULL OR empty != 1) AND deleted = 0 and folder_id = ?",
                                                  new String[] {
                                                      Long.toString(mFolderId)
                                                  });
@@ -1901,7 +1959,7 @@ public class LocalStore extends Store implements Serializable {
                                        listener,
                                        LocalFolder.this,
                                        "SELECT " + GET_MESSAGES_COLS
-                                       + "FROM messages WHERE "
+                                       + "FROM messages WHERE (empty IS NULL OR empty != 1) AND "
                                        + (includeDeleted ? "" : "deleted = 0 AND ")
                                        + " folder_id = ? ORDER BY date DESC"
                                        , new String[] {
@@ -1918,7 +1976,6 @@ public class LocalStore extends Store implements Serializable {
             }
         }
 
-
         @Override
         public Message[] getMessages(String[] uids, MessageRetrievalListener listener)
         throws MessagingException {
@@ -1975,30 +2032,118 @@ public class LocalStore extends Store implements Serializable {
 
                                 String oldUID = message.getUid();
 
-                                if (K9.DEBUG)
+                                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());
+                                }
 
                                 String newUid = K9.LOCAL_UID_PREFIX + UUID.randomUUID().toString();
                                 message.setUid(newUid);
 
                                 uidMap.put(oldUID, newUid);
 
-                                db.execSQL("UPDATE messages " + "SET folder_id = ?, uid = ? " + "WHERE id = ?", new Object[] {
-                                               lDestFolder.getId(),
-                                               message.getUid(),
-                                               lMessage.getId()
-                                           });
+                                // Message threading in the target folder
+                                ThreadInfo threadInfo = lDestFolder.doMessageThreading(db, message);
+
+                                /*
+                                 * "Move" the message into the new folder and thread structure
+                                 */
+                                long id = lMessage.getId();
+                                String[] idArg = new String[] { Long.toString(id) };
+
+                                ContentValues cv = new ContentValues();
+                                cv.put("folder_id", lDestFolder.getId());
+                                cv.put("uid", newUid);
+
+                                if (threadInfo.rootId != -1) {
+                                    cv.put("thread_root", threadInfo.rootId);
+                                }
+
+                                if (threadInfo.parentId != -1) {
+                                    cv.put("thread_parent", threadInfo.parentId);
+                                }
+
+                                db.update("messages", cv, "id = ?", idArg);
+
+                                if (threadInfo.id != -1) {
+                                    String[] oldIdArg =
+                                            new String[] { Long.toString(threadInfo.id) };
+
+                                    cv.clear();
+                                    cv.put("thread_root", id);
+                                    db.update("messages", cv, "thread_root = ?", oldIdArg);
+
+                                    cv.clear();
+                                    cv.put("thread_parent", id);
+                                    db.update("messages", cv, "thread_parent = ?", oldIdArg);
+                                }
 
                                 /*
                                  * Add a placeholder message so we won't download the original
                                  * message again if we synchronize before the remote move is
                                  * complete.
                                  */
-                                LocalMessage placeHolder = new LocalMessage(oldUID, LocalFolder.this);
-                                placeHolder.setFlagInternal(Flag.DELETED, true);
-                                placeHolder.setFlagInternal(Flag.SEEN, true);
-                                appendMessages(new Message[] { placeHolder });
+
+                                // We need to open this folder to get the folder id
+                                open(OpenMode.READ_WRITE);
+
+                                cv.clear();
+                                cv.put("uid", oldUID);
+                                cv.put("flags", serializeFlags(new Flag[] { Flag.DELETED, Flag.SEEN }));
+                                cv.put("deleted", 1);
+                                cv.put("folder_id", mFolderId);
+
+                                String messageId = message.getMessageId();
+                                if (messageId != null) {
+                                    cv.put("message_id", messageId);
+                                    cv.put("empty", 1);
+
+                                    long rootId = lMessage.getRootId();
+                                    if (rootId != -1) {
+                                        cv.put("thread_root", rootId);
+                                    }
+
+                                    long parentId = lMessage.getParentId();
+                                    if (parentId != -1) {
+                                        cv.put("thread_parent", parentId);
+                                    }
+                                }
+
+                                final long newId;
+                                if (threadInfo.id != -1) {
+                                    // There already existed an empty message in the target folder.
+                                    // Let's use it as placeholder.
+
+                                    newId = threadInfo.id;
+
+                                    db.update("messages", cv, "id = ?",
+                                            new String[] { Long.toString(newId) });
+                                } else {
+                                    newId = db.insert("messages", null, cv);
+                                }
+
+                                /*
+                                 * Replace all "thread links" to the original message with links to
+                                 * the placeholder message.
+                                 */
+
+                                String[] whereArgs = new String[] {
+                                        Long.toString(mFolderId),
+                                        Long.toString(id) };
+
+                                // Note: If there was an empty message in the target folder we
+                                // already reconnected some messages to point to 'id'. We don't
+                                // want to change those links again, so we limit the update
+                                // statements below to the source folder.
+                                cv.clear();
+                                cv.put("thread_root", newId);
+                                db.update("messages", cv, "folder_id = ? AND thread_root = ?",
+                                        whereArgs);
+
+                                cv.clear();
+                                cv.put("thread_parent", newId);
+                                db.update("messages", cv, "folder_id = ? AND thread_parent = ?",
+                                        whereArgs);
                             }
                         } catch (MessagingException e) {
                             throw new WrappedException(e);
@@ -2006,6 +2151,9 @@ public class LocalStore extends Store implements Serializable {
                         return null;
                     }
                 });
+
+                notifyChange();
+
                 return uidMap;
             } catch (WrappedException e) {
                 throw(MessagingException) e.getCause();
@@ -2076,6 +2224,70 @@ public class LocalStore extends Store implements Serializable {
             }
         }
 
+        /**
+         * Get the (database) ID of the message with the specified message ID.
+         *
+         * @param db
+         *         A {@link SQLiteDatabase} instance to access the database.
+         * @param messageId
+         *         The message ID to search for.
+         * @param onlyEmptyMessages
+         *         {@code true} if only "empty messages" (placeholders for threading) should be
+         *         searched
+         *
+         * @return If exactly one message with the specified message ID was found, the database ID
+         *         of this message; {@code -1} otherwise.
+         */
+        private long getDatabaseIdByMessageId(SQLiteDatabase db, String messageId,
+                boolean onlyEmptyMessages) {
+            long id = -1;
+
+            Cursor cursor = db.query("messages",
+                    new String[] { "id" },
+                    (onlyEmptyMessages) ?
+                            "empty=1 AND folder_id=? AND message_id=?" :
+                            "folder_id=? AND message_id=?",
+                    new String[] { Long.toString(mFolderId), messageId },
+                    null, null, null);
+
+            if (cursor != null) {
+                try {
+                    if (cursor.getCount() == 1) {
+                        cursor.moveToFirst();
+                        id = cursor.getLong(0);
+                    }
+                } finally {
+                    cursor.close();
+                }
+            }
+
+            return id;
+        }
+
+        private ThreadInfo getThreadInfo(SQLiteDatabase db, String messageId) {
+            Cursor cursor = db.query("messages",
+                    new String[] { "id", "thread_root", "thread_parent" },
+                            "folder_id=? AND message_id=?",
+                    new String[] { Long.toString(mFolderId), messageId },
+                    null, null, null);
+
+            if (cursor != null) {
+                try {
+                    if (cursor.getCount() == 1) {
+                        cursor.moveToFirst();
+                        long id = cursor.getLong(0);
+                        long rootId = (cursor.isNull(1)) ? -1 : cursor.getLong(1);
+                        long parentId = (cursor.isNull(2)) ? -1 : cursor.getLong(2);
+
+                        return new ThreadInfo(id, messageId, rootId, parentId);
+                    }
+                } finally {
+                    cursor.close();
+                }
+            }
+
+            return null;
+        }
 
         /**
          * The method differs slightly from the contract; If an incoming message already has a uid
@@ -2143,6 +2355,17 @@ public class LocalStore extends Store implements Serializable {
                                     deleteAttachments(message.getUid());
                                 }
 
+                                long rootId = -1;
+                                long parentId = -1;
+
+                                if (oldMessageId == -1) {
+                                    // This is a new message. Do the message threading.
+                                    ThreadInfo threadInfo = doMessageThreading(db, message);
+                                    oldMessageId = threadInfo.id;
+                                    rootId = threadInfo.rootId;
+                                    parentId = threadInfo.parentId;
+                                }
+
                                 boolean isDraft = (message.getHeader(K9.IDENTITY_HEADER) != null);
 
                                 List attachments;
@@ -2176,7 +2399,7 @@ public class LocalStore extends Store implements Serializable {
                                     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(Locale.US));
+                                    cv.put("flags", serializeFlags(message.getFlags()));
                                     cv.put("deleted", message.isSet(Flag.DELETED) ? 1 : 0);
                                     cv.put("folder_id", mFolderId);
                                     cv.put("to_list", Address.pack(message.getRecipients(RecipientType.TO)));
@@ -2190,11 +2413,20 @@ public class LocalStore extends Store implements Serializable {
                                     cv.put("internal_date",  message.getInternalDate() == null
                                            ? System.currentTimeMillis() : message.getInternalDate().getTime());
                                     cv.put("mime_type", message.getMimeType());
+                                    cv.put("empty", 0);
 
                                     String messageId = message.getMessageId();
                                     if (messageId != null) {
                                         cv.put("message_id", messageId);
                                     }
+
+                                    if (rootId != -1) {
+                                        cv.put("thread_root", rootId);
+                                    }
+                                    if (parentId != -1) {
+                                        cv.put("thread_parent", parentId);
+                                    }
+
                                     long messageUid;
 
                                     if (oldMessageId == -1) {
@@ -2223,6 +2455,9 @@ public class LocalStore extends Store implements Serializable {
                         return null;
                     }
                 });
+
+                notifyChange();
+
                 return uidMap;
             } catch (WrappedException e) {
                 throw(MessagingException) e.getCause();
@@ -2270,7 +2505,7 @@ public class LocalStore extends Store implements Serializable {
                                                message.getSentDate() == null ? System
                                                .currentTimeMillis() : message.getSentDate()
                                                .getTime(),
-                                               Utility.combine(message.getFlags(), ',').toUpperCase(Locale.US),
+                                               serializeFlags(message.getFlags()),
                                                mFolderId,
                                                Address.pack(message
                                                             .getRecipients(RecipientType.TO)),
@@ -2303,6 +2538,8 @@ public class LocalStore extends Store implements Serializable {
             } catch (WrappedException e) {
                 throw(MessagingException) e.getCause();
             }
+
+            notifyChange();
         }
 
         /**
@@ -2337,7 +2574,8 @@ public class LocalStore extends Store implements Serializable {
 
                     db.execSQL("UPDATE messages " + "SET flags = ? " + " WHERE id = ?",
                                new Object[]
-                               { Utility.combine(appendedFlags.toArray(), ',').toUpperCase(Locale.US), id });
+                               { serializeFlags(appendedFlags.toArray(EMPTY_FLAG_ARRAY)), id });
+
                     return null;
                 }
             });
@@ -2551,6 +2789,9 @@ public class LocalStore extends Store implements Serializable {
                     return null;
                 }
             });
+
+            //TODO: remove this once the UI code exclusively uses the database id
+            notifyChange();
         }
 
         @Override
@@ -2581,7 +2822,7 @@ public class LocalStore extends Store implements Serializable {
             Message[] messages  = LocalStore.this.getMessages(
                                       null,
                                       this,
-                                      "SELECT " + GET_MESSAGES_COLS + "FROM messages WHERE " + whereClause,
+                                      "SELECT " + GET_MESSAGES_COLS + "FROM messages WHERE (empty IS NULL OR empty != 1) AND (" + whereClause + ")",
                                       params);
 
             for (Message message : messages) {
@@ -2595,6 +2836,8 @@ public class LocalStore extends Store implements Serializable {
                 }
             });
             resetUnreadAndFlaggedCounts();
+
+            notifyChange();
         }
 
         public void clearMessagesOlderThan(long cutoff) throws MessagingException {
@@ -2883,6 +3126,167 @@ public class LocalStore extends Store implements Serializable {
             });
         }
 
+        private String serializeFlags(Flag[] flags) {
+            return Utility.combine(flags, ',').toUpperCase(Locale.US);
+        }
+
+        private ThreadInfo doMessageThreading(SQLiteDatabase db, Message message)
+                throws MessagingException {
+            long rootId = -1;
+            long parentId = -1;
+
+            String messageId = message.getMessageId();
+
+            // If there's already an empty message in the database, update that
+            long id = getDatabaseIdByMessageId(db, messageId, true);
+
+            // Get the message IDs from the "References" header line
+            String[] referencesArray = message.getHeader("References");
+            List messageIds = null;
+            if (referencesArray != null && referencesArray.length > 0) {
+                messageIds = Utility.extractMessageIds(referencesArray[0]);
+            }
+
+            // Append the first message ID from the "In-Reply-To" header line
+            String[] inReplyToArray = message.getHeader("In-Reply-To");
+            String inReplyTo = null;
+            if (inReplyToArray != null && inReplyToArray.length > 0) {
+                inReplyTo = Utility.extractMessageId(inReplyToArray[0]);
+                if (inReplyTo != null) {
+                    if (messageIds == null) {
+                        messageIds = new ArrayList(1);
+                        messageIds.add(inReplyTo);
+                    } else if (!messageIds.contains(inReplyTo)) {
+                        messageIds.add(inReplyTo);
+                    }
+                }
+            }
+
+            if (messageIds == null) {
+                // This is not a reply, nothing to do for us.
+                return new ThreadInfo(id, messageId, -1, -1);
+            }
+
+            for (String reference : messageIds) {
+                ThreadInfo threadInfo = getThreadInfo(db, reference);
+
+                if (threadInfo == null) {
+                    // Create placeholder message
+                    ContentValues cv = new ContentValues();
+                    cv.put("message_id", reference);
+                    cv.put("folder_id", mFolderId);
+                    cv.put("empty", 1);
+
+                    if (rootId != -1) {
+                        cv.put("thread_root", rootId);
+                    }
+                    if (parentId != -1) {
+                        cv.put("thread_parent", parentId);
+                    }
+
+                    parentId = db.insert("messages", null, cv);
+                    if (rootId == -1) {
+                        rootId = parentId;
+                    }
+                } else {
+                    if (rootId != -1 && threadInfo.rootId == -1 && rootId != threadInfo.id) {
+                        // We found an existing root container that is not
+                        // the root of our current path (References).
+                        // Connect it to the current parent.
+
+                        // Let all children know who's the new root
+                        ContentValues cv = new ContentValues();
+                        cv.put("thread_root", rootId);
+                        db.update("messages", cv, "thread_root=?",
+                                new String[] { Long.toString(threadInfo.id) });
+
+                        // Connect the message to the current parent
+                        cv.put("thread_parent", parentId);
+                        db.update("messages", cv, "id=?",
+                                new String[] { Long.toString(threadInfo.id) });
+                    } else {
+                        rootId = (threadInfo.rootId == -1) ? threadInfo.id : threadInfo.rootId;
+                    }
+                    parentId = threadInfo.id;
+                }
+            }
+
+            //TODO: set in-reply-to "link" even if one already exists
+
+            return new ThreadInfo(id, messageId, rootId, parentId);
+        }
+
+        public List extractNewMessages(final List messages)
+                throws MessagingException {
+
+            try {
+                return database.execute(false, new DbCallback>() {
+                    @Override
+                    public List doDbWork(final SQLiteDatabase db) throws WrappedException {
+                        try {
+                            open(OpenMode.READ_WRITE);
+                        } catch (MessagingException e) {
+                            throw new WrappedException(e);
+                        }
+
+                        List result = new ArrayList();
+
+                        List selectionArgs = new ArrayList();
+                        Set existingMessages = new HashSet();
+                        int start = 0;
+
+                        while (start < messages.size()) {
+                            StringBuilder selection = new StringBuilder();
+
+                            selection.append("folder_id = ? AND UID IN (");
+                            selectionArgs.add(Long.toString(mFolderId));
+
+                            int count = Math.min(messages.size() - start, UID_CHECK_BATCH_SIZE);
+
+                            for (int i = start, end = start + count; i < end; i++) {
+                                if (i > start) {
+                                    selection.append(",?");
+                                } else {
+                                    selection.append("?");
+                                }
+
+                                selectionArgs.add(messages.get(i).getUid());
+                            }
+
+                            selection.append(")");
+
+                            Cursor cursor = db.query("messages", UID_CHECK_PROJECTION,
+                                    selection.toString(), selectionArgs.toArray(EMPTY_STRING_ARRAY),
+                                    null, null, null);
+
+                            try {
+                                while (cursor.moveToNext()) {
+                                    String uid = cursor.getString(0);
+                                    existingMessages.add(uid);
+                                }
+                            } finally {
+                                Utility.closeQuietly(cursor);
+                            }
+
+                            for (int i = start, end = start + count; i < end; i++) {
+                                Message message = messages.get(i);
+                                if (!existingMessages.contains(message.getUid())) {
+                                    result.add(message);
+                                }
+                            }
+
+                            existingMessages.clear();
+                            selectionArgs.clear();
+                            start += count;
+                        }
+
+                        return result;
+                    }
+                });
+            } catch (WrappedException e) {
+                throw(MessagingException) e.getCause();
+            }
+        }
     }
 
     public static class LocalTextBody extends TextBody {
@@ -2917,19 +3321,12 @@ public class LocalStore extends Store implements Serializable {
 
         private String mPreview = "";
 
-        private boolean mToMeCalculated = false;
-        private boolean mCcMeCalculated = false;
-        private boolean mFromMeCalculated = false;
-        private boolean mToMe = false;
-        private boolean mCcMe = false;
-        private boolean mFromMe = false;
-
-
-
-
         private boolean mHeadersLoaded = false;
         private boolean mMessageDirty = false;
 
+        private long mRootId;
+        private long mParentId;
+
         public LocalMessage() {
         }
 
@@ -2983,6 +3380,9 @@ public class LocalStore extends Store implements Serializable {
                 f.open(LocalFolder.OpenMode.READ_WRITE);
                 this.mFolder = f;
             }
+
+            mRootId = (cursor.isNull(15)) ? -1 : cursor.getLong(15);
+            mParentId = (cursor.isNull(16)) ? -1 : cursor.getLong(16);
         }
 
         /**
@@ -3122,64 +3522,6 @@ public class LocalStore extends Store implements Serializable {
             mMessageDirty = true;
         }
 
-
-        public boolean fromMe() {
-            if (!mFromMeCalculated) {
-                if (mAccount.isAnIdentity(getFrom())) {
-                    mFromMe = true;
-                    mFromMeCalculated = true;
-                }
-            }
-            return mFromMe;
-        }
-
-
-        public boolean toMe() {
-            try {
-                if (!mToMeCalculated) {
-                    for (Address address : getRecipients(RecipientType.TO)) {
-                        if (mAccount.isAnIdentity(address)) {
-                            mToMe = true;
-                            mToMeCalculated = true;
-                        }
-                    }
-                }
-            } catch (MessagingException e) {
-                // do something better than ignore this
-                // getRecipients can throw a messagingexception
-            }
-            return mToMe;
-        }
-
-
-
-
-
-        public boolean ccMe() {
-            try {
-
-                if (!mCcMeCalculated) {
-                    for (Address address : getRecipients(RecipientType.CC)) {
-                        if (mAccount.isAnIdentity(address)) {
-                            mCcMe = true;
-                            mCcMeCalculated = true;
-                        }
-                    }
-
-                }
-            } catch (MessagingException e) {
-                // do something better than ignore this
-                // getRecipients can throw a messagingexception
-            }
-
-            return mCcMe;
-        }
-
-
-
-
-
-
         public void setFlagInternal(Flag flag, boolean set) throws MessagingException {
             super.setFlag(flag, set);
         }
@@ -3219,7 +3561,7 @@ public class LocalStore extends Store implements Serializable {
                 throw(MessagingException) e.getCause();
             }
 
-
+            notifyChange();
         }
 
         /*
@@ -3236,13 +3578,26 @@ public class LocalStore extends Store implements Serializable {
             try {
                 database.execute(true, new DbCallback() {
                     @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 });
+                    public Void doDbWork(final SQLiteDatabase db) throws WrappedException,
+                            UnavailableStorageException {
+                        String[] idArg = new String[] { Long.toString(mId) };
+
+                        ContentValues cv = new ContentValues();
+                        cv.put("deleted", 1);
+                        cv.put("empty", 1);
+                        cv.putNull("subject");
+                        cv.putNull("sender_list");
+                        cv.putNull("date");
+                        cv.putNull("to_list");
+                        cv.putNull("cc_list");
+                        cv.putNull("bcc_list");
+                        cv.putNull("preview");
+                        cv.putNull("html_content");
+                        cv.putNull("text_content");
+                        cv.putNull("reply_to_list");
+
+                        db.update("messages", cv, "id = ?", idArg);
+
                         /*
                          * Delete all of the message's attachments to save space.
                          * We do this explicit deletion here because we're not deleting the record
@@ -3253,8 +3608,8 @@ public class LocalStore extends Store implements Serializable {
                         } catch (MessagingException e) {
                             throw new WrappedException(e);
                         }
-                        db.execSQL("DELETE FROM attachments WHERE message_id = ?", new Object[]
-                                   { mId });
+
+                        db.delete("attachments", "message_id = ?", idArg);
                         return null;
                     }
                 });
@@ -3263,11 +3618,13 @@ public class LocalStore extends Store implements Serializable {
             }
             ((LocalFolder)mFolder).deleteHeaders(mId);
 
-
+            notifyChange();
         }
 
         /*
          * Completely remove a message from the local database
+         *
+         * TODO: document how this updates the thread structure
          */
         @Override
         public void destroy() throws MessagingException {
@@ -3277,9 +3634,102 @@ public class LocalStore extends Store implements Serializable {
                     public Void doDbWork(final SQLiteDatabase db) throws WrappedException,
                         UnavailableStorageException {
                         try {
+                            LocalFolder localFolder = (LocalFolder) mFolder;
+
                             updateFolderCountsOnFlag(Flag.X_DESTROYED, true);
-                            ((LocalFolder) mFolder).deleteAttachments(mId);
-                            db.execSQL("DELETE FROM messages WHERE id = ?", new Object[] { mId });
+                            localFolder.deleteAttachments(mId);
+
+                            String id = Long.toString(mId);
+
+                            // Check if this message has children in the thread hierarchy
+                            Cursor cursor = db.query("messages", new String[] { "COUNT(id)" },
+                                    "thread_root = ? OR thread_parent = ?",
+                                    new String[] {id, id},
+                                    null, null, null);
+
+                            try {
+                                if (cursor.moveToFirst() && cursor.getLong(0) > 0) {
+                                    // Make the message an empty message
+                                    ContentValues cv = new ContentValues();
+                                    cv.put("id", mId);
+                                    cv.put("folder_id", localFolder.getId());
+                                    cv.put("deleted", 0);
+                                    cv.put("message_id", getMessageId());
+                                    cv.put("empty", 1);
+
+                                    if (getRootId() != -1) {
+                                        cv.put("thread_root", getRootId());
+                                    }
+
+                                    if (getParentId() != -1) {
+                                        cv.put("thread_parent", getParentId());
+                                    }
+
+                                    db.replace("messages", null, cv);
+
+                                    // Nothing else to do
+                                    return null;
+                                }
+                            } finally {
+                                cursor.close();
+                            }
+
+                            long parentId = getParentId();
+
+                            // Check if 'parentId' is empty
+                            cursor = db.query("messages", new String[] { "id" },
+                                    "id = ? AND empty = 1",
+                                    new String[] { Long.toString(parentId) },
+                                    null, null, null);
+
+                            try {
+                                if (cursor.getCount() == 0) {
+                                    // If the message isn't empty we skip the loop below
+                                    parentId = -1;
+                                }
+                            } finally {
+                                cursor.close();
+                            }
+
+                            while (parentId != -1) {
+                                String parentIdString = Long.toString(parentId);
+
+                                // Get the parent of the message 'parentId'
+                                cursor = db.query("messages", new String[] { "thread_parent" },
+                                        "id = ? AND empty = 1",
+                                        new String[] { parentIdString },
+                                        null, null, null);
+                                try {
+                                    if (cursor.moveToFirst() && !cursor.isNull(0)) {
+                                        parentId = cursor.getLong(0);
+                                    } else {
+                                        parentId = -1;
+                                    }
+                                } finally {
+                                    cursor.close();
+                                }
+
+                                // Check if (the old) 'parentId' has any children
+                                cursor = db.query("messages", new String[] { "COUNT(id)" },
+                                        "thread_parent = ? AND id != ?",
+                                        new String[] { parentIdString, id },
+                                        null, null, null);
+
+                                try {
+                                    if (cursor.moveToFirst() && cursor.getLong(0) == 0) {
+                                        // If it has no children we can remove it
+                                        db.delete("messages", "id = ?",
+                                                new String[] { parentIdString });
+                                    } else {
+                                        break;
+                                    }
+                                } finally {
+                                    cursor.close();
+                                }
+                            }
+
+                            // Remove the placeholder message
+                            db.delete("messages", "id = ?",  new String[] { id });
                         } catch (MessagingException e) {
                             throw new WrappedException(e);
                         }
@@ -3289,6 +3739,8 @@ public class LocalStore extends Store implements Serializable {
             } catch (WrappedException e) {
                 throw(MessagingException) e.getCause();
             }
+
+            notifyChange();
         }
 
         private void updateFolderCountsOnFlag(Flag flag, boolean set) {
@@ -3376,15 +3828,19 @@ public class LocalStore extends Store implements Serializable {
             message.mAttachmentCount = mAttachmentCount;
             message.mSubject = mSubject;
             message.mPreview = mPreview;
-            message.mToMeCalculated = mToMeCalculated;
-            message.mCcMeCalculated = mCcMeCalculated;
-            message.mToMe = mToMe;
-            message.mCcMe = mCcMe;
             message.mHeadersLoaded = mHeadersLoaded;
             message.mMessageDirty = mMessageDirty;
 
             return message;
         }
+
+        public long getRootId() {
+            return mRootId;
+        }
+
+        public long getParentId() {
+            return mParentId;
+        }
     }
 
     public static class LocalAttachmentBodyPart extends MimeBodyPart {
@@ -3449,4 +3905,27 @@ public class LocalStore extends Store implements Serializable {
             return mUri;
         }
     }
+
+    static class ThreadInfo {
+        public final long id;
+        public final String messageId;
+        public final long rootId;
+        public final long parentId;
+
+        public ThreadInfo(long id, String messageId, long rootId, long parentId) {
+            this.id = id;
+            this.messageId = messageId;
+            this.rootId = rootId;
+            this.parentId = parentId;
+        }
+    }
+
+    public LockableDatabase getDatabase() {
+        return database;
+    }
+
+    private void notifyChange() {
+        Uri uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + uUid + "/messages");
+        mContentResolver.notifyChange(uri, null);
+    }
 }
diff --git a/src/com/fsck/k9/mail/store/LockableDatabase.java b/src/com/fsck/k9/mail/store/LockableDatabase.java
index b303c2aef..67090aace 100644
--- a/src/com/fsck/k9/mail/store/LockableDatabase.java
+++ b/src/com/fsck/k9/mail/store/LockableDatabase.java
@@ -49,7 +49,7 @@ public class LockableDatabase {
      * Workaround exception wrapper used to keep the inner exception generated
      * in a {@link DbCallback}.
      */
-    protected static class WrappedException extends RuntimeException {
+    public static class WrappedException extends RuntimeException {
         /**
          *
          */
diff --git a/src/com/fsck/k9/preferences/GlobalSettings.java b/src/com/fsck/k9/preferences/GlobalSettings.java
index 985826605..190b46765 100644
--- a/src/com/fsck/k9/preferences/GlobalSettings.java
+++ b/src/com/fsck/k9/preferences/GlobalSettings.java
@@ -222,6 +222,9 @@ public class GlobalSettings {
         s.put("useBackgroundAsUnreadIndicator", Settings.versions(
                 new V(19, new BooleanSetting(true))
             ));
+        s.put("threadedView", Settings.versions(
+                new V(20, new BooleanSetting(true))
+            ));
 
         SETTINGS = Collections.unmodifiableMap(s);
 
diff --git a/src/com/fsck/k9/preferences/Settings.java b/src/com/fsck/k9/preferences/Settings.java
index 725c562f7..56a6646b9 100644
--- a/src/com/fsck/k9/preferences/Settings.java
+++ b/src/com/fsck/k9/preferences/Settings.java
@@ -35,7 +35,7 @@ public class Settings {
      *
      * @see SettingsExporter
      */
-    public static final int VERSION = 19;
+    public static final int VERSION = 20;
 
     public static Map validate(int version, Map> settings,
diff --git a/src/com/fsck/k9/provider/EmailProvider.java b/src/com/fsck/k9/provider/EmailProvider.java
new file mode 100644
index 000000000..150c272dc
--- /dev/null
+++ b/src/com/fsck/k9/provider/EmailProvider.java
@@ -0,0 +1,579 @@
+package com.fsck.k9.provider;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.fsck.k9.Account;
+import com.fsck.k9.Preferences;
+import com.fsck.k9.helper.StringUtils;
+import com.fsck.k9.helper.Utility;
+import com.fsck.k9.mail.MessagingException;
+import com.fsck.k9.mail.store.LocalStore;
+import com.fsck.k9.mail.store.LockableDatabase;
+import com.fsck.k9.mail.store.LockableDatabase.DbCallback;
+import com.fsck.k9.mail.store.LockableDatabase.WrappedException;
+import com.fsck.k9.mail.store.UnavailableStorageException;
+import com.fsck.k9.search.SqlQueryBuilder;
+
+import android.annotation.TargetApi;
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.CursorWrapper;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+
+/**
+ * Content Provider used to display the message list etc.
+ *
+ * 

+ * For now this content provider is for internal use only. In the future we may allow third-party + * apps to access K-9 Mail content using this content provider. + *

+ */ +/* + * TODO: + * - add support for account list and folder list + */ +public class EmailProvider extends ContentProvider { + private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); + + public static final String AUTHORITY = "org.k9mail.provider.email"; + + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY); + + + /* + * Constants that are used for the URI matching. + */ + private static final int MESSAGE_BASE = 0; + private static final int MESSAGES = MESSAGE_BASE; + private static final int MESSAGES_THREADED = MESSAGE_BASE + 1; + //private static final int MESSAGES_THREAD = MESSAGE_BASE + 2; + + + private static final String MESSAGES_TABLE = "messages"; + + private static final String[] MESSAGES_COLUMNS = { + MessageColumns.ID, + MessageColumns.UID, + MessageColumns.INTERNAL_DATE, + MessageColumns.SUBJECT, + MessageColumns.DATE, + MessageColumns.MESSAGE_ID, + MessageColumns.SENDER_LIST, + MessageColumns.TO_LIST, + MessageColumns.CC_LIST, + MessageColumns.BCC_LIST, + MessageColumns.REPLY_TO_LIST, + MessageColumns.FLAGS, + MessageColumns.ATTACHMENT_COUNT, + MessageColumns.FOLDER_ID, + MessageColumns.PREVIEW, + MessageColumns.THREAD_ROOT, + MessageColumns.THREAD_PARENT, + InternalMessageColumns.DELETED, + InternalMessageColumns.EMPTY, + InternalMessageColumns.TEXT_CONTENT, + InternalMessageColumns.HTML_CONTENT, + InternalMessageColumns.MIME_TYPE + }; + + static { + UriMatcher matcher = sUriMatcher; + + matcher.addURI(AUTHORITY, "account/*/messages", MESSAGES); + matcher.addURI(AUTHORITY, "account/*/messages/threaded", MESSAGES_THREADED); + //matcher.addURI(AUTHORITY, "account/*/thread/#", MESSAGES_THREAD); + } + + public interface SpecialColumns { + public static final String ACCOUNT_UUID = "account_uuid"; + + public static final String FOLDER_NAME = "name"; + public static final String INTEGRATE = "integrate"; + } + + public interface MessageColumns { + public static final String ID = "id"; + public static final String UID = "uid"; + public static final String INTERNAL_DATE = "internal_date"; + public static final String SUBJECT = "subject"; + public static final String DATE = "date"; + public static final String MESSAGE_ID = "message_id"; + public static final String SENDER_LIST = "sender_list"; + public static final String TO_LIST = "to_list"; + public static final String CC_LIST = "cc_list"; + public static final String BCC_LIST = "bcc_list"; + public static final String REPLY_TO_LIST = "reply_to_list"; + public static final String FLAGS = "flags"; + public static final String ATTACHMENT_COUNT = "attachment_count"; + public static final String FOLDER_ID = "folder_id"; + public static final String PREVIEW = "preview"; + public static final String THREAD_ROOT = "thread_root"; + public static final String THREAD_PARENT = "thread_parent"; + public static final String THREAD_COUNT = "thread_count"; + } + + private interface InternalMessageColumns extends MessageColumns { + public static final String DELETED = "deleted"; + public static final String EMPTY = "empty"; + public static final String TEXT_CONTENT = "text_content"; + public static final String HTML_CONTENT = "html_content"; + public static final String MIME_TYPE = "mime_type"; + } + + + private Preferences mPreferences; + + + @Override + public boolean onCreate() { + return true; + } + + @Override + public String getType(Uri uri) { + throw new RuntimeException("not implemented yet"); + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + + int match = sUriMatcher.match(uri); + if (match < 0) { + throw new IllegalArgumentException("Unknown URI: " + uri); + } + + ContentResolver contentResolver = getContext().getContentResolver(); + Cursor cursor = null; + switch (match) { + case MESSAGES: + case MESSAGES_THREADED: { + List segments = uri.getPathSegments(); + String accountUuid = segments.get(1); + + List dbColumnNames = new ArrayList(projection.length); + Map specialColumns = new HashMap(); + for (String columnName : projection) { + if (SpecialColumns.ACCOUNT_UUID.equals(columnName)) { + specialColumns.put(SpecialColumns.ACCOUNT_UUID, accountUuid); + } else { + dbColumnNames.add(columnName); + } + } + + String[] dbProjection = dbColumnNames.toArray(new String[0]); + + if (match == MESSAGES) { + cursor = getMessages(accountUuid, dbProjection, selection, selectionArgs, + sortOrder); + } else if (match == MESSAGES_THREADED) { + cursor = getThreadedMessages(accountUuid, dbProjection, selection, + selectionArgs, sortOrder); + } else { + throw new RuntimeException("Not implemented"); + } + + cursor.setNotificationUri(contentResolver, uri); + + cursor = new SpecialColumnsCursor(new IdTrickeryCursor(cursor), projection, + specialColumns); + + break; + } + } + + return cursor; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + throw new RuntimeException("not implemented yet"); + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + throw new RuntimeException("not implemented yet"); + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + throw new RuntimeException("not implemented yet"); + } + + protected Cursor getMessages(String accountUuid, final String[] projection, + final String selection, final String[] selectionArgs, final String sortOrder) { + + Account account = getAccount(accountUuid); + LockableDatabase database = getDatabase(account); + + try { + return database.execute(false, new DbCallback() { + @Override + public Cursor doDbWork(SQLiteDatabase db) throws WrappedException, + UnavailableStorageException { + + String where; + if (StringUtils.isNullOrEmpty(selection)) { + where = InternalMessageColumns.DELETED + "=0 AND (" + + InternalMessageColumns.EMPTY + " IS NULL OR " + + InternalMessageColumns.EMPTY + "!=1)"; + } else { + where = "(" + selection + ") AND " + + InternalMessageColumns.DELETED + "=0 AND (" + + InternalMessageColumns.EMPTY + " IS NULL OR " + + InternalMessageColumns.EMPTY + "!=1)"; + } + + final Cursor cursor; + //TODO: check projection and selection for folder columns + if (Utility.arrayContains(projection, SpecialColumns.FOLDER_NAME)) { + StringBuilder query = new StringBuilder(); + query.append("SELECT "); + boolean first = true; + for (String columnName : projection) { + if (!first) { + query.append(","); + } else { + first = false; + } + + if (MessageColumns.ID.equals(columnName)) { + query.append("m."); + query.append(MessageColumns.ID); + query.append(" AS "); + query.append(MessageColumns.ID); + } else { + query.append(columnName); + } + } + + query.append(" FROM messages m " + + "LEFT JOIN folders f ON (m.folder_id = f.id) " + + "WHERE "); + query.append(SqlQueryBuilder.addPrefixToSelection(MESSAGES_COLUMNS, + "m.", where)); + query.append(" ORDER BY "); + query.append(SqlQueryBuilder.addPrefixToSelection(MESSAGES_COLUMNS, + "m.", sortOrder)); + + cursor = db.rawQuery(query.toString(), selectionArgs); + } else { + cursor = db.query(MESSAGES_TABLE, projection, where, selectionArgs, null, + null, sortOrder); + } + + return cursor; + } + }); + } catch (UnavailableStorageException e) { + throw new RuntimeException("Storage not available", e); + } + } + + protected Cursor getThreadedMessages(String accountUuid, final String[] projection, + final String selection, final String[] selectionArgs, final String sortOrder) { + + Account account = getAccount(accountUuid); + LockableDatabase database = getDatabase(account); + + try { + return database.execute(false, new DbCallback() { + @Override + public Cursor doDbWork(SQLiteDatabase db) throws WrappedException, + UnavailableStorageException { + + StringBuilder query = new StringBuilder(); + query.append("SELECT "); + boolean first = true; + for (String columnName : projection) { + if (!first) { + query.append(","); + } else { + first = false; + } + + if (MessageColumns.DATE.equals(columnName)) { + query.append("MAX(m.date) AS " + MessageColumns.DATE); + } else if (MessageColumns.THREAD_COUNT.equals(columnName)) { + query.append("COUNT(h.id) AS " + MessageColumns.THREAD_COUNT); + } else if (SpecialColumns.FOLDER_NAME.equals(columnName)) { + query.append("f." + SpecialColumns.FOLDER_NAME + " AS " + + SpecialColumns.FOLDER_NAME); + } else if (SpecialColumns.INTEGRATE.equals(columnName)) { + query.append("f." + SpecialColumns.INTEGRATE + " AS " + + SpecialColumns.INTEGRATE); + } else { + query.append("m."); + query.append(columnName); + query.append(" AS "); + query.append(columnName); + } + } + + query.append( + " FROM messages h JOIN messages m " + + "ON (h.id = m.thread_root OR h.id = m.id) "); + + //TODO: check projection and selection for folder columns + if (Utility.arrayContains(projection, SpecialColumns.FOLDER_NAME)) { + query.append("LEFT JOIN folders f ON (m.folder_id = f.id) "); + } + + query.append( + "WHERE " + + "(h.deleted = 0 AND m.deleted = 0 AND " + + "(m.empty IS NULL OR m.empty != 1) AND " + + "h.thread_root IS NULL) "); + + if (!StringUtils.isNullOrEmpty(selection)) { + query.append("AND ("); + query.append(SqlQueryBuilder.addPrefixToSelection(MESSAGES_COLUMNS, + "h.", selection)); + query.append(") "); + } + + query.append("GROUP BY h.id"); + + if (!StringUtils.isNullOrEmpty(sortOrder)) { + query.append(" ORDER BY "); + query.append(SqlQueryBuilder.addPrefixToSelection(MESSAGES_COLUMNS, + "m.", sortOrder)); + } + + return db.rawQuery(query.toString(), selectionArgs); + } + }); + } catch (UnavailableStorageException e) { + throw new RuntimeException("Storage not available", e); + } + } + + private Account getAccount(String accountUuid) { + if (mPreferences == null) { + Context appContext = getContext().getApplicationContext(); + mPreferences = Preferences.getPreferences(appContext); + } + + Account account = mPreferences.getAccount(accountUuid); + + if (account == null) { + throw new IllegalArgumentException("Unknown account: " + accountUuid); + } + + return account; + } + + private LockableDatabase getDatabase(Account account) { + LocalStore localStore; + try { + localStore = account.getLocalStore(); + } catch (MessagingException e) { + throw new RuntimeException("Couldn't get LocalStore", e); + } + + return localStore.getDatabase(); + } + + /** + * This class is needed to make {@link CursorAdapter} work with our database schema. + * + *

+ * {@code CursorAdapter} requires a column named {@code "_id"} containing a stable id. We use + * the column name {@code "id"} as primary key in all our tables. So this {@link CursorWrapper} + * maps all queries for {@code "_id"} to {@code "id"}. + *

+ * Please note that this only works for the returned {@code Cursor}. When querying the content + * provider you still need to use {@link MessageColumns#ID}. + *

+ */ + static class IdTrickeryCursor extends CursorWrapper { + public IdTrickeryCursor(Cursor cursor) { + super(cursor); + } + + @Override + public int getColumnIndex(String columnName) { + if ("_id".equals(columnName)) { + return super.getColumnIndex("id"); + } + + return super.getColumnIndex(columnName); + } + + @Override + public int getColumnIndexOrThrow(String columnName) { + if ("_id".equals(columnName)) { + return super.getColumnIndexOrThrow("id"); + } + + return super.getColumnIndexOrThrow(columnName); + } + } + + static class SpecialColumnsCursor extends CursorWrapper { + private int[] mColumnMapping; + private String[] mSpecialColumnValues; + private String[] mColumnNames; + + public SpecialColumnsCursor(Cursor cursor, String[] allColumnNames, + Map specialColumns) { + super(cursor); + + mColumnNames = allColumnNames; + mColumnMapping = new int[allColumnNames.length]; + mSpecialColumnValues = new String[specialColumns.size()]; + for (int i = 0, columnIndex = 0, specialColumnCount = 0, len = allColumnNames.length; + i < len; i++) { + + String columnName = allColumnNames[i]; + + if (specialColumns.containsKey(columnName)) { + // This is a special column name, so save the value in mSpecialColumnValues + mSpecialColumnValues[specialColumnCount] = specialColumns.get(columnName); + + // Write the index into mSpecialColumnValues negated into mColumnMapping + mColumnMapping[i] = -(specialColumnCount + 1); + specialColumnCount++; + } else { + mColumnMapping[i] = columnIndex++; + } + } + } + + @Override + public byte[] getBlob(int columnIndex) { + int realColumnIndex = mColumnMapping[columnIndex]; + if (realColumnIndex < 0) { + throw new RuntimeException("Special column can only be retrieved as string."); + } + + return super.getBlob(realColumnIndex); + } + + @Override + public int getColumnCount() { + return mColumnMapping.length; + } + + @Override + public int getColumnIndex(String columnName) { + for (int i = 0, len = mColumnNames.length; i < len; i++) { + if (mColumnNames[i].equals(columnName)) { + return i; + } + } + + return super.getColumnIndex(columnName); + } + + @Override + public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException { + int index = getColumnIndex(columnName); + + if (index == -1) { + throw new IllegalArgumentException("Unknown column name"); + } + + return index; + } + + @Override + public String getColumnName(int columnIndex) { + return mColumnNames[columnIndex]; + } + + @Override + public String[] getColumnNames() { + return mColumnNames.clone(); + } + + @Override + public double getDouble(int columnIndex) { + int realColumnIndex = mColumnMapping[columnIndex]; + if (realColumnIndex < 0) { + throw new RuntimeException("Special column can only be retrieved as string."); + } + + return super.getDouble(realColumnIndex); + } + + @Override + public float getFloat(int columnIndex) { + int realColumnIndex = mColumnMapping[columnIndex]; + if (realColumnIndex < 0) { + throw new RuntimeException("Special column can only be retrieved as string."); + } + + return super.getFloat(realColumnIndex); + } + + @Override + public int getInt(int columnIndex) { + int realColumnIndex = mColumnMapping[columnIndex]; + if (realColumnIndex < 0) { + throw new RuntimeException("Special column can only be retrieved as string."); + } + + return super.getInt(realColumnIndex); + } + + @Override + public long getLong(int columnIndex) { + int realColumnIndex = mColumnMapping[columnIndex]; + if (realColumnIndex < 0) { + throw new RuntimeException("Special column can only be retrieved as string."); + } + + return super.getLong(realColumnIndex); + } + + @Override + public short getShort(int columnIndex) { + int realColumnIndex = mColumnMapping[columnIndex]; + if (realColumnIndex < 0) { + throw new RuntimeException("Special column can only be retrieved as string."); + } + + return super.getShort(realColumnIndex); + } + + @Override + public String getString(int columnIndex) { + int realColumnIndex = mColumnMapping[columnIndex]; + if (realColumnIndex < 0) { + return mSpecialColumnValues[-realColumnIndex - 1]; + } + + return super.getString(realColumnIndex); + } + + @TargetApi(11) + @Override + public int getType(int columnIndex) { + int realColumnIndex = mColumnMapping[columnIndex]; + if (realColumnIndex < 0) { + return FIELD_TYPE_STRING; + } + + return super.getType(realColumnIndex); + } + + @Override + public boolean isNull(int columnIndex) { + int realColumnIndex = mColumnMapping[columnIndex]; + if (realColumnIndex < 0) { + return (mSpecialColumnValues[-realColumnIndex - 1] == null); + } + + return super.isNull(realColumnIndex); + } + } +} diff --git a/src/com/fsck/k9/provider/MessageProvider.java b/src/com/fsck/k9/provider/MessageProvider.java index 1e351d057..7035baa65 100644 --- a/src/com/fsck/k9/provider/MessageProvider.java +++ b/src/com/fsck/k9/provider/MessageProvider.java @@ -21,23 +21,22 @@ import com.fsck.k9.Account; import com.fsck.k9.AccountStats; import com.fsck.k9.K9; import com.fsck.k9.Preferences; -import com.fsck.k9.SearchAccount; import com.fsck.k9.activity.FolderInfoHolder; import com.fsck.k9.activity.MessageInfoHolder; -import com.fsck.k9.activity.MessageList; import com.fsck.k9.controller.MessagingController; import com.fsck.k9.controller.MessagingListener; -import com.fsck.k9.fragment.MessageListFragment; import com.fsck.k9.helper.MessageHelper; 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.store.LocalStore; +import com.fsck.k9.search.SearchAccount; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.concurrent.BlockingQueue; @@ -302,14 +301,13 @@ public class MessageProvider extends ContentProvider { final SearchAccount integratedInboxAccount = SearchAccount.createUnifiedInboxAccount(getContext()); final MessagingController msgController = MessagingController.getInstance(K9.app); - msgController.searchLocalMessages(integratedInboxAccount, null, + msgController.searchLocalMessages(integratedInboxAccount.getRelatedSearch(), new MesssageInfoHolderRetrieverListener(queue)); final List holders = queue.take(); // TODO add sort order parameter - Collections.sort(holders, new MessageListFragment.ReverseComparator( - new MessageListFragment.DateComparator())); + Collections.sort(holders, new ReverseDateComparator()); final String[] projectionToUse; if (projection == null) { @@ -1025,7 +1023,8 @@ public class MessageProvider extends ContentProvider { // launch command to delete the message if ((myAccount != null) && (msg != null)) { - MessagingController.getInstance(K9.app).deleteMessages(new Message[] { msg }, null); + MessagingController controller = MessagingController.getInstance(K9.app); + controller.deleteMessages(Collections.singletonList(msg), null); } // FIXME return the actual number of deleted messages @@ -1123,4 +1122,16 @@ public class MessageProvider extends ContentProvider { mUriMatcher.addURI(AUTHORITY, handler.getPath(), code); } + public static class ReverseDateComparator implements Comparator { + @Override + public int compare(MessageInfoHolder object2, MessageInfoHolder object1) { + if (object1.compareDate == null) { + return (object2.compareDate == null ? 0 : 1); + } else if (object2.compareDate == null) { + return -1; + } else { + return object1.compareDate.compareTo(object2.compareDate); + } + } + } } diff --git a/src/com/fsck/k9/provider/UnreadWidgetProvider.java b/src/com/fsck/k9/provider/UnreadWidgetProvider.java index 2af479d15..75acd1073 100644 --- a/src/com/fsck/k9/provider/UnreadWidgetProvider.java +++ b/src/com/fsck/k9/provider/UnreadWidgetProvider.java @@ -8,6 +8,7 @@ import com.fsck.k9.R; import com.fsck.k9.activity.UnreadWidgetConfiguration; import com.fsck.k9.activity.FolderList; import com.fsck.k9.activity.MessageList; +import com.fsck.k9.search.LocalSearch; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; @@ -62,8 +63,11 @@ public class UnreadWidgetProvider extends AppWidgetProvider { clickIntent = FolderList.actionHandleAccountIntent(context, account, null, false); } else { - clickIntent = MessageList.actionHandleFolderIntent(context, account, - account.getAutoExpandFolderName()); + LocalSearch search = new LocalSearch(account.getAutoExpandFolderName()); + search.addAllowedFolder(account.getAutoExpandFolderName()); + search.addAccountUuid(account.getUuid()); + clickIntent = MessageList.intentDisplaySearch(context, search, false, true, + true); } clickIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); } diff --git a/src/com/fsck/k9/search/ConditionsTreeNode.java b/src/com/fsck/k9/search/ConditionsTreeNode.java new file mode 100644 index 000000000..1a1be85d7 --- /dev/null +++ b/src/com/fsck/k9/search/ConditionsTreeNode.java @@ -0,0 +1,431 @@ +package com.fsck.k9.search; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Stack; + +import android.database.Cursor; +import android.os.Parcel; +import android.os.Parcelable; + +import com.fsck.k9.search.SearchSpecification.Attribute; +import com.fsck.k9.search.SearchSpecification.Searchfield; +import com.fsck.k9.search.SearchSpecification.SearchCondition; + +/** + * This class stores search conditions. It's basically a boolean expression binary tree. + * The output will be SQL queries ( obtained by traversing inorder ). + * + * TODO removing conditions from the tree + * TODO implement NOT as a node again + */ +public class ConditionsTreeNode implements Parcelable { + + public enum Operator { + AND, OR, CONDITION; + } + + public ConditionsTreeNode mLeft; + public ConditionsTreeNode mRight; + public ConditionsTreeNode mParent; + + /* + * If mValue isn't CONDITION then mCondition contains a real + * condition, otherwise it's null. + */ + public Operator mValue; + public SearchCondition mCondition; + + /* + * Used for storing and retrieving the tree to/from the database. + * The algorithm is called "modified preorder tree traversal". + */ + public int mLeftMPTTMarker; + public int mRightMPTTMarker; + + + /////////////////////////////////////////////////////////////// + // Static Helpers to restore a tree from a database cursor + /////////////////////////////////////////////////////////////// + /** + * Builds a condition tree starting from a database cursor. The cursor + * should point to rows representing the nodes of the tree. + * + * @param cursor Cursor pointing to the first of a bunch or rows. Each rows + * should contains 1 tree node. + * @return A condition tree. + */ + public static ConditionsTreeNode buildTreeFromDB(Cursor cursor) { + Stack stack = new Stack(); + ConditionsTreeNode tmp = null; + + // root node + if (cursor.moveToFirst()) { + tmp = buildNodeFromRow(cursor); + stack.push(tmp); + } + + // other nodes + while (cursor.moveToNext()) { + tmp = buildNodeFromRow(cursor); + if (tmp.mRightMPTTMarker < stack.peek().mRightMPTTMarker) { + stack.peek().mLeft = tmp; + stack.push(tmp); + } else { + while (stack.peek().mRightMPTTMarker < tmp.mRightMPTTMarker) { + stack.pop(); + } + stack.peek().mRight = tmp; + } + } + return tmp; + } + + /** + * Converts a single database row to a single condition node. + * + * @param cursor Cursor pointing to the row we want to convert. + * @return A single ConditionsTreeNode + */ + private static ConditionsTreeNode buildNodeFromRow(Cursor cursor) { + ConditionsTreeNode result = null; + SearchCondition condition = null; + + Operator tmpValue = ConditionsTreeNode.Operator.valueOf(cursor.getString(5)); + + if (tmpValue == Operator.CONDITION) { + condition = new SearchCondition(Searchfield.valueOf(cursor.getString(0)), + Attribute.valueOf(cursor.getString(2)), cursor.getString(1)); + } + + result = new ConditionsTreeNode(condition); + result.mValue = tmpValue; + result.mLeftMPTTMarker = cursor.getInt(3); + result.mRightMPTTMarker = cursor.getInt(4); + + return result; + } + + + /////////////////////////////////////////////////////////////// + // Constructors + /////////////////////////////////////////////////////////////// + public ConditionsTreeNode(SearchCondition condition) { + mParent = null; + mCondition = condition; + mValue = Operator.CONDITION; + } + + public ConditionsTreeNode(ConditionsTreeNode parent, Operator op) { + mParent = parent; + mValue = op; + mCondition = null; + } + + + /* package */ ConditionsTreeNode cloneTree() { + if (mParent != null) { + throw new IllegalStateException("Can't call cloneTree() for a non-root node"); + } + + ConditionsTreeNode copy = new ConditionsTreeNode(mCondition.clone()); + + copy.mLeftMPTTMarker = mLeftMPTTMarker; + copy.mRightMPTTMarker = mRightMPTTMarker; + + copy.mLeft = (mLeft == null) ? null : mLeft.cloneNode(copy); + copy.mRight = (mRight == null) ? null : mRight.cloneNode(copy); + + return copy; + } + + private ConditionsTreeNode cloneNode(ConditionsTreeNode parent) { + ConditionsTreeNode copy = new ConditionsTreeNode(parent, mValue); + + copy.mCondition = mCondition.clone(); + copy.mLeftMPTTMarker = mLeftMPTTMarker; + copy.mRightMPTTMarker = mRightMPTTMarker; + + copy.mLeft = (mLeft == null) ? null : mLeft.cloneNode(copy); + copy.mRight = (mRight == null) ? null : mRight.cloneNode(copy); + + return copy; + } + + /////////////////////////////////////////////////////////////// + // Public modifiers + /////////////////////////////////////////////////////////////// + /** + * Adds the expression as the second argument of an AND + * clause to this node. + * + * @param expr Expression to 'AND' with. + * @return New top AND node. + * @throws Exception + */ + public ConditionsTreeNode and(ConditionsTreeNode expr) throws Exception { + return add(expr, Operator.AND); + } + + /** + * Convenience method. + * Adds the provided condition as the second argument of an AND + * clause to this node. + * + * @param condition Condition to 'AND' with. + * @return New top AND node, new root. + */ + public ConditionsTreeNode and(SearchCondition condition) { + try { + ConditionsTreeNode tmp = new ConditionsTreeNode(condition); + return and(tmp); + } catch (Exception e) { + // impossible + return null; + } + } + + /** + * Adds the expression as the second argument of an OR + * clause to this node. + * + * @param expr Expression to 'OR' with. + * @return New top OR node. + * @throws Exception + */ + public ConditionsTreeNode or(ConditionsTreeNode expr) throws Exception { + return add(expr, Operator.OR); + } + + /** + * Convenience method. + * Adds the provided condition as the second argument of an OR + * clause to this node. + * + * @param condition Condition to 'OR' with. + * @return New top OR node, new root. + */ + public ConditionsTreeNode or(SearchCondition condition) { + try { + ConditionsTreeNode tmp = new ConditionsTreeNode(condition); + return or(tmp); + } catch (Exception e) { + // impossible + return null; + } + } + + /** + * This applies the MPTT labeling to the subtree of which this node + * is the root node. + * + * For a description on MPTT see: + * http://www.sitepoint.com/hierarchical-data-database-2/ + */ + public void applyMPTTLabel() { + applyMPTTLabel(1); + } + + + /////////////////////////////////////////////////////////////// + // Public accessors + /////////////////////////////////////////////////////////////// + /** + * Returns the condition stored in this node. + * @return Condition stored in the node. + */ + public SearchCondition getCondition() { + return mCondition; + } + + /** + * Get a set of all the leaves in the tree. + * @return Set of all the leaves. + */ + public HashSet getLeafSet() { + HashSet leafSet = new HashSet(); + return getLeafSet(leafSet); + } + + /** + * Returns a list of all the nodes in the subtree of which this node + * is the root. The list contains the nodes in a pre traversal order. + * + * @return List of all nodes in subtree in preorder. + */ + public List preorder() { + ArrayList result = new ArrayList(); + Stack stack = new Stack(); + stack.push(this); + + while (!stack.isEmpty()) { + ConditionsTreeNode current = stack.pop(); + + if (current.mLeft != null) { + stack.push(current.mLeft); + } + + if (current.mRight != null) { + stack.push(current.mRight); + } + + result.add(current); + } + + return result; + } + + + /////////////////////////////////////////////////////////////// + // Private class logic + /////////////////////////////////////////////////////////////// + /** + * Adds two new ConditionTreeNodes, one for the operator and one for the + * new condition. The current node will end up on the same level as the + * one provided in the arguments, they will be siblings. Their common + * parent node will be one containing the operator provided in the arguments. + * The method will update all the required references so the tree ends up in + * a valid state. + * + * This method only supports node arguments with a null parent node. + * + * @param Node to add. + * @param Operator that will connect the new node with this one. + * @return New parent node, containing the operator. + * @throws Exception Throws when the provided new node does not have a null parent. + */ + private ConditionsTreeNode add(ConditionsTreeNode node, Operator op) throws Exception { + if (node.mParent != null) { + throw new Exception("Can only add new expressions from root node down."); + } + + ConditionsTreeNode tmpNode = new ConditionsTreeNode(mParent, op); + tmpNode.mLeft = this; + tmpNode.mRight = node; + + if (mParent != null) { + mParent.updateChild(this, tmpNode); + } + + this.mParent = tmpNode; + node.mParent = tmpNode; + + return tmpNode; + } + + /** + * Helper method that replaces a child of the current node with a new node. + * If the provided old child node was the left one, left will be replaced with + * the new one. Same goes for the right one. + * + * @param oldChild Old child node to be replaced. + * @param newChild New child node. + */ + private void updateChild(ConditionsTreeNode oldChild, ConditionsTreeNode newChild) { + // we can compare objects id's because this is the desired behaviour in this case + if (mLeft == oldChild) { + mLeft = newChild; + } else if (mRight == oldChild) { + mRight = newChild; + } + } + + /** + * Recursive function to gather all the leaves in the subtree of which + * this node is the root. + * + * @param leafSet Leafset that's being built. + * @return Set of leaves being completed. + */ + private HashSet getLeafSet(HashSet leafSet) { + if (mLeft == null && mRight == null) { + // if we ended up in a leaf, add ourself and return + leafSet.add(this); + return leafSet; + } + + // we didn't end up in a leaf + if (mLeft != null) { + mLeft.getLeafSet(leafSet); + } + + if (mRight != null) { + mRight.getLeafSet(leafSet); + } + return leafSet; + } + + /** + * This applies the MPTT labeling to the subtree of which this node + * is the root node. + * + * For a description on MPTT see: + * http://www.sitepoint.com/hierarchical-data-database-2/ + */ + private int applyMPTTLabel(int label) { + mLeftMPTTMarker = label; + + if (mLeft != null) { + label = mLeft.applyMPTTLabel(label += 1); + } + + if (mRight != null) { + label = mRight.applyMPTTLabel(label += 1); + } + + ++label; + mRightMPTTMarker = label; + return label; + } + + + /////////////////////////////////////////////////////////////// + // Parcelable + // + // This whole class has to be parcelable because it's passed + // on through intents. + /////////////////////////////////////////////////////////////// + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mValue.ordinal()); + dest.writeParcelable(mCondition, flags); + dest.writeParcelable(mLeft, flags); + dest.writeParcelable(mRight, flags); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public ConditionsTreeNode createFromParcel(Parcel in) { + return new ConditionsTreeNode(in); + } + + @Override + public ConditionsTreeNode[] newArray(int size) { + return new ConditionsTreeNode[size]; + } + }; + + private ConditionsTreeNode(Parcel in) { + mValue = Operator.values()[in.readInt()]; + mCondition = in.readParcelable(ConditionsTreeNode.class.getClassLoader()); + mLeft = in.readParcelable(ConditionsTreeNode.class.getClassLoader()); + mRight = in.readParcelable(ConditionsTreeNode.class.getClassLoader()); + mParent = null; + + if (mLeft != null) { + mLeft.mParent = this; + } + + if (mRight != null) { + mRight.mParent = this; + } + } +} diff --git a/src/com/fsck/k9/search/LocalSearch.java b/src/com/fsck/k9/search/LocalSearch.java new file mode 100644 index 000000000..a6041c41f --- /dev/null +++ b/src/com/fsck/k9/search/LocalSearch.java @@ -0,0 +1,428 @@ +package com.fsck.k9.search; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.fsck.k9.mail.Flag; + +/** + * This class represents a local search. + * + * Removing conditions could be done through matching there unique id in the leafset and then + * removing them from the tree. + * + * TODO implement a complete addAllowedFolder method + * TODO conflicting conditions check on add + * TODO duplicate condition checking? + * TODO assign each node a unique id that's used to retrieve it from the leaveset and remove. + * + */ + +public class LocalSearch implements SearchSpecification { + + private String mName; + private boolean mPredefined; + private boolean mManualSearch = false; + + // since the uuid isn't in the message table it's not in the tree neither + private HashSet mAccountUuids = new HashSet(); + private ConditionsTreeNode mConditions = null; + private HashSet mLeafSet = new HashSet(); + + + /////////////////////////////////////////////////////////////// + // Constructors + /////////////////////////////////////////////////////////////// + /** + * Use this only if the search won't be saved. Saved searches need + * a name! + */ + public LocalSearch() {} + + /** + * + * @param name + */ + public LocalSearch(String name) { + this.mName = name; + } + + /** + * Use this constructor when you know what you'r doing. Normally it's only used + * when restoring these search objects from the database. + * + * @param name Name of the search + * @param searchConditions SearchConditions, may contains flags and folders + * @param accounts Relative Account's uuid's + * @param predefined Is this a predefined search or a user created one? + */ + protected LocalSearch(String name, ConditionsTreeNode searchConditions, + String accounts, boolean predefined) { + this(name); + mConditions = searchConditions; + mPredefined = predefined; + mLeafSet = new HashSet(); + if (mConditions != null) { + mLeafSet.addAll(mConditions.getLeafSet()); + } + + // initialize accounts + if (accounts != null) { + for (String account : accounts.split(",")) { + mAccountUuids.add(account); + } + } else { + // impossible but still not unrecoverable + } + } + + @Override + public LocalSearch clone() { + ConditionsTreeNode conditions = (mConditions == null) ? null : mConditions.cloneTree(); + + LocalSearch copy = new LocalSearch(mName, conditions, null, mPredefined); + copy.mManualSearch = mManualSearch; + copy.mAccountUuids = new HashSet(mAccountUuids); + + return copy; + } + + /////////////////////////////////////////////////////////////// + // Public manipulation methods + /////////////////////////////////////////////////////////////// + /** + * Sets the name of the saved search. If one existed it will + * be overwritten. + * + * @param name Name to be set. + */ + public void setName(String name) { + this.mName = name; + } + + /** + * Add a new account to the search. When no accounts are + * added manually we search all accounts on the device. + * + * @param uuid Uuid of the account to be added. + */ + public void addAccountUuid(String uuid) { + if (uuid.equals(ALL_ACCOUNTS)) { + mAccountUuids.clear(); + return; + } + mAccountUuids.add(uuid); + } + + /** + * Adds all the account uuids in the provided array to + * be matched by the seach. + * + * @param accountUuids + */ + public void addAccountUuids(String[] accountUuids) { + for (String acc : accountUuids) { + addAccountUuid(acc); + } + } + + /** + * Removes an account UUID from the current search. + * + * @param uuid Account UUID to remove. + * @return True if removed, false otherwise. + */ + public boolean removeAccountUuid(String uuid) { + return mAccountUuids.remove(uuid); + } + + /** + * Adds the provided node as the second argument of an AND + * clause to this node. + * + * @param field Message table field to match against. + * @param string Value to look for. + * @param contains Attribute to use when matching. + * + * @throws IllegalConditionException + */ + public void and(Searchfield field, String value, Attribute attribute) { + and(new SearchCondition(field, attribute, value)); + } + + /** + * Adds the provided condition as the second argument of an AND + * clause to this node. + * + * @param condition Condition to 'AND' with. + * @return New top AND node, new root. + */ + public ConditionsTreeNode and(SearchCondition condition) { + try { + ConditionsTreeNode tmp = new ConditionsTreeNode(condition); + return and(tmp); + } catch (Exception e) { + // impossible + return null; + } + } + + /** + * Adds the provided node as the second argument of an AND + * clause to this node. + * + * @param node Node to 'AND' with. + * @return New top AND node, new root. + * @throws Exception + */ + public ConditionsTreeNode and(ConditionsTreeNode node) throws Exception { + mLeafSet.addAll(node.getLeafSet()); + + if (mConditions == null) { + mConditions = node; + return node; + } + + mConditions = mConditions.and(node); + return mConditions; + } + + /** + * Adds the provided condition as the second argument of an OR + * clause to this node. + * + * @param condition Condition to 'OR' with. + * @return New top OR node, new root. + */ + public ConditionsTreeNode or(SearchCondition condition) { + try { + ConditionsTreeNode tmp = new ConditionsTreeNode(condition); + return or(tmp); + } catch (Exception e) { + // impossible + return null; + } + } + + /** + * Adds the provided node as the second argument of an OR + * clause to this node. + * + * @param node Node to 'OR' with. + * @return New top OR node, new root. + * @throws Exception + */ + public ConditionsTreeNode or(ConditionsTreeNode node) throws Exception { + mLeafSet.addAll(node.getLeafSet()); + + if (mConditions == null) { + mConditions = node; + return node; + } + + mConditions = mConditions.or(node); + return mConditions; + } + + /** + * Add all the flags to this node as required flags. The + * provided flags will be combined using AND with the root. + * + * @param requiredFlags Array of required flags. + */ + public void allRequiredFlags(Flag[] requiredFlags) { + if (requiredFlags != null) { + for (Flag f : requiredFlags) { + and(new SearchCondition(Searchfield.FLAG, Attribute.CONTAINS, f.name())); + } + } + } + + /** + * Add all the flags to this node as forbidden flags. The + * provided flags will be combined using AND with the root. + * + * @param forbiddenFlags Array of forbidden flags. + */ + public void allForbiddenFlags(Flag[] forbiddenFlags) { + if (forbiddenFlags != null) { + for (Flag f : forbiddenFlags) { + and(new SearchCondition(Searchfield.FLAG, Attribute.NOT_CONTAINS, f.name())); + } + } + } + + /** + * TODO + * FOR NOW: And the folder with the root. + * + * Add the folder as another folder to search in. The folder + * will be added AND to the root if no 'folder subtree' was found. + * Otherwise the folder will be added OR to that tree. + * + * @param name Name of the folder to add. + */ + public void addAllowedFolder(String name) { + /* + * TODO find folder sub-tree + * - do and on root of it & rest of search + * - do or between folder nodes + */ + mConditions = and(new SearchCondition(Searchfield.FOLDER, Attribute.EQUALS, name)); + } + + /* + * TODO make this more advanced! + * This is a temporarely solution that does NOT WORK for + * real searches because of possible extra conditions to a folder requirement. + */ + public List getFolderNames() { + ArrayList results = new ArrayList(); + for (ConditionsTreeNode node : mLeafSet) { + if (node.mCondition.field == Searchfield.FOLDER && + node.mCondition.attribute == Attribute.EQUALS) { + results.add(node.mCondition.value); + } + } + return results; + } + + /** + * Gets the leafset of the related condition tree. + * + * @return All the leaf conditions as a set. + */ + public Set getLeafSet() { + return mLeafSet; + } + + /////////////////////////////////////////////////////////////// + // Public accesor methods + /////////////////////////////////////////////////////////////// + /** + * TODO THIS HAS TO GO!!!! + * very dirty fix for remotesearch support atm + */ + public String getRemoteSearchArguments() { + Set leafSet = getLeafSet(); + if (leafSet == null) { + return null; + } + + for (ConditionsTreeNode node : leafSet) { + if (node.getCondition().field == Searchfield.SUBJECT || + node.getCondition().field == Searchfield.SENDER ) { + return node.getCondition().value; + } + } + return null; + } + + /** + * Returns the name of the saved search. + * + * @return Name of the search. + */ + @Override + public String getName() { + return (mName == null) ? "" : mName; + } + + /** + * Checks if this search was hard coded and shipped with K-9 + * + * @return True is search was shipped with K-9 + */ + public boolean isPredefined() { + return mPredefined; + } + + public boolean isManualSearch() { + return mManualSearch; + } + + public void setManualSearch(boolean manualSearch) { + mManualSearch = manualSearch; + } + + /** + * Returns all the account uuids that this search will try to + * match against. + * + * @return Array of account uuids. + */ + @Override + public String[] getAccountUuids() { + if (mAccountUuids.size() == 0) { + return new String[] { SearchSpecification.ALL_ACCOUNTS }; + } + + String[] tmp = new String[mAccountUuids.size()]; + mAccountUuids.toArray(tmp); + return tmp; + } + + /** + * Returns whether or not to search all accounts. + * + * @return {@code true} if all accounts should be searched. + */ + public boolean searchAllAccounts() { + return (mAccountUuids.size() == 0); + } + + /** + * Get the condition tree. + * + * @return The root node of the related conditions tree. + */ + @Override + public ConditionsTreeNode getConditions() { + return mConditions; + } + + /////////////////////////////////////////////////////////////// + // Parcelable + /////////////////////////////////////////////////////////////// + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mName); + dest.writeByte((byte) (mPredefined ? 1 : 0)); + dest.writeByte((byte) (mManualSearch ? 1 : 0)); + dest.writeStringList(new ArrayList(mAccountUuids)); + dest.writeParcelable(mConditions, flags); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public LocalSearch createFromParcel(Parcel in) { + return new LocalSearch(in); + } + + @Override + public LocalSearch[] newArray(int size) { + return new LocalSearch[size]; + } + }; + + public LocalSearch(Parcel in) { + mName = in.readString(); + mPredefined = (in.readByte() == 1); + mManualSearch = (in.readByte() == 1); + mAccountUuids.addAll(in.createStringArrayList()); + mConditions = in.readParcelable(LocalSearch.class.getClassLoader()); + mLeafSet = (mConditions == null) ? null : mConditions.getLeafSet(); + } +} \ No newline at end of file diff --git a/src/com/fsck/k9/search/SearchAccount.java b/src/com/fsck/k9/search/SearchAccount.java new file mode 100644 index 000000000..1eb20a7b5 --- /dev/null +++ b/src/com/fsck/k9/search/SearchAccount.java @@ -0,0 +1,91 @@ +package com.fsck.k9.search; + +import java.util.UUID; + +import android.content.Context; + +import com.fsck.k9.BaseAccount; +import com.fsck.k9.R; +import com.fsck.k9.search.SearchSpecification.Attribute; +import com.fsck.k9.search.SearchSpecification.Searchfield; + +/** + * This class is basically a wrapper around a LocalSearch. It allows to expose it as + * an account. This is a meta-account containing all the e-mail that matches the search. + */ +public class SearchAccount implements BaseAccount { + + // create the all messages search ( all accounts is default when none specified ) + public static SearchAccount createAllMessagesAccount(Context context) { + String name = context.getString(R.string.search_all_messages_title); + LocalSearch tmpSearch = new LocalSearch(name); + return new SearchAccount(tmpSearch, name, + context.getString(R.string.search_all_messages_detail)); + } + + + // create the unified inbox meta account ( all accounts is default when none specified ) + public static SearchAccount createUnifiedInboxAccount(Context context) { + String name = context.getString(R.string.integrated_inbox_title); + LocalSearch tmpSearch = new LocalSearch(name); + tmpSearch.and(Searchfield.INTEGRATE, "1", Attribute.EQUALS); + return new SearchAccount(tmpSearch, name, + context.getString(R.string.integrated_inbox_detail)); + } + + private String mEmail; + private String mDescription; + private LocalSearch mSearch; + private String mFakeUuid; + + public SearchAccount(LocalSearch search, String description, String email) + throws IllegalArgumentException { + + if (search == null) { + throw new IllegalArgumentException("Provided LocalSearch was null"); + } + + mSearch = search; + mDescription = description; + mEmail = email; + } + + @Override + public synchronized String getEmail() { + return mEmail; + } + + @Override + public synchronized void setEmail(String email) { + this.mEmail = email; + } + + @Override + public String getDescription() { + return mDescription; + } + + @Override + public void setDescription(String description) { + this.mDescription = description; + } + + public LocalSearch getRelatedSearch() { + return mSearch; + } + + /* + * This will only be used when accessed as an Account. If that + * is the case we don't want to return the uuid of a real account since + * this is posing as a fake meta-account. If this object is accesed as + * a Search then methods from LocalSearch will be called which do handle + * things nice. + */ + @Override + public String getUuid() { + if (mFakeUuid == null) { + mFakeUuid = UUID.randomUUID().toString(); + } + return mFakeUuid; + } +} diff --git a/src/com/fsck/k9/search/SearchModifier.java b/src/com/fsck/k9/search/SearchModifier.java new file mode 100644 index 000000000..22ec2740b --- /dev/null +++ b/src/com/fsck/k9/search/SearchModifier.java @@ -0,0 +1,23 @@ +package com.fsck.k9.search; + +import com.fsck.k9.R; +import com.fsck.k9.mail.Flag; + +/** + * This enum represents filtering parameters used by {@link com.fsck.k9.search.SearchAccount}. + */ +public enum SearchModifier { + FLAGGED(R.string.flagged_modifier, new Flag[] { Flag.FLAGGED }, null), + UNREAD(R.string.unread_modifier, null, new Flag[] { Flag.SEEN }); + + public final int resId; + public final Flag[] requiredFlags; + public final Flag[] forbiddenFlags; + + SearchModifier(int nResId, Flag[] nRequiredFlags, Flag[] nForbiddenFlags) { + resId = nResId; + requiredFlags = nRequiredFlags; + forbiddenFlags = nForbiddenFlags; + } + +} \ No newline at end of file diff --git a/src/com/fsck/k9/search/SearchSpecification.java b/src/com/fsck/k9/search/SearchSpecification.java new file mode 100644 index 000000000..1e599aed9 --- /dev/null +++ b/src/com/fsck/k9/search/SearchSpecification.java @@ -0,0 +1,163 @@ +package com.fsck.k9.search; + +import android.os.Parcel; +import android.os.Parcelable; + +public interface SearchSpecification extends Parcelable { + + /** + * Get all the uuids of accounts this search acts on. + * @return Array of uuids. + */ + public String[] getAccountUuids(); + + /** + * Returns the search's name if it was named. + * @return Name of the search. + */ + public String getName(); + + /** + * Returns the root node of the condition tree accompanying + * the search. + * + * @return Root node of conditions tree. + */ + public ConditionsTreeNode getConditions(); + + /* + * Some meta names for certain conditions. + */ + public static final String ALL_ACCOUNTS = "allAccounts"; + + /////////////////////////////////////////////////////////////// + // ATTRIBUTE enum + /////////////////////////////////////////////////////////////// + public enum Attribute { + CONTAINS, + NOT_CONTAINS, + + EQUALS, + NOT_EQUALS, + + STARTSWITH, + NOT_STARTSWITH, + + ENDSWITH, + NOT_ENDSWITH + } + + /////////////////////////////////////////////////////////////// + // SEARCHFIELD enum + /////////////////////////////////////////////////////////////// + /* + * Using an enum in order to have more robust code. Users ( & coders ) + * are prevented from passing illegal fields. No database overhead + * when invalid fields passed. + * + * By result, only the fields in here are searchable. + * + * Fields not in here at this moment ( and by effect not searchable ): + * id, html_content, internal_date, message_id, + * preview, mime_type + * + */ + public enum Searchfield { + SUBJECT, + DATE, + UID, + FLAG, + SENDER, + TO, + CC, + FOLDER, + BCC, + REPLY_TO, + MESSAGE_CONTENTS, + ATTACHMENT_COUNT, + DELETED, + THREAD_ROOT, + ID, + INTEGRATE + } + + + /////////////////////////////////////////////////////////////// + // SearchCondition class + /////////////////////////////////////////////////////////////// + /** + * This class represents 1 value for a certain search field. One + * value consists of three things: + * an attribute: equals, starts with, contains,... + * a searchfield: date, flags, sender, subject,... + * a value: "apple", "jesse",.. + * + * @author dzan + */ + public class SearchCondition implements Parcelable { + public final String value; + public final Attribute attribute; + public final Searchfield field; + + public SearchCondition(Searchfield field, Attribute attribute, String value) { + this.value = value; + this.attribute = attribute; + this.field = field; + } + + private SearchCondition(Parcel in) { + this.value = in.readString(); + this.attribute = Attribute.values()[in.readInt()]; + this.field = Searchfield.values()[in.readInt()]; + } + + @Override + public SearchCondition clone() { + return new SearchCondition(field, attribute, value); + } + + public String toHumanString() { + return field.toString() + attribute.toString(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof SearchCondition) { + SearchCondition tmp = (SearchCondition) o; + if (tmp.attribute == attribute && + tmp.field == field && + tmp.value.equals(value)) { + return true; + } + } + + return false; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(value); + dest.writeInt(attribute.ordinal()); + dest.writeInt(field.ordinal()); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public SearchCondition createFromParcel(Parcel in) { + return new SearchCondition(in); + } + + @Override + public SearchCondition[] newArray(int size) { + return new SearchCondition[size]; + } + }; + } +} \ No newline at end of file diff --git a/src/com/fsck/k9/search/SqlQueryBuilder.java b/src/com/fsck/k9/search/SqlQueryBuilder.java new file mode 100644 index 000000000..22d087ba0 --- /dev/null +++ b/src/com/fsck/k9/search/SqlQueryBuilder.java @@ -0,0 +1,232 @@ +package com.fsck.k9.search; + +import java.util.List; + +import com.fsck.k9.Account; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Folder.OpenMode; +import com.fsck.k9.mail.store.LocalStore; +import com.fsck.k9.mail.store.LocalStore.LocalFolder; +import com.fsck.k9.search.SearchSpecification.SearchCondition; +import com.fsck.k9.search.SearchSpecification.Searchfield; + + +public class SqlQueryBuilder { + public static void buildWhereClause(Account account, ConditionsTreeNode node, + StringBuilder query, List selectionArgs) { + buildWhereClauseInternal(account, node, query, selectionArgs); + } + + private static void buildWhereClauseInternal(Account account, ConditionsTreeNode node, + StringBuilder query, List selectionArgs) { + if (node == null) { + return; + } + + if (node.mLeft == null && node.mRight == null) { + SearchCondition condition = node.mCondition; + switch (condition.field) { + case FOLDER: { + String folderName = condition.value; + long folderId = getFolderId(account, folderName); + query.append("folder_id = ?"); + selectionArgs.add(Long.toString(folderId)); + break; + } + default: { + appendCondition(condition, query, selectionArgs); + } + } + } else { + query.append("("); + buildWhereClauseInternal(account, node.mLeft, query, selectionArgs); + query.append(") "); + query.append(node.mValue.name()); + query.append(" ("); + buildWhereClauseInternal(account, node.mRight, query, selectionArgs); + query.append(")"); + } + } + + private static void appendCondition(SearchCondition condition, StringBuilder query, + List selectionArgs) { + query.append(getColumnName(condition)); + appendExprRight(condition, query, selectionArgs); + } + + private static long getFolderId(Account account, String folderName) { + long folderId = 0; + try { + LocalStore localStore = account.getLocalStore(); + LocalFolder folder = localStore.getFolder(folderName); + folder.open(OpenMode.READ_ONLY); + folderId = folder.getId(); + } catch (MessagingException e) { + //FIXME + e.printStackTrace(); + } + + return folderId; + } + + private static String getColumnName(SearchCondition condition) { + String columnName = null; + switch (condition.field) { + case ATTACHMENT_COUNT: { + columnName = "attachment_count"; + break; + } + case BCC: { + columnName = "bcc_list"; + break; + } + case CC: { + columnName = "cc_list"; + break; + } + case DATE: { + columnName = "date"; + break; + } + case DELETED: { + columnName = "deleted"; + break; + } + case FLAG: { + columnName = "flags"; + break; + } + case FOLDER: { + columnName = "folder_id"; + break; + } + case ID: { + columnName = "id"; + break; + } + case MESSAGE_CONTENTS: { + columnName = "text_content"; + break; + } + case REPLY_TO: { + columnName = "reply_to_list"; + break; + } + case SENDER: { + columnName = "sender_list"; + break; + } + case SUBJECT: { + columnName = "subject"; + break; + } + case THREAD_ROOT: { + columnName = "thread_root"; + break; + } + case TO: { + columnName = "to_list"; + break; + } + case UID: { + columnName = "uid"; + break; + } + case INTEGRATE: { + columnName = "integrate"; + break; + } + } + + if (columnName == null) { + throw new RuntimeException("Unhandled case"); + } + + return columnName; + } + + private static void appendExprRight(SearchCondition condition, StringBuilder query, + List selectionArgs) { + String value = condition.value; + Searchfield field = condition.field; + + query.append(" "); + String selectionArg = null; + switch (condition.attribute) { + case NOT_CONTAINS: + query.append("NOT "); + //$FALL-THROUGH$ + case CONTAINS: { + query.append("LIKE ?"); + selectionArg = "%" + value + "%"; + break; + } + case NOT_STARTSWITH: + query.append("NOT "); + //$FALL-THROUGH$ + case STARTSWITH: { + query.append("LIKE ?"); + selectionArg = "%" + value; + break; + } + case NOT_ENDSWITH: + query.append("NOT "); + //$FALL-THROUGH$ + case ENDSWITH: { + query.append("LIKE ?"); + selectionArg = value + "%"; + break; + } + case NOT_EQUALS: { + if (isNumberColumn(field)) { + query.append("!= ?"); + } else { + query.append("NOT LIKE ?"); + } + selectionArg = value; + break; + } + case EQUALS: { + if (isNumberColumn(field)) { + query.append("= ?"); + } else { + query.append("LIKE ?"); + } + selectionArg = value; + break; + } + } + + if (selectionArg == null) { + throw new RuntimeException("Unhandled case"); + } + + selectionArgs.add(selectionArg); + } + + private static boolean isNumberColumn(Searchfield field) { + switch (field) { + case ATTACHMENT_COUNT: + case DATE: + case DELETED: + case FOLDER: + case ID: + case INTEGRATE: + case THREAD_ROOT: { + return true; + } + default: { + return false; + } + } + } + + public static String addPrefixToSelection(String[] columnNames, String prefix, String selection) { + String result = selection; + for (String columnName : columnNames) { + result = result.replaceAll("\\b" + columnName + "\\b", prefix + columnName); + } + + return result; + } +}