diff --git a/res/values/arrays.xml b/res/values/arrays.xml index e495a3435..6bc8918d4 100644 --- a/res/values/arrays.xml +++ b/res/values/arrays.xml @@ -668,6 +668,26 @@ HTML AUTO + + @string/account_settings_remote_search_num_results_entries_10 + @string/account_settings_remote_search_num_results_entries_25 + @string/account_settings_remote_search_num_results_entries_50 + @string/account_settings_remote_search_num_results_entries_100 + @string/account_settings_remote_search_num_results_entries_250 + @string/account_settings_remote_search_num_results_entries_500 + @string/account_settings_remote_search_num_results_entries_1000 + @string/account_settings_remote_search_num_results_entries_all + + + 10 + 25 + 50 + 100 + 250 + 500 + 1000 + 0 + @string/global_settings_notification_hide_subject_never diff --git a/res/values/strings.xml b/res/values/strings.xml index 26ae0cb63..02359d8b5 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1120,4 +1120,29 @@ http://k9mail.googlecode.com/ Saved image as \"%s\" Saving the image failed. + Allow remote search + Enable remote searching for this account + All + 10 + 25 + 50 + 100 + 250 + 500 + 1000 + Results limit + All Local Folders + Remote Folder + Search Location + Remote folder searching + Can be slow + Include body text + + To perform a remote search, press your device\'s Search button while viewing a folder from this account. + \n\nNote: Remote search is NOT available from the Unified Inbox or from a list of folders. + + Sending query to server + Don\'t Show Again + Fetching %d results + Fetching %1$d of %2$d results diff --git a/res/xml/account_settings_preferences.xml b/res/xml/account_settings_preferences.xml index d9751dee3..c5ff71338 100644 --- a/res/xml/account_settings_preferences.xml +++ b/res/xml/account_settings_preferences.xml @@ -23,7 +23,9 @@ can be displayed after the device has been rotated. --> - + + + + + + + + + + diff --git a/src/com/fsck/k9/Account.java b/src/com/fsck/k9/Account.java index b3e1a2efe..098448468 100644 --- a/src/com/fsck/k9/Account.java +++ b/src/com/fsck/k9/Account.java @@ -1,6 +1,18 @@ package com.fsck.k9; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + import android.content.Context; import android.content.SharedPreferences; import android.net.ConnectivityManager; @@ -68,6 +80,7 @@ public class Account implements BaseAccount { public static final boolean DEFAULT_QUOTED_TEXT_SHOWN = true; public static final boolean DEFAULT_REPLY_AFTER_QUOTE = false; public static final boolean DEFAULT_STRIP_SIGNATURE = true; + public static final int DEFAULT_REMOTE_SEARCH_NUM_RESULTS = 25; public static final String ACCOUNT_DESCRIPTION_KEY = "description"; public static final String STORE_URI_KEY = "storeUri"; @@ -186,6 +199,9 @@ public class Account implements BaseAccount { private boolean mCryptoAutoEncrypt; private boolean mMarkMessageAsReadOnView; private boolean mAlwaysShowCcBcc; + private boolean mAllowRemoteSearch; + private boolean mRemoteSearchFullText; + private int mRemoteSearchNumResults; private CryptoProvider mCryptoProvider = null; @@ -276,6 +292,9 @@ public class Account implements BaseAccount { mCryptoApp = Apg.NAME; mCryptoAutoSignature = false; mCryptoAutoEncrypt = false; + mAllowRemoteSearch = false; + mRemoteSearchFullText = false; + mRemoteSearchNumResults = DEFAULT_REMOTE_SEARCH_NUM_RESULTS; mEnabled = true; mMarkMessageAsReadOnView = true; mAlwaysShowCcBcc = false; @@ -441,6 +460,10 @@ public class Account implements BaseAccount { mCryptoApp = prefs.getString(mUuid + ".cryptoApp", Apg.NAME); mCryptoAutoSignature = prefs.getBoolean(mUuid + ".cryptoAutoSignature", false); mCryptoAutoEncrypt = prefs.getBoolean(mUuid + ".cryptoAutoEncrypt", false); + mAllowRemoteSearch = prefs.getBoolean(mUuid + ".allowRemoteSearch", false); + mRemoteSearchFullText = prefs.getBoolean(mUuid + ".remoteSearchFullText", false); + mRemoteSearchNumResults = prefs.getInt(mUuid + ".remoteSearchNumResults", DEFAULT_REMOTE_SEARCH_NUM_RESULTS); + mEnabled = prefs.getBoolean(mUuid + ".enabled", true); mMarkMessageAsReadOnView = prefs.getBoolean(mUuid + ".markMessageAsReadOnView", true); mAlwaysShowCcBcc = prefs.getBoolean(mUuid + ".alwaysShowCcBcc", false); @@ -688,6 +711,9 @@ public class Account implements BaseAccount { editor.putString(mUuid + ".cryptoApp", mCryptoApp); editor.putBoolean(mUuid + ".cryptoAutoSignature", mCryptoAutoSignature); editor.putBoolean(mUuid + ".cryptoAutoEncrypt", mCryptoAutoEncrypt); + editor.putBoolean(mUuid + ".allowRemoteSearch", mAllowRemoteSearch); + editor.putBoolean(mUuid + ".remoteSearchFullText", mRemoteSearchFullText); + editor.putInt(mUuid + ".remoteSearchNumResults", mRemoteSearchNumResults); editor.putBoolean(mUuid + ".enabled", mEnabled); editor.putBoolean(mUuid + ".markMessageAsReadOnView", mMarkMessageAsReadOnView); editor.putBoolean(mUuid + ".alwaysShowCcBcc", mAlwaysShowCcBcc); @@ -1572,6 +1598,22 @@ public class Account implements BaseAccount { mCryptoAutoEncrypt = cryptoAutoEncrypt; } + public boolean allowRemoteSearch() { + return mAllowRemoteSearch; + } + + public void setAllowRemoteSearch(boolean val) { + mAllowRemoteSearch = val; + } + + public int getRemoteSearchNumResults() { + return mRemoteSearchNumResults; + } + + public void setRemoteSearchNumResults(int val) { + mRemoteSearchNumResults = (val >= 0 ? val : 0); + } + public String getInboxFolderName() { return mInboxFolderName; } @@ -1642,4 +1684,13 @@ public class Account implements BaseAccount { public synchronized void setAlwaysShowCcBcc(boolean show) { mAlwaysShowCcBcc = show; } + public boolean isRemoteSearchFullText() { + return mRemoteSearchFullText; + } + + public void setRemoteSearchFullText(boolean val) { + mRemoteSearchFullText = val; + } + + } diff --git a/src/com/fsck/k9/activity/MessageInfoHolder.java b/src/com/fsck/k9/activity/MessageInfoHolder.java index 531faf1b3..4669daec6 100644 --- a/src/com/fsck/k9/activity/MessageInfoHolder.java +++ b/src/com/fsck/k9/activity/MessageInfoHolder.java @@ -2,7 +2,7 @@ package com.fsck.k9.activity; import java.util.Date; import com.fsck.k9.helper.MessageHelper; -import com.fsck.k9.mail.store.LocalStore.LocalMessage; +import com.fsck.k9.mail.Message; public class MessageInfoHolder { public String date; @@ -19,7 +19,7 @@ public class MessageInfoHolder { public boolean forwarded; public boolean flagged; public boolean dirty; - public LocalMessage message; + public Message message; public FolderInfoHolder folder; public boolean selected; public String account; diff --git a/src/com/fsck/k9/activity/MessageList.java b/src/com/fsck/k9/activity/MessageList.java index 8dfe6d0d3..61634c872 100644 --- a/src/com/fsck/k9/activity/MessageList.java +++ b/src/com/fsck/k9/activity/MessageList.java @@ -11,7 +11,9 @@ import java.util.Map; import android.app.AlertDialog; import android.app.Dialog; +import android.app.SearchManager; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences.Editor; import android.graphics.Color; @@ -74,7 +76,6 @@ import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.store.LocalStore; import com.fsck.k9.mail.store.LocalStore.LocalFolder; -import com.fsck.k9.mail.store.LocalStore.LocalMessage; import com.fsck.k9.mail.store.StorageManager; import com.handmark.pulltorefresh.library.PullToRefreshBase; import com.handmark.pulltorefresh.library.PullToRefreshListView; @@ -173,7 +174,13 @@ public class MessageList extends K9ListActivity implements OnItemClickListener { @Override public int compare(MessageInfoHolder object1, MessageInfoHolder object2) { - return object1.compareCounterparty.toLowerCase().compareTo(object2.compareCounterparty.toLowerCase()); + if (object1.compareCounterparty == null) { + return (object2.compareCounterparty == null ? 0 : 1); + } else if (object2.compareCounterparty == null) { + return -1; + } else { + return object1.compareCounterparty.toLowerCase().compareTo(object2.compareCounterparty.toLowerCase()); + } } } @@ -182,7 +189,13 @@ public class MessageList extends K9ListActivity implements OnItemClickListener { @Override public int compare(MessageInfoHolder object1, MessageInfoHolder object2) { - return object1.compareDate.compareTo(object2.compareDate); + if (object1.compareDate == null) { + return (object2.compareDate == null ? 0 : 1); + } else if (object2.compareDate == null) { + return -1; + } else { + return object1.compareDate.compareTo(object2.compareDate); + } } } @@ -223,7 +236,9 @@ public class MessageList extends K9ListActivity implements OnItemClickListener { private static final String EXTRA_ACCOUNT = "account"; private static final String EXTRA_FOLDER = "folder"; - private static final String EXTRA_QUERY = "query"; + private static final String EXTRA_REMOTE_SEARCH = "com.fsck.k9.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"; @@ -284,6 +299,9 @@ public class MessageList extends K9ListActivity implements OnItemClickListener { 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 mIntegrate = false; private String[] mAccountUuids = null; private String[] mFolderNames = null; @@ -363,7 +381,6 @@ public class MessageList extends K9ListActivity implements OnItemClickListener { private static final int ACTION_REFRESH_TITLE = 5; private static final int ACTION_PROGRESS = 6; - public void removeMessage(MessageReference messageReference) { android.os.Message msg = android.os.Message.obtain(this, ACTION_REMOVE_MESSAGE, messageReference); @@ -392,6 +409,15 @@ public class MessageList extends K9ListActivity implements OnItemClickListener { sendMessage(msg); } + public void updateFooter(final String message, final boolean showProgress) { + runOnUiThread(new Runnable() { + @Override + public void run() { + MessageList.this.updateFooter(message, showProgress); + } + }); + } + 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. @@ -481,10 +507,7 @@ public class MessageList extends K9ListActivity implements OnItemClickListener { } } - // build the comparator chain - final Comparator chainComparator = new ComparatorChain(chain); - - return chainComparator; + return new ComparatorChain(chain); } private void folderLoading(String folder, boolean loading) { @@ -496,7 +519,9 @@ public class MessageList extends K9ListActivity implements OnItemClickListener { private void refreshTitle() { setWindowTitle(); - setWindowProgress(); + if (!mRemoteSearch) { + setWindowProgress(); + } } private void setWindowProgress() { @@ -625,7 +650,7 @@ public class MessageList extends K9ListActivity implements OnItemClickListener { public static void actionHandle(Context context, String title, String queryString, boolean integrate, Flag[] flags, Flag[] forbiddenFlags) { Intent intent = new Intent(context, MessageList.class); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); - intent.putExtra(EXTRA_QUERY, queryString); + intent.putExtra(SearchManager.QUERY, queryString); if (flags != null) { intent.putExtra(EXTRA_QUERY_FLAGS, Utility.combine(flags, ',')); } @@ -643,7 +668,7 @@ public class MessageList extends K9ListActivity implements OnItemClickListener { public static Intent actionHandleAccountIntent(Context context, String title, SearchSpecification searchSpecification) { Intent intent = new Intent(context, MessageList.class); - intent.putExtra(EXTRA_QUERY, searchSpecification.getQuery()); + intent.putExtra(SearchManager.QUERY, searchSpecification.getQuery()); if (searchSpecification.getRequiredFlags() != null) { intent.putExtra(EXTRA_QUERY_FLAGS, Utility.combine(searchSpecification.getRequiredFlags(), ',')); } @@ -670,8 +695,25 @@ public class MessageList extends K9ListActivity implements OnItemClickListener { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { if (view == mFooterView) { - if (mCurrentFolder != null) { + 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(); + Account account = Preferences.getPreferences(this).getAccount(mSearchAccount); + if (account == null) { + mHandler.updateFooter("", false); + return; + } + int limit = account.getRemoteSearchNumResults(); + List toProcess = mAdapter.mExtraSearchResults; + if (limit > 0 && numResults > limit) { + toProcess = toProcess.subList(0, limit); + mAdapter.mExtraSearchResults = mAdapter.mExtraSearchResults.subList(limit, mAdapter.mExtraSearchResults.size()); + } else { + mAdapter.mExtraSearchResults = null; + mHandler.updateFooter("", false); + } + mController.loadSearchResults(account, mSearchFolder, toProcess, mAdapter.mListener); } return; } @@ -693,7 +735,7 @@ public class MessageList extends K9ListActivity implements OnItemClickListener { mActionBarProgressView = getLayoutInflater().inflate(R.layout.actionbar_indeterminate_progress_actionview, null); // need this for actionbar initialization - mQueryString = getIntent().getStringExtra(EXTRA_QUERY); + mQueryString = getIntent().getStringExtra(SearchManager.QUERY); mPullToRefreshView = (PullToRefreshListView) findViewById(R.id.message_list); @@ -741,7 +783,30 @@ public class MessageList extends K9ListActivity implements OnItemClickListener { return; } + mQueryString = intent.getStringExtra(SearchManager.QUERY); + mFolderName = null; + mRemoteSearch = false; + mSearchAccount = null; + mSearchFolder = null; + if (mQueryString != null) { + if (Intent.ACTION_SEARCH.equals(intent.getAction())) { + //Query was received from Search Dialog + Bundle appData = getIntent().getBundleExtra(SearchManager.APP_DATA); + if (appData != null) { + mSearchAccount = appData.getString(EXTRA_SEARCH_ACCOUNT); + mSearchFolder = appData.getString(EXTRA_SEARCH_FOLDER); + mRemoteSearch = appData.getBoolean(EXTRA_REMOTE_SEARCH); + } + } else { + mSearchAccount = intent.getStringExtra(EXTRA_SEARCH_ACCOUNT); + mSearchFolder = intent.getStringExtra(EXTRA_SEARCH_FOLDER); + + } + } + String accountUuid = intent.getStringExtra(EXTRA_ACCOUNT); + mFolderName = intent.getStringExtra(EXTRA_FOLDER); + mAccount = Preferences.getPreferences(this).getAccount(accountUuid); if (mAccount != null && !mAccount.isAvailable(this)) { @@ -750,8 +815,8 @@ public class MessageList extends K9ListActivity implements OnItemClickListener { return; } - mFolderName = intent.getStringExtra(EXTRA_FOLDER); - mQueryString = intent.getStringExtra(EXTRA_QUERY); + + String queryFlags = intent.getStringExtra(EXTRA_QUERY_FLAGS); if (queryFlags != null) { @@ -787,8 +852,8 @@ public class MessageList extends K9ListActivity implements OnItemClickListener { mCurrentFolder = mAdapter.getFolder(mFolderName, mAccount); } - // Hide "Load up to x more" footer for search views - mFooterView.setVisibility((mQueryString != null) ? View.GONE : View.VISIBLE); + // Hide "Load up to x more" footer for local search views + mFooterView.setVisibility((mQueryString != null && !mRemoteSearch) ? View.GONE : View.VISIBLE); mController = MessagingController.getInstance(getApplication()); mListView.setAdapter(mAdapter); @@ -848,6 +913,14 @@ public class MessageList extends K9ListActivity implements OnItemClickListener { onAccountUnavailable(); return; } + + + if (!(this instanceof Search)) { + //necessary b/c no guarantee Search.onStop will be called before MessageList.onResume + //when returning from search results + Search.setActive(false); + } + StorageManager.getInstance(getApplication()).addListener(mStorageListener); mStars = K9.messageListStars(); @@ -867,6 +940,7 @@ public class MessageList extends K9ListActivity implements OnItemClickListener { mController.addListener(mAdapter.mListener); + //Cancel pending new mail notifications when we open an account Account[] accountsWithNotification; Preferences prefs = Preferences.getPreferences(getApplicationContext()); @@ -888,8 +962,12 @@ public class MessageList extends K9ListActivity implements OnItemClickListener { mController.notifyAccountCancel(this, accountWithNotification); } + if (mAdapter.isEmpty()) { - if (mFolderName != null) { + if (mRemoteSearch) { + //TODO: Support flag based search + 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()) { @@ -900,7 +978,6 @@ public class MessageList extends K9ListActivity implements OnItemClickListener { // 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(); @@ -910,27 +987,29 @@ public class MessageList extends K9ListActivity implements OnItemClickListener { 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); - } - - runOnUiThread(new Runnable() { - @Override - public void run() { - mAdapter.pruneDirtyMessages(); - mAdapter.notifyDataSetChanged(); - restoreListState(); + if (!mRemoteSearch) { + if (mFolderName != null) { + mController.listLocalMessagesSynchronous(mAccount, mFolderName, mAdapter.mListener); + } else if (mQueryString != null) { + mController.searchLocalMessagesSynchronous(mAccountUuids, mFolderNames, null, mQueryString, mIntegrate, mQueryFlags, mForbiddenFlags, mAdapter.mListener); } - }); + + runOnUiThread(new Runnable() { + @Override + public void run() { + mAdapter.pruneDirtyMessages(); + mAdapter.notifyDataSetChanged(); + restoreListState(); + } + }); + } } } .start(); } - if (mAccount != null && mFolderName != null) { + if (mAccount != null && mFolderName != null && !mRemoteSearch) { mController.getFolderUnreadMessageCount(mAccount, mFolderName, mAdapter.mListener); } @@ -1065,6 +1144,7 @@ public class MessageList extends K9ListActivity implements OnItemClickListener { } } + //Shortcuts that only work when a message is selected boolean retval = true; int position = mListView.getSelectedItemPosition(); try { @@ -1205,6 +1285,44 @@ public class MessageList extends K9ListActivity implements OnItemClickListener { AccountSettings.actionSettings(this, mAccount); } + @Override + public boolean onSearchRequested() { + + if (mAccount != null && mCurrentFolder != null && mAccount.allowRemoteSearch()) { + //if in a remote searchable folder, ask user what they want. + //TODO: Add ability to remember selection? + final CharSequence[] items = new CharSequence[2]; + items[0] = getString(R.string.search_mode_local_all); + items[1] = getString(R.string.search_mode_remote); + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.search_mode_title)); + builder.setItems(items, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int item) { + + Bundle appData = null; + if (item == 1) { + appData = new Bundle(); + appData.putString(EXTRA_SEARCH_ACCOUNT, mAccount.getUuid()); + appData.putString(EXTRA_SEARCH_FOLDER, mCurrentFolder.name); + appData.putBoolean(EXTRA_REMOTE_SEARCH, true); + } + //else do regular search, which doesn't require any special parameter setup + + startSearch(null, false, appData, false); + } + }); + AlertDialog alert = builder.create(); + alert.show(); + + return true; + } + + startSearch(null, false, null, false); + return true; + } + + private void changeSort(SortType sortType) { Boolean sortAscending = (mSortType == sortType) ? !mSortAscending : null; changeSort(sortType, sortAscending); @@ -1401,7 +1519,7 @@ public class MessageList extends K9ListActivity implements OnItemClickListener { } private void onToggleRead(final List holders) { - LocalMessage message; + Message message; Folder folder; Account account; String folderName; @@ -1422,7 +1540,7 @@ public class MessageList extends K9ListActivity implements OnItemClickListener { } private void onToggleFlag(final List holders) { - LocalMessage message; + Message message; Folder folder; Account account; String folderName; @@ -1548,7 +1666,7 @@ public class MessageList extends K9ListActivity implements OnItemClickListener { @Override public boolean onPrepareOptionsMenu(Menu menu) { - if (mQueryString != null) { + if (mQueryString != null || mIntegrate) { menu.findItem(R.id.expunge).setVisible(false); menu.findItem(R.id.check_mail).setVisible(false); menu.findItem(R.id.send_messages).setVisible(false); @@ -1582,8 +1700,65 @@ public class MessageList extends K9ListActivity implements OnItemClickListener { private final List mMessages = Collections.synchronizedList(new ArrayList()); + public List mExtraSearchResults; + private final ActivityListener mListener = new ActivityListener() { + // TODO achen - may need to add a setSupportProgress to mHandler to run on the UI thread. + + @Override + public void remoteSearchAddMessage(Account account, String folderName, Message message, final int numDone, final int numTotal) { + + if (numTotal > 0 && numDone < numTotal) { + setSupportProgress(Window.PROGRESS_END / numTotal * numDone); + } else { + setSupportProgress(Window.PROGRESS_END); + } + + addOrUpdateMessages(account, folderName, Collections.singletonList(message), false); + } + + @Override + public void remoteSearchFailed(Account acct, String folder, final String err) { + //TODO: Better error handling + runOnUiThread(new Runnable() { + public void run() { + Toast.makeText(getApplication(), 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); + } + setSupportProgress(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); + } + setSupportProgress(Window.PROGRESS_START); + } + @Override public void informUserOfStatus() { mHandler.refreshTitle(); @@ -2370,26 +2545,41 @@ public class MessageList extends K9ListActivity implements OnItemClickListener { } private void updateFooterView() { - FooterViewHolder holder = (FooterViewHolder) mFooterView.getTag(); - if (mCurrentFolder != null && mAccount != null) { if (mCurrentFolder.loading) { - holder.main.setText(getString(R.string.status_loading_more)); - holder.progress.setVisibility(ProgressBar.VISIBLE); + final boolean showProgress = true; + updateFooter(getString(R.string.status_loading_more), showProgress); } else { + String message; if (!mCurrentFolder.lastCheckFailed) { if (mAccount.getDisplayCount() == 0) { - holder.main.setText(getString(R.string.message_list_load_more_messages_action)); + message = getString(R.string.message_list_load_more_messages_action); } else { - holder.main.setText(String.format(getString(R.string.load_more_messages_fmt), mAccount.getDisplayCount())); + message = String.format(getString(R.string.load_more_messages_fmt), mAccount.getDisplayCount()); } } else { - holder.main.setText(getString(R.string.status_loading_more_failed)); + message = getString(R.string.status_loading_more_failed); } - holder.progress.setVisibility(ProgressBar.INVISIBLE); + final boolean showProgress = false; + updateFooter(message, showProgress); } } else { - holder.progress.setVisibility(ProgressBar.INVISIBLE); + final boolean showProgress = false; + updateFooter(null, showProgress); + } + } + + public void updateFooter(final String text, final boolean progressVisible) { + FooterViewHolder holder = (FooterViewHolder) mFooterView.getTag(); + + holder.progress.setVisibility(progressVisible ? ProgressBar.VISIBLE : ProgressBar.INVISIBLE); + if (text != null) { + holder.main.setText(text); + } + if (progressVisible || holder.main.getText().length() > 0) { + holder.main.setVisibility(View.VISIBLE); + } else { + holder.main.setVisibility(View.GONE); } } diff --git a/src/com/fsck/k9/activity/Search.java b/src/com/fsck/k9/activity/Search.java index 3f2a2a077..292ea9c70 100644 --- a/src/com/fsck/k9/activity/Search.java +++ b/src/com/fsck/k9/activity/Search.java @@ -1,7 +1,29 @@ package com.fsck.k9.activity; -import com.fsck.k9.activity.MessageList; public class Search extends MessageList { + protected static boolean isActive = false; + + public static boolean isActive() { + return isActive; + } + + public static void setActive(boolean val) { + isActive = val; + } + + @Override + public void onStart() { + setActive(true); + super.onStart(); + } + + @Override + public void onStop() { + setActive(false); + super.onStop(); + } + + } diff --git a/src/com/fsck/k9/activity/setup/AccountSettings.java b/src/com/fsck/k9/activity/setup/AccountSettings.java index d6ba87762..0ea74b7bc 100644 --- a/src/com/fsck/k9/activity/setup/AccountSettings.java +++ b/src/com/fsck/k9/activity/setup/AccountSettings.java @@ -2,20 +2,31 @@ package com.fsck.k9.activity.setup; import android.app.Dialog; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import android.app.AlertDialog; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Vibrator; -import android.preference.*; +import android.preference.CheckBoxPreference; +import android.preference.EditTextPreference; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceChangeListener; +import android.preference.PreferenceScreen; +import android.preference.RingtonePreference; import android.util.Log; - -import java.util.Iterator; -import java.util.Map; -import java.util.LinkedList; -import java.util.List; +import android.view.View; +import android.widget.CheckBox; import com.fsck.k9.Account; import com.fsck.k9.Account.FolderMode; @@ -24,18 +35,17 @@ import com.fsck.k9.K9; import com.fsck.k9.NotificationSetting; import com.fsck.k9.Preferences; import com.fsck.k9.R; -import com.fsck.k9.mail.Folder; import com.fsck.k9.activity.ChooseFolder; import com.fsck.k9.activity.ChooseIdentity; import com.fsck.k9.activity.ColorPickerDialog; import com.fsck.k9.activity.K9PreferenceActivity; import com.fsck.k9.activity.ManageIdentities; import com.fsck.k9.crypto.Apg; +import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.Store; -import com.fsck.k9.service.MailService; - -import com.fsck.k9.mail.store.StorageManager; import com.fsck.k9.mail.store.LocalStore.LocalFolder; +import com.fsck.k9.mail.store.StorageManager; +import com.fsck.k9.service.MailService; public class AccountSettings extends K9PreferenceActivity { @@ -52,6 +62,7 @@ public class AccountSettings extends K9PreferenceActivity { private static final String PREFERENCE_SCREEN_INCOMING = "incoming_prefs"; private static final String PREFERENCE_SCREEN_PUSH_ADVANCED = "push_advanced"; private static final String PREFERENCE_SCREEN_NOTIFICATIONS = "notifications"; + private static final String PREFERENCE_SCREEN_REMOTE_SEARCH = "remote_search"; private static final String PREFERENCE_DESCRIPTION = "account_description"; private static final String PREFERENCE_MARK_MESSAGE_AS_READ_ON_VIEW = "mark_message_as_read_on_view"; @@ -101,6 +112,10 @@ public class AccountSettings extends K9PreferenceActivity { private static final String PREFERENCE_CRYPTO_APP = "crypto_app"; private static final String PREFERENCE_CRYPTO_AUTO_SIGNATURE = "crypto_auto_signature"; private static final String PREFERENCE_CRYPTO_AUTO_ENCRYPT = "crypto_auto_encrypt"; + private static final String PREFERENCE_ALLOW_REMOTE_SEARCH = "account_allow_remote_search"; + private static final String PREFERENCE_REMOTE_SEARCH_NUM_RESULTS = "account_remote_search_num_results"; + private static final String PREFERENCE_REMOTE_SEARCH_FULL_TEXT = "account_remote_search_full_text"; + private static final String PREFERENCE_LOCAL_STORAGE_PROVIDER = "local_storage_provider"; private static final String PREFERENCE_CATEGORY_FOLDERS = "folders"; private static final String PREFERENCE_ARCHIVE_FOLDER = "archive_folder"; @@ -162,6 +177,9 @@ public class AccountSettings extends K9PreferenceActivity { private ListPreference mCryptoApp; private CheckBoxPreference mCryptoAutoSignature; private CheckBoxPreference mCryptoAutoEncrypt; + private CheckBoxPreference mAllowRemoteSearch; + private ListPreference mRemoteSearchNumResults; + private CheckBoxPreference mRemoteSearchFullText; private ListPreference mLocalStorageProvider; private ListPreference mArchiveFolder; private ListPreference mDraftsFolder; @@ -472,13 +490,29 @@ public class AccountSettings extends K9PreferenceActivity { }); } // IMAP-specific preferences + mAllowRemoteSearch = (CheckBoxPreference) findPreference(PREFERENCE_ALLOW_REMOTE_SEARCH); + mAllowRemoteSearch.setOnPreferenceChangeListener( + new OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference pref, Object newVal) { + if ((Boolean) newVal) { + showRemoteSearchHelp(); + } + return true; + } + }); + mRemoteSearchNumResults = (ListPreference) findPreference(PREFERENCE_REMOTE_SEARCH_NUM_RESULTS); + mRemoteSearchFullText = (CheckBoxPreference) findPreference(PREFERENCE_REMOTE_SEARCH_FULL_TEXT); mPushPollOnConnect = (CheckBoxPreference) findPreference(PREFERENCE_PUSH_POLL_ON_CONNECT); mIdleRefreshPeriod = (ListPreference) findPreference(PREFERENCE_IDLE_REFRESH_PERIOD); mMaxPushFolders = (ListPreference) findPreference(PREFERENCE_MAX_PUSH_FOLDERS); if (mIsPushCapable) { mPushPollOnConnect.setChecked(mAccount.isPushPollOnConnect()); + mAllowRemoteSearch.setChecked(mAccount.allowRemoteSearch()); + mRemoteSearchNumResults.setValue(Integer.toString(mAccount.getRemoteSearchNumResults())); + mRemoteSearchFullText.setChecked(mAccount.isRemoteSearchFullText()); + mIdleRefreshPeriod.setValue(String.valueOf(mAccount.getIdleRefreshMinutes())); mIdleRefreshPeriod.setSummary(mIdleRefreshPeriod.getEntry()); mIdleRefreshPeriod.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @@ -516,8 +550,11 @@ public class AccountSettings extends K9PreferenceActivity { }); } else { PreferenceScreen incomingPrefs = (PreferenceScreen) findPreference(PREFERENCE_SCREEN_INCOMING); - incomingPrefs.removePreference( (PreferenceScreen) findPreference(PREFERENCE_SCREEN_PUSH_ADVANCED)); - incomingPrefs.removePreference( (ListPreference) findPreference(PREFERENCE_PUSH_MODE)); + incomingPrefs.removePreference((PreferenceScreen) findPreference(PREFERENCE_SCREEN_PUSH_ADVANCED)); + incomingPrefs.removePreference((ListPreference) findPreference(PREFERENCE_PUSH_MODE)); + + ((PreferenceScreen) findPreference("main")).removePreference((PreferenceScreen) findPreference(PREFERENCE_SCREEN_REMOTE_SEARCH)); + } mAccountNotify = (CheckBoxPreference) findPreference(PREFERENCE_NOTIFY); @@ -677,6 +714,31 @@ public class AccountSettings extends K9PreferenceActivity { handleCryptoAppDependencies(); } + protected void showRemoteSearchHelp() { + final String noShowHelpPref = "account_settings_remote_search_hide_help"; + final SharedPreferences prefs = getPreferences(MODE_PRIVATE); + if (!prefs.getBoolean(noShowHelpPref, false)) { + AlertDialog.Builder adb = new AlertDialog.Builder(this); + final CheckBox noShowAgain = new CheckBox(this); + noShowAgain.setChecked(false); + noShowAgain.setText(R.string.no_show_again); + adb.setView(noShowAgain) + .setMessage(getString(R.string.account_settings_allow_remote_search_help)) + .setCancelable(false) + .setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + if(noShowAgain.isChecked()){ + Editor edit = prefs.edit(); + edit.putBoolean(noShowHelpPref, true); + edit.commit(); + } + } + }); + adb.create().show(); + } + + } + private void handleCryptoAppDependencies() { if ("".equals(mCryptoApp.getValue())) { mCryptoAutoSignature.setEnabled(false); @@ -745,11 +807,14 @@ public class AccountSettings extends K9PreferenceActivity { mAccount.setTrashFolderName(mTrashFolder.getValue()); } - + //IMAP stuff if (mIsPushCapable) { mAccount.setPushPollOnConnect(mPushPollOnConnect.isChecked()); mAccount.setIdleRefreshMinutes(Integer.parseInt(mIdleRefreshPeriod.getValue())); mAccount.setMaxPushFolders(Integer.parseInt(mMaxPushFolders.getValue())); + mAccount.setAllowRemoteSearch(mAllowRemoteSearch.isChecked()); + mAccount.setRemoteSearchNumResults(Integer.parseInt(mRemoteSearchNumResults.getValue())); + mAccount.setRemoteSearchFullText(mRemoteSearchFullText.isChecked()); } if (!mIsMoveCapable) { @@ -776,6 +841,7 @@ public class AccountSettings extends K9PreferenceActivity { mAccount.setShowPictures(Account.ShowPictures.valueOf(mAccountShowPictures.getValue())); + //IMAP specific stuff if (mIsPushCapable) { boolean needsPushRestart = mAccount.setFolderPushMode(Account.FolderMode.valueOf(mPushMode.getValue())); if (mAccount.getFolderPushMode() != FolderMode.NONE) { diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index 3cd767856..af1eb57e4 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -3,7 +3,19 @@ package com.fsck.k9.controller; import java.io.CharArrayWriter; import java.io.PrintWriter; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; @@ -31,11 +43,11 @@ import com.fsck.k9.Account; import com.fsck.k9.AccountStats; import com.fsck.k9.K9; import com.fsck.k9.K9.NotificationHideSubject; +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.K9.Intents; import com.fsck.k9.activity.FolderList; import com.fsck.k9.activity.MessageList; import com.fsck.k9.helper.NotificationBuilder; @@ -48,8 +60,8 @@ import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.Folder.FolderType; import com.fsck.k9.mail.Folder.OpenMode; -import com.fsck.k9.mail.Message.RecipientType; import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.Message.RecipientType; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Part; import com.fsck.k9.mail.PushReceiver; @@ -59,12 +71,12 @@ import com.fsck.k9.mail.Transport; import com.fsck.k9.mail.internet.MimeMessage; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.TextBody; -import com.fsck.k9.mail.store.UnavailableAccountException; import com.fsck.k9.mail.store.LocalStore; -import com.fsck.k9.mail.store.UnavailableStorageException; import com.fsck.k9.mail.store.LocalStore.LocalFolder; import com.fsck.k9.mail.store.LocalStore.LocalMessage; import com.fsck.k9.mail.store.LocalStore.PendingCommand; +import com.fsck.k9.mail.store.UnavailableAccountException; +import com.fsck.k9.mail.store.UnavailableStorageException; /** @@ -126,6 +138,28 @@ public class MessagingController implements Runnable { private static final String PENDING_COMMAND_MARK_ALL_AS_READ = "com.fsck.k9.MessagingController.markAllAsRead"; private static final String PENDING_COMMAND_EXPUNGE = "com.fsck.k9.MessagingController.expunge"; + public static class UidReverseComparator implements Comparator { + @Override + public int compare(Message o1, Message o2) { + if (o1 == null || o2 == null || o1.getUid() == null || o2.getUid() == null) { + return 0; + } + int id1, id2; + try { + id1 = Integer.parseInt(o1.getUid()); + id2 = Integer.parseInt(o2.getUid()); + } catch (NumberFormatException e) { + return 0; + } + //reversed intentionally. + if (id1 < id2) + return 1; + if (id1 > id2) + return -1; + return 0; + } + } + /** * Maximum number of unsynced messages to store at once */ @@ -516,7 +550,7 @@ public class MessagingController implements Runnable { l.listLocalMessagesStarted(account, folder); } - Folder localFolder = null; + LocalFolder localFolder = null; MessageRetrievalListener retrievalListener = new MessageRetrievalListener() { List pendingMessages = new ArrayList(); @@ -554,10 +588,13 @@ public class MessagingController implements Runnable { try { - Store localStore = account.getLocalStore(); + 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 @@ -750,6 +787,140 @@ public class MessagingController implements Runnable { listener.searchStats(stats); } } + + + + public void searchRemoteMessages(final String acctUuid, final String folderName, final String query, final Flag[] requiredFlags, final Flag[] forbiddenFlags, final MessagingListener listener) { + if (K9.DEBUG) { + String msg = "searchRemoteMessages (" + + "acct=" + acctUuid + + ", folderName = " + folderName + + ", query = " + query + + ")"; + Log.i(K9.LOG_TAG, msg); + } + + threadPool.execute(new Runnable() { + @Override + public void run() { + searchRemoteMessagesSynchronous(acctUuid, folderName, query, requiredFlags, forbiddenFlags, listener); + } + }); + } + public void searchRemoteMessagesSynchronous(final String acctUuid, final String folderName, final String query, + final Flag[] requiredFlags, final Flag[] forbiddenFlags, final MessagingListener listener) { + final Account acct = Preferences.getPreferences(mApplication.getApplicationContext()).getAccount(acctUuid); + + if (listener != null) { + listener.remoteSearchStarted(acct, folderName); + } + + List extraResults = new ArrayList(); + try { + Store remoteStore = acct.getRemoteStore(); + LocalStore localStore = acct.getLocalStore(); + + if (remoteStore == null || localStore == null) { + throw new MessagingException("Could not get store"); + } + + Folder remoteFolder = remoteStore.getFolder(folderName); + LocalFolder localFolder = localStore.getFolder(folderName); + if (remoteFolder == null || localFolder == null) { + throw new MessagingException("Folder not found"); + } + + if (listener != null) { + listener.remoteSearchStarted(acct, folderName); + } + List messages = remoteStore.searchRemoteMessages(query, folderName, 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()); + + + int resultLimit = acct.getRemoteSearchNumResults(); + if (resultLimit > 0 && messages.size() > resultLimit) { + extraResults = messages.subList(resultLimit, messages.size()); + messages = messages.subList(0, resultLimit); + } + + loadSearchResultsSynchronous(messages, localFolder, remoteFolder, listener); + + + } catch (Exception e) { + if (listener != null) { + listener.remoteSearchFailed(acct, null, e.getMessage()); + } + addErrorMessage(acct, null, e); + } finally { + if (listener != null) { + listener.remoteSearchFinished(acct, folderName, 0, extraResults); + } + } + + } + + public void loadSearchResults(final Account account, final String folderName, final List messages, final MessagingListener listener) { + threadPool.execute(new Runnable() { + @Override + public void run() { + try { + Store remoteStore = account.getRemoteStore(); + LocalStore localStore = account.getLocalStore(); + + if (remoteStore == null || localStore == null) { + throw new MessagingException("Could not get store"); + } + + Folder remoteFolder = remoteStore.getFolder(folderName); + LocalFolder localFolder = localStore.getFolder(folderName); + if (remoteFolder == null || localFolder == null) { + throw new MessagingException("Folder not found"); + } + + loadSearchResultsSynchronous(messages, localFolder, remoteFolder, listener); + } catch (MessagingException e) { + Log.e(K9.LOG_TAG, "Exception in loadSearchResults: " + e); + addErrorMessage(account, null, e); + } + } + }); + } + + public void loadSearchResultsSynchronous(List messages, LocalFolder localFolder, Folder remoteFolder, MessagingListener listener) throws MessagingException { + FetchProfile fp_header = new FetchProfile(); + fp_header.add(FetchProfile.Item.FLAGS); + fp_header.add(FetchProfile.Item.ENVELOPE); + FetchProfile fp_structure = new FetchProfile(); + fp_structure.add(FetchProfile.Item.STRUCTURE); + + int i = 0; + for (Message message : messages) { + i++; + LocalMessage localMsg = localFolder.getMessage(message.getUid()); + + if (localMsg == null) { + remoteFolder.fetch(new Message [] {message}, fp_header, null); + //fun fact: ImapFolder.fetch can't handle getting STRUCTURE at same time as headers + remoteFolder.fetch(new Message [] {message}, fp_structure, null); + localFolder.appendMessages(new Message [] {message}); + localMsg = localFolder.getMessage(message.getUid()); + } + + if (listener != null) { + listener.remoteSearchAddMessage(remoteFolder.getAccount(), remoteFolder.getName(), localMsg, i, messages.size()); + } + } + } + + public void loadMoreMessages(Account account, String folder, MessagingListener listener) { try { LocalStore localStore = account.getLocalStore(); @@ -1193,12 +1364,12 @@ public class MessagingController implements Runnable { * Reverse the order of the messages. Depending on the server this may get us * fetch results for newest to oldest. If not, no harm done. */ - Collections.reverse(unsyncedMessages); + Collections.sort(unsyncedMessages, new UidReverseComparator()); int visibleLimit = localFolder.getVisibleLimit(); int listSize = unsyncedMessages.size(); if ((visibleLimit > 0) && (listSize > visibleLimit)) { - unsyncedMessages = unsyncedMessages.subList(listSize - visibleLimit, listSize); + unsyncedMessages = unsyncedMessages.subList(0, visibleLimit); } FetchProfile fp = new FetchProfile(); @@ -1933,7 +2104,7 @@ public class MessagingController implements Runnable { LocalStore localStore = account.getLocalStore(); localFolder = localStore.getFolder(folder); - LocalMessage localMessage = (LocalMessage) localFolder.getMessage(uid); + LocalMessage localMessage = localFolder.getMessage(uid); if (localMessage == null) { return; @@ -2811,7 +2982,7 @@ public class MessagingController implements Runnable { LocalFolder localFolder = localStore.getFolder(folder); localFolder.open(OpenMode.READ_WRITE); - LocalMessage message = (LocalMessage)localFolder.getMessage(uid); + LocalMessage message = localFolder.getMessage(uid); if (message == null || message.getId() == 0) { throw new IllegalArgumentException("Message not found: folder=" + folder + ", uid=" + uid); diff --git a/src/com/fsck/k9/controller/MessagingListener.java b/src/com/fsck/k9/controller/MessagingListener.java index 49ddbf4da..5e1080a45 100644 --- a/src/com/fsck/k9/controller/MessagingListener.java +++ b/src/com/fsck/k9/controller/MessagingListener.java @@ -1,7 +1,10 @@ package com.fsck.k9.controller; +import java.util.List; + import android.content.Context; + import com.fsck.k9.Account; import com.fsck.k9.AccountStats; import com.fsck.k9.BaseAccount; @@ -9,8 +12,6 @@ import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.Part; -import java.util.List; - /** * Defines the interface that {@link MessagingController} will use to callback to requesters. * @@ -152,6 +153,50 @@ public class MessagingListener { public void pendingCommandsFinished(Account account) {} + /** + * Called when a remote search is started + * + * @param acct + * @param folder + */ + public void remoteSearchStarted(Account acct, String folder) {} + + + /** + * Called when server has responded to our query. Messages have not yet been downloaded. + * + * @param numResults + */ + public void remoteSearchServerQueryComplete(Account account, String folderName, int numResults) { } + + + /** + * Called when a new result message is available for a remote search + * Can assume headers have been downloaded, but potentially not body. + * @param account + * @param folder + * @param message + */ + public void remoteSearchAddMessage(Account account, String folder, Message message, int numDone, int numTotal) { } + + /** + * Called when Remote Search is fully complete + * + * @param acct + * @param folder + * @param numResults + */ + public void remoteSearchFinished(Account acct, String folder, int numResults, List extraResults) {} + + /** + * Called when there was a problem with a remote search operation. + * + * @param acct + * @param folder + * @param err + */ + public void remoteSearchFailed(Account acct, String folder, String err) { } + /** * General notification messages subclasses can override to be notified that the controller * has completed a command. This is useful for turning off progress indicators that may have diff --git a/src/com/fsck/k9/helper/MessageHelper.java b/src/com/fsck/k9/helper/MessageHelper.java index 15394c339..9c2abb491 100644 --- a/src/com/fsck/k9/helper/MessageHelper.java +++ b/src/com/fsck/k9/helper/MessageHelper.java @@ -17,7 +17,6 @@ import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Message.RecipientType; -import com.fsck.k9.mail.store.LocalStore.LocalMessage; import com.fsck.k9.helper.DateFormatter; public class MessageHelper { @@ -43,11 +42,10 @@ public class MessageHelper { mTodayDateFormat = android.text.format.DateFormat.getTimeFormat(mContext); } - public void populate(final MessageInfoHolder target, final Message m, + public void populate(final MessageInfoHolder target, final Message message, final FolderInfoHolder folder, final Account account) { final Contacts contactHelper = K9.showContactName() ? Contacts.getInstance(mContext) : null; try { - LocalMessage message = (LocalMessage) m; target.message = message; target.compareArrival = message.getInternalDate(); target.compareDate = message.getSentDate(); @@ -86,13 +84,16 @@ public class MessageHelper { target.uid = message.getUid(); target.account = account.getUuid(); - target.uri = "email://messages/" + account.getAccountNumber() + "/" + m.getFolder().getName() + "/" + m.getUid(); + target.uri = "email://messages/" + account.getAccountNumber() + "/" + message.getFolder().getName() + "/" + message.getUid(); } catch (MessagingException me) { Log.w(K9.LOG_TAG, "Unable to load message info", me); } } public String formatDate(Date date) { + if (date == null) { + return ""; + } if (Utility.isDateToday(date)) { return mTodayDateFormat.format(date); } else { diff --git a/src/com/fsck/k9/mail/Message.java b/src/com/fsck/k9/mail/Message.java index 75a080289..25ae90755 100644 --- a/src/com/fsck/k9/mail/Message.java +++ b/src/com/fsck/k9/mail/Message.java @@ -1,19 +1,18 @@ package com.fsck.k9.mail; +import java.io.IOException; import java.util.Date; import java.util.HashSet; import java.util.Set; -import java.io.IOException; import android.util.Log; +import com.fsck.k9.K9; import com.fsck.k9.activity.MessageReference; import com.fsck.k9.mail.filter.CountingOutputStream; import com.fsck.k9.mail.filter.EOLConvertingOutputStream; - import com.fsck.k9.mail.store.UnavailableStorageException; -import com.fsck.k9.K9; public abstract class Message implements Part, Body { @@ -144,6 +143,16 @@ 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 long getId(); + + public abstract String getPreview(); + public abstract boolean hasAttachments(); + + + public void delete(String trashFolderName) throws MessagingException {} /* diff --git a/src/com/fsck/k9/mail/Store.java b/src/com/fsck/k9/mail/Store.java index 5013a8f29..7de55d325 100644 --- a/src/com/fsck/k9/mail/Store.java +++ b/src/com/fsck/k9/mail/Store.java @@ -1,6 +1,9 @@ package com.fsck.k9.mail; +import java.util.HashMap; +import java.util.List; + import android.app.Application; import android.content.Context; @@ -8,11 +11,9 @@ import com.fsck.k9.Account; import com.fsck.k9.mail.store.ImapStore; import com.fsck.k9.mail.store.LocalStore; import com.fsck.k9.mail.store.Pop3Store; -import com.fsck.k9.mail.store.WebDavStore; import com.fsck.k9.mail.store.StorageManager.StorageProvider; - -import java.util.HashMap; -import java.util.List; +import com.fsck.k9.mail.store.UnavailableStorageException; +import com.fsck.k9.mail.store.WebDavStore; /** * Store is the access point for an email message store. It's location can be @@ -176,4 +177,9 @@ public abstract class Store { public Account getAccount() { return mAccount; } + + public List searchRemoteMessages(String queryString, + String folder, final Flag[] requiredFlags, final Flag[] forbiddenFlags) throws MessagingException { + throw new MessagingException("K-9 does not support remote searching on this account type"); + } } diff --git a/src/com/fsck/k9/mail/internet/MimeMessage.java b/src/com/fsck/k9/mail/internet/MimeMessage.java index d61d85f00..8538bc7e6 100644 --- a/src/com/fsck/k9/mail/internet/MimeMessage.java +++ b/src/com/fsck/k9/mail/internet/MimeMessage.java @@ -592,4 +592,28 @@ public class MimeMessage extends Message { copy(message); return message; } + + public boolean toMe() { + return false; + } + + public boolean ccMe() { + return false; + } + + public boolean bccMe() { + return false; + } + + public long getId() { + return Long.parseLong(mUid); //or maybe .mMessageId? + } + + public String getPreview() { + return ""; + } + + public boolean hasAttachments() { + return false; + } } diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java index 307d52e5b..0fcc18dc6 100644 --- a/src/com/fsck/k9/mail/store/ImapStore.java +++ b/src/com/fsck/k9/mail/store/ImapStore.java @@ -45,11 +45,15 @@ import java.util.StringTokenizer; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.TrustManager; +import org.apache.commons.io.IOUtils; + import android.content.Context; import android.net.ConnectivityManager; import android.net.NetworkInfo; @@ -73,13 +77,14 @@ import com.fsck.k9.mail.ConnectionSecurity; import com.fsck.k9.mail.FetchProfile; import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Folder; +import com.fsck.k9.mail.Folder.OpenMode; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Part; import com.fsck.k9.mail.PushReceiver; import com.fsck.k9.mail.Pusher; -import com.fsck.k9.mail.Store; import com.fsck.k9.mail.ServerSettings; +import com.fsck.k9.mail.Store; import com.fsck.k9.mail.filter.EOLConvertingOutputStream; import com.fsck.k9.mail.filter.FixedLengthInputStream; import com.fsck.k9.mail.filter.PeekableInputStream; @@ -94,9 +99,6 @@ import com.fsck.k9.mail.store.imap.ImapUtility; import com.fsck.k9.mail.transport.imap.ImapSettings; import com.jcraft.jzlib.JZlib; import com.jcraft.jzlib.ZOutputStream; -import java.util.zip.Inflater; -import java.util.zip.InflaterInputStream; -import org.apache.commons.io.IOUtils; /** *
@@ -1127,7 +1129,7 @@ public class ImapStore extends Store {
             }
 
             ImapFolder iFolder = (ImapFolder)folder;
