Merge branch 'imapsearch'

* imapsearch:
  Change settings version to 18 to match what's currrently on master.
  Handle aborted imap searches by nuking in-progress connections.
  Move IMAP search into the Folder level.
  Remove duplicate notification on remote search start.
  Rename variables
  changed PREFERENCE_CLOUD_SEARCH_ENABLED from "cloud_search_enabled" to "remote_search_enabled" in activity/setup/AccountSettings.java to resolve FC.
  Add cloud search icon to local search result screen. Implement pull-to-remote-search.
  Log remote search exceptions in addition to toasting them.
  Add settings export for remote search settings.
  Whitespace; no functional changes.
  Handle implicit vs. explicit searches in ActionBar home button behavior.
  Whitespace fix; no functional changes.
  Add remote search actionbar icons.
  IMAP Search: log exceptions on remote search, properly dispatch MessageList changes.
  modified loadMessageForView() to dowload message if neither X_DOWNLOADED_FULL nor X_DOWNLOADED_PARTIAL.
  Add remote IMAP search support.

Conflicts:
	res/menu/message_list_option.xml
	res/values/attrs.xml
	res/values/themes.xml
	src/com/fsck/k9/activity/MessageList.java
	src/com/fsck/k9/preferences/Settings.java
This commit is contained in:
Andrew Chen 2012-09-25 16:01:11 -07:00
commit 39e2a973a1
32 changed files with 5693 additions and 214 deletions

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 801 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 963 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -39,4 +39,15 @@
android:textColor="?android:attr/textColorTertiary" android:textColor="?android:attr/textColorTertiary"
android:textSize="36sp" /> android:textSize="36sp" />
<ImageButton
android:id="@+id/actionbar_remote_search"
android:gravity="center_vertical"
android:focusable="false"
android:layout_marginRight="3dip"
android:background="?attr/iconActionRemoteSearch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/action_remote_search"
android:visibility="gone"/>
</LinearLayout> </LinearLayout>

View file

@ -1,6 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" > <menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/search_remote"
android:icon="?attr/iconActionRemoteSearch"
android:showAsAction="always"
android:title="@string/action_remote_search"
android:visible="false"
/>
<item <item
android:id="@+id/check_mail" android:id="@+id/check_mail"
android:alphabeticShortcut="r" android:alphabeticShortcut="r"

View file

@ -668,6 +668,26 @@
<item>HTML</item> <item>HTML</item>
<item>AUTO</item> <item>AUTO</item>
</string-array> </string-array>
<string-array name="account_settings_remote_search_num_results_entries">
<item >@string/account_settings_remote_search_num_results_entries_10</item>
<item>@string/account_settings_remote_search_num_results_entries_25</item>
<item>@string/account_settings_remote_search_num_results_entries_50</item>
<item>@string/account_settings_remote_search_num_results_entries_100</item>
<item>@string/account_settings_remote_search_num_results_entries_250</item>
<item>@string/account_settings_remote_search_num_results_entries_500</item>
<item>@string/account_settings_remote_search_num_results_entries_1000</item>
<item>@string/account_settings_remote_search_num_results_entries_all</item>
</string-array>
<string-array name="account_settings_remote_search_num_results_values">
<item>10</item>
<item>25</item>
<item>50</item>
<item>100</item>
<item>250</item>
<item>500</item>
<item>1000</item>
<item>0</item>
</string-array>
<string-array name="global_settings_notification_hide_subject_entries"> <string-array name="global_settings_notification_hide_subject_entries">
<item name="1">@string/global_settings_notification_hide_subject_never</item> <item name="1">@string/global_settings_notification_hide_subject_never</item>

View file

@ -25,6 +25,7 @@
<attr name="iconActionUnflag" format="reference" /> <attr name="iconActionUnflag" format="reference" />
<attr name="iconActionMarkAsRead" format="reference" /> <attr name="iconActionMarkAsRead" format="reference" />
<attr name="iconActionMarkAsUnread" format="reference" /> <attr name="iconActionMarkAsUnread" format="reference" />
<attr name="iconActionRemoteSearch" format="reference" />
<attr name="iconMenuAdd" format="reference" /> <attr name="iconMenuAdd" format="reference" />
<attr name="iconMenuAttachment" format="reference" /> <attr name="iconMenuAttachment" format="reference" />
<attr name="iconMenuClear" format="reference" /> <attr name="iconMenuClear" format="reference" />

View file

@ -1122,4 +1122,28 @@ http://k9mail.googlecode.com/
<string name="image_saved_as">Saved image as \"<xliff:g id="filename">%s</xliff:g>\"</string> <string name="image_saved_as">Saved image as \"<xliff:g id="filename">%s</xliff:g>\"</string>
<string name="image_saving_failed">Saving the image failed.</string> <string name="image_saving_failed">Saving the image failed.</string>
<string name="account_settings_remote_search_num_results_entries_all">All</string>
<string name="account_settings_remote_search_num_results_entries_10">10</string>
<string name="account_settings_remote_search_num_results_entries_25">25</string>
<string name="account_settings_remote_search_num_results_entries_50">50</string>
<string name="account_settings_remote_search_num_results_entries_100">100</string>
<string name="account_settings_remote_search_num_results_entries_250">250</string>
<string name="account_settings_remote_search_num_results_entries_500">500</string>
<string name="account_settings_remote_search_num_results_entries_1000">1000</string>
<string name="account_settings_remote_search_num_label">Server search result limit</string>
<string name="account_settings_remote_search_num_summary">Search will stop after <xliff:g id="num_results">%s</xliff:g> results have been found.</string>
<string name="account_settings_remote_search">Remote folder searching</string>
<string name="account_settings_remote_search_full_text">Include body text in server search</string>
<string name="account_settings_remote_search_full_text_summary">Full text searches can be slow.</string>
<string name="remote_search_sending_query">Sending query to server</string>
<string name="remote_search_downloading">Fetching %d results</string>
<string name="remote_search_downloading_limited">Fetching %1$d of %2$d results</string>
<string name="account_settings_search">Search</string>
<string name="account_settings_remote_search_enabled">Enable server search</string>
<string name="account_settings_remote_search_enabled_summary">Search messages on the server in addition to those on your device</string>
<string name="action_remote_search">Search messages on server</string>
<string name="pull_to_refresh_remote_search_from_local_search_pull">Pull to search server…</string>
<string name="pull_to_refresh_remote_search_from_local_search_release">Release to search server…</string>
</resources> </resources>

View file

@ -25,6 +25,7 @@
<item name="iconActionUnflag">@drawable/ic_action_unflag_light</item> <item name="iconActionUnflag">@drawable/ic_action_unflag_light</item>
<item name="iconActionMarkAsRead">@drawable/ic_action_mark_as_read_light</item> <item name="iconActionMarkAsRead">@drawable/ic_action_mark_as_read_light</item>
<item name="iconActionMarkAsUnread">@drawable/ic_action_mark_as_unread_light</item> <item name="iconActionMarkAsUnread">@drawable/ic_action_mark_as_unread_light</item>
<item name="iconActionRemoteSearch">@drawable/ic_action_remote_search_light</item>
<item name="iconMenuAdd">@drawable/ic_menu_add</item> <item name="iconMenuAdd">@drawable/ic_menu_add</item>
<item name="iconMenuAttachment">@drawable/ic_menu_attachment</item> <item name="iconMenuAttachment">@drawable/ic_menu_attachment</item>
<item name="iconMenuClear">@drawable/ic_menu_clear</item> <item name="iconMenuClear">@drawable/ic_menu_clear</item>
@ -67,6 +68,7 @@
<item name="iconActionUnflag">@drawable/ic_action_unflag_dark</item> <item name="iconActionUnflag">@drawable/ic_action_unflag_dark</item>
<item name="iconActionMarkAsRead">@drawable/ic_action_mark_as_read_dark</item> <item name="iconActionMarkAsRead">@drawable/ic_action_mark_as_read_dark</item>
<item name="iconActionMarkAsUnread">@drawable/ic_action_mark_as_unread_dark</item> <item name="iconActionMarkAsUnread">@drawable/ic_action_mark_as_unread_dark</item>
<item name="iconActionRemoteSearch">@drawable/ic_action_remote_search_dark</item>
<item name="iconMenuAdd">@drawable/ic_menu_add</item> <item name="iconMenuAdd">@drawable/ic_menu_add</item>
<item name="iconMenuAttachment">@drawable/ic_menu_attachment</item> <item name="iconMenuAttachment">@drawable/ic_menu_attachment</item>
<item name="iconMenuClear">@drawable/ic_menu_clear</item> <item name="iconMenuClear">@drawable/ic_menu_clear</item>

View file

@ -23,7 +23,9 @@
can be displayed after the device has been rotated. can be displayed after the device has been rotated.
--> -->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
android:key="main">
<PreferenceScreen <PreferenceScreen
android:title="@string/account_settings_general_title" android:title="@string/account_settings_general_title"
@ -460,6 +462,30 @@
</PreferenceScreen> </PreferenceScreen>
<PreferenceScreen android:title="@string/account_settings_search" android:key="search">
<CheckBoxPreference
android:key="remote_search_enabled"
android:title="@string/account_settings_remote_search_enabled"
android:summary="@string/account_settings_remote_search_enabled_summary"
android:persistent="false"/>
<ListPreference
android:entries="@array/account_settings_remote_search_num_results_entries"
android:entryValues="@array/account_settings_remote_search_num_results_values"
android:key="account_remote_search_num_results"
android:title="@string/account_settings_remote_search_num_label"
android:dialogTitle="@string/account_settings_remote_search_num_label"
android:dependency="remote_search_enabled"/>
<CheckBoxPreference
android:key="account_remote_search_full_text"
android:title="@string/account_settings_remote_search_full_text"
android:summary="@string/account_settings_remote_search_full_text_summary"
android:persistent="false"
android:dependency="remote_search_enabled"/>
</PreferenceScreen>
<PreferenceScreen <PreferenceScreen
android:title="@string/account_settings_crypto" android:title="@string/account_settings_crypto"
android:key="crypto"> android:key="crypto">

View file