-            checkOpen();
+            checkOpen(); //only need READ access
 
             String[] uids = new String[messages.length];
             for (int i = 0, count = messages.length; i < count; i++) {
@@ -1253,7 +1255,7 @@ public class ImapStore extends Store {
 
 
         private int getRemoteMessageCount(String criteria) throws MessagingException {
-            checkOpen();
+            checkOpen(); //only need READ access
             try {
                 int count = 0;
                 int start = 1;
@@ -1289,7 +1291,7 @@ public class ImapStore extends Store {
                         return executeSimpleCommand("UID SEARCH *:*");
                     }
                 };
-                Message[] messages = search(searcher, null);
+                Message[] messages = search(searcher, null).toArray(EMPTY_MESSAGE_ARRAY);
                 if (messages.length > 0) {
                     return Long.parseLong(messages[0].getUid());
                 }
@@ -1338,7 +1340,7 @@ public class ImapStore extends Store {
                     return executeSimpleCommand(String.format("UID SEARCH %d:%d%s%s", start, end, dateSearchString, includeDeleted ? "" : " NOT DELETED"));
                 }
             };
-            return search(searcher, listener);
+            return search(searcher, listener).toArray(EMPTY_MESSAGE_ARRAY);
 
         }
         protected Message[] getMessages(final List mesgSeqs, final boolean includeDeleted, final MessageRetrievalListener listener)
@@ -1348,7 +1350,7 @@ public class ImapStore extends Store {
                     return executeSimpleCommand(String.format("UID SEARCH %s%s", Utility.combine(mesgSeqs.toArray(), ','), includeDeleted ? "" : " NOT DELETED"));
                 }
             };
-            return search(searcher, listener);
+            return search(searcher, listener).toArray(EMPTY_MESSAGE_ARRAY);
         }
 
         protected Message[] getMessagesFromUids(final List mesgUids, final boolean includeDeleted, final MessageRetrievalListener listener)
@@ -1358,12 +1360,12 @@ public class ImapStore extends Store {
                     return executeSimpleCommand(String.format("UID SEARCH UID %s%s", Utility.combine(mesgUids.toArray(), ','), includeDeleted ? "" : " NOT DELETED"));
                 }
             };
-            return search(searcher, listener);
+            return search(searcher, listener).toArray(EMPTY_MESSAGE_ARRAY);
         }
 
-        private Message[] search(ImapSearcher searcher, MessageRetrievalListener listener) throws MessagingException {
+        private List search(ImapSearcher searcher, MessageRetrievalListener listener) throws MessagingException {
 
-            checkOpen();
+            checkOpen(); //only need READ access
             ArrayList messages = new ArrayList();
             try {
                 ArrayList uids = new ArrayList();
@@ -1378,8 +1380,12 @@ public class ImapStore extends Store {
                     }
                 }
 
-                // Sort the uids in numerically ascending order
-                Collections.sort(uids);
+                // Sort the uids in numerically decreasing order
+                // By doing it in decreasing order, we ensure newest messages are dealt with first
+                // This makes the most sense when a limit is imposed, and also prevents UI from going
+                // crazy adding stuff at the top.
+                Collections.sort(uids, Collections.reverseOrder());
+
                 for (int i = 0, count = uids.size(); i < count; i++) {
                     String uid = uids.get(i).toString();
                     if (listener != null) {
@@ -1394,7 +1400,7 @@ public class ImapStore extends Store {
             } catch (IOException ioe) {
                 throw ioExceptionHandler(mConnection, ioe);
             }
-            return messages.toArray(EMPTY_MESSAGE_ARRAY);
+            return messages;
         }
 
 
@@ -1406,7 +1412,7 @@ public class ImapStore extends Store {
         @Override
         public Message[] getMessages(String[] uids, MessageRetrievalListener listener)
         throws MessagingException {
-            checkOpen();
+            checkOpen(); //only need READ access
             ArrayList messages = new ArrayList();
             try {
                 if (uids == null) {
@@ -1443,7 +1449,7 @@ public class ImapStore extends Store {
             if (messages == null || messages.length == 0) {
                 return;
             }
-            checkOpen();
+            checkOpen(); //only need READ access
             List uids = new ArrayList(messages.length);
             HashMap messageMap = new HashMap();
             for (int i = 0, count = messages.length; i < count; i++) {
@@ -1568,7 +1574,7 @@ public class ImapStore extends Store {
         @Override
         public void fetchPart(Message message, Part part, MessageRetrievalListener listener)
         throws MessagingException {
-            checkOpen();
+            checkOpen(); //only need READ access
 
             String[] parts = part.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA);
             if (parts == null) {
@@ -1971,6 +1977,7 @@ public class ImapStore extends Store {
          */
         @Override
         public Map appendMessages(Message[] messages) throws MessagingException {
+            open(OpenMode.READ_WRITE);
             checkOpen();
             try {
                 Map uidMap = new HashMap();
@@ -2083,6 +2090,7 @@ public class ImapStore extends Store {
 
         @Override
         public void expunge() throws MessagingException {
+            open(OpenMode.READ_WRITE);
             checkOpen();
             try {
                 executeSimpleCommand("EXPUNGE");
@@ -2115,6 +2123,7 @@ public class ImapStore extends Store {
         @Override
         public void setFlags(Flag[] flags, boolean value)
         throws MessagingException {
+            open(OpenMode.READ_WRITE);
             checkOpen();
 
 
@@ -2149,6 +2158,7 @@ public class ImapStore extends Store {
         @Override
         public void setFlags(Message[] messages, Flag[] flags, boolean value)
         throws MessagingException {
+            open(OpenMode.READ_WRITE);
             checkOpen();
             String[] uids = new String[messages.length];
             for (int i = 0, count = messages.length; i < count; i++) {
@@ -3415,4 +3425,105 @@ public class ImapStore extends Store {
         }
     }
 
+    @Override
+    public List searchRemoteMessages(final String queryString,
+            final String folderName,  final Flag[] requiredFlags, final Flag[] forbiddenFlags) throws MessagingException {
+
+        if (!mAccount.allowRemoteSearch()) {
+            throw new MessagingException("Your settings do not allow remote searching of this account");
+        }
+
+        final ImapFolder folder = (ImapFolder) getFolder(folderName);
+        if (folder == null) {
+            throw new MessagingException("Invalid folder specified");
+        }
+
+        try {
+            folder.open(OpenMode.READ_ONLY);
+            folder.checkOpen();
+
+
+            ImapSearcher searcher = new ImapSearcher() {
+                public List search() throws IOException, MessagingException {
+                    String imapQuery = "UID SEARCH ";
+                    if (requiredFlags != null) {
+                        for (Flag f : requiredFlags) {
+                            switch (f) {
+                            case DELETED:
+                                imapQuery += "DELETED ";
+                                break;
+
+                            case SEEN:
+                                imapQuery +=  "SEEN ";
+                                break;
+
+                            case ANSWERED:
+                                imapQuery += "ANSWERED ";
+                                break;
+
+                            case FLAGGED:
+                                imapQuery += "FLAGGED ";
+                                break;
+
+                            case DRAFT:
+                                imapQuery += "DRAFT ";
+                                break;
+
+                            case RECENT:
+                                imapQuery += "RECENT ";
+                                break;
+                            }
+                        }
+                    }
+                    if (forbiddenFlags != null) {
+                        for (Flag f : forbiddenFlags) {
+                            switch (f) {
+                            case DELETED:
+                                imapQuery += "UNDELETED ";
+                                break;
+
+                            case SEEN:
+                                imapQuery +=  "UNSEEN ";
+                                break;
+
+                            case ANSWERED:
+                                imapQuery += "UNANSWERED ";
+                                break;
+
+                            case FLAGGED:
+                                imapQuery += "UNFLAGGED ";
+                                break;
+
+                            case DRAFT:
+                                imapQuery += "UNDRAFT ";
+                                break;
+
+                            case RECENT:
+                                imapQuery += "UNRECENT ";
+                                break;
+                            }
+                        }
+                    }
+                    String encodedQry = encodeString(queryString);
+                    if (mAccount.isRemoteSearchFullText()) {
+                        imapQuery += "TEXT " + encodedQry;
+                    } else {
+                        imapQuery += "OR SUBJECT " + encodedQry + " FROM " + encodedQry;
+                    }
+                    return folder.executeSimpleCommand(imapQuery);
+                }
+            };
+
+            //don't pass listener--we don't want to add messages until we've downloaded them
+            return folder.search(searcher, null);
+
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new MessagingException("Error during search: " + e.toString());
+        }
+
+    }
+
+
+
 }
diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java
index 5d56aa24d..b055dbfda 100644
--- a/src/com/fsck/k9/mail/store/LocalStore.java
+++ b/src/com/fsck/k9/mail/store/LocalStore.java
@@ -1,7 +1,15 @@
 
 package com.fsck.k9.mail.store;
 
-import java.io.*;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Serializable;
+import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -17,7 +25,6 @@ import java.util.Set;
 import java.util.UUID;
 import java.util.regex.Pattern;
 
-import com.fsck.k9.helper.HtmlConverter;
 import org.apache.commons.io.IOUtils;
 
 import android.app.Application;
@@ -36,8 +43,10 @@ import com.fsck.k9.K9;
 import com.fsck.k9.Preferences;
 import com.fsck.k9.R;
 import com.fsck.k9.Account.MessageFormat;
+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.Utility;
 import com.fsck.k9.mail.Address;
 import com.fsck.k9.mail.Body;
@@ -1127,9 +1136,15 @@ public class LocalStore extends Store implements Serializable {
 
         @Override
         public void open(final OpenMode mode) throws MessagingException {
-            if (isOpen()) {
+
+            if (isOpen() && (getMode() == mode || mode == OpenMode.READ_ONLY)) {
                 return;
+            } else if (isOpen()) {
+                //previously opened in READ_ONLY and now requesting READ_WRITE
+                //so close connection and reopen
+                close();
             }
+
             try {
                 database.execute(false, new DbCallback() {
                     @Override
@@ -1336,17 +1351,19 @@ public class LocalStore extends Store implements Serializable {
         }
 
         public void purgeToVisibleLimit(MessageRemovalListener listener) throws MessagingException {
-            if (mVisibleLimit == 0) {
-                return ;
-            }
-            open(OpenMode.READ_WRITE);
-            Message[] messages = getMessages(null, false);
-            for (int i = mVisibleLimit; i < messages.length; i++) {
-                if (listener != null) {
-                    listener.messageRemoved(messages[i]);
+            //don't purge messages while a Search is active since it might throw away search results
+            if (!Search.isActive()) {
+                if (mVisibleLimit == 0) {
+                    return ;
+                }
+                open(OpenMode.READ_WRITE);
+                Message[] messages = getMessages(null, false);
+                for (int i = mVisibleLimit; i < messages.length; i++) {
+                    if (listener != null) {
+                        listener.messageRemoved(messages[i]);
+                    }
+                    messages[i].destroy();
                 }
-                messages[i].destroy();
-
             }
         }
 
@@ -1831,11 +1848,11 @@ public class LocalStore extends Store implements Serializable {
         }
 
         @Override
-        public Message getMessage(final String uid) throws MessagingException {
+        public LocalMessage getMessage(final String uid) throws MessagingException {
             try {
-                return database.execute(false, new DbCallback() {
+                return database.execute(false, new DbCallback() {
                     @Override
-                    public Message doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
+                    public LocalMessage doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
                         try {
                             open(OpenMode.READ_WRITE);
                             LocalMessage message = new LocalMessage(uid, LocalFolder.this);
@@ -2110,7 +2127,7 @@ public class LocalStore extends Store implements Serializable {
                                     /*
                                      * Replace an existing message in the database
                                      */
-                                    LocalMessage oldMessage = (LocalMessage) getMessage(uid);
+                                    LocalMessage oldMessage = getMessage(uid);
 
                                     if (oldMessage != null) {
                                         oldMessageId = oldMessage.getId();