@ -1,6 +1,18 @@
package com.fsck.k9; 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.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.net.ConnectivityManager; 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_QUOTED_TEXT_SHOWN = true;
public static final boolean DEFAULT_REPLY_AFTER_QUOTE = false; public static final boolean DEFAULT_REPLY_AFTER_QUOTE = false;
public static final boolean DEFAULT_STRIP_SIGNATURE = true; 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 ACCOUNT_DESCRIPTION_KEY = "description";
public static final String STORE_URI_KEY = "storeUri"; public static final String STORE_URI_KEY = "storeUri";
@ -186,6 +199,9 @@ public class Account implements BaseAccount {
private boolean mCryptoAutoEncrypt; private boolean mCryptoAutoEncrypt;
private boolean mMarkMessageAsReadOnView; private boolean mMarkMessageAsReadOnView;
private boolean mAlwaysShowCcBcc; private boolean mAlwaysShowCcBcc;
private boolean mAllowRemoteSearch;
private boolean mRemoteSearchFullText;
private int mRemoteSearchNumResults;
private CryptoProvider mCryptoProvider = null; private CryptoProvider mCryptoProvider = null;
@ -289,6 +305,9 @@ public class Account implements BaseAccount {
mCryptoApp = Apg.NAME; mCryptoApp = Apg.NAME;
mCryptoAutoSignature = false; mCryptoAutoSignature = false;
mCryptoAutoEncrypt = false; mCryptoAutoEncrypt = false;
mAllowRemoteSearch = false;
mRemoteSearchFullText = false;
mRemoteSearchNumResults = DEFAULT_REMOTE_SEARCH_NUM_RESULTS;
mEnabled = true; mEnabled = true;
mMarkMessageAsReadOnView = true; mMarkMessageAsReadOnView = true;
mAlwaysShowCcBcc = false; mAlwaysShowCcBcc = false;
@ -456,6 +475,10 @@ public class Account implements BaseAccount {
mCryptoApp = prefs.getString(mUuid + ".cryptoApp", Apg.NAME); mCryptoApp = prefs.getString(mUuid + ".cryptoApp", Apg.NAME);
mCryptoAutoSignature = prefs.getBoolean(mUuid + ".cryptoAutoSignature", false); mCryptoAutoSignature = prefs.getBoolean(mUuid + ".cryptoAutoSignature", false);
mCryptoAutoEncrypt = prefs.getBoolean(mUuid + ".cryptoAutoEncrypt", 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); mEnabled = prefs.getBoolean(mUuid + ".enabled", true);
mMarkMessageAsReadOnView = prefs.getBoolean(mUuid + ".markMessageAsReadOnView", true); mMarkMessageAsReadOnView = prefs.getBoolean(mUuid + ".markMessageAsReadOnView", true);
mAlwaysShowCcBcc = prefs.getBoolean(mUuid + ".alwaysShowCcBcc", false); mAlwaysShowCcBcc = prefs.getBoolean(mUuid + ".alwaysShowCcBcc", false);
@ -706,6 +729,9 @@ public class Account implements BaseAccount {
editor.putString(mUuid + ".cryptoApp", mCryptoApp); editor.putString(mUuid + ".cryptoApp", mCryptoApp);
editor.putBoolean(mUuid + ".cryptoAutoSignature", mCryptoAutoSignature); editor.putBoolean(mUuid + ".cryptoAutoSignature", mCryptoAutoSignature);
editor.putBoolean(mUuid + ".cryptoAutoEncrypt", mCryptoAutoEncrypt); 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 + ".enabled", mEnabled);
editor.putBoolean(mUuid + ".markMessageAsReadOnView", mMarkMessageAsReadOnView); editor.putBoolean(mUuid + ".markMessageAsReadOnView", mMarkMessageAsReadOnView);
editor.putBoolean(mUuid + ".alwaysShowCcBcc", mAlwaysShowCcBcc); editor.putBoolean(mUuid + ".alwaysShowCcBcc", mAlwaysShowCcBcc);
@ -1623,6 +1649,22 @@ public class Account implements BaseAccount {
mCryptoAutoEncrypt = cryptoAutoEncrypt; 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() { public String getInboxFolderName() {
return mInboxFolderName; return mInboxFolderName;
} }
@ -1693,4 +1735,13 @@ public class Account implements BaseAccount {
public synchronized void setAlwaysShowCcBcc(boolean show) { public synchronized void setAlwaysShowCcBcc(boolean show) {
mAlwaysShowCcBcc = show; mAlwaysShowCcBcc = show;
} }
public boolean isRemoteSearchFullText() {
return mRemoteSearchFullText;
}
public void setRemoteSearchFullText(boolean val) {
mRemoteSearchFullText = val;
}
} }

View file

@ -2,7 +2,7 @@ package com.fsck.k9.activity;
import java.util.Date; import java.util.Date;
import com.fsck.k9.helper.MessageHelper; import com.fsck.k9.helper.MessageHelper;
import com.fsck.k9.mail.store.LocalStore.LocalMessage; import com.fsck.k9.mail.Message;
public class MessageInfoHolder { public class MessageInfoHolder {
public String date; public String date;
@ -19,7 +19,7 @@ public class MessageInfoHolder {
public boolean forwarded; public boolean forwarded;
public boolean flagged; public boolean flagged;
public boolean dirty; public boolean dirty;
public LocalMessage message; public Message message;
public FolderInfoHolder folder; public FolderInfoHolder folder;
public boolean selected; public boolean selected;
public String account; public String account;

View file

@ -7,9 +7,12 @@ import java.util.EnumMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Future;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.Dialog; import android.app.Dialog;
import android.app.SearchManager;
import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences.Editor; import android.content.SharedPreferences.Editor;
@ -25,15 +28,9 @@ import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan; import android.text.style.StyleSpan;
import android.util.Log; import android.util.Log;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.ContextMenu; import android.view.*;
import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextMenu.ContextMenuInfo;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemClickListener;
@ -75,9 +72,10 @@ import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.Folder;
import com.fsck.k9.mail.Message; import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.Store;
import com.fsck.k9.mail.store.ImapStore;
import com.fsck.k9.mail.store.LocalStore; import com.fsck.k9.mail.store.LocalStore;
import com.fsck.k9.mail.store.LocalStore.LocalFolder; 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.fsck.k9.mail.store.StorageManager;
import com.handmark.pulltorefresh.library.PullToRefreshBase; import com.handmark.pulltorefresh.library.PullToRefreshBase;
import com.handmark.pulltorefresh.library.PullToRefreshListView; import com.handmark.pulltorefresh.library.PullToRefreshListView;
@ -177,7 +175,13 @@ public class MessageList extends K9ListActivity implements OnItemClickListener,
@Override @Override
public int compare(MessageInfoHolder object1, MessageInfoHolder object2) { 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());
}
} }
} }
@ -186,7 +190,13 @@ public class MessageList extends K9ListActivity implements OnItemClickListener,
@Override @Override
public int compare(MessageInfoHolder object1, MessageInfoHolder object2) { 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);
}
} }
} }
@ -227,7 +237,9 @@ public class MessageList extends K9ListActivity implements OnItemClickListener,
private static final String EXTRA_ACCOUNT = "account"; private static final String EXTRA_ACCOUNT = "account";
private static final String EXTRA_FOLDER = "folder"; 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_QUERY_FLAGS = "queryFlags";
private static final String EXTRA_FORBIDDEN_FLAGS = "forbiddenFlags"; private static final String EXTRA_FORBIDDEN_FLAGS = "forbiddenFlags";
private static final String EXTRA_INTEGRATE = "integrate"; private static final String EXTRA_INTEGRATE = "integrate";
@ -287,6 +299,10 @@ public class MessageList extends K9ListActivity implements OnItemClickListener,
private String mQueryString; private String mQueryString;
private Flag[] mQueryFlags = null; private Flag[] mQueryFlags = null;
private Flag[] mForbiddenFlags = null; private Flag[] mForbiddenFlags = null;
private boolean mRemoteSearch = false;
private String mSearchAccount = null;
private String mSearchFolder = null;
private Future mRemoteSearchFuture = null;
private boolean mIntegrate = false; private boolean mIntegrate = false;
private String[] mAccountUuids = null; private String[] mAccountUuids = null;
private String[] mFolderNames = null; private String[] mFolderNames = null;
@ -365,7 +381,6 @@ public class MessageList extends K9ListActivity implements OnItemClickListener,
private static final int ACTION_REFRESH_TITLE = 5; private static final int ACTION_REFRESH_TITLE = 5;
private static final int ACTION_PROGRESS = 6; private static final int ACTION_PROGRESS = 6;
public void removeMessage(MessageReference messageReference) { public void removeMessage(MessageReference messageReference) {
android.os.Message msg = android.os.Message.obtain(this, ACTION_REMOVE_MESSAGE, android.os.Message msg = android.os.Message.obtain(this, ACTION_REMOVE_MESSAGE,
messageReference); messageReference);
@ -394,6 +409,15 @@ public class MessageList extends K9ListActivity implements OnItemClickListener,
sendMessage(msg); 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) { public void changeMessageUid(final MessageReference ref, final String newUid) {
// Instead of explicitly creating a container to be able to pass both arguments in a // Instead of explicitly creating a container to be able to pass both arguments in a
// Message we post a Runnable to the message queue. // Message we post a Runnable to the message queue.
@ -483,10 +507,7 @@ public class MessageList extends K9ListActivity implements OnItemClickListener,
} }
} }
// build the comparator chain return new ComparatorChain<MessageInfoHolder>(chain);
final Comparator<MessageInfoHolder> chainComparator = new ComparatorChain<MessageInfoHolder>(chain);
return chainComparator;
} }
private void folderLoading(String folder, boolean loading) { private void folderLoading(String folder, boolean loading) {
@ -498,7 +519,9 @@ public class MessageList extends K9ListActivity implements OnItemClickListener,
private void refreshTitle() { private void refreshTitle() {
setWindowTitle(); setWindowTitle();
setWindowProgress(); if (!mRemoteSearch) {
setWindowProgress();
}
} }
private void setWindowProgress() { private void setWindowProgress() {
@ -599,7 +622,7 @@ public class MessageList extends K9ListActivity implements OnItemClickListener,
public static void actionHandle(Context context, String title, String queryString, boolean integrate, Flag[] flags, Flag[] forbiddenFlags) { public static void actionHandle(Context context, String title, String queryString, boolean integrate, Flag[] flags, Flag[] forbiddenFlags) {
Intent intent = new Intent(context, MessageList.class); Intent intent = new Intent(context, MessageList.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); 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) { if (flags != null) {
intent.putExtra(EXTRA_QUERY_FLAGS, Utility.combine(flags, ',')); intent.putExtra(EXTRA_QUERY_FLAGS, Utility.combine(flags, ','));
} }
@ -617,7 +640,7 @@ public class MessageList extends K9ListActivity implements OnItemClickListener,
public static Intent actionHandleAccountIntent(Context context, String title, public static Intent actionHandleAccountIntent(Context context, String title,
SearchSpecification searchSpecification) { SearchSpecification searchSpecification) {
Intent intent = new Intent(context, MessageList.class); Intent intent = new Intent(context, MessageList.class);
intent.putExtra(EXTRA_QUERY, searchSpecification.getQuery()); intent.putExtra(SearchManager.QUERY, searchSpecification.getQuery());
if (searchSpecification.getRequiredFlags() != null) { if (searchSpecification.getRequiredFlags() != null) {
intent.putExtra(EXTRA_QUERY_FLAGS, Utility.combine(searchSpecification.getRequiredFlags(), ',')); intent.putExtra(EXTRA_QUERY_FLAGS, Utility.combine(searchSpecification.getRequiredFlags(), ','));
} }
@ -644,8 +667,25 @@ public class MessageList extends K9ListActivity implements OnItemClickListener,
@Override @Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) { public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (view == mFooterView) { if (view == mFooterView) {
if (mCurrentFolder != null) { if (mCurrentFolder != null && !mRemoteSearch) {
mController.loadMoreMessages(mAccount, mFolderName, mAdapter.mListener); 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<Message> 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; return;
} }
@ -667,7 +707,7 @@ public class MessageList extends K9ListActivity implements OnItemClickListener,
mActionBarProgressView = getLayoutInflater().inflate(R.layout.actionbar_indeterminate_progress_actionview, null); mActionBarProgressView = getLayoutInflater().inflate(R.layout.actionbar_indeterminate_progress_actionview, null);
// need this for actionbar initialization // need this for actionbar initialization
mQueryString = getIntent().getStringExtra(EXTRA_QUERY); mQueryString = getIntent().getStringExtra(SearchManager.QUERY);
mPullToRefreshView = (PullToRefreshListView) findViewById(R.id.message_list); mPullToRefreshView = (PullToRefreshListView) findViewById(R.id.message_list);
@ -699,7 +739,30 @@ public class MessageList extends K9ListActivity implements OnItemClickListener,
} }
private void initializeMessageList(Intent intent, boolean create) { private void initializeMessageList(Intent intent, boolean create) {
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); String accountUuid = intent.getStringExtra(EXTRA_ACCOUNT);
mFolderName = intent.getStringExtra(EXTRA_FOLDER);
mAccount = Preferences.getPreferences(this).getAccount(accountUuid); mAccount = Preferences.getPreferences(this).getAccount(accountUuid);
if (mAccount != null && !mAccount.isAvailable(this)) { if (mAccount != null && !mAccount.isAvailable(this)) {
@ -708,8 +771,8 @@ public class MessageList extends K9ListActivity implements OnItemClickListener,
return; return;
} }
mFolderName = intent.getStringExtra(EXTRA_FOLDER);
mQueryString = intent.getStringExtra(EXTRA_QUERY);
String queryFlags = intent.getStringExtra(EXTRA_QUERY_FLAGS); String queryFlags = intent.getStringExtra(EXTRA_QUERY_FLAGS);
if (queryFlags != null) { if (queryFlags != null) {
@ -745,8 +808,8 @@ public class MessageList extends K9ListActivity implements OnItemClickListener,
mCurrentFolder = mAdapter.getFolder(mFolderName, mAccount); mCurrentFolder = mAdapter.getFolder(mFolderName, mAccount);
} }
// Hide "Load up to x more" footer for search views // Hide "Load up to x more" footer for local search views
mFooterView.setVisibility((mQueryString != null) ? View.GONE : View.VISIBLE); mFooterView.setVisibility((mQueryString != null && !mRemoteSearch) ? View.GONE : View.VISIBLE);
mController = MessagingController.getInstance(getApplication()); mController = MessagingController.getInstance(getApplication());
mListView.setAdapter(mAdapter); mListView.setAdapter(mAdapter);
@ -806,26 +869,54 @@ public class MessageList extends K9ListActivity implements OnItemClickListener,
onAccountUnavailable(); onAccountUnavailable();
return; 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); StorageManager.getInstance(getApplication()).addListener(mStorageListener);
mSenderAboveSubject = K9.messageListSenderAboveSubject(); mSenderAboveSubject = K9.messageListSenderAboveSubject();
// TODO Add support for pull to fresh on searches. final Preferences prefs = Preferences.getPreferences(getApplicationContext());
if(mQueryString == null) {
boolean allowRemoteSearch = false;
if (mSearchAccount != null) {
final Account searchAccount = prefs.getAccount(mSearchAccount);
if (searchAccount != null) {
allowRemoteSearch = searchAccount.allowRemoteSearch();
}
}
if (mQueryString == null) {
mPullToRefreshView.setOnRefreshListener(new PullToRefreshBase.OnRefreshListener<ListView>() { mPullToRefreshView.setOnRefreshListener(new PullToRefreshBase.OnRefreshListener<ListView>() {
@Override @Override
public void onRefresh(PullToRefreshBase<ListView> refreshView) { public void onRefresh(PullToRefreshBase<ListView> refreshView) {
checkMail(mAccount, mFolderName); checkMail(mAccount, mFolderName);
} }
}); });
} else if (allowRemoteSearch && !mRemoteSearch && !mIntegrate) {
// mQueryString != null is implied if we get this far.
mPullToRefreshView.setOnRefreshListener(new PullToRefreshBase.OnRefreshListener<ListView>() {
@Override
public void onRefresh(PullToRefreshBase<ListView> 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));
} else { } else {
mPullToRefreshView.setMode(PullToRefreshBase.Mode.DISABLED); mPullToRefreshView.setMode(PullToRefreshBase.Mode.DISABLED);
} }
mController.addListener(mAdapter.mListener); mController.addListener(mAdapter.mListener);
//Cancel pending new mail notifications when we open an account
Account[] accountsWithNotification; Account[] accountsWithNotification;
Preferences prefs = Preferences.getPreferences(getApplicationContext());
Account account = getCurrentAccount(prefs); Account account = getCurrentAccount(prefs);
if (account != null) { if (account != null) {
@ -844,8 +935,12 @@ public class MessageList extends K9ListActivity implements OnItemClickListener,
mController.notifyAccountCancel(this, accountWithNotification); mController.notifyAccountCancel(this, accountWithNotification);
} }
if (mAdapter.isEmpty()) { if (mAdapter.isEmpty()) {
if (mFolderName != null) { 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); mController.listLocalMessages(mAccount, mFolderName, mAdapter.mListener);
// Hide the archive button if we don't have an archive folder. // Hide the archive button if we don't have an archive folder.
if (!mAccount.hasArchiveFolder()) { if (!mAccount.hasArchiveFolder()) {
@ -856,7 +951,6 @@ public class MessageList extends K9ListActivity implements OnItemClickListener,
// Don't show the archive button if this is a search. // Don't show the archive button if this is a search.
// mBatchArchiveButton.setVisibility(View.GONE); // mBatchArchiveButton.setVisibility(View.GONE);
} }
} else { } else {
// reread the selected date format preference in case it has changed // reread the selected date format preference in case it has changed
mMessageHelper.refresh(); mMessageHelper.refresh();
@ -866,27 +960,29 @@ public class MessageList extends K9ListActivity implements OnItemClickListener,
new Thread() { new Thread() {
@Override @Override
public void run() { public void run() {
if (mFolderName != null) { if (!mRemoteSearch) {
mController.listLocalMessagesSynchronous(mAccount, mFolderName, mAdapter.mListener); if (mFolderName != null) {
} else if (mQueryString != null) { mController.listLocalMessagesSynchronous(mAccount, mFolderName, mAdapter.mListener);
mController.searchLocalMessagesSynchronous(mAccountUuids, mFolderNames, null, mQueryString, mIntegrate, mQueryFlags, mForbiddenFlags, 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();
} }
});
runOnUiThread(new Runnable() {
@Override
public void run() {
mAdapter.pruneDirtyMessages();
mAdapter.notifyDataSetChanged();
restoreListState();
}
});
}
} }
} }
.start(); .start();
} }
if (mAccount != null && mFolderName != null) { if (mAccount != null && mFolderName != null && !mRemoteSearch) {
mController.getFolderUnreadMessageCount(mAccount, mFolderName, mAdapter.mListener); mController.getFolderUnreadMessageCount(mAccount, mFolderName, mAdapter.mListener);
} }
@ -1021,6 +1117,7 @@ public class MessageList extends K9ListActivity implements OnItemClickListener,
} }
} }
//Shortcuts that only work when a message is selected
boolean retval = true; boolean retval = true;
int position = mListView.getSelectedItemPosition(); int position = mListView.getSelectedItemPosition();
try { try {
@ -1161,6 +1258,47 @@ public class MessageList extends K9ListActivity implements OnItemClickListener,
AccountSettings.actionSettings(this, mAccount); AccountSettings.actionSettings(this, mAccount);
} }
/**
* 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) {
final Bundle appData = new Bundle();
if (fromLocalSearch) {
appData.putString(EXTRA_SEARCH_ACCOUNT, mSearchAccount);
appData.putString(EXTRA_SEARCH_FOLDER, mSearchFolder);
} else {
appData.putString(EXTRA_SEARCH_ACCOUNT, mAccount.getUuid());
appData.putString(EXTRA_SEARCH_FOLDER, mCurrentFolder.name);
}
appData.putBoolean(EXTRA_REMOTE_SEARCH, true);
final Intent intent = new Intent(Intent.ACTION_SEARCH);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(SearchManager.QUERY, mQueryString);
intent.putExtra(SearchManager.APP_DATA, appData);
intent.setComponent(new ComponentName(context.getApplicationContext(), MessageList.class));
context.startActivity(intent);
}
@Override
public boolean onSearchRequested() {
// If this search was started from a MessageList of a single folder, pass along that folder info
// so that we can enable remote search.
if (mAccount != null && mCurrentFolder != null) {
final Bundle appData = new Bundle();
appData.putString(EXTRA_SEARCH_ACCOUNT, mAccount.getUuid());
appData.putString(EXTRA_SEARCH_FOLDER, mCurrentFolder.name);
startSearch(null, false, appData, false);
} else {
// TODO Handle the case where we're searching from within a search result.
startSearch(null, false, null, false);
}
return true;
}
private void changeSort(SortType sortType) { private void changeSort(SortType sortType) {
Boolean sortAscending = (mSortType == sortType) ? !mSortAscending : null; Boolean sortAscending = (mSortType == sortType) ? !mSortAscending : null;
changeSort(sortType, sortAscending); changeSort(sortType, sortAscending);
@ -1357,7 +1495,7 @@ public class MessageList extends K9ListActivity implements OnItemClickListener,
} }
private void onToggleFlag(MessageInfoHolder messageInfo) { private void onToggleFlag(MessageInfoHolder messageInfo) {
LocalMessage message = messageInfo.message; Message message = messageInfo.message;
Folder folder = message.getFolder(); Folder folder = message.getFolder();
Account account = folder.getAccount(); Account account = folder.getAccount();
String folderName = folder.getName(); String folderName = folder.getName();
@ -1376,6 +1514,36 @@ public class MessageList extends K9ListActivity implements OnItemClickListener,
mController.sendPendingMessages(account, mAdapter.mListener); mController.sendPendingMessages(account, mAdapter.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.
*/
@Override
public void onBackPressed() {
// If we represent a remote search, then kill that before going back.
if(mSearchAccount != null && mSearchFolder != null && mRemoteSearchFuture != null) {
try {
Log.i(K9.LOG_TAG, "Remote search in progress, attempting to abort...");
// Canceling the future stops any message fetches in progress.
final boolean cancelSuccess = mRemoteSearchFuture.cancel(true); // mayInterruptIfRunning = true
if (!cancelSuccess) {
Log.e(K9.LOG_TAG, "Could not cancel remote search future.");
}
// Closing the folder will kill off the connection if we're mid-search.
final Account searchAccount = Preferences.getPreferences(this).getAccount(mSearchAccount);
final Store remoteStore = searchAccount.getRemoteStore();
final Folder remoteFolder = remoteStore.getFolder(mSearchFolder);
remoteFolder.close();
// Send a remoteSearchFinished() message for good measure.
mAdapter.mListener.remoteSearchFinished(searchAccount, mSearchFolder, 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);
}
}
super.onBackPressed();
}
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId(); int itemId = item.getItemId();
@ -1438,6 +1606,10 @@ public class MessageList extends K9ListActivity implements OnItemClickListener,
onEditPrefs(); onEditPrefs();
return true; return true;
} }
case R.id.search_remote: {
onRemoteSearchRequested(true);
return true;
}
} }
if (mQueryString != null) { if (mQueryString != null) {
@ -1602,12 +1774,27 @@ public class MessageList extends K9ListActivity implements OnItemClickListener,
@Override @Override
public boolean onPrepareOptionsMenu(Menu menu) { public boolean onPrepareOptionsMenu(Menu menu) {
if (mQueryString != null) { if (mQueryString != null || mIntegrate) {
menu.findItem(R.id.expunge).setVisible(false); menu.findItem(R.id.expunge).setVisible(false);
menu.findItem(R.id.check_mail).setVisible(false); menu.findItem(R.id.check_mail).setVisible(false);
menu.findItem(R.id.send_messages).setVisible(false); menu.findItem(R.id.send_messages).setVisible(false);
menu.findItem(R.id.folder_settings).setVisible(false); menu.findItem(R.id.folder_settings).setVisible(false);
menu.findItem(R.id.account_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.
final Preferences prefs = Preferences.getPreferences(getApplicationContext());
boolean allowRemoteSearch = false;
if (mSearchAccount != null) {
final Account searchAccount = prefs.getAccount(mSearchAccount);
if (searchAccount != null) {
allowRemoteSearch = searchAccount.allowRemoteSearch();
}
}
if (allowRemoteSearch && mQueryString != null && !mRemoteSearch && !mIntegrate && mSearchFolder != null) {
menu.findItem(R.id.search_remote).setVisible(true);
}
} else { } else {
if (mCurrentFolder != null && mCurrentFolder.name.equals(mAccount.getOutboxFolderName())) { if (mCurrentFolder != null && mCurrentFolder.name.equals(mAccount.getOutboxFolderName())) {
menu.findItem(R.id.check_mail).setVisible(false); menu.findItem(R.id.check_mail).setVisible(false);
@ -1663,8 +1850,63 @@ public class MessageList extends K9ListActivity implements OnItemClickListener,
private final List<MessageInfoHolder> mMessages = private final List<MessageInfoHolder> mMessages =
Collections.synchronizedList(new ArrayList<MessageInfoHolder>()); Collections.synchronizedList(new ArrayList<MessageInfoHolder>());
public List<Message> mExtraSearchResults;
private final ActivityListener mListener = new ActivityListener() { 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) {
setSupportProgress(Window.PROGRESS_END / numTotal * numDone);
} else {
setSupportProgress(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
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<Message> 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 @Override
public void informUserOfStatus() { public void informUserOfStatus() {
mHandler.refreshTitle(); mHandler.refreshTitle();
@ -2374,26 +2616,41 @@ public class MessageList extends K9ListActivity implements OnItemClickListener,
} }
private void updateFooterView() { private void updateFooterView() {
FooterViewHolder holder = (FooterViewHolder) mFooterView.getTag();
if (mCurrentFolder != null && mAccount != null) { if (mCurrentFolder != null && mAccount != null) {
if (mCurrentFolder.loading) { if (mCurrentFolder.loading) {
holder.main.setText(getString(R.string.status_loading_more)); final boolean showProgress = true;
holder.progress.setVisibility(ProgressBar.VISIBLE); updateFooter(getString(R.string.status_loading_more), showProgress);
} else { } else {
String message;
if (!mCurrentFolder.lastCheckFailed) { if (!mCurrentFolder.lastCheckFailed) {
if (mAccount.getDisplayCount() == 0) { 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 { } 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 { } 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 { } 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);
} }
} }

View file

@ -1,7 +1,29 @@
package com.fsck.k9.activity; package com.fsck.k9.activity;
import com.fsck.k9.activity.MessageList;
public class Search extends 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();
}
} }

View file

@ -2,20 +2,31 @@
package com.fsck.k9.activity.setup; package com.fsck.k9.activity.setup;
import android.app.Dialog; 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.Context;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Vibrator; 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 android.util.Log;
import android.view.View;
import java.util.Iterator; import android.widget.CheckBox;
import java.util.Map;
import java.util.LinkedList;
import java.util.List;
import com.fsck.k9.Account; import com.fsck.k9.Account;
import com.fsck.k9.Account.FolderMode; import com.fsck.k9.Account.FolderMode;
@ -24,18 +35,17 @@ import com.fsck.k9.K9;
import com.fsck.k9.NotificationSetting; import com.fsck.k9.NotificationSetting;
import com.fsck.k9.Preferences; import com.fsck.k9.Preferences;
import com.fsck.k9.R; import com.fsck.k9.R;
import com.fsck.k9.mail.Folder;
import com.fsck.k9.activity.ChooseFolder; import com.fsck.k9.activity.ChooseFolder;
import com.fsck.k9.activity.ChooseIdentity; import com.fsck.k9.activity.ChooseIdentity;
import com.fsck.k9.activity.ColorPickerDialog; import com.fsck.k9.activity.ColorPickerDialog;
import com.fsck.k9.activity.K9PreferenceActivity; import com.fsck.k9.activity.K9PreferenceActivity;
import com.fsck.k9.activity.ManageIdentities; import com.fsck.k9.activity.ManageIdentities;
import com.fsck.k9.crypto.Apg; import com.fsck.k9.crypto.Apg;
import com.fsck.k9.mail.Folder;
import com.fsck.k9.mail.Store; 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.LocalStore.LocalFolder;
import com.fsck.k9.mail.store.StorageManager;
import com.fsck.k9.service.MailService;
public class AccountSettings extends K9PreferenceActivity { public class AccountSettings extends K9PreferenceActivity {
@ -48,10 +58,12 @@ public class AccountSettings extends K9PreferenceActivity {
private static final int ACTIVITY_MANAGE_IDENTITIES = 2; private static final int ACTIVITY_MANAGE_IDENTITIES = 2;
private static final String PREFERENCE_SCREEN_MAIN = "main";
private static final String PREFERENCE_SCREEN_COMPOSING = "composing"; private static final String PREFERENCE_SCREEN_COMPOSING = "composing";
private static final String PREFERENCE_SCREEN_INCOMING = "incoming_prefs"; 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_PUSH_ADVANCED = "push_advanced";
private static final String PREFERENCE_SCREEN_NOTIFICATIONS = "notifications"; private static final String PREFERENCE_SCREEN_NOTIFICATIONS = "notifications";
private static final String PREFERENCE_SCREEN_SEARCH = "search";
private static final String PREFERENCE_DESCRIPTION = "account_description"; 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"; private static final String PREFERENCE_MARK_MESSAGE_AS_READ_ON_VIEW = "mark_message_as_read_on_view";
@ -101,6 +113,10 @@ public class AccountSettings extends K9PreferenceActivity {
private static final String PREFERENCE_CRYPTO_APP = "crypto_app"; 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_SIGNATURE = "crypto_auto_signature";
private static final String PREFERENCE_CRYPTO_AUTO_ENCRYPT = "crypto_auto_encrypt"; private static final String PREFERENCE_CRYPTO_AUTO_ENCRYPT = "crypto_auto_encrypt";
private static final String PREFERENCE_CLOUD_SEARCH_ENABLED = "remote_search_enabled";
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_LOCAL_STORAGE_PROVIDER = "local_storage_provider";
private static final String PREFERENCE_CATEGORY_FOLDERS = "folders"; private static final String PREFERENCE_CATEGORY_FOLDERS = "folders";
private static final String PREFERENCE_ARCHIVE_FOLDER = "archive_folder"; private static final String PREFERENCE_ARCHIVE_FOLDER = "archive_folder";
@ -116,6 +132,7 @@ public class AccountSettings extends K9PreferenceActivity {
private boolean mIsPushCapable = false; private boolean mIsPushCapable = false;
private boolean mIsExpungeCapable = false; private boolean mIsExpungeCapable = false;
private PreferenceScreen mMainScreen;
private PreferenceScreen mComposingScreen; private PreferenceScreen mComposingScreen;
private EditTextPreference mAccountDescription; private EditTextPreference mAccountDescription;
@ -162,6 +179,12 @@ public class AccountSettings extends K9PreferenceActivity {
private ListPreference mCryptoApp; private ListPreference mCryptoApp;
private CheckBoxPreference mCryptoAutoSignature; private CheckBoxPreference mCryptoAutoSignature;
private CheckBoxPreference mCryptoAutoEncrypt; private CheckBoxPreference mCryptoAutoEncrypt;
private PreferenceScreen mSearchScreen;
private CheckBoxPreference mCloudSearchEnabled;
private ListPreference mRemoteSearchNumResults;
private CheckBoxPreference mRemoteSearchFullText;
private ListPreference mLocalStorageProvider; private ListPreference mLocalStorageProvider;
private ListPreference mArchiveFolder; private ListPreference mArchiveFolder;
private ListPreference mDraftsFolder; private ListPreference mDraftsFolder;
@ -195,6 +218,8 @@ public class AccountSettings extends K9PreferenceActivity {
addPreferencesFromResource(R.xml.account_settings_preferences); addPreferencesFromResource(R.xml.account_settings_preferences);
mMainScreen = (PreferenceScreen) findPreference(PREFERENCE_SCREEN_MAIN);
mAccountDescription = (EditTextPreference) findPreference(PREFERENCE_DESCRIPTION); mAccountDescription = (EditTextPreference) findPreference(PREFERENCE_DESCRIPTION);
mAccountDescription.setSummary(mAccount.getDescription()); mAccountDescription.setSummary(mAccount.getDescription());
mAccountDescription.setText(mAccount.getDescription()); mAccountDescription.setText(mAccount.getDescription());
@ -397,7 +422,7 @@ public class AccountSettings extends K9PreferenceActivity {
if (!mAccount.isSearchByDateCapable()) { if (!mAccount.isSearchByDateCapable()) {
((PreferenceScreen) findPreference(PREFERENCE_SCREEN_INCOMING)).removePreference(mMessageAge); ((PreferenceScreen) findPreference(PREFERENCE_SCREEN_INCOMING)).removePreference(mMessageAge);
} else { } else {
mMessageAge.setValue(String.valueOf(mAccount.getMaximumPolledMessageAge())); mMessageAge.setValue(String.valueOf(mAccount.getMaximumPolledMessageAge()));
mMessageAge.setSummary(mMessageAge.getEntry()); mMessageAge.setSummary(mMessageAge.getEntry());
mMessageAge.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { mMessageAge.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@ -471,14 +496,33 @@ public class AccountSettings extends K9PreferenceActivity {
} }
}); });
} }
// IMAP-specific preferences // 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(
new OnPreferenceChangeListener() {
public boolean onPreferenceChange(Preference pref, Object newVal) {
updateRemoteSearchLimit((String)newVal);
return true;
}
}
);
updateRemoteSearchLimit(mRemoteSearchNumResults.getValue());
mRemoteSearchFullText = (CheckBoxPreference) findPreference(PREFERENCE_REMOTE_SEARCH_FULL_TEXT);
mPushPollOnConnect = (CheckBoxPreference) findPreference(PREFERENCE_PUSH_POLL_ON_CONNECT); mPushPollOnConnect = (CheckBoxPreference) findPreference(PREFERENCE_PUSH_POLL_ON_CONNECT);
mIdleRefreshPeriod = (ListPreference) findPreference(PREFERENCE_IDLE_REFRESH_PERIOD); mIdleRefreshPeriod = (ListPreference) findPreference(PREFERENCE_IDLE_REFRESH_PERIOD);
mMaxPushFolders = (ListPreference) findPreference(PREFERENCE_MAX_PUSH_FOLDERS); mMaxPushFolders = (ListPreference) findPreference(PREFERENCE_MAX_PUSH_FOLDERS);
if (mIsPushCapable) { if (mIsPushCapable) {
mPushPollOnConnect.setChecked(mAccount.isPushPollOnConnect()); mPushPollOnConnect.setChecked(mAccount.isPushPollOnConnect());
mCloudSearchEnabled.setChecked(mAccount.allowRemoteSearch());
mRemoteSearchNumResults.setValue(Integer.toString(mAccount.getRemoteSearchNumResults()));
mRemoteSearchFullText.setChecked(mAccount.isRemoteSearchFullText());
mIdleRefreshPeriod.setValue(String.valueOf(mAccount.getIdleRefreshMinutes())); mIdleRefreshPeriod.setValue(String.valueOf(mAccount.getIdleRefreshMinutes()));
mIdleRefreshPeriod.setSummary(mIdleRefreshPeriod.getEntry()); mIdleRefreshPeriod.setSummary(mIdleRefreshPeriod.getEntry());
mIdleRefreshPeriod.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { mIdleRefreshPeriod.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@ -516,8 +560,9 @@ public class AccountSettings extends K9PreferenceActivity {
}); });
} else { } else {
PreferenceScreen incomingPrefs = (PreferenceScreen) findPreference(PREFERENCE_SCREEN_INCOMING); PreferenceScreen incomingPrefs = (PreferenceScreen) findPreference(PREFERENCE_SCREEN_INCOMING);
incomingPrefs.removePreference( (PreferenceScreen) findPreference(PREFERENCE_SCREEN_PUSH_ADVANCED)); incomingPrefs.removePreference((PreferenceScreen) findPreference(PREFERENCE_SCREEN_PUSH_ADVANCED));
incomingPrefs.removePreference( (ListPreference) findPreference(PREFERENCE_PUSH_MODE)); incomingPrefs.removePreference((ListPreference) findPreference(PREFERENCE_PUSH_MODE));
mMainScreen.removePreference(mSearchScreen);
} }
mAccountNotify = (CheckBoxPreference) findPreference(PREFERENCE_NOTIFY); mAccountNotify = (CheckBoxPreference) findPreference(PREFERENCE_NOTIFY);
@ -745,11 +790,14 @@ public class AccountSettings extends K9PreferenceActivity {
mAccount.setTrashFolderName(mTrashFolder.getValue()); mAccount.setTrashFolderName(mTrashFolder.getValue());
} }
//IMAP stuff
if (mIsPushCapable) { if (mIsPushCapable) {
mAccount.setPushPollOnConnect(mPushPollOnConnect.isChecked()); mAccount.setPushPollOnConnect(mPushPollOnConnect.isChecked());
mAccount.setIdleRefreshMinutes(Integer.parseInt(mIdleRefreshPeriod.getValue())); mAccount.setIdleRefreshMinutes(Integer.parseInt(mIdleRefreshPeriod.getValue()));
mAccount.setMaxPushFolders(Integer.parseInt(mMaxPushFolders.getValue())); mAccount.setMaxPushFolders(Integer.parseInt(mMaxPushFolders.getValue()));
mAccount.setAllowRemoteSearch(mCloudSearchEnabled.isChecked());
mAccount.setRemoteSearchNumResults(Integer.parseInt(mRemoteSearchNumResults.getValue()));
mAccount.setRemoteSearchFullText(mRemoteSearchFullText.isChecked());
} }
if (!mIsMoveCapable) { if (!mIsMoveCapable) {
@ -776,6 +824,7 @@ public class AccountSettings extends K9PreferenceActivity {
mAccount.setShowPictures(Account.ShowPictures.valueOf(mAccountShowPictures.getValue())); mAccount.setShowPictures(Account.ShowPictures.valueOf(mAccountShowPictures.getValue()));
//IMAP specific stuff
if (mIsPushCapable) { if (mIsPushCapable) {
boolean needsPushRestart = mAccount.setFolderPushMode(Account.FolderMode.valueOf(mPushMode.getValue())); boolean needsPushRestart = mAccount.setFolderPushMode(Account.FolderMode.valueOf(mPushMode.getValue()));
if (mAccount.getFolderPushMode() != FolderMode.NONE) { if (mAccount.getFolderPushMode() != FolderMode.NONE) {
@ -923,6 +972,20 @@ public class AccountSettings extends K9PreferenceActivity {
Integer.parseInt(mAccountVibrateTimes.getValue())), -1); Integer.parseInt(mAccountVibrateTimes.getValue())), -1);
} }
/**
* Remote search result limit summary contains the current limit. On load or change, update this value.
* @param maxResults Search limit to update the summary with.
*/
private void updateRemoteSearchLimit(String maxResults) {
if (maxResults != null) {
if (maxResults.equals("0")) {
maxResults = getString(R.string.account_settings_remote_search_num_results_entries_all);
}
mRemoteSearchNumResults.setSummary(String.format(getString(R.string.account_settings_remote_search_num_summary), maxResults));
}
}
private class PopulateFolderPrefsTask extends AsyncTask<Void, Void, Void> { private class PopulateFolderPrefsTask extends AsyncTask<Void, Void, Void> {
List <? extends Folder > folders = new LinkedList<LocalFolder>(); List <? extends Folder > folders = new LinkedList<LocalFolder>();
String[] allFolderValues; String[] allFolderValues;

View file

@ -1,16 +1,21 @@
package com.fsck.k9.controller; package com.fsck.k9.controller;
import java.io.CharArrayWriter; import java.io.CharArrayWriter;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.util.*; import java.util.ArrayList;
import java.util.concurrent.BlockingQueue; import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap; import java.util.Collection;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.Collections;
import java.util.concurrent.CountDownLatch; import java.util.Comparator;
import java.util.concurrent.ExecutorService; import java.util.Date;
import java.util.concurrent.Executors; import java.util.HashMap;
import java.util.concurrent.PriorityBlockingQueue; 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.*;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@ -31,11 +36,11 @@ import com.fsck.k9.Account;
import com.fsck.k9.AccountStats; import com.fsck.k9.AccountStats;
import com.fsck.k9.K9; import com.fsck.k9.K9;
import com.fsck.k9.K9.NotificationHideSubject; import com.fsck.k9.K9.NotificationHideSubject;
import com.fsck.k9.K9.Intents;
import com.fsck.k9.NotificationSetting; import com.fsck.k9.NotificationSetting;
import com.fsck.k9.Preferences; import com.fsck.k9.Preferences;
import com.fsck.k9.R; import com.fsck.k9.R;
import com.fsck.k9.SearchSpecification; import com.fsck.k9.SearchSpecification;
import com.fsck.k9.K9.Intents;
import com.fsck.k9.activity.FolderList; import com.fsck.k9.activity.FolderList;
import com.fsck.k9.activity.MessageList; import com.fsck.k9.activity.MessageList;
import com.fsck.k9.helper.NotificationBuilder; import com.fsck.k9.helper.NotificationBuilder;
@ -48,8 +53,8 @@ import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.Folder;
import com.fsck.k9.mail.Folder.FolderType; import com.fsck.k9.mail.Folder.FolderType;
import com.fsck.k9.mail.Folder.OpenMode; 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;
import com.fsck.k9.mail.Message.RecipientType;
import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Part; import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.PushReceiver; import com.fsck.k9.mail.PushReceiver;
@ -59,12 +64,12 @@ import com.fsck.k9.mail.Transport;
import com.fsck.k9.mail.internet.MimeMessage; import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.internet.TextBody; 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.LocalStore;
import com.fsck.k9.mail.store.UnavailableStorageException;
import com.fsck.k9.mail.store.LocalStore.LocalFolder; import com.fsck.k9.mail.store.LocalStore.LocalFolder;
import com.fsck.k9.mail.store.LocalStore.LocalMessage; import com.fsck.k9.mail.store.LocalStore.LocalMessage;
import com.fsck.k9.mail.store.LocalStore.PendingCommand; import com.fsck.k9.mail.store.LocalStore.PendingCommand;
import com.fsck.k9.mail.store.UnavailableAccountException;
import com.fsck.k9.mail.store.UnavailableStorageException;
/** /**
@ -126,6 +131,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_MARK_ALL_AS_READ = "com.fsck.k9.MessagingController.markAllAsRead";
private static final String PENDING_COMMAND_EXPUNGE = "com.fsck.k9.MessagingController.expunge"; private static final String PENDING_COMMAND_EXPUNGE = "com.fsck.k9.MessagingController.expunge";
public static class UidReverseComparator implements Comparator<Message> {
@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 * Maximum number of unsynced messages to store at once
*/ */
@ -516,7 +543,7 @@ public class MessagingController implements Runnable {
l.listLocalMessagesStarted(account, folder); l.listLocalMessagesStarted(account, folder);
} }
Folder localFolder = null; LocalFolder localFolder = null;
MessageRetrievalListener retrievalListener = MessageRetrievalListener retrievalListener =
new MessageRetrievalListener() { new MessageRetrievalListener() {
List<Message> pendingMessages = new ArrayList<Message>(); List<Message> pendingMessages = new ArrayList<Message>();
@ -554,10 +581,13 @@ public class MessagingController implements Runnable {
try { try {
Store localStore = account.getLocalStore(); LocalStore localStore = account.getLocalStore();
localFolder = localStore.getFolder(folder); localFolder = localStore.getFolder(folder);
localFolder.open(OpenMode.READ_WRITE); 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( localFolder.getMessages(
retrievalListener, retrievalListener,
false // Skip deleted messages false // Skip deleted messages
@ -750,6 +780,142 @@ public class MessagingController implements Runnable {
listener.searchStats(stats); listener.searchStats(stats);
} }
} }
public Future 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);
}
return threadPool.submit(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<Message> extraResults = new ArrayList<Message>();
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");
}
List<Message> 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());
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 (Thread.currentThread().isInterrupted()) {
Log.i(K9.LOG_TAG, "Caught exception on aborted remote search; safe to ignore.", e);
} else {
Log.e(K9.LOG_TAG, "Could not complete remote search", 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<Message> 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<Message> messages, LocalFolder localFolder, Folder remoteFolder, MessagingListener listener) throws MessagingException {
final FetchProfile header = new FetchProfile();
header.add(FetchProfile.Item.FLAGS);
header.add(FetchProfile.Item.ENVELOPE);
final FetchProfile structure = new FetchProfile();
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}, header, null);
//fun fact: ImapFolder.fetch can't handle getting STRUCTURE at same time as headers
remoteFolder.fetch(new Message [] {message}, 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) { public void loadMoreMessages(Account account, String folder, MessagingListener listener) {
try { try {
LocalStore localStore = account.getLocalStore(); LocalStore localStore = account.getLocalStore();
@ -1193,12 +1359,12 @@ public class MessagingController implements Runnable {
* Reverse the order of the messages. Depending on the server this may get us * 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. * fetch results for newest to oldest. If not, no harm done.
*/ */
Collections.reverse(unsyncedMessages); Collections.sort(unsyncedMessages, new UidReverseComparator());
int visibleLimit = localFolder.getVisibleLimit(); int visibleLimit = localFolder.getVisibleLimit();
int listSize = unsyncedMessages.size(); int listSize = unsyncedMessages.size();
if ((visibleLimit > 0) && (listSize > visibleLimit)) { if ((visibleLimit > 0) && (listSize > visibleLimit)) {
unsyncedMessages = unsyncedMessages.subList(listSize - visibleLimit, listSize); unsyncedMessages = unsyncedMessages.subList(0, visibleLimit);
} }
FetchProfile fp = new FetchProfile(); FetchProfile fp = new FetchProfile();
@ -1933,7 +2099,7 @@ public class MessagingController implements Runnable {
LocalStore localStore = account.getLocalStore(); LocalStore localStore = account.getLocalStore();
localFolder = localStore.getFolder(folder); localFolder = localStore.getFolder(folder);
LocalMessage localMessage = (LocalMessage) localFolder.getMessage(uid); LocalMessage localMessage = localFolder.getMessage(uid);
if (localMessage == null) { if (localMessage == null) {
return; return;
@ -2725,78 +2891,111 @@ public class MessagingController implements Runnable {
put("loadMessageForViewRemote", listener, new Runnable() { put("loadMessageForViewRemote", listener, new Runnable() {
@Override @Override
public void run() { public void run() {
Folder remoteFolder = null; loadMessageForViewRemoteSynchronous(account, folder, uid, listener, false, false);
LocalFolder localFolder = null; }
try {
LocalStore localStore = account.getLocalStore();
localFolder = localStore.getFolder(folder);
localFolder.open(OpenMode.READ_WRITE);
Message message = localFolder.getMessage(uid);
if (message.isSet(Flag.X_DOWNLOADED_FULL)) {
/*
* If the message has been synchronized since we were called we'll
* just hand it back cause it's ready to go.
*/
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.ENVELOPE);
fp.add(FetchProfile.Item.BODY);
localFolder.fetch(new Message[] { message }, fp, null);
} else {
/*
* At this point the message is not available, so we need to download it
* fully if possible.
*/
Store remoteStore = account.getRemoteStore();
remoteFolder = remoteStore.getFolder(folder);
remoteFolder.open(OpenMode.READ_WRITE);
// Get the remote message and fully download it
Message remoteMessage = remoteFolder.getMessage(uid);
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.BODY);
remoteFolder.fetch(new Message[] { remoteMessage }, fp, null);
// Store the message locally and load the stored message into memory
localFolder.appendMessages(new Message[] { remoteMessage });
fp.add(FetchProfile.Item.ENVELOPE);
message = localFolder.getMessage(uid);
localFolder.fetch(new Message[] { message }, fp, null);
// Mark that this message is now fully synched
if (account.isMarkMessageAsReadOnView()) {
message.setFlag(Flag.SEEN, true);
}
message.setFlag(Flag.X_DOWNLOADED_FULL, true);
}
// now that we have the full message, refresh the headers
for (MessagingListener l : getListeners(listener)) {
l.loadMessageForViewHeadersAvailable(account, folder, uid, message);
}
for (MessagingListener l : getListeners(listener)) {
l.loadMessageForViewBodyAvailable(account, folder, uid, message);
}
for (MessagingListener l : getListeners(listener)) {
l.loadMessageForViewFinished(account, folder, uid, message);
}
} catch (Exception e) {
for (MessagingListener l : getListeners(listener)) {
l.loadMessageForViewFailed(account, folder, uid, e);
}
addErrorMessage(account, null, e);
} finally {
closeFolder(remoteFolder);
closeFolder(localFolder);
}
}//run
}); });
} }
public boolean loadMessageForViewRemoteSynchronous(final Account account, final String folder,
final String uid, final MessagingListener listener, final boolean force,
final boolean loadPartialFromSearch) {
Folder remoteFolder = null;
LocalFolder localFolder = null;
try {
LocalStore localStore = account.getLocalStore();
localFolder = localStore.getFolder(folder);
localFolder.open(OpenMode.READ_WRITE);
Message message = localFolder.getMessage(uid);
if (uid.startsWith(K9.LOCAL_UID_PREFIX)) {
Log.w(K9.LOG_TAG, "Message has local UID so cannot download fully.");
// ASH move toast
android.widget.Toast.makeText(mApplication,
"Message has local UID so cannot download fully",
android.widget.Toast.LENGTH_LONG).show();
// TODO: Using X_DOWNLOADED_FULL is wrong because it's only a partial message. But
// one we can't download completely. Maybe add a new flag; X_PARTIAL_MESSAGE ?
message.setFlag(Flag.X_DOWNLOADED_FULL, true);
message.setFlag(Flag.X_DOWNLOADED_PARTIAL, false);
}
/* commented out because this was pulled from another unmerged branch:
} else if (localFolder.isLocalOnly() && !force) {
Log.w(K9.LOG_TAG, "Message in local-only folder so cannot download fully.");
// ASH move toast
android.widget.Toast.makeText(mApplication,
"Message in local-only folder so cannot download fully",
android.widget.Toast.LENGTH_LONG).show();
message.setFlag(Flag.X_DOWNLOADED_FULL, true);
message.setFlag(Flag.X_DOWNLOADED_PARTIAL, false);
}*/
if (message.isSet(Flag.X_DOWNLOADED_FULL)) {
/*
* If the message has been synchronized since we were called we'll
* just hand it back cause it's ready to go.
*/
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.ENVELOPE);
fp.add(FetchProfile.Item.BODY);
localFolder.fetch(new Message[] { message }, fp, null);
} else {
/*
* At this point the message is not available, so we need to download it
* fully if possible.
*/
Store remoteStore = account.getRemoteStore();
remoteFolder = remoteStore.getFolder(folder);
remoteFolder.open(OpenMode.READ_WRITE);
// Get the remote message and fully download it
Message remoteMessage = remoteFolder.getMessage(uid);
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.BODY);
remoteFolder.fetch(new Message[] { remoteMessage }, fp, null);
// Store the message locally and load the stored message into memory
localFolder.appendMessages(new Message[] { remoteMessage });
if (loadPartialFromSearch) {
fp.add(FetchProfile.Item.BODY);
}
fp.add(FetchProfile.Item.ENVELOPE);
message = localFolder.getMessage(uid);
localFolder.fetch(new Message[] { message }, fp, null);
// Mark that this message is now fully synched
if (account.isMarkMessageAsReadOnView()) {
message.setFlag(Flag.SEEN, true);
}
message.setFlag(Flag.X_DOWNLOADED_FULL, true);
}
// now that we have the full message, refresh the headers
for (MessagingListener l : getListeners(listener)) {
l.loadMessageForViewHeadersAvailable(account, folder, uid, message);
}
for (MessagingListener l : getListeners(listener)) {
l.loadMessageForViewBodyAvailable(account, folder, uid, message);
}
for (MessagingListener l : getListeners(listener)) {
l.loadMessageForViewFinished(account, folder, uid, message);
}
return true;
} catch (Exception e) {
for (MessagingListener l : getListeners(listener)) {
l.loadMessageForViewFailed(account, folder, uid, e);
}
addErrorMessage(account, null, e);
return false;
} finally {
closeFolder(remoteFolder);
closeFolder(localFolder);
}
}
public void loadMessageForView(final Account account, final String folder, final String uid, public void loadMessageForView(final Account account, final String folder, final String uid,
final MessagingListener listener) { final MessagingListener listener) {
for (MessagingListener l : getListeners(listener)) { for (MessagingListener l : getListeners(listener)) {
@ -2811,12 +3010,25 @@ public class MessagingController implements Runnable {
LocalFolder localFolder = localStore.getFolder(folder); LocalFolder localFolder = localStore.getFolder(folder);
localFolder.open(OpenMode.READ_WRITE); localFolder.open(OpenMode.READ_WRITE);
LocalMessage message = (LocalMessage)localFolder.getMessage(uid); LocalMessage message = localFolder.getMessage(uid);
if (message == null if (message == null
|| message.getId() == 0) { || message.getId() == 0) {
throw new IllegalArgumentException("Message not found: folder=" + folder + ", uid=" + uid); throw new IllegalArgumentException("Message not found: folder=" + folder + ", uid=" + uid);
} }
if (account.isMarkMessageAsReadOnView() && !message.isSet(Flag.SEEN)) { // IMAP search results will usually need to be downloaded before viewing.
// TODO: limit by account.getMaximumAutoDownloadMessageSize().
if (!message.isSet(Flag.X_DOWNLOADED_FULL) &&
!message.isSet(Flag.X_DOWNLOADED_PARTIAL)) {
if (loadMessageForViewRemoteSynchronous(account, folder, uid, listener,
false, true)) {
if (account.isMarkMessageAsReadOnView() && !message.isSet(Flag.SEEN)) {
message.setFlag(Flag.SEEN, true);
setFlag(new Message[] { message }, Flag.SEEN, true);
}
}
return;
}
if (!message.isSet(Flag.SEEN)) {
message.setFlag(Flag.SEEN, true); message.setFlag(Flag.SEEN, true);
setFlag(new Message[] { message }, Flag.SEEN, true); setFlag(new Message[] { message }, Flag.SEEN, true);
} }

View file

@ -1,7 +1,10 @@
package com.fsck.k9.controller; package com.fsck.k9.controller;
import java.util.List;
import android.content.Context; import android.content.Context;
import com.fsck.k9.Account; import com.fsck.k9.Account;
import com.fsck.k9.AccountStats; import com.fsck.k9.AccountStats;
import com.fsck.k9.BaseAccount; 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.Message;
import com.fsck.k9.mail.Part; import com.fsck.k9.mail.Part;
import java.util.List;
/** /**
* Defines the interface that {@link MessagingController} will use to callback to requesters. * Defines the interface that {@link MessagingController} will use to callback to requesters.
* *
@ -152,6 +153,50 @@ public class MessagingListener {
public void pendingCommandsFinished(Account account) {} 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<Message> 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 * 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 * has completed a command. This is useful for turning off progress indicators that may have

View file

@ -17,7 +17,6 @@ import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Message; import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Message.RecipientType; import com.fsck.k9.mail.Message.RecipientType;
import com.fsck.k9.mail.store.LocalStore.LocalMessage;
import com.fsck.k9.helper.DateFormatter; import com.fsck.k9.helper.DateFormatter;
public class MessageHelper { public class MessageHelper {
@ -43,11 +42,10 @@ public class MessageHelper {
mTodayDateFormat = android.text.format.DateFormat.getTimeFormat(mContext); 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 FolderInfoHolder folder, final Account account) {
final Contacts contactHelper = K9.showContactName() ? Contacts.getInstance(mContext) : null; final Contacts contactHelper = K9.showContactName() ? Contacts.getInstance(mContext) : null;
try { try {
LocalMessage message = (LocalMessage) m;
target.message = message; target.message = message;
target.compareArrival = message.getInternalDate(); target.compareArrival = message.getInternalDate();
target.compareDate = message.getSentDate(); target.compareDate = message.getSentDate();
@ -86,13 +84,16 @@ public class MessageHelper {
target.uid = message.getUid(); target.uid = message.getUid();
target.account = account.getUuid(); 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) { } catch (MessagingException me) {
Log.w(K9.LOG_TAG, "Unable to load message info", me); Log.w(K9.LOG_TAG, "Unable to load message info", me);
} }
} }
public String formatDate(Date date) { public String formatDate(Date date) {
if (date == null) {
return "";
}
if (Utility.isDateToday(date)) { if (Utility.isDateToday(date)) {
return mTodayDateFormat.format(date); return mTodayDateFormat.format(date);
} else { } else {

View file

@ -1,6 +1,7 @@
package com.fsck.k9.mail; package com.fsck.k9.mail;
import java.util.Date; import java.util.Date;
import java.util.List;
import java.util.Map; import java.util.Map;
import android.util.Log; import android.util.Log;
@ -226,4 +227,9 @@ public abstract class Folder {
public Account getAccount() { public Account getAccount() {
return mAccount; return mAccount;
} }
public List<Message> search(String queryString, final Flag[] requiredFlags, final Flag[] forbiddenFlags)
throws MessagingException {
throw new MessagingException("K-9 does not support searches on this folder type");
}
} }

View file

@ -1,19 +1,18 @@
package com.fsck.k9.mail; package com.fsck.k9.mail;
import java.io.IOException;
import java.util.Date; import java.util.Date;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.io.IOException;
import android.util.Log; import android.util.Log;
import com.fsck.k9.K9;
import com.fsck.k9.activity.MessageReference; import com.fsck.k9.activity.MessageReference;
import com.fsck.k9.mail.filter.CountingOutputStream; import com.fsck.k9.mail.filter.CountingOutputStream;
import com.fsck.k9.mail.filter.EOLConvertingOutputStream; import com.fsck.k9.mail.filter.EOLConvertingOutputStream;
import com.fsck.k9.mail.store.UnavailableStorageException; import com.fsck.k9.mail.store.UnavailableStorageException;
import com.fsck.k9.K9;
public abstract class Message implements Part, Body { public abstract class Message implements Part, Body {
@ -144,6 +143,16 @@ public abstract class Message implements Part, Body {
return getContentType().startsWith(mimeType); 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 {} public void delete(String trashFolderName) throws MessagingException {}
/* /*

View file

@ -1,6 +1,9 @@
package com.fsck.k9.mail; package com.fsck.k9.mail;
import java.util.HashMap;
import java.util.List;
import android.app.Application; import android.app.Application;
import android.content.Context; 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.ImapStore;
import com.fsck.k9.mail.store.LocalStore; import com.fsck.k9.mail.store.LocalStore;
import com.fsck.k9.mail.store.Pop3Store; import com.fsck.k9.mail.store.Pop3Store;
import com.fsck.k9.mail.store.WebDavStore;
import com.fsck.k9.mail.store.StorageManager.StorageProvider; import com.fsck.k9.mail.store.StorageManager.StorageProvider;
import com.fsck.k9.mail.store.UnavailableStorageException;
import java.util.HashMap; import com.fsck.k9.mail.store.WebDavStore;
import java.util.List;
/** /**
* Store is the access point for an email message store. It's location can be * Store is the access point for an email message store. It's location can be
@ -176,4 +177,6 @@ public abstract class Store {
public Account getAccount() { public Account getAccount() {
return mAccount; return mAccount;
} }
} }

View file

@ -592,4 +592,28 @@ public class MimeMessage extends Message {
copy(message); copy(message);
return 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;
}
} }

View file

@ -45,11 +45,15 @@ import java.util.StringTokenizer;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; 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.SSLContext;
import javax.net.ssl.SSLException; import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManager;
import org.apache.commons.io.IOUtils;
import android.content.Context; import android.content.Context;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.net.NetworkInfo; 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.FetchProfile;
import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.Folder;
import com.fsck.k9.mail.Folder.OpenMode;
import com.fsck.k9.mail.Message; import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Part; import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.PushReceiver; import com.fsck.k9.mail.PushReceiver;
import com.fsck.k9.mail.Pusher; import com.fsck.k9.mail.Pusher;
import com.fsck.k9.mail.Store;
import com.fsck.k9.mail.ServerSettings; 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.EOLConvertingOutputStream;
import com.fsck.k9.mail.filter.FixedLengthInputStream; import com.fsck.k9.mail.filter.FixedLengthInputStream;
import com.fsck.k9.mail.filter.PeekableInputStream; 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.fsck.k9.mail.transport.imap.ImapSettings;
import com.jcraft.jzlib.JZlib; import com.jcraft.jzlib.JZlib;
import com.jcraft.jzlib.ZOutputStream; import com.jcraft.jzlib.ZOutputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
import org.apache.commons.io.IOUtils;
/** /**
* <pre> * <pre>
@ -818,7 +820,7 @@ public class ImapStore extends Store {
private volatile boolean mExists; private volatile boolean mExists;
private ImapStore store = null; private ImapStore store = null;
Map<Long, String> msgSeqUidMap = new ConcurrentHashMap<Long, String>(); Map<Long, String> msgSeqUidMap = new ConcurrentHashMap<Long, String>();
private boolean mInSearch = false;
public ImapFolder(ImapStore nStore, String name) { public ImapFolder(ImapStore nStore, String name) {
super(nStore.getAccount()); super(nStore.getAccount());
@ -998,7 +1000,13 @@ public class ImapStore extends Store {
} }
synchronized (this) { synchronized (this) {
releaseConnection(mConnection); // If we are mid-search and we get a close request, we gotta trash the connection.
if (mInSearch && mConnection != null) {
Log.i(K9.LOG_TAG, "IMAP search was aborted, shutting down connection.");
mConnection.close();
} else {
releaseConnection(mConnection);
}
mConnection = null; mConnection = null;
} }
} }
@ -1127,7 +1135,7 @@ public class ImapStore extends Store {
} }
ImapFolder iFolder = (ImapFolder)folder; ImapFolder iFolder = (ImapFolder)folder;
checkOpen(); checkOpen(); //only need READ access
String[] uids = new String[messages.length]; String[] uids = new String[messages.length];
for (int i = 0, count = messages.length; i < count; i++) { for (int i = 0, count = messages.length; i < count; i++) {
@ -1251,7 +1259,7 @@ public class ImapStore extends Store {
private int getRemoteMessageCount(String criteria) throws MessagingException { private int getRemoteMessageCount(String criteria) throws MessagingException {
checkOpen(); checkOpen(); //only need READ access
try { try {
int count = 0; int count = 0;
int start = 1; int start = 1;
@ -1287,7 +1295,7 @@ public class ImapStore extends Store {
return executeSimpleCommand("UID SEARCH *:*"); return executeSimpleCommand("UID SEARCH *:*");
} }
}; };
Message[] messages = search(searcher, null); Message[] messages = search(searcher, null).toArray(EMPTY_MESSAGE_ARRAY);
if (messages.length > 0) { if (messages.length > 0) {
return Long.parseLong(messages[0].getUid()); return Long.parseLong(messages[0].getUid());
} }
@ -1336,7 +1344,7 @@ public class ImapStore extends Store {
return executeSimpleCommand(String.format("UID SEARCH %d:%d%s%s", start, end, dateSearchString, includeDeleted ? "" : " NOT DELETED")); 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<Long> mesgSeqs, final boolean includeDeleted, final MessageRetrievalListener listener) protected Message[] getMessages(final List<Long> mesgSeqs, final boolean includeDeleted, final MessageRetrievalListener listener)
@ -1346,7 +1354,7 @@ public class ImapStore extends Store {
return executeSimpleCommand(String.format("UID SEARCH %s%s", Utility.combine(mesgSeqs.toArray(), ','), includeDeleted ? "" : " NOT DELETED")); 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<String> mesgUids, final boolean includeDeleted, final MessageRetrievalListener listener) protected Message[] getMessagesFromUids(final List<String> mesgUids, final boolean includeDeleted, final MessageRetrievalListener listener)
@ -1356,12 +1364,12 @@ public class ImapStore extends Store {
return executeSimpleCommand(String.format("UID SEARCH UID %s%s", Utility.combine(mesgUids.toArray(), ','), includeDeleted ? "" : " NOT DELETED")); 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<Message> search(ImapSearcher searcher, MessageRetrievalListener listener) throws MessagingException {
checkOpen(); checkOpen(); //only need READ access
ArrayList<Message> messages = new ArrayList<Message>(); ArrayList<Message> messages = new ArrayList<Message>();
try { try {
ArrayList<Long> uids = new ArrayList<Long>(); ArrayList<Long> uids = new ArrayList<Long>();
@ -1376,8 +1384,12 @@ public class ImapStore extends Store {
} }
} }
// Sort the uids in numerically ascending order // Sort the uids in numerically decreasing order
Collections.sort(uids); // 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++) { for (int i = 0, count = uids.size(); i < count; i++) {
String uid = uids.get(i).toString(); String uid = uids.get(i).toString();
if (listener != null) { if (listener != null) {
@ -1392,7 +1404,7 @@ public class ImapStore extends Store {
} catch (IOException ioe) { } catch (IOException ioe) {
throw ioExceptionHandler(mConnection, ioe); throw ioExceptionHandler(mConnection, ioe);
} }
return messages.toArray(EMPTY_MESSAGE_ARRAY); return messages;
} }
@ -1404,7 +1416,7 @@ public class ImapStore extends Store {
@Override @Override
public Message[] getMessages(String[] uids, MessageRetrievalListener listener) public Message[] getMessages(String[] uids, MessageRetrievalListener listener)
throws MessagingException { throws MessagingException {
checkOpen(); checkOpen(); //only need READ access
ArrayList<Message> messages = new ArrayList<Message>(); ArrayList<Message> messages = new ArrayList<Message>();
try { try {
if (uids == null) { if (uids == null) {
@ -1441,7 +1453,7 @@ public class ImapStore extends Store {
if (messages == null || messages.length == 0) { if (messages == null || messages.length == 0) {
return; return;
} }
checkOpen(); checkOpen(); //only need READ access
List<String> uids = new ArrayList<String>(messages.length); List<String> uids = new ArrayList<String>(messages.length);
HashMap<String, Message> messageMap = new HashMap<String, Message>(); HashMap<String, Message> messageMap = new HashMap<String, Message>();
for (int i = 0, count = messages.length; i < count; i++) { for (int i = 0, count = messages.length; i < count; i++) {
@ -1566,7 +1578,7 @@ public class ImapStore extends Store {
@Override @Override
public void fetchPart(Message message, Part part, MessageRetrievalListener listener) public void fetchPart(Message message, Part part, MessageRetrievalListener listener)
throws MessagingException { throws MessagingException {
checkOpen(); checkOpen(); //only need READ access
String[] parts = part.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA); String[] parts = part.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA);
if (parts == null) { if (parts == null) {
@ -1969,6 +1981,7 @@ public class ImapStore extends Store {
*/ */
@Override @Override
public Map<String, String> appendMessages(Message[] messages) throws MessagingException { public Map<String, String> appendMessages(Message[] messages) throws MessagingException {
open(OpenMode.READ_WRITE);
checkOpen(); checkOpen();
try { try {
Map<String, String> uidMap = new HashMap<String, String>(); Map<String, String> uidMap = new HashMap<String, String>();
@ -2081,6 +2094,7 @@ public class ImapStore extends Store {
@Override @Override
public void expunge() throws MessagingException { public void expunge() throws MessagingException {
open(OpenMode.READ_WRITE);
checkOpen(); checkOpen();
try { try {
executeSimpleCommand("EXPUNGE"); executeSimpleCommand("EXPUNGE");
@ -2113,6 +2127,7 @@ public class ImapStore extends Store {
@Override @Override
public void setFlags(Flag[] flags, boolean value) public void setFlags(Flag[] flags, boolean value)
throws MessagingException { throws MessagingException {
open(OpenMode.READ_WRITE);
checkOpen(); checkOpen();
@ -2147,6 +2162,7 @@ public class ImapStore extends Store {
@Override @Override
public void setFlags(Message[] messages, Flag[] flags, boolean value) public void setFlags(Message[] messages, Flag[] flags, boolean value)
throws MessagingException { throws MessagingException {
open(OpenMode.READ_WRITE);
checkOpen(); checkOpen();
String[] uids = new String[messages.length]; String[] uids = new String[messages.length];
for (int i = 0, count = messages.length; i < count; i++) { for (int i = 0, count = messages.length; i < count; i++) {
@ -2201,6 +2217,108 @@ public class ImapStore extends Store {
} }
return id; return id;
} }
/**
* Search the remote ImapFolder.
* @param queryString String to query for.
* @param requiredFlags Mandatory flags
* @param forbiddenFlags Flags to exclude
* @return List of messages found
* @throws MessagingException On any error.
*/
@Override
public List<Message> search(final String queryString, final Flag[] requiredFlags, final Flag[] forbiddenFlags)
throws MessagingException {
if (!mAccount.allowRemoteSearch()) {
throw new MessagingException("Your settings do not allow remote searching of this account");
}
// Setup the searcher
final ImapSearcher searcher = new ImapSearcher() {
public List<ImapResponse> 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;
}
}
}
final String encodedQry = encodeString(queryString);
if (mAccount.isRemoteSearchFullText()) {
imapQuery += "TEXT " + encodedQry;
} else {
imapQuery += "OR SUBJECT " + encodedQry + " FROM " + encodedQry;
}
return executeSimpleCommand(imapQuery);
}
};
// Execute the search
try {
open(OpenMode.READ_ONLY);
checkOpen();
mInSearch = true;
// don't pass listener--we don't want to add messages until we've downloaded them
return search(searcher, null);
} finally {
mInSearch = false;
}
}
} }
/** /**
@ -3412,5 +3530,4 @@ public class ImapStore extends Store {
return null; return null;
} }
} }
} }

View file

@ -1,7 +1,15 @@
package com.fsck.k9.mail.store; 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.net.URLEncoder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -17,7 +25,6 @@ import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import com.fsck.k9.helper.HtmlConverter;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import android.app.Application; import android.app.Application;
@ -36,8 +43,10 @@ import com.fsck.k9.K9;
import com.fsck.k9.Preferences; import com.fsck.k9.Preferences;
import com.fsck.k9.R; import com.fsck.k9.R;
import com.fsck.k9.Account.MessageFormat; import com.fsck.k9.Account.MessageFormat;
import com.fsck.k9.activity.Search;
import com.fsck.k9.controller.MessageRemovalListener; import com.fsck.k9.controller.MessageRemovalListener;
import com.fsck.k9.controller.MessageRetrievalListener; import com.fsck.k9.controller.MessageRetrievalListener;
import com.fsck.k9.helper.HtmlConverter;
import com.fsck.k9.helper.Utility; import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.Body; import com.fsck.k9.mail.Body;
@ -1127,9 +1136,15 @@ public class LocalStore extends Store implements Serializable {
@Override @Override
public void open(final OpenMode mode) throws MessagingException { public void open(final OpenMode mode) throws MessagingException {
if (isOpen()) {
if (isOpen() && (getMode() == mode || mode == OpenMode.READ_ONLY)) {
return; return;
} else if (isOpen()) {
//previously opened in READ_ONLY and now requesting READ_WRITE
//so close connection and reopen
close();
} }
try { try {
database.execute(false, new DbCallback<Void>() { database.execute(false, new DbCallback<Void>() {
@Override @Override
@ -1336,17 +1351,19 @@ public class LocalStore extends Store implements Serializable {
} }
public void purgeToVisibleLimit(MessageRemovalListener listener) throws MessagingException { public void purgeToVisibleLimit(MessageRemovalListener listener) throws MessagingException {
if (mVisibleLimit == 0) { //don't purge messages while a Search is active since it might throw away search results
return ; if (!Search.isActive()) {
} if (mVisibleLimit == 0) {
open(OpenMode.READ_WRITE); return ;
Message[] messages = getMessages(null, false); }
for (int i = mVisibleLimit; i < messages.length; i++) { open(OpenMode.READ_WRITE);
if (listener != null) { Message[] messages = getMessages(null, false);
listener.messageRemoved(messages[i]); 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 @Override
public Message getMessage(final String uid) throws MessagingException { public LocalMessage getMessage(final String uid) throws MessagingException {
try { try {
return database.execute(false, new DbCallback<Message>() { return database.execute(false, new DbCallback<LocalMessage>() {
@Override @Override
public Message doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { public LocalMessage doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
try { try {
open(OpenMode.READ_WRITE); open(OpenMode.READ_WRITE);
LocalMessage message = new LocalMessage(uid, LocalFolder.this); 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 * Replace an existing message in the database
*/ */
LocalMessage oldMessage = (LocalMessage) getMessage(uid); LocalMessage oldMessage = getMessage(uid);
if (oldMessage != null) { if (oldMessage != null) {
oldMessageId = oldMessage.getId(); oldMessageId = oldMessage.getId();

View file

@ -215,6 +215,16 @@ public class AccountSettings {
new V(1, new IntegerResourceSetting(5, new V(1, new IntegerResourceSetting(5,
R.array.account_settings_vibrate_times_label)) R.array.account_settings_vibrate_times_label))
)); ));
s.put("allowRemoteSearch", Settings.versions(
new V(18, new BooleanSetting(false))
));
s.put("remoteSearchNumResults", Settings.versions(
new V(18, new IntegerResourceSetting(Account.DEFAULT_REMOTE_SEARCH_NUM_RESULTS,
R.array.account_settings_remote_search_num_results_values))
));
s.put("remoteSearchFullText", Settings.versions(
new V(18, new BooleanSetting(false))
));
SETTINGS = Collections.unmodifiableMap(s); SETTINGS = Collections.unmodifiableMap(s);

View file

@ -35,7 +35,7 @@ public class Settings {
* *
* @see SettingsExporter * @see SettingsExporter
*/ */
public static final int VERSION = 17; public static final int VERSION = 18;
public static Map<String, Object> validate(int version, Map<String, public static Map<String, Object> validate(int version, Map<String,
TreeMap<Integer, SettingsDescription>> settings, TreeMap<Integer, SettingsDescription>> settings,