Merge branch 'content_provider'

This commit is contained in:
cketti 2012-11-18 16:50:31 +01:00
commit 08615f205f
40 changed files with 5109 additions and 2553 deletions

View File

@ -396,6 +396,11 @@ otherwise it would make K-9 start at the wrong time
android:readPermission="com.fsck.k9.permission.READ_MESSAGES"
android:writePermission="com.fsck.k9.permission.DELETE_MESSAGES"
/>
<provider
android:name="com.fsck.k9.provider.EmailProvider"
android:authorities="org.k9mail.provider.email"
android:exported="false"
/>
<receiver
android:name=".provider.UnreadWidgetProvider"

View File

@ -55,14 +55,32 @@
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/chip_wrapper"
android:layout_below="@+id/subject"
android:layout_toLeftOf="@+id/thread_count"
android:layout_marginLeft="1dip"
android:layout_marginBottom="3dip"
android:layout_marginRight="16dip"
android:layout_alignParentRight="true"
android:layout_marginRight="3dip"
android:bufferType="spannable"
android:singleLine="false"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary" />
<TextView
android:id="@+id/thread_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="6dip"
android:layout_marginRight="12dip"
android:layout_alignParentRight="true"
android:layout_below="@+id/subject"
android:gravity="center_vertical|center_horizontal"
android:paddingLeft="4dip"
android:paddingRight="4dip"
android:paddingTop="2dip"
android:paddingBottom="2dip"
android:background="#50000000"
android:focusable="false" />
<TextView
android:id="@+id/date"
android:layout_width="wrap_content"
@ -75,7 +93,4 @@
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary" />
</RelativeLayout>

View File

@ -34,9 +34,11 @@
<item
android:id="@+id/set_sort_subject"
android:title="@string/sort_by_subject"/>
<!--
<item
android:id="@+id/set_sort_sender"
android:title="@string/sort_by_sender"/>
-->
<item
android:id="@+id/set_sort_flag"
android:title="@string/sort_by_flag"/>

View File

@ -1138,6 +1138,7 @@ http://k9mail.googlecode.com/
<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="remote_search_error">Remote search failed</string>
<string name="account_settings_search">Search</string>
<string name="account_settings_remote_search_enabled">Enable server search</string>
@ -1149,4 +1150,7 @@ http://k9mail.googlecode.com/
<string name="global_settings_background_as_unread_indicator_label">Use background as (un)read indicator</string>
<string name="global_settings_background_as_unread_indicator_summary">Show read and unread messages with different background colors</string>
<string name="global_settings_threaded_view_label">Threaded view</string>
<string name="global_settings_threaded_view_summary">Collapse messages belonging to the same thread</string>
</resources>

View File

@ -150,6 +150,12 @@
android:summary="@string/global_settings_background_as_unread_indicator_summary"
/>
<CheckBoxPreference
android:persistent="false"
android:key="threaded_view"
android:title="@string/global_settings_threaded_view_label"
android:summary="@string/global_settings_threaded_view_summary" />
</PreferenceCategory>
<PreferenceCategory

View File

@ -94,7 +94,7 @@ public class Account implements BaseAccount {
SORT_DATE(R.string.sort_earliest_first, R.string.sort_latest_first, false),
SORT_ARRIVAL(R.string.sort_earliest_first, R.string.sort_latest_first, false),
SORT_SUBJECT(R.string.sort_subject_alpha, R.string.sort_subject_re_alpha, true),
SORT_SENDER(R.string.sort_sender_alpha, R.string.sort_sender_re_alpha, true),
// SORT_SENDER(R.string.sort_sender_alpha, R.string.sort_sender_re_alpha, true),
SORT_UNREAD(R.string.sort_unread_first, R.string.sort_unread_last, true),
SORT_FLAGGED(R.string.sort_flagged_first, R.string.sort_flagged_last, true),
SORT_ATTACHMENT(R.string.sort_attach_first, R.string.sort_unattached_first, true);

View File

@ -206,6 +206,8 @@ public class K9 extends Application {
private static HashMap<SortType, Boolean> mSortAscending = new HashMap<SortType, Boolean>();
private static boolean sUseBackgroundAsUnreadIndicator = true;
private static boolean sThreadedViewEnabled = true;
/**
* The MIME type(s) of attachments we're willing to view.
@ -473,6 +475,7 @@ public class K9 extends Application {
editor.putString("attachmentdefaultpath", mAttachmentDefaultPath);
editor.putBoolean("useBackgroundAsUnreadIndicator", sUseBackgroundAsUnreadIndicator);
editor.putBoolean("threadedView", sThreadedViewEnabled);
fontSizes.save(editor);
}
@ -643,6 +646,7 @@ public class K9 extends Application {
mAttachmentDefaultPath = sprefs.getString("attachmentdefaultpath", Environment.getExternalStorageDirectory().toString());
sUseBackgroundAsUnreadIndicator = sprefs.getBoolean("useBackgroundAsUnreadIndicator", true);
sThreadedViewEnabled = sprefs.getBoolean("threadedView", true);
fontSizes.load(sprefs);
try {
@ -1138,4 +1142,12 @@ public class K9 extends Application {
public static synchronized void setUseBackgroundAsUnreadIndicator(boolean enabled) {
sUseBackgroundAsUnreadIndicator = enabled;
}
public static synchronized boolean isThreadedViewEnabled() {
return sThreadedViewEnabled;
}
public static synchronized void setThreadedViewEnabled(boolean enable) {
sThreadedViewEnabled = enable;
}
}

View File

@ -1,152 +0,0 @@
package com.fsck.k9;
import java.io.Serializable;
import java.util.UUID;
import android.content.Context;
import com.fsck.k9.mail.Flag;
/**
* This is a meta-Account that represents one or more accounts with filters on them. The filter specification
* is defined by {@link com.fsck.k9.activity.SearchModifier}.
*/
public class SearchAccount implements BaseAccount, SearchSpecification, Serializable {
/**
* Create a {@code SearchAccount} instance for the Unified Inbox.
*
* @param context
* A {@link Context} instance that will be used to get localized strings and will be
* passed on to the {@code SearchAccount} instance.
*
* @return The {@link SearchAccount} instance for the Unified Inbox.
*/
public static SearchAccount createUnifiedInboxAccount(Context context) {
SearchAccount unifiedInbox = new SearchAccount(context, true, null, null);
unifiedInbox.setDescription(context.getString(R.string.integrated_inbox_title));
unifiedInbox.setEmail(context.getString(R.string.integrated_inbox_detail));
return unifiedInbox;
}
/**
* Create a {@code SearchAccount} instance for the special account "All messages".
*
* @param context
* A {@link Context} instance that will be used to get localized strings and will be
* passed on to the {@code SearchAccount} instance.
*
* @return The {@link SearchAccount} instance for the Unified Inbox.
*/
public static SearchAccount createAllMessagesAccount(Context context) {
SearchAccount allMessages = new SearchAccount(context, false, null, null);
allMessages.setDescription(context.getString(R.string.search_all_messages_title));
allMessages.setEmail(context.getString(R.string.search_all_messages_detail));
return allMessages;
}
private static final long serialVersionUID = -4388420303235543976L;
private Flag[] mRequiredFlags = null;
private Flag[] mForbiddenFlags = null;
private String email = null;
private String description = null;
private String query = "";
private boolean integrate = false;
private String mUuid = null;
private boolean builtin = false;
private String[] accountUuids = null;
private String[] folderNames = null;
public SearchAccount(Preferences preferences) {
}
protected synchronized void delete(Preferences preferences) {
}
public synchronized void save(Preferences preferences) {
}
public SearchAccount(Context context, boolean nintegrate, Flag[] requiredFlags, Flag[] forbiddenFlags) {
mRequiredFlags = requiredFlags;
mForbiddenFlags = forbiddenFlags;
integrate = nintegrate;
}
@Override
public synchronized String getEmail() {
return email;
}
@Override
public synchronized void setEmail(String email) {
this.email = email;
}
public Flag[] getRequiredFlags() {
return mRequiredFlags;
}
public Flag[] getForbiddenFlags() {
return mForbiddenFlags;
}
public boolean isIntegrate() {
return integrate;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getQuery() {
return query;
}
public void setQuery(String query) {
this.query = query;
}
public String getUuid() {
if (mUuid == null) {
setUuid(UUID.randomUUID().toString());
}
return mUuid;
}
public void setUuid(String nUuid) {
mUuid = nUuid;
}
public void setIntegrate(boolean integrate) {
this.integrate = integrate;
}
public boolean isBuiltin() {
return builtin;
}
public void setBuiltin(boolean builtin) {
this.builtin = builtin;
}
public String[] getAccountUuids() {
return accountUuids;
}
public void setAccountUuids(String[] accountUuids) {
this.accountUuids = accountUuids;
}
@Override
public String[] getFolderNames() {
return folderNames;
}
public void setFolderNames(String[] folderNames) {
this.folderNames = folderNames;
}
}

View File

@ -1,19 +0,0 @@
package com.fsck.k9;
import com.fsck.k9.mail.Flag;
public interface SearchSpecification {
public Flag[] getRequiredFlags();
public Flag[] getForbiddenFlags();
public boolean isIntegrate();
public String getQuery();
public String[] getAccountUuids();
public String[] getFolderNames();
}

View File

@ -21,7 +21,7 @@ import com.fsck.k9.FontSizes;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
import com.fsck.k9.SearchAccount;
import com.fsck.k9.search.SearchAccount;
/**

View File

@ -62,11 +62,8 @@ import com.fsck.k9.FontSizes;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
import com.fsck.k9.SearchAccount;
import com.fsck.k9.SearchSpecification;
import com.fsck.k9.activity.misc.ExtendedAsyncTask;
import com.fsck.k9.activity.misc.NonConfigurationInstance;
import com.fsck.k9.activity.setup.AccountSettings;
import com.fsck.k9.activity.setup.AccountSetupBasics;
import com.fsck.k9.activity.setup.Prefs;
import com.fsck.k9.activity.setup.WelcomeMessage;
@ -80,6 +77,9 @@ import com.fsck.k9.mail.Transport;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.store.StorageManager;
import com.fsck.k9.mail.store.WebDavStore;
import com.fsck.k9.search.LocalSearch;
import com.fsck.k9.search.SearchAccount;
import com.fsck.k9.search.SearchModifier;
import com.fsck.k9.view.ColorChip;
import com.fsck.k9.preferences.SettingsExporter;
import com.fsck.k9.preferences.SettingsImportExportException;
@ -126,8 +126,8 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
private AccountsHandler mHandler = new AccountsHandler();
private AccountsAdapter mAdapter;
private SearchAccount unreadAccount = null;
private SearchAccount integratedInboxAccount = null;
private SearchAccount mAllMessagesAccount = null;
private SearchAccount mUnifiedInboxAccount = null;
private FontSizes mFontSizes = K9.getFontSizes();
private MenuItem mRefreshMenuItem;
@ -375,7 +375,7 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
boolean startup = intent.getBooleanExtra(EXTRA_STARTUP, true);
if (startup && K9.startIntegratedInbox() && !K9.isHideSpecialAccounts()) {
onOpenAccount(integratedInboxAccount);
onOpenAccount(mUnifiedInboxAccount);
finish();
return;
} else if (startup && accounts.length == 1 && onOpenAccount(accounts[0])) {
@ -424,8 +424,8 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
* Creates and initializes the special accounts ('Unified Inbox' and 'All Messages')
*/
private void createSpecialAccounts() {
integratedInboxAccount = SearchAccount.createUnifiedInboxAccount(this);
unreadAccount = SearchAccount.createAllMessagesAccount(this);
mUnifiedInboxAccount = SearchAccount.createUnifiedInboxAccount(this);
mAllMessagesAccount = SearchAccount.createAllMessagesAccount(this);
}
@SuppressWarnings("unchecked")
@ -519,14 +519,14 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
List<BaseAccount> newAccounts;
if (!K9.isHideSpecialAccounts() && accounts.length > 0) {
if (integratedInboxAccount == null || unreadAccount == null) {
if (mUnifiedInboxAccount == null || mAllMessagesAccount == null) {
createSpecialAccounts();
}
newAccounts = new ArrayList<BaseAccount>(accounts.length +
SPECIAL_ACCOUNTS_COUNT);
newAccounts.add(integratedInboxAccount);
newAccounts.add(unreadAccount);
newAccounts.add(mUnifiedInboxAccount);
newAccounts.add(mAllMessagesAccount);
} else {
newAccounts = new ArrayList<BaseAccount>(accounts.length);
}
@ -550,7 +550,8 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
pendingWork.put(account, "true");
final SearchAccount searchAccount = (SearchAccount)account;
MessagingController.getInstance(getApplication()).searchLocalMessages(searchAccount, null, new MessagingListener() {
MessagingController.getInstance(getApplication())
.searchLocalMessages(searchAccount.getRelatedSearch(), new MessagingListener() {
@Override
public void searchStats(AccountStats stats) {
mListener.accountStatusChanged(searchAccount, stats);
@ -607,7 +608,7 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
private boolean onOpenAccount(BaseAccount account) {
if (account instanceof SearchAccount) {
SearchAccount searchAccount = (SearchAccount)account;
MessageList.actionHandle(this, searchAccount.getDescription(), searchAccount);
MessageList.actionDisplaySearch(this, searchAccount.getRelatedSearch(), false, false);
} else {
Account realAccount = (Account)account;
if (!realAccount.isEnabled()) {
@ -624,8 +625,10 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
if (K9.FOLDER_NONE.equals(realAccount.getAutoExpandFolderName())) {
FolderList.actionHandleAccount(this, realAccount);
} else {
MessageList.actionHandleFolder(this, realAccount, realAccount.getAutoExpandFolderName());
}
LocalSearch search = new LocalSearch(realAccount.getAutoExpandFolderName());
search.addAllowedFolder(realAccount.getAutoExpandFolderName());
search.addAccountUuid(realAccount.getUuid());
MessageList.actionDisplaySearch(this, search, false, true);}
}
return true;
}
@ -1782,49 +1785,20 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
}
@Override
public void onClick(View v) {
String description = getString(R.string.search_title, account.getDescription(), getString(searchModifier.resId));
final String description = getString(R.string.search_title, account.getDescription(), getString(searchModifier.resId));
LocalSearch search = null;
if (account instanceof SearchAccount) {
SearchAccount searchAccount = (SearchAccount)account;
MessageList.actionHandle(Accounts.this,
description, "", searchAccount.isIntegrate(),
combine(searchAccount.getRequiredFlags(), searchModifier.requiredFlags),
combine(searchAccount.getForbiddenFlags(), searchModifier.forbiddenFlags));
search = ((SearchAccount) account).getRelatedSearch().clone();
search.setName(description);
} else {
SearchSpecification searchSpec = new SearchSpecification() {
@Override
public String[] getAccountUuids() {
return new String[] { account.getUuid() };
}
@Override
public Flag[] getForbiddenFlags() {
return searchModifier.forbiddenFlags;
}
@Override
public String getQuery() {
return "";
}
@Override
public Flag[] getRequiredFlags() {
return searchModifier.requiredFlags;
}
@Override
public boolean isIntegrate() {
return false;
}
@Override
public String[] getFolderNames() {
return null;
}
};
MessageList.actionHandle(Accounts.this, description, searchSpec);
search = new LocalSearch(description);
search.addAccountUuid(account.getUuid());
}
search.allRequiredFlags(searchModifier.requiredFlags);
search.allForbiddenFlags(searchModifier.forbiddenFlags);
MessageList.actionDisplaySearch(Accounts.this, search, true, false);
}
}

View File

@ -52,7 +52,6 @@ import com.fsck.k9.FontSizes;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
import com.fsck.k9.SearchSpecification;
import com.fsck.k9.activity.FolderList.FolderListAdapter.FolderListFilter;
import com.fsck.k9.activity.misc.ActionBarNavigationSpinner;
import com.fsck.k9.activity.setup.AccountSettings;
@ -68,6 +67,9 @@ import com.fsck.k9.mail.Folder;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.store.LocalStore.LocalFolder;
import com.fsck.k9.search.LocalSearch;
import com.fsck.k9.search.SearchModifier;
import com.fsck.k9.search.SearchSpecification;
import com.fsck.k9.service.MailService;
/**
@ -620,7 +622,10 @@ public class FolderList extends K9ListActivity implements OnNavigationListener {
}
private void onOpenFolder(String folder) {
MessageList.actionHandleFolder(this, mAccount, folder);
LocalSearch search = new LocalSearch(folder);
search.addAccountUuid(mAccount.getUuid());
search.addAllowedFolder(folder);
MessageList.actionDisplaySearch(this, search, false, false);
}
private void onCompact(Account account) {
@ -1267,86 +1272,34 @@ public class FolderList extends K9ListActivity implements OnNavigationListener {
}
@Override
public void onClick(View v) {
String description = getString(R.string.search_title,
final String description = getString(R.string.search_title,
getString(R.string.message_list_title, account.getDescription(), displayName),
getString(searchModifier.resId));
SearchSpecification searchSpec = new SearchSpecification() {
@Override
public String[] getAccountUuids() {
return new String[] { account.getUuid() };
}
@Override
public Flag[] getForbiddenFlags() {
return searchModifier.forbiddenFlags;
}
@Override
public String getQuery() {
return "";
}
@Override
public Flag[] getRequiredFlags() {
return searchModifier.requiredFlags;
}
@Override
public boolean isIntegrate() {
return false;
}
@Override
public String[] getFolderNames() {
return new String[] { folderName };
}
};
MessageList.actionHandle(FolderList.this, description, searchSpec);
LocalSearch search = new LocalSearch(description);
try {
search.allRequiredFlags(searchModifier.requiredFlags);
search.allForbiddenFlags(searchModifier.forbiddenFlags);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
search.addAllowedFolder(folderName);
search.addAccountUuid(account.getUuid());
MessageList.actionDisplaySearch(FolderList.this, search, true, false);
}
}
private static Flag[] UNREAD_FLAG_ARRAY = { Flag.SEEN };
private void openUnreadSearch(Context context, final Account account) {
String description = getString(R.string.search_title, mAccount.getDescription(), getString(R.string.unread_modifier));
SearchSpecification searchSpec = new SearchSpecification() {
//interface has no override @Override
public String[] getAccountUuids() {
return new String[] { account.getUuid() };
}
//interface has no override @Override
public Flag[] getForbiddenFlags() {
return UNREAD_FLAG_ARRAY;
}
//interface has no override @Override
public String getQuery() {
return "";
}
@Override
public Flag[] getRequiredFlags() {
return null;
}
@Override
public boolean isIntegrate() {
return false;
}
@Override
public String[] getFolderNames() {
return null;
}
};
MessageList.actionHandle(context, description, searchSpec);
LocalSearch search = new LocalSearch(description);
search.addAccountUuid(account.getUuid());
try {
search.allRequiredFlags(new Flag[]{Flag.SEEN});
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

View File

@ -7,7 +7,7 @@ import android.os.Parcelable;
import com.fsck.k9.Account;
import com.fsck.k9.BaseAccount;
import com.fsck.k9.R;
import com.fsck.k9.SearchSpecification;
import com.fsck.k9.search.SearchSpecification;
public class LauncherShortcuts extends AccountList {
@Override
@ -31,8 +31,8 @@ public class LauncherShortcuts extends AccountList {
Intent shortcutIntent = null;
if (account instanceof SearchSpecification) {
shortcutIntent = MessageList.actionHandleAccountIntent(this, account.getDescription(),
(SearchSpecification) account);
shortcutIntent = MessageList.intentDisplaySearch(this, (SearchSpecification) account,
false, true, true);
} else {
shortcutIntent = FolderList.actionHandleAccountIntent(this, (Account) account, null,
true);

View File

@ -24,17 +24,19 @@ import com.fsck.k9.Account.SortType;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
import com.fsck.k9.SearchSpecification;
import com.fsck.k9.activity.misc.SwipeGestureDetector.OnSwipeGestureListener;
import com.fsck.k9.activity.setup.AccountSettings;
import com.fsck.k9.activity.setup.FolderSettings;
import com.fsck.k9.activity.setup.Prefs;
import com.fsck.k9.fragment.MessageListFragment;
import com.fsck.k9.fragment.MessageListFragment.MessageListFragmentListener;
import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.store.StorageManager;
import com.fsck.k9.search.LocalSearch;
import com.fsck.k9.search.SearchSpecification;
import com.fsck.k9.search.SearchSpecification.Attribute;
import com.fsck.k9.search.SearchSpecification.Searchfield;
import com.fsck.k9.search.SearchSpecification.SearchCondition;
/**
@ -44,86 +46,42 @@ import com.fsck.k9.mail.store.StorageManager;
*/
public class MessageList extends K9FragmentActivity implements MessageListFragmentListener,
OnBackStackChangedListener, OnSwipeGestureListener {
private static final String EXTRA_ACCOUNT = "account";
private static final String EXTRA_FOLDER = "folder";
// for this activity
private static final String EXTRA_SEARCH = "search";
private static final String EXTRA_NO_THREADING = "no_threading";
// used for remote search
private static final String EXTRA_SEARCH_ACCOUNT = "com.fsck.k9.search_account";
private static final String EXTRA_SEARCH_FOLDER = "com.fsck.k9.search_folder";
private static final String EXTRA_QUERY_FLAGS = "queryFlags";
private static final String EXTRA_FORBIDDEN_FLAGS = "forbiddenFlags";
private static final String EXTRA_INTEGRATE = "integrate";
private static final String EXTRA_ACCOUNT_UUIDS = "accountUuids";
private static final String EXTRA_FOLDER_NAMES = "folderNames";
private static final String EXTRA_TITLE = "title";
public static void actionHandleFolder(Context context, Account account, String folder) {
Intent intent = new Intent(context, MessageList.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtra(EXTRA_ACCOUNT, account.getUuid());
if (folder != null) {
intent.putExtra(EXTRA_FOLDER, folder);
}
context.startActivity(intent);
public static void actionDisplaySearch(Context context, SearchSpecification search,
boolean noThreading, boolean newTask) {
actionDisplaySearch(context, search, noThreading, newTask, true);
}
public static Intent actionHandleFolderIntent(Context context, Account account, String folder) {
Intent intent = new Intent(context, MessageList.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP |
Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtra(EXTRA_ACCOUNT, account.getUuid());
if (folder != null) {
intent.putExtra(EXTRA_FOLDER, folder);
}
return intent;
public static void actionDisplaySearch(Context context, SearchSpecification search,
boolean noThreading, boolean newTask, boolean clearTop) {
context.startActivity(
intentDisplaySearch(context, search, noThreading, newTask, clearTop));
}
public static void actionHandle(Context context, String title, String queryString, boolean integrate, Flag[] flags, Flag[] forbiddenFlags) {
public static Intent intentDisplaySearch(Context context, SearchSpecification search,
boolean noThreading, boolean newTask, boolean clearTop) {
Intent intent = new Intent(context, MessageList.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtra(SearchManager.QUERY, queryString);
if (flags != null) {
intent.putExtra(EXTRA_QUERY_FLAGS, Utility.combine(flags, ','));
}
if (forbiddenFlags != null) {
intent.putExtra(EXTRA_FORBIDDEN_FLAGS, Utility.combine(forbiddenFlags, ','));
}
intent.putExtra(EXTRA_INTEGRATE, integrate);
intent.putExtra(EXTRA_TITLE, title);
context.startActivity(intent);
}
intent.putExtra(EXTRA_SEARCH, search);
intent.putExtra(EXTRA_NO_THREADING, noThreading);
/**
* Creates and returns an intent that opens Unified Inbox or All Messages screen.
*/
public static Intent actionHandleAccountIntent(Context context, String title,
SearchSpecification searchSpecification) {
Intent intent = new Intent(context, MessageList.class);
intent.putExtra(SearchManager.QUERY, searchSpecification.getQuery());
if (searchSpecification.getRequiredFlags() != null) {
intent.putExtra(EXTRA_QUERY_FLAGS, Utility.combine(searchSpecification.getRequiredFlags(), ','));
if (clearTop) {
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
}
if (searchSpecification.getForbiddenFlags() != null) {
intent.putExtra(EXTRA_FORBIDDEN_FLAGS, Utility.combine(searchSpecification.getForbiddenFlags(), ','));
if (newTask) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
intent.putExtra(EXTRA_INTEGRATE, searchSpecification.isIntegrate());
intent.putExtra(EXTRA_ACCOUNT_UUIDS, searchSpecification.getAccountUuids());
intent.putExtra(EXTRA_FOLDER_NAMES, searchSpecification.getFolderNames());
intent.putExtra(EXTRA_TITLE, title);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
return intent;
}
public static void actionHandle(Context context, String title,
SearchSpecification searchSpecification) {
Intent intent = actionHandleAccountIntent(context, title, searchSpecification);
context.startActivity(intent);
}
private StorageManager.StorageListener mStorageListener = new StorageListenerImplementation();
@ -131,31 +89,28 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
private TextView mActionBarTitle;
private TextView mActionBarSubTitle;
private TextView mActionBarUnread;
private String mTitle;
private Menu mMenu;
private MessageListFragment mMessageListFragment;
private Account mAccount;
private String mQueryString;
private String mFolderName;
private Flag[] mQueryFlags;
private Flag[] mForbiddenFlags;
private String mSearchAccount = null;
private String mSearchFolder = null;
private boolean mIntegrate;
private String[] mAccountUuids;
private String[] mFolderNames;
private LocalSearch mSearch;
private boolean mSingleFolderMode;
private boolean mSingleAccountMode;
/**
* {@code true} if the message list should be displayed as flat list (i.e. no threading)
* regardless whether or not message threading was enabled in the settings. This is used for
* filtered views, e.g. when only displaying the unread messages in a folder.
*/
private boolean mNoThreading;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.message_list);
// need this for actionbar initialization
mQueryString = getIntent().getStringExtra(SearchManager.QUERY);
mActionBar = getSupportActionBar();
initializeActionBar();
@ -171,76 +126,63 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
if (mMessageListFragment == null) {
FragmentTransaction ft = fragmentManager.beginTransaction();
if (mQueryString == null) {
mMessageListFragment = MessageListFragment.newInstance(mAccount, mFolderName);
} else if (mSearchAccount != null) {
mMessageListFragment = MessageListFragment.newInstance(mSearchAccount,
mSearchFolder, mQueryString, false);
} else {
mMessageListFragment = MessageListFragment.newInstance(mTitle, mAccountUuids,
mFolderNames, mQueryString, mQueryFlags, mForbiddenFlags, mIntegrate);
}
mMessageListFragment = MessageListFragment.newInstance(mSearch,
(K9.isThreadedViewEnabled() && !mNoThreading));
ft.add(R.id.message_list_container, mMessageListFragment);
ft.commit();
}
}
private void decodeExtras(Intent intent) {
mQueryString = intent.getStringExtra(SearchManager.QUERY);
mFolderName = null;
mSearchAccount = null;
mSearchFolder = null;
if (mQueryString != null) {
// check if this intent comes from the system search ( remote )
if (intent.getStringExtra(SearchManager.QUERY) != null) {
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
//Query was received from Search Dialog
String query = intent.getStringExtra(SearchManager.QUERY);
mSearch = new LocalSearch(getString(R.string.search_results));
mSearch.setManualSearch(true);
mNoThreading = true;
mSearch.or(new SearchCondition(Searchfield.SENDER, Attribute.CONTAINS, query));
mSearch.or(new SearchCondition(Searchfield.SUBJECT, Attribute.CONTAINS, query));
mSearch.or(new SearchCondition(Searchfield.MESSAGE_CONTENTS, Attribute.CONTAINS, query));
Bundle appData = getIntent().getBundleExtra(SearchManager.APP_DATA);
if (appData != null) {
mSearchAccount = appData.getString(EXTRA_SEARCH_ACCOUNT);
mSearchFolder = appData.getString(EXTRA_SEARCH_FOLDER);
mSearch.addAccountUuid(appData.getString(EXTRA_SEARCH_ACCOUNT));
mSearch.addAllowedFolder(appData.getString(EXTRA_SEARCH_FOLDER));
} else {
mSearch.addAccountUuid(LocalSearch.ALL_ACCOUNTS);
}
} else {
mSearchAccount = intent.getStringExtra(EXTRA_SEARCH_ACCOUNT);
mSearchFolder = intent.getStringExtra(EXTRA_SEARCH_FOLDER);
}
} else {
// regular LocalSearch object was passed
mSearch = intent.getParcelableExtra(EXTRA_SEARCH);
mNoThreading = intent.getBooleanExtra(EXTRA_NO_THREADING, false);
}
String[] accountUuids = mSearch.getAccountUuids();
mSingleAccountMode = (accountUuids.length == 1 && !mSearch.searchAllAccounts());
mSingleFolderMode = mSingleAccountMode && (mSearch.getFolderNames().size() == 1);
if (mSingleAccountMode) {
Preferences prefs = Preferences.getPreferences(getApplicationContext());
mAccount = prefs.getAccount(accountUuids[0]);
if (mAccount != null && !mAccount.isAvailable(this)) {
Log.i(K9.LOG_TAG, "not opening MessageList of unavailable account");
onAccountUnavailable();
return;
}
}
String accountUuid = intent.getStringExtra(EXTRA_ACCOUNT);
mFolderName = intent.getStringExtra(EXTRA_FOLDER);
mAccount = Preferences.getPreferences(this).getAccount(accountUuid);
if (mAccount != null && !mAccount.isAvailable(this)) {
Log.i(K9.LOG_TAG, "not opening MessageList of unavailable account");
onAccountUnavailable();
return;
if (mSingleFolderMode) {
mFolderName = mSearch.getFolderNames().get(0);
}
String queryFlags = intent.getStringExtra(EXTRA_QUERY_FLAGS);
if (queryFlags != null) {
String[] flagStrings = queryFlags.split(",");
mQueryFlags = new Flag[flagStrings.length];
for (int i = 0; i < flagStrings.length; i++) {
mQueryFlags[i] = Flag.valueOf(flagStrings[i]);
}
}
String forbiddenFlags = intent.getStringExtra(EXTRA_FORBIDDEN_FLAGS);
if (forbiddenFlags != null) {
String[] flagStrings = forbiddenFlags.split(",");
mForbiddenFlags = new Flag[flagStrings.length];
for (int i = 0; i < flagStrings.length; i++) {
mForbiddenFlags[i] = Flag.valueOf(flagStrings[i]);
}
}
mIntegrate = intent.getBooleanExtra(EXTRA_INTEGRATE, false);
mAccountUuids = intent.getStringArrayExtra(EXTRA_ACCOUNT_UUIDS);
mFolderNames = intent.getStringArrayExtra(EXTRA_FOLDER_NAMES);
mTitle = intent.getStringExtra(EXTRA_TITLE);
// Take the initial folder into account only if we are *not* restoring
// the activity already.
if (mFolderName == null && mQueryString == null) {
mFolderName = mAccount.getAutoExpandFolderName();
}
// now we know if we are in single account mode and need a subtitle
mActionBarSubTitle.setVisibility((!mSingleFolderMode) ? View.GONE : View.VISIBLE);
}
@Override
@ -276,10 +218,6 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
mActionBarSubTitle = (TextView) customView.findViewById(R.id.actionbar_title_sub);
mActionBarUnread = (TextView) customView.findViewById(R.id.actionbar_unread_count);
if (mQueryString != null) {
mActionBarSubTitle.setVisibility(View.GONE);
}
mActionBar.setDisplayHomeAsUpEnabled(true);
}
@ -407,15 +345,9 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
FragmentManager fragmentManager = getSupportFragmentManager();
if (fragmentManager.getBackStackEntryCount() > 0) {
fragmentManager.popBackStack();
} else if (mIntegrate) {
// If we were in one of the integrated mailboxes (think All Mail or Integrated Inbox), then
// go to accounts.
onAccounts();
} else if (mQueryString != null) {
// We did a search of some sort. Go back to wherever the user searched from.
} else if (!mSingleFolderMode || mMessageListFragment.isManualSearch()) {
onBackPressed();
} else {
// In a standard message list of a folder. Go to folder list.
onShowFolderList();
}
return true;
@ -440,10 +372,10 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
mMessageListFragment.changeSort(SortType.SORT_SUBJECT);
return true;
}
case R.id.set_sort_sender: {
mMessageListFragment.changeSort(SortType.SORT_SENDER);
return true;
}
// case R.id.set_sort_sender: {
// mMessageListFragment.changeSort(SortType.SORT_SENDER);
// return true;
// }
case R.id.set_sort_flag: {
mMessageListFragment.changeSort(SortType.SORT_FLAGGED);
return true;
@ -464,6 +396,10 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
onEditPrefs();
return true;
}
case R.id.account_settings: {
onEditAccount();
return true;
}
case R.id.search: {
mMessageListFragment.onSearchRequested();
return true;
@ -474,7 +410,7 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
}
}
if (mQueryString != null) {
if (!mSingleFolderMode) {
// None of the options after this point are "safe" for search results
//TODO: This is not true for "unread" and "starred" searches in regular folders
return false;
@ -491,10 +427,6 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
}
return true;
}
case R.id.account_settings: {
onEditAccount();
return true;
}
case R.id.expunge: {
mMessageListFragment.onExpunge();
return true;
@ -539,22 +471,14 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
menu.findItem(R.id.select_all).setVisible(true);
menu.findItem(R.id.settings).setVisible(true);
if (mMessageListFragment.isSearchQuery()) {
if (!mSingleAccountMode) {
menu.findItem(R.id.expunge).setVisible(false);
menu.findItem(R.id.check_mail).setVisible(false);
menu.findItem(R.id.send_messages).setVisible(false);
menu.findItem(R.id.folder_settings).setVisible(false);
menu.findItem(R.id.account_settings).setVisible(false);
// If this is an explicit local search, show the option to search the cloud.
if (!mMessageListFragment.isRemoteSearch() &&
mMessageListFragment.isRemoteSearchAllowed()) {
menu.findItem(R.id.search_remote).setVisible(true);
}
} else {
menu.findItem(R.id.search).setVisible(true);
menu.findItem(R.id.folder_settings).setVisible(true);
menu.findItem(R.id.folder_settings).setVisible(mSingleFolderMode);
menu.findItem(R.id.account_settings).setVisible(true);
if (mMessageListFragment.isOutbox()) {
@ -571,6 +495,14 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
menu.findItem(R.id.expunge).setVisible(false);
}
}
// If this is an explicit local search, show the option to search the cloud.
if (!mMessageListFragment.isRemoteSearch() &&
mMessageListFragment.isRemoteSearchAllowed()) {
menu.findItem(R.id.search_remote).setVisible(true);
} else if (!mMessageListFragment.isManualSearch()) {
menu.findItem(R.id.search).setVisible(true);
}
}
}
@ -672,8 +604,11 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
@Override
public void showMoreFromSameSender(String senderAddress) {
MessageListFragment fragment = MessageListFragment.newInstance("From " + senderAddress,
null, null, senderAddress, null, null, false);
LocalSearch tmpSearch = new LocalSearch("From " + senderAddress);
tmpSearch.addAccountUuids(mSearch.getAccountUuids());
tmpSearch.and(Searchfield.SENDER, senderAddress, Attribute.CONTAINS);
MessageListFragment fragment = MessageListFragment.newInstance(tmpSearch, false);
addMessageListFragment(fragment, true);
}
@ -720,13 +655,6 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
}
}
public void remoteSearch(String searchAccount, String searchFolder, String queryString) {
MessageListFragment fragment = MessageListFragment.newInstance(searchAccount, searchFolder,
queryString, true);
mMenu.findItem(R.id.search_remote).setVisible(false);
addMessageListFragment(fragment, false);
}
private void addMessageListFragment(MessageListFragment fragment, boolean addToBackStack) {
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
@ -754,4 +682,21 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
return true;
}
@Override
public void showThread(Account account, String folderName, long threadRootId) {
LocalSearch tmpSearch = new LocalSearch();
tmpSearch.addAccountUuid(account.getUuid());
tmpSearch.and(Searchfield.THREAD_ROOT, String.valueOf(threadRootId), Attribute.EQUALS);
tmpSearch.or(new SearchCondition(Searchfield.ID, Attribute.EQUALS, String.valueOf(threadRootId)));
MessageListFragment fragment = MessageListFragment.newInstance(tmpSearch, false);
addMessageListFragment(fragment, true);
}
@Override
public void remoteSearchStarted() {
// Remove action button for remote search
configureMenu(mMenu);
}
}

View File

@ -1,23 +0,0 @@
package com.fsck.k9.activity;
import com.fsck.k9.R;
import com.fsck.k9.mail.Flag;
/**
* This enum represents filtering parameters used by {@link com.fsck.k9.SearchAccount}.
*/
enum SearchModifier {
FLAGGED(R.string.flagged_modifier, new Flag[]{Flag.FLAGGED}, null),
UNREAD(R.string.unread_modifier, null, new Flag[]{Flag.SEEN});
final int resId;
final Flag[] requiredFlags;
final Flag[] forbiddenFlags;
SearchModifier(int nResId, Flag[] nRequiredFlags, Flag[] nForbiddenFlags) {
resId = nResId;
requiredFlags = nRequiredFlags;
forbiddenFlags = nForbiddenFlags;
}
}

View File

@ -499,6 +499,7 @@ public class AccountSettings extends K9PreferenceActivity {
// IMAP-specific preferences
mSearchScreen = (PreferenceScreen) findPreference(PREFERENCE_SCREEN_SEARCH);
mCloudSearchEnabled = (CheckBoxPreference) findPreference(PREFERENCE_CLOUD_SEARCH_ENABLED);
mRemoteSearchNumResults = (ListPreference) findPreference(PREFERENCE_REMOTE_SEARCH_NUM_RESULTS);
mRemoteSearchNumResults.setOnPreferenceChangeListener(

View File

@ -87,8 +87,11 @@ public class Prefs extends K9PreferenceActivity {
private static final String PREFERENCE_ATTACHMENT_DEF_PATH = "attachment_default_path";
private static final String PREFERENCE_BACKGROUND_AS_UNREAD_INDICATOR = "messagelist_background_as_unread_indicator";
private static final String PREFERENCE_THREADED_VIEW = "threaded_view";
private static final int ACTIVITY_CHOOSE_FOLDER = 1;
private ListPreference mLanguage;
private ListPreference mTheme;
private ListPreference mDateFormat;
@ -128,6 +131,8 @@ public class Prefs extends K9PreferenceActivity {
private CheckBoxPreference mBatchButtonsFlag;
private CheckBoxPreference mBatchButtonsUnselect;
private CheckBoxPreference mBackgroundAsUnreadIndicator;
private CheckBoxPreference mThreadedView;
public static void actionPrefs(Context context) {
Intent i = new Intent(context, Prefs.class);
@ -235,6 +240,10 @@ public class Prefs extends K9PreferenceActivity {
mChangeContactNameColor = (CheckBoxPreference)findPreference(PREFERENCE_MESSAGELIST_CONTACT_NAME_COLOR);
mChangeContactNameColor.setChecked(K9.changeContactNameColor());
mThreadedView = (CheckBoxPreference) findPreference(PREFERENCE_THREADED_VIEW);
mThreadedView.setChecked(K9.isThreadedViewEnabled());
if (K9.changeContactNameColor()) {
mChangeContactNameColor.setSummary(R.string.global_settings_registered_name_color_changed);
} else {
@ -418,6 +427,7 @@ public class Prefs extends K9PreferenceActivity {
K9.setMessageListSenderAboveSubject(mSenderAboveSubject.isChecked());
K9.setShowContactName(mShowContactName.isChecked());
K9.setUseBackgroundAsUnreadIndicator(mBackgroundAsUnreadIndicator.isChecked());
K9.setThreadedViewEnabled(mThreadedView.isChecked());
K9.setChangeContactNameColor(mChangeContactNameColor.isChecked());
K9.setMessageViewFixedWidthFont(mFixedWidth.isChecked());
K9.setMessageViewReturnToList(mReturnToList.isChecked());

View File

@ -40,11 +40,9 @@ import com.fsck.k9.K9.Intents;
import com.fsck.k9.NotificationSetting;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
import com.fsck.k9.SearchSpecification;
import com.fsck.k9.activity.FolderList;
import com.fsck.k9.activity.MessageList;
import com.fsck.k9.helper.NotificationBuilder;
import com.fsck.k9.helper.Utility;
import com.fsck.k9.helper.power.TracingPowerManager;
import com.fsck.k9.helper.power.TracingPowerManager.TracingWakeLock;
import com.fsck.k9.mail.Address;
@ -70,6 +68,8 @@ import com.fsck.k9.mail.store.LocalStore.LocalMessage;
import com.fsck.k9.mail.store.LocalStore.PendingCommand;
import com.fsck.k9.mail.store.UnavailableAccountException;
import com.fsck.k9.mail.store.UnavailableStorageException;
import com.fsck.k9.search.LocalSearch;
import com.fsck.k9.search.SearchSpecification;
/**
@ -509,235 +509,39 @@ public class MessagingController implements Runnable {
});
}
/**
* List the messages in the local message store for the given folder asynchronously.
*
* @param account
* @param folder
* @param listener
* @throws MessagingException
*/
public void listLocalMessages(final Account account, final String folder, final MessagingListener listener) {
threadPool.execute(new Runnable() {
@Override
public void run() {
listLocalMessagesSynchronous(account, folder, listener);
}
});
}
/**
* List the messages in the local message store for the given folder synchronously.
*
* @param account
* @param folder
* @param listener
* @throws MessagingException
*/
public void listLocalMessagesSynchronous(final Account account, final String folder, final MessagingListener listener) {
for (MessagingListener l : getListeners(listener)) {
l.listLocalMessagesStarted(account, folder);
}
LocalFolder localFolder = null;
MessageRetrievalListener retrievalListener =
new MessageRetrievalListener() {
List<Message> pendingMessages = new ArrayList<Message>();
@Override
public void messageStarted(String message, int number, int ofTotal) {}
@Override
public void messageFinished(Message message, int number, int ofTotal) {
if (!isMessageSuppressed(account, folder, message)) {
pendingMessages.add(message);
if (pendingMessages.size() > 10) {
addPendingMessages();
}
} else {
for (MessagingListener l : getListeners(listener)) {
l.listLocalMessagesRemoveMessage(account, folder, message);
}
}
}
@Override
public void messagesFinished(int number) {
addPendingMessages();
}
private void addPendingMessages() {
for (MessagingListener l : getListeners(listener)) {
l.listLocalMessagesAddMessages(account, folder, pendingMessages);
}
pendingMessages.clear();
}
};
try {
LocalStore localStore = account.getLocalStore();
localFolder = localStore.getFolder(folder);
localFolder.open(OpenMode.READ_WRITE);
//Purging followed by getting requires 2 DB queries.
//TODO: Fix getMessages to allow auto-pruning at visible limit?
localFolder.purgeToVisibleLimit(null);
localFolder.getMessages(
retrievalListener,
false // Skip deleted messages
);
if (K9.DEBUG)
Log.v(K9.LOG_TAG, "Got ack that callbackRunner finished");
for (MessagingListener l : getListeners(listener)) {
l.listLocalMessagesFinished(account, folder);
}
} catch (Exception e) {
for (MessagingListener l : getListeners(listener)) {
l.listLocalMessagesFailed(account, folder, e.getMessage());
}
addErrorMessage(account, null, e);
} finally {
closeFolder(localFolder);
}
}
public void searchLocalMessages(SearchSpecification searchSpecification, final Message[] messages, final MessagingListener listener) {
searchLocalMessages(searchSpecification.getAccountUuids(), searchSpecification.getFolderNames(), messages,
searchSpecification.getQuery(), searchSpecification.isIntegrate(), searchSpecification.getRequiredFlags(), searchSpecification.getForbiddenFlags(), listener);
}
/**
* Find all messages in any local account which match the query 'query'
* @throws MessagingException
*/
public void searchLocalMessages(final String[] accountUuids, final String[] folderNames, final Message[] messages, final String query, final boolean integrate,
final Flag[] requiredFlags, final Flag[] forbiddenFlags, final MessagingListener listener) {
if (K9.DEBUG) {
Log.i(K9.LOG_TAG, "searchLocalMessages ("
+ "accountUuids=" + Utility.combine(accountUuids, ',')
+ ", folderNames = " + Utility.combine(folderNames, ',')
+ ", messages.size() = " + (messages != null ? messages.length : -1)
+ ", query = " + query
+ ", integrate = " + integrate
+ ", requiredFlags = " + Utility.combine(requiredFlags, ',')
+ ", forbiddenFlags = " + Utility.combine(forbiddenFlags, ',')
+ ")");
}
public void searchLocalMessages(final LocalSearch search, final MessagingListener listener) {
threadPool.execute(new Runnable() {
@Override
public void run() {
searchLocalMessagesSynchronous(accountUuids, folderNames, messages, query, integrate, requiredFlags, forbiddenFlags, listener);
searchLocalMessagesSynchronous(search, listener);
}
});
}
public void searchLocalMessagesSynchronous(final String[] accountUuids, final String[] folderNames, final Message[] messages, final String query, final boolean integrate, final Flag[] requiredFlags, final Flag[] forbiddenFlags, final MessagingListener listener) {
public void searchLocalMessagesSynchronous(final LocalSearch search, final MessagingListener listener) {
final AccountStats stats = new AccountStats();
final Set<String> accountUuidsSet = new HashSet<String>();
if (accountUuids != null) {
accountUuidsSet.addAll(Arrays.asList(accountUuids));
}
final Preferences prefs = Preferences.getPreferences(mApplication.getApplicationContext());
List<LocalFolder> foldersToSearch = null;
boolean displayableOnly = false;
boolean noSpecialFolders = true;
for (final Account account : prefs.getAvailableAccounts()) {
if (accountUuids != null && !accountUuidsSet.contains(account.getUuid())) {
final HashSet<String> uuidSet = new HashSet<String>(Arrays.asList(search.getAccountUuids()));
Account[] accounts = Preferences.getPreferences(mApplication.getApplicationContext()).getAccounts();
boolean allAccounts = uuidSet.contains(SearchSpecification.ALL_ACCOUNTS);
// for every account we want to search do the query in the localstore
for (final Account account : accounts) {
if (!allAccounts && !uuidSet.contains(account.getUuid())) {
continue;
}
if (accountUuids != null && accountUuidsSet.contains(account.getUuid())) {
displayableOnly = true;
noSpecialFolders = true;
} else if (!integrate && folderNames == null) {
Account.Searchable searchableFolders = account.getSearchableFolders();
switch (searchableFolders) {
case NONE:
continue;
case DISPLAYABLE:
displayableOnly = true;
break;
}
}
List<Message> messagesToSearch = null;
if (messages != null) {
messagesToSearch = new LinkedList<Message>();
for (Message message : messages) {
if (message.getFolder().getAccount().getUuid().equals(account.getUuid())) {
messagesToSearch.add(message);
}
}
if (messagesToSearch.isEmpty()) {
continue;
}
}
if (listener != null) {
listener.listLocalMessagesStarted(account, null);
}
if (integrate || displayableOnly || folderNames != null || noSpecialFolders) {
List<LocalFolder> tmpFoldersToSearch = new LinkedList<LocalFolder>();
try {
LocalStore store = account.getLocalStore();
List <? extends Folder > folders = store.getPersonalNamespaces(false);
Set<String> folderNameSet = null;
if (folderNames != null) {
folderNameSet = new HashSet<String>();
folderNameSet.addAll(Arrays.asList(folderNames));
}
for (Folder folder : folders) {
LocalFolder localFolder = (LocalFolder)folder;
boolean include = true;
folder.refresh(prefs);
String localFolderName = localFolder.getName();
if (integrate) {
include = localFolder.isIntegrate();
} else {
if (folderNameSet != null) {
if (!folderNameSet.contains(localFolderName))
{
include = false;
}
}
// Never exclude the INBOX (see issue 1817)
else if (noSpecialFolders && !localFolderName.equalsIgnoreCase(account.getInboxFolderName()) &&
!localFolderName.equals(account.getArchiveFolderName()) && account.isSpecialFolder(localFolderName)) {
include = false;
} else if (displayableOnly && modeMismatch(account.getFolderDisplayMode(), folder.getDisplayClass())) {
include = false;
}
}
if (include) {
tmpFoldersToSearch.add(localFolder);
}
}
if (tmpFoldersToSearch.size() < 1) {
continue;
}
foldersToSearch = tmpFoldersToSearch;
} catch (MessagingException me) {
Log.e(K9.LOG_TAG, "Unable to restrict search folders in Account " + account.getDescription() + ", searching all", me);
addErrorMessage(account, null, me);
}
}
// Collecting statistics of the search result
MessageRetrievalListener retrievalListener = new MessageRetrievalListener() {
@Override
public void messageStarted(String message, int number, int ofTotal) {}
@Override
public void messagesFinished(int number) {}
@Override
public void messageFinished(Message message, int number, int ofTotal) {
if (!isMessageSuppressed(message.getFolder().getAccount(), message.getFolder().getName(), message)) {
List<Message> messages = new ArrayList<Message>();
@ -749,22 +553,18 @@ public class MessagingController implements Runnable {
listener.listLocalMessagesAddMessages(account, null, messages);
}
}
}
@Override
public void messagesFinished(int number) {
}
};
try {
String[] queryFields = {"html_content", "subject", "sender_list"};
LocalStore localStore = account.getLocalStore();
localStore.searchForMessages(retrievalListener, queryFields
, query, foldersToSearch,
messagesToSearch == null ? null : messagesToSearch.toArray(EMPTY_MESSAGE_ARRAY),
requiredFlags, forbiddenFlags);
// alert everyone the search has started
if (listener != null) {
listener.listLocalMessagesStarted(account, null);
}
// build and do the query in the localstore
try {
LocalStore localStore = account.getLocalStore();
localStore.searchForMessages(retrievalListener, search);
} catch (Exception e) {
if (listener != null) {
listener.listLocalMessagesFailed(account, null, e.getMessage());
@ -776,6 +576,8 @@ public class MessagingController implements Runnable {
}
}
}
// publish the total search statistics
if (listener != null) {
listener.searchStats(stats);
}
@ -824,24 +626,28 @@ public class MessagingController implements Runnable {
}
List<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());
// There's no need to fetch messages already completely downloaded
List<Message> remoteMessages = localFolder.extractNewMessages(messages);
messages.clear();
int resultLimit = acct.getRemoteSearchNumResults();
if (resultLimit > 0 && messages.size() > resultLimit) {
extraResults = messages.subList(resultLimit, messages.size());
messages = messages.subList(0, resultLimit);
if (listener != null) {
listener.remoteSearchServerQueryComplete(acct, folderName, remoteMessages.size());
}
loadSearchResultsSynchronous(messages, localFolder, remoteFolder, listener);
Collections.sort(remoteMessages, new UidReverseComparator());
int resultLimit = acct.getRemoteSearchNumResults();
if (resultLimit > 0 && remoteMessages.size() > resultLimit) {
extraResults = remoteMessages.subList(resultLimit, remoteMessages.size());
remoteMessages = remoteMessages.subList(0, resultLimit);
}
loadSearchResultsSynchronous(remoteMessages, localFolder, remoteFolder, listener);
} catch (Exception e) {
@ -2753,20 +2559,39 @@ public class MessagingController implements Runnable {
processPendingCommands(account);
}
public void setFlag(
final Message[] messages,
final Flag flag,
final boolean newState) {
public void setFlag(final List<Message> messages, final Flag flag, final boolean newState) {
actOnMessages(messages, new MessageActor() {
@Override
public void act(final Account account, final Folder folder,
final List<Message> messages) {
setFlag(account, folder.getName(), messages.toArray(EMPTY_MESSAGE_ARRAY), flag,
final List<Message> accountMessages) {
setFlag(account, folder.getName(), accountMessages.toArray(EMPTY_MESSAGE_ARRAY), flag,
newState);
}
});
}
public void setFlagForThreads(final List<Message> messages, final Flag flag,
final boolean newState) {
actOnMessages(messages, new MessageActor() {
@Override
public void act(final Account account, final Folder folder,
final List<Message> accountMessages) {
try {
List<Message> messagesInThreads = collectMessagesInThreads(account,
accountMessages);
setFlag(account, folder.getName(),
messagesInThreads.toArray(EMPTY_MESSAGE_ARRAY), flag, newState);
} catch (MessagingException e) {
addErrorMessage(account, "Something went wrong in setFlagForThreads()", e);
}
}
});
}
/**
@ -3023,14 +2848,15 @@ public class MessagingController implements Runnable {
false, true)) {
if (account.isMarkMessageAsReadOnView() && !message.isSet(Flag.SEEN)) {
message.setFlag(Flag.SEEN, true);
setFlag(new Message[] { message }, Flag.SEEN, true);
setFlag(Collections.singletonList((Message) message),
Flag.SEEN, true);
}
}
return;
}
if (!message.isSet(Flag.SEEN)) {
message.setFlag(Flag.SEEN, true);
setFlag(new Message[] { message }, Flag.SEEN, true);
setFlag(Collections.singletonList((Message) message), Flag.SEEN, true);
}
for (MessagingListener l : getListeners(listener)) {
@ -3247,8 +3073,11 @@ public class MessagingController implements Runnable {
builder.setContentTitle(mApplication.getString(R.string.notification_bg_send_title));
builder.setContentText(account.getDescription());
Intent intent = MessageList.actionHandleFolderIntent(mApplication, account,
account.getInboxFolderName());
LocalSearch search = new LocalSearch(account.getInboxFolderName());
search.addAllowedFolder(account.getInboxFolderName());
search.addAccountUuid(account.getUuid());
Intent intent = MessageList.intentDisplaySearch(mApplication, search, false, true, true);
PendingIntent pi = PendingIntent.getActivity(mApplication, 0, intent, 0);
builder.setContentIntent(pi);
@ -3330,8 +3159,11 @@ public class MessagingController implements Runnable {
mApplication.getString(R.string.notification_bg_title_separator) +
folder.getName());
Intent intent = MessageList.actionHandleFolderIntent(mApplication, account,
account.getInboxFolderName());
LocalSearch search = new LocalSearch(account.getInboxFolderName());
search.addAllowedFolder(account.getInboxFolderName());
search.addAccountUuid(account.getUuid());
Intent intent = MessageList.intentDisplaySearch(mApplication, search, false, true, true);
PendingIntent pi = PendingIntent.getActivity(mApplication, 0, intent, 0);
builder.setContentIntent(pi);
@ -3597,40 +3429,90 @@ public class MessagingController implements Runnable {
return false;
}
}
public void moveMessages(final Account account, final String srcFolder, final Message[] messages, final String destFolder,
final MessagingListener listener) {
public void moveMessages(final Account account, final String srcFolder,
final List<Message> messages, final String destFolder,
final MessagingListener listener) {
for (Message message : messages) {
suppressMessage(account, srcFolder, message);
}
putBackground("moveMessages", null, new Runnable() {
@Override
public void run() {
moveOrCopyMessageSynchronous(account, srcFolder, messages, destFolder, false, listener);
moveOrCopyMessageSynchronous(account, srcFolder, messages, destFolder, false,
listener);
}
});
}
public void moveMessage(final Account account, final String srcFolder, final Message message, final String destFolder,
final MessagingListener listener) {
moveMessages(account, srcFolder, new Message[] { message }, destFolder, listener);
public void moveMessagesInThread(final Account account, final String srcFolder,
final List<Message> messages, final String destFolder) {
for (Message message : messages) {
suppressMessage(account, srcFolder, message);
}
putBackground("moveMessagesInThread", null, new Runnable() {
@Override
public void run() {
try {
List<Message> messagesInThreads = collectMessagesInThreads(account, messages);
moveOrCopyMessageSynchronous(account, srcFolder, messagesInThreads, destFolder,
false, null);
} catch (MessagingException e) {
addErrorMessage(account, "Exception while moving messages", e);
}
}
});
}
public void copyMessages(final Account account, final String srcFolder, final Message[] messages, final String destFolder,
final MessagingListener listener) {
public void moveMessage(final Account account, final String srcFolder, final Message message,
final String destFolder, final MessagingListener listener) {
moveMessages(account, srcFolder, Collections.singletonList(message), destFolder, listener);
}
public void copyMessages(final Account account, final String srcFolder,
final List<Message> messages, final String destFolder,
final MessagingListener listener) {
putBackground("copyMessages", null, new Runnable() {
@Override
public void run() {
moveOrCopyMessageSynchronous(account, srcFolder, messages, destFolder, true, listener);
moveOrCopyMessageSynchronous(account, srcFolder, messages, destFolder, true,
listener);
}
});
}
public void copyMessage(final Account account, final String srcFolder, final Message message, final String destFolder,
final MessagingListener listener) {
copyMessages(account, srcFolder, new Message[] { message }, destFolder, listener);
public void copyMessagesInThread(final Account account, final String srcFolder,
final List<Message> messages, final String destFolder) {
putBackground("copyMessagesInThread", null, new Runnable() {
@Override
public void run() {
try {
List<Message> messagesInThreads = collectMessagesInThreads(account, messages);
moveOrCopyMessageSynchronous(account, srcFolder, messagesInThreads, destFolder,
true, null);
} catch (MessagingException e) {
addErrorMessage(account, "Exception while copying messages", e);
}
}
});
}
private void moveOrCopyMessageSynchronous(final Account account, final String srcFolder, final Message[] inMessages,
final String destFolder, final boolean isCopy, MessagingListener listener) {
public void copyMessage(final Account account, final String srcFolder, final Message message,
final String destFolder, final MessagingListener listener) {
copyMessages(account, srcFolder, Collections.singletonList(message), destFolder, listener);
}
private void moveOrCopyMessageSynchronous(final Account account, final String srcFolder,
final List<Message> inMessages, final String destFolder, final boolean isCopy,
MessagingListener listener) {
try {
Map<String, String> uidMap = new HashMap<String, String>();
Store localStore = account.getLocalStore();
@ -3741,7 +3623,7 @@ public class MessagingController implements Runnable {
if (uid != null) {
Message message = localFolder.getMessage(uid);
if (message != null) {
deleteMessages(new Message[] { message }, null);
deleteMessages(Collections.singletonList(message), null);
}
}
} catch (MessagingException me) {
@ -3751,20 +3633,72 @@ public class MessagingController implements Runnable {
}
}
public void deleteMessages(final Message[] messages, final MessagingListener listener) {
public void deleteThreads(final List<Message> messages) {
actOnMessages(messages, new MessageActor() {
@Override
public void act(final Account account, final Folder folder,
final List<Message> messages) {
for (Message message : messages) {
final List<Message> accountMessages) {
for (Message message : accountMessages) {
suppressMessage(account, folder.getName(), message);
}
putBackground("deleteThreads", null, new Runnable() {
@Override
public void run() {
deleteThreadsSynchronous(account, folder.getName(), accountMessages);
}
});
}
});
}
public void deleteThreadsSynchronous(Account account, String folderName,
List<Message> messages) {
try {
List<Message> messagesToDelete = collectMessagesInThreads(account, messages);
deleteMessagesSynchronous(account, folderName,
messagesToDelete.toArray(EMPTY_MESSAGE_ARRAY), null);
} catch (MessagingException e) {
Log.e(K9.LOG_TAG, "Something went wrong while deleting threads", e);
}
}
public List<Message> collectMessagesInThreads(Account account, List<Message> messages)
throws MessagingException {
LocalStore localStore = account.getLocalStore();
List<Message> messagesInThreads = new ArrayList<Message>();
for (Message message : messages) {
long rootId = ((LocalMessage) message).getRootId();
long threadId = (rootId == -1) ? message.getId() : rootId;
Message[] messagesInThread = localStore.getMessagesInThread(threadId);
Collections.addAll(messagesInThreads, messagesInThread);
}
return messagesInThreads;
}
public void deleteMessages(final List<Message> messages, final MessagingListener listener) {
actOnMessages(messages, new MessageActor() {
@Override
public void act(final Account account, final Folder folder,
final List<Message> accountMessages) {
for (Message message : accountMessages) {
suppressMessage(account, folder.getName(), message);
}
putBackground("deleteMessages", null, new Runnable() {
@Override
public void run() {
deleteMessagesSynchronous(account, folder.getName(), messages.toArray(EMPTY_MESSAGE_ARRAY), listener);
deleteMessagesSynchronous(account, folder.getName(),
accountMessages.toArray(EMPTY_MESSAGE_ARRAY), listener);
}
});
}
@ -5024,7 +4958,7 @@ public class MessagingController implements Runnable {
}
private void actOnMessages(Message[] messages, MessageActor actor) {
private void actOnMessages(List<Message> messages, MessageActor actor) {
Map<Account, Map<Folder, List<Message>>> accountMap = new HashMap<Account, Map<Folder, List<Message>>>();
for (Message message : messages) {

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
package com.fsck.k9.fragment;
import java.io.File;
import java.util.Collections;
import android.app.Activity;
import android.content.Context;
@ -274,7 +275,7 @@ public class MessageViewFragment extends SherlockFragment implements OnClickList
mMenu.findItem(R.id.delete).setEnabled(false);
Message messageToDelete = mMessage;
mFragmentListener.showNextMessageOrReturn();
mController.deleteMessages(new Message[] {messageToDelete}, null);
mController.deleteMessages(Collections.singletonList(messageToDelete), null);
}
}

View File

@ -0,0 +1,463 @@
/*
* Copyright (C) 2012 The K-9 Dog Walkers
* Copyright (C) 2006 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.fsck.k9.helper;
import java.util.Comparator;
import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.database.CharArrayBuffer;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.net.Uri;
import android.os.Bundle;
/**
* This class can be used to combine multiple {@link Cursor}s into one.
*/
public class MergeCursor implements Cursor {
/**
* List of the cursors combined in this object.
*/
protected final Cursor[] mCursors;
/**
* The currently active cursor.
*/
protected Cursor mActiveCursor;
/**
* The index of the currently active cursor in {@link #mCursors}.
*
* @see #mActiveCursor
*/
protected int mActiveCursorIndex;
/**
* The cursor's current position.
*/
protected int mPosition;
/**
* Used to cache the value of {@link #getCount()}.
*/
private int mCount = -1;
/**
* The comparator that is used to decide how the individual cursors are merged.
*/
private final Comparator<Cursor> mComparator;
/**
* Constructor
*
* @param cursors
* The list of cursors this {@code MultiCursor} should combine.
* @param comparator
* A comparator that is used to decide in what order the individual cursors are merged.
*/
public MergeCursor(Cursor[] cursors, Comparator<Cursor> comparator) {
mCursors = cursors.clone();
mComparator = comparator;
resetCursors();
}
private void resetCursors() {
mActiveCursorIndex = -1;
mActiveCursor = null;
mPosition = -1;
for (int i = 0, len = mCursors.length; i < len; i++) {
Cursor cursor = mCursors[i];
if (cursor != null) {
cursor.moveToPosition(-1);
if (mActiveCursor == null) {
mActiveCursorIndex = i;
mActiveCursor = mCursors[mActiveCursorIndex];
}
}
}
}
@Override
public void close() {
for (Cursor cursor : mCursors) {
if (cursor != null) {
cursor.close();
}
}
}
@Override
public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
mActiveCursor.copyStringToBuffer(columnIndex, buffer);
}
@Override
public void deactivate() {
for (Cursor cursor : mCursors) {
if (cursor != null) {
cursor.deactivate();
}
}
}
@Override
public byte[] getBlob(int columnIndex) {
return mActiveCursor.getBlob(columnIndex);
}
@Override
public int getColumnCount() {
return mActiveCursor.getColumnCount();
}
@Override
public int getColumnIndex(String columnName) {
return mActiveCursor.getColumnIndex(columnName);
}
@Override
public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
return mActiveCursor.getColumnIndexOrThrow(columnName);
}
@Override
public String getColumnName(int columnIndex) {
return mActiveCursor.getColumnName(columnIndex);
}
@Override
public String[] getColumnNames() {
return mActiveCursor.getColumnNames();
}
@Override
public int getCount() {
// CursorLoaders seem to call getCount() a lot. So we're caching the aggregated count.
if (mCount == -1) {
int count = 0;
for (Cursor cursor : mCursors) {
if (cursor != null) {
count += cursor.getCount();
}
}
mCount = count;
}
return mCount;
}
@Override
public double getDouble(int columnIndex) {
return mActiveCursor.getDouble(columnIndex);
}
@Override
public float getFloat(int columnIndex) {
return mActiveCursor.getFloat(columnIndex);
}
@Override
public int getInt(int columnIndex) {
return mActiveCursor.getInt(columnIndex);
}
@Override
public long getLong(int columnIndex) {
return mActiveCursor.getLong(columnIndex);
}
@Override
public int getPosition() {
return mPosition;
}
@Override
public short getShort(int columnIndex) {
return mActiveCursor.getShort(columnIndex);
}
@Override
public String getString(int columnIndex) {
return mActiveCursor.getString(columnIndex);
}
@TargetApi(11)
@Override
public int getType(int columnIndex) {
return mActiveCursor.getType(columnIndex);
}
@Override
public boolean getWantsAllOnMoveCalls() {
return mActiveCursor.getWantsAllOnMoveCalls();
}
@Override
public boolean isAfterLast() {
int count = getCount();
if (count == 0) {
return true;
}
return (mPosition == count);
}
@Override
public boolean isBeforeFirst() {
if (getCount() == 0) {
return true;
}
return (mPosition == -1);
}
@Override
public boolean isClosed() {
return mActiveCursor.isClosed();
}
@Override
public boolean isFirst() {
if (getCount() == 0) {
return false;
}
return (mPosition == 0);
}
@Override
public boolean isLast() {
int count = getCount();
if (count == 0) {
return false;
}
return (mPosition == (count - 1));
}
@Override
public boolean isNull(int columnIndex) {
return mActiveCursor.isNull(columnIndex);
}
@Override
public boolean move(int offset) {
return moveToPosition(mPosition + offset);
}
@Override
public boolean moveToFirst() {
return moveToPosition(0);
}
@Override
public boolean moveToLast() {
return moveToPosition(getCount() - 1);
}
@Override
public boolean moveToNext() {
int count = getCount();
if (mPosition == count) {
return false;
}
if (mPosition == count - 1) {
mActiveCursor.moveToNext();
mPosition++;
return false;
}
int smallest = -1;
for (int i = 0, len = mCursors.length; i < len; i++) {
if (mCursors[i] == null || mCursors[i].getCount() == 0 || mCursors[i].isLast()) {
continue;
}
if (smallest == -1) {
smallest = i;
mCursors[smallest].moveToNext();
continue;
}
Cursor left = mCursors[smallest];
Cursor right = mCursors[i];
right.moveToNext();
int result = mComparator.compare(left, right);
if (result > 0) {
smallest = i;
left.moveToPrevious();
} else {
right.moveToPrevious();
}
}
mPosition++;
if (smallest != -1) {
mActiveCursorIndex = smallest;
mActiveCursor = mCursors[mActiveCursorIndex];
}
return true;
}
@Override
public boolean moveToPosition(int position) {
// Make sure position isn't past the end of the cursor
final int count = getCount();
if (position >= count) {
mPosition = count;
return false;
}
// Make sure position isn't before the beginning of the cursor
if (position < 0) {
mPosition = -1;
return false;
}
// Check for no-op moves, and skip the rest of the work for them
if (position == mPosition) {
return true;
}
if (position > mPosition) {
for (int i = 0, end = position - mPosition; i < end; i++) {
if (!moveToNext()) {
return false;
}
}
} else {
for (int i = 0, end = mPosition - position; i < end; i++) {
if (!moveToPrevious()) {
return false;
}
}
}
return true;
}
@Override
public boolean moveToPrevious() {
if (mPosition < 0) {
return false;
}
mActiveCursor.moveToPrevious();
if (mPosition == 0) {
mPosition = -1;
return false;
}
int greatest = -1;
for (int i = 0, len = mCursors.length; i < len; i++) {
if (mCursors[i] == null || mCursors[i].isBeforeFirst()) {
continue;
}
if (greatest == -1) {
greatest = i;
continue;
}
Cursor left = mCursors[greatest];
Cursor right = mCursors[i];
int result = mComparator.compare(left, right);
if (result <= 0) {
greatest = i;
}
}
mPosition--;
if (greatest != -1) {
mActiveCursorIndex = greatest;
mActiveCursor = mCursors[mActiveCursorIndex];
}
return true;
}
@Override
public void registerContentObserver(ContentObserver observer) {
for (Cursor cursor : mCursors) {
cursor.registerContentObserver(observer);
}
}
@Override
public void registerDataSetObserver(DataSetObserver observer) {
for (Cursor cursor : mCursors) {
cursor.registerDataSetObserver(observer);
}
}
@Deprecated
@Override
public boolean requery() {
boolean success = true;
for (Cursor cursor : mCursors) {
success &= cursor.requery();
}
return success;
}
@Override
public void setNotificationUri(ContentResolver cr, Uri uri) {
for (Cursor cursor : mCursors) {
cursor.setNotificationUri(cr, uri);
}
}
@Override
public void unregisterContentObserver(ContentObserver observer) {
for (Cursor cursor : mCursors) {
cursor.unregisterContentObserver(observer);
}
}
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
for (Cursor cursor : mCursors) {
cursor.unregisterDataSetObserver(observer);
}
}
@Override
public Bundle getExtras() {
throw new RuntimeException("Not implemented");
}
@Override
public Bundle respond(Bundle extras) {
throw new RuntimeException("Not implemented");
}
}

View File

@ -0,0 +1,83 @@
package com.fsck.k9.helper;
import java.util.Comparator;
import android.database.Cursor;
public class MergeCursorWithUniqueId extends MergeCursor {
private static final int SHIFT = 48;
private static final long MAX_ID = (1L << SHIFT) - 1;
private static final long MAX_CURSORS = 1L << (63 - SHIFT);
private int mColumnCount = -1;
private int mIdColumnIndex = -1;
public MergeCursorWithUniqueId(Cursor[] cursors, Comparator<Cursor> comparator) {
super(cursors, comparator);
if (cursors.length > MAX_CURSORS) {
throw new IllegalArgumentException("This class only supports up to " +
MAX_CURSORS + " cursors");
}
}
@Override
public int getColumnCount() {
if (mColumnCount == -1) {
mColumnCount = super.getColumnCount();
}
return mColumnCount + 1;
}
@Override
public int getColumnIndex(String columnName) {
if ("_id".equals(columnName)) {
return getUniqueIdColumnIndex();
}
return super.getColumnIndexOrThrow(columnName);
}
@Override
public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
if ("_id".equals(columnName)) {
return getUniqueIdColumnIndex();
}
return super.getColumnIndexOrThrow(columnName);
}
@Override
public long getLong(int columnIndex) {
if (columnIndex == getUniqueIdColumnIndex()) {
long id = getPerCursorId();
if (id > MAX_ID) {
throw new RuntimeException("Sorry, " + this.getClass().getName() +
" can only handle '_id' values up to " + SHIFT + " bits.");
}
return (((long) mActiveCursorIndex) << SHIFT) + id;
}
return super.getLong(columnIndex);
}
protected int getUniqueIdColumnIndex() {
if (mColumnCount == -1) {
mColumnCount = super.getColumnCount();
}
return mColumnCount;
}
protected long getPerCursorId() {
if (mIdColumnIndex == -1) {
mIdColumnIndex = super.getColumnIndexOrThrow("_id");
}
return super.getLong(mIdColumnIndex);
}
}

View File

@ -105,4 +105,28 @@ public class MessageHelper {
mDateFormat = DateFormatter.getDateFormat(mContext);
mTodayDateFormat = android.text.format.DateFormat.getTimeFormat(mContext);
}
public CharSequence getDisplayName(Account account, Address[] fromAddrs, Address[] toAddrs) {
final Contacts contactHelper = K9.showContactName() ? Contacts.getInstance(mContext) : null;
CharSequence displayName;
if (fromAddrs.length > 0 && account.isAnIdentity(fromAddrs[0])) {
CharSequence to = Address.toFriendly(toAddrs, contactHelper);
displayName = new SpannableStringBuilder(
mContext.getString(R.string.message_to_label)).append(to);
} else {
displayName = Address.toFriendly(fromAddrs, contactHelper);
}
return displayName;
}
public boolean toMe(Account account, Address[] toAddrs) {
for (Address address : toAddrs) {
if (account.isAnIdentity(address)) {
return true;
}
}
return false;
}
}

View File

@ -1,11 +1,13 @@
package com.fsck.k9.helper;
import android.annotation.SuppressLint;
import android.app.Application;
import android.content.Context;
import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Build;
import android.text.Editable;
import android.util.Log;
import android.widget.EditText;
@ -18,7 +20,10 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -666,4 +671,57 @@ public class Utility {
return false;
}
}
private static final Pattern MESSAGE_ID = Pattern.compile("<" +
"(?:" +
"[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~]+" +
"(?:\\.[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~]+)*" +
"|" +
"\"(?:[^\\\\\"]|\\\\.)*\"" +
")" +
"@" +
"(?:" +
"[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~]+" +
"(?:\\.[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~]+)*" +
"|" +
"\\[(?:[^\\\\\\]]|\\\\.)*\\]" +
")" +
">");
public static List<String> extractMessageIds(final String text) {
List<String> messageIds = new ArrayList<String>();
Matcher matcher = MESSAGE_ID.matcher(text);
int start = 0;
while (matcher.find(start)) {
String messageId = text.substring(matcher.start(), matcher.end());
messageIds.add(messageId);
start = matcher.end();
}
return messageIds;
}
public static String extractMessageId(final String text) {
Matcher matcher = MESSAGE_ID.matcher(text);
if (matcher.find()) {
return text.substring(matcher.start(), matcher.end());
}
return null;
}
@SuppressLint("NewApi")
public static String[] copyOf(String[] original, int newLength) {
if (Build.VERSION.SDK_INT >= 9) {
return Arrays.copyOf(original, newLength);
}
String[] newArray = new String[newLength];
int copyLength = (original.length >= newLength) ? newLength : original.length;
System.arraycopy(original, 0, newArray, 0, copyLength);
return newArray;
}
}

View File

@ -143,10 +143,6 @@ public abstract class Message implements Part, Body {
return getContentType().startsWith(mimeType);
}
public abstract boolean toMe();
public abstract boolean ccMe();
public abstract boolean bccMe();
public abstract boolean fromMe();
public abstract long getId();
public abstract String getPreview();
@ -197,8 +193,6 @@ public abstract class Message implements Part, Body {
public void destroy() throws MessagingException {}
public abstract void saveChanges() throws MessagingException;
public abstract void setEncoding(String encoding) throws UnavailableStorageException;
public abstract void setCharset(String charset) throws MessagingException;
@ -213,11 +207,6 @@ public abstract class Message implements Part, Body {
return mReference;
}
public boolean equalsReference(MessageReference ref) {
MessageReference tmpReference = makeMessageReference();
return tmpReference.equals(ref);
}
public long calculateSize() {
try {

View File

@ -15,7 +15,6 @@ import java.util.UUID;
import org.apache.james.mime4j.MimeException;
import org.apache.james.mime4j.dom.field.DateTimeField;
import org.apache.james.mime4j.dom.field.ParsedField;
import org.apache.james.mime4j.field.DefaultFieldParser;
import org.apache.james.mime4j.io.EOLConvertingInputStream;
import org.apache.james.mime4j.parser.ContentHandler;
@ -23,7 +22,6 @@ import org.apache.james.mime4j.parser.MimeStreamParser;
import org.apache.james.mime4j.stream.BodyDescriptor;
import org.apache.james.mime4j.stream.Field;
import org.apache.james.mime4j.stream.MimeConfig;
import org.apache.james.mime4j.stream.RawField;
import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.Body;
@ -345,11 +343,6 @@ public class MimeMessage extends Message {
setHeader("References", references);
}
@Override
public void saveChanges() throws MessagingException {
throw new MessagingException("saveChanges not yet implemented");
}
@Override
public Body getBody() {
return mBody;
@ -593,22 +586,6 @@ public class MimeMessage extends Message {
return message;
}
public boolean toMe() {
return false;
}
public boolean ccMe() {
return false;
}
public boolean bccMe() {
return false;
}
public boolean fromMe() {
return false;
}
public long getId() {
return Long.parseLong(mUid); //or maybe .mMessageId?
}

View File

@ -1478,7 +1478,7 @@ public class ImapStore extends Store {
fetchFields.add("INTERNALDATE");
fetchFields.add("RFC822.SIZE");
fetchFields.add("BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc " +
"reply-to message-id " + K9.IDENTITY_HEADER + ")]");
"reply-to message-id references " + K9.IDENTITY_HEADER + ")]");
}
if (fp.contains(FetchProfile.Item.STRUCTURE)) {
fetchFields.add("BODYSTRUCTURE");

File diff suppressed because it is too large Load Diff

View File

@ -49,7 +49,7 @@ public class LockableDatabase {
* Workaround exception wrapper used to keep the inner exception generated
* in a {@link DbCallback}.
*/
protected static class WrappedException extends RuntimeException {
public static class WrappedException extends RuntimeException {
/**
*
*/

View File

@ -222,6 +222,9 @@ public class GlobalSettings {
s.put("useBackgroundAsUnreadIndicator", Settings.versions(
new V(19, new BooleanSetting(true))
));
s.put("threadedView", Settings.versions(
new V(20, new BooleanSetting(true))
));
SETTINGS = Collections.unmodifiableMap(s);

View File

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

View File

@ -0,0 +1,579 @@
package com.fsck.k9.provider;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.fsck.k9.Account;
import com.fsck.k9.Preferences;
import com.fsck.k9.helper.StringUtils;
import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.store.LocalStore;
import com.fsck.k9.mail.store.LockableDatabase;
import com.fsck.k9.mail.store.LockableDatabase.DbCallback;
import com.fsck.k9.mail.store.LockableDatabase.WrappedException;
import com.fsck.k9.mail.store.UnavailableStorageException;
import com.fsck.k9.search.SqlQueryBuilder;
import android.annotation.TargetApi;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.CursorWrapper;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
/**
* Content Provider used to display the message list etc.
*
* <p>
* For now this content provider is for internal use only. In the future we may allow third-party
* apps to access K-9 Mail content using this content provider.
* </p>
*/
/*
* TODO:
* - add support for account list and folder list
*/
public class EmailProvider extends ContentProvider {
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
public static final String AUTHORITY = "org.k9mail.provider.email";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY);
/*
* Constants that are used for the URI matching.
*/
private static final int MESSAGE_BASE = 0;
private static final int MESSAGES = MESSAGE_BASE;
private static final int MESSAGES_THREADED = MESSAGE_BASE + 1;
//private static final int MESSAGES_THREAD = MESSAGE_BASE + 2;
private static final String MESSAGES_TABLE = "messages";
private static final String[] MESSAGES_COLUMNS = {
MessageColumns.ID,
MessageColumns.UID,
MessageColumns.INTERNAL_DATE,
MessageColumns.SUBJECT,
MessageColumns.DATE,
MessageColumns.MESSAGE_ID,
MessageColumns.SENDER_LIST,
MessageColumns.TO_LIST,
MessageColumns.CC_LIST,
MessageColumns.BCC_LIST,
MessageColumns.REPLY_TO_LIST,
MessageColumns.FLAGS,
MessageColumns.ATTACHMENT_COUNT,
MessageColumns.FOLDER_ID,
MessageColumns.PREVIEW,
MessageColumns.THREAD_ROOT,
MessageColumns.THREAD_PARENT,
InternalMessageColumns.DELETED,
InternalMessageColumns.EMPTY,
InternalMessageColumns.TEXT_CONTENT,
InternalMessageColumns.HTML_CONTENT,
InternalMessageColumns.MIME_TYPE
};
static {
UriMatcher matcher = sUriMatcher;
matcher.addURI(AUTHORITY, "account/*/messages", MESSAGES);
matcher.addURI(AUTHORITY, "account/*/messages/threaded", MESSAGES_THREADED);
//matcher.addURI(AUTHORITY, "account/*/thread/#", MESSAGES_THREAD);
}
public interface SpecialColumns {
public static final String ACCOUNT_UUID = "account_uuid";
public static final String FOLDER_NAME = "name";
public static final String INTEGRATE = "integrate";
}
public interface MessageColumns {
public static final String ID = "id";
public static final String UID = "uid";
public static final String INTERNAL_DATE = "internal_date";
public static final String SUBJECT = "subject";
public static final String DATE = "date";
public static final String MESSAGE_ID = "message_id";
public static final String SENDER_LIST = "sender_list";
public static final String TO_LIST = "to_list";
public static final String CC_LIST = "cc_list";
public static final String BCC_LIST = "bcc_list";
public static final String REPLY_TO_LIST = "reply_to_list";
public static final String FLAGS = "flags";
public static final String ATTACHMENT_COUNT = "attachment_count";
public static final String FOLDER_ID = "folder_id";
public static final String PREVIEW = "preview";
public static final String THREAD_ROOT = "thread_root";
public static final String THREAD_PARENT = "thread_parent";
public static final String THREAD_COUNT = "thread_count";
}
private interface InternalMessageColumns extends MessageColumns {
public static final String DELETED = "deleted";
public static final String EMPTY = "empty";
public static final String TEXT_CONTENT = "text_content";
public static final String HTML_CONTENT = "html_content";
public static final String MIME_TYPE = "mime_type";
}
private Preferences mPreferences;
@Override
public boolean onCreate() {
return true;
}
@Override
public String getType(Uri uri) {
throw new RuntimeException("not implemented yet");
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
int match = sUriMatcher.match(uri);
if (match < 0) {
throw new IllegalArgumentException("Unknown URI: " + uri);
}
ContentResolver contentResolver = getContext().getContentResolver();
Cursor cursor = null;
switch (match) {
case MESSAGES:
case MESSAGES_THREADED: {
List<String> segments = uri.getPathSegments();
String accountUuid = segments.get(1);
List<String> dbColumnNames = new ArrayList<String>(projection.length);
Map<String, String> specialColumns = new HashMap<String, String>();
for (String columnName : projection) {
if (SpecialColumns.ACCOUNT_UUID.equals(columnName)) {
specialColumns.put(SpecialColumns.ACCOUNT_UUID, accountUuid);
} else {
dbColumnNames.add(columnName);
}
}
String[] dbProjection = dbColumnNames.toArray(new String[0]);
if (match == MESSAGES) {
cursor = getMessages(accountUuid, dbProjection, selection, selectionArgs,
sortOrder);
} else if (match == MESSAGES_THREADED) {
cursor = getThreadedMessages(accountUuid, dbProjection, selection,
selectionArgs, sortOrder);
} else {
throw new RuntimeException("Not implemented");
}
cursor.setNotificationUri(contentResolver, uri);
cursor = new SpecialColumnsCursor(new IdTrickeryCursor(cursor), projection,
specialColumns);
break;
}
}
return cursor;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
throw new RuntimeException("not implemented yet");
}
@Override
public Uri insert(Uri uri, ContentValues values) {
throw new RuntimeException("not implemented yet");
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
throw new RuntimeException("not implemented yet");
}
protected Cursor getMessages(String accountUuid, final String[] projection,
final String selection, final String[] selectionArgs, final String sortOrder) {
Account account = getAccount(accountUuid);
LockableDatabase database = getDatabase(account);
try {
return database.execute(false, new DbCallback<Cursor>() {
@Override
public Cursor doDbWork(SQLiteDatabase db) throws WrappedException,
UnavailableStorageException {
String where;
if (StringUtils.isNullOrEmpty(selection)) {
where = InternalMessageColumns.DELETED + "=0 AND (" +
InternalMessageColumns.EMPTY + " IS NULL OR " +
InternalMessageColumns.EMPTY + "!=1)";
} else {
where = "(" + selection + ") AND " +
InternalMessageColumns.DELETED + "=0 AND (" +
InternalMessageColumns.EMPTY + " IS NULL OR " +
InternalMessageColumns.EMPTY + "!=1)";
}
final Cursor cursor;
//TODO: check projection and selection for folder columns
if (Utility.arrayContains(projection, SpecialColumns.FOLDER_NAME)) {
StringBuilder query = new StringBuilder();
query.append("SELECT ");
boolean first = true;
for (String columnName : projection) {
if (!first) {
query.append(",");
} else {
first = false;
}
if (MessageColumns.ID.equals(columnName)) {
query.append("m.");
query.append(MessageColumns.ID);
query.append(" AS ");
query.append(MessageColumns.ID);
} else {
query.append(columnName);
}
}
query.append(" FROM messages m " +
"LEFT JOIN folders f ON (m.folder_id = f.id) " +
"WHERE ");
query.append(SqlQueryBuilder.addPrefixToSelection(MESSAGES_COLUMNS,
"m.", where));
query.append(" ORDER BY ");
query.append(SqlQueryBuilder.addPrefixToSelection(MESSAGES_COLUMNS,
"m.", sortOrder));
cursor = db.rawQuery(query.toString(), selectionArgs);
} else {
cursor = db.query(MESSAGES_TABLE, projection, where, selectionArgs, null,
null, sortOrder);
}
return cursor;
}
});
} catch (UnavailableStorageException e) {
throw new RuntimeException("Storage not available", e);
}
}
protected Cursor getThreadedMessages(String accountUuid, final String[] projection,
final String selection, final String[] selectionArgs, final String sortOrder) {
Account account = getAccount(accountUuid);
LockableDatabase database = getDatabase(account);
try {
return database.execute(false, new DbCallback<Cursor>() {
@Override
public Cursor doDbWork(SQLiteDatabase db) throws WrappedException,
UnavailableStorageException {
StringBuilder query = new StringBuilder();
query.append("SELECT ");
boolean first = true;
for (String columnName : projection) {
if (!first) {
query.append(",");
} else {
first = false;
}
if (MessageColumns.DATE.equals(columnName)) {
query.append("MAX(m.date) AS " + MessageColumns.DATE);
} else if (MessageColumns.THREAD_COUNT.equals(columnName)) {
query.append("COUNT(h.id) AS " + MessageColumns.THREAD_COUNT);
} else if (SpecialColumns.FOLDER_NAME.equals(columnName)) {
query.append("f." + SpecialColumns.FOLDER_NAME + " AS " +
SpecialColumns.FOLDER_NAME);
} else if (SpecialColumns.INTEGRATE.equals(columnName)) {
query.append("f." + SpecialColumns.INTEGRATE + " AS " +
SpecialColumns.INTEGRATE);
} else {
query.append("m.");
query.append(columnName);
query.append(" AS ");
query.append(columnName);
}
}
query.append(
" FROM messages h JOIN messages m " +
"ON (h.id = m.thread_root OR h.id = m.id) ");
//TODO: check projection and selection for folder columns
if (Utility.arrayContains(projection, SpecialColumns.FOLDER_NAME)) {
query.append("LEFT JOIN folders f ON (m.folder_id = f.id) ");
}
query.append(
"WHERE " +
"(h.deleted = 0 AND m.deleted = 0 AND " +
"(m.empty IS NULL OR m.empty != 1) AND " +
"h.thread_root IS NULL) ");
if (!StringUtils.isNullOrEmpty(selection)) {
query.append("AND (");
query.append(SqlQueryBuilder.addPrefixToSelection(MESSAGES_COLUMNS,
"h.", selection));
query.append(") ");
}
query.append("GROUP BY h.id");
if (!StringUtils.isNullOrEmpty(sortOrder)) {
query.append(" ORDER BY ");
query.append(SqlQueryBuilder.addPrefixToSelection(MESSAGES_COLUMNS,
"m.", sortOrder));
}
return db.rawQuery(query.toString(), selectionArgs);
}
});
} catch (UnavailableStorageException e) {
throw new RuntimeException("Storage not available", e);
}
}
private Account getAccount(String accountUuid) {
if (mPreferences == null) {
Context appContext = getContext().getApplicationContext();
mPreferences = Preferences.getPreferences(appContext);
}
Account account = mPreferences.getAccount(accountUuid);
if (account == null) {
throw new IllegalArgumentException("Unknown account: " + accountUuid);
}
return account;
}
private LockableDatabase getDatabase(Account account) {
LocalStore localStore;
try {
localStore = account.getLocalStore();
} catch (MessagingException e) {
throw new RuntimeException("Couldn't get LocalStore", e);
}
return localStore.getDatabase();
}
/**
* This class is needed to make {@link CursorAdapter} work with our database schema.
*
* <p>
* {@code CursorAdapter} requires a column named {@code "_id"} containing a stable id. We use
* the column name {@code "id"} as primary key in all our tables. So this {@link CursorWrapper}
* maps all queries for {@code "_id"} to {@code "id"}.
* </p><p>
* Please note that this only works for the returned {@code Cursor}. When querying the content
* provider you still need to use {@link MessageColumns#ID}.
* </p>
*/
static class IdTrickeryCursor extends CursorWrapper {
public IdTrickeryCursor(Cursor cursor) {
super(cursor);
}
@Override
public int getColumnIndex(String columnName) {
if ("_id".equals(columnName)) {
return super.getColumnIndex("id");
}
return super.getColumnIndex(columnName);
}
@Override
public int getColumnIndexOrThrow(String columnName) {
if ("_id".equals(columnName)) {
return super.getColumnIndexOrThrow("id");
}
return super.getColumnIndexOrThrow(columnName);
}
}
static class SpecialColumnsCursor extends CursorWrapper {
private int[] mColumnMapping;
private String[] mSpecialColumnValues;
private String[] mColumnNames;
public SpecialColumnsCursor(Cursor cursor, String[] allColumnNames,
Map<String, String> specialColumns) {
super(cursor);
mColumnNames = allColumnNames;
mColumnMapping = new int[allColumnNames.length];
mSpecialColumnValues = new String[specialColumns.size()];
for (int i = 0, columnIndex = 0, specialColumnCount = 0, len = allColumnNames.length;
i < len; i++) {
String columnName = allColumnNames[i];
if (specialColumns.containsKey(columnName)) {
// This is a special column name, so save the value in mSpecialColumnValues
mSpecialColumnValues[specialColumnCount] = specialColumns.get(columnName);
// Write the index into mSpecialColumnValues negated into mColumnMapping
mColumnMapping[i] = -(specialColumnCount + 1);
specialColumnCount++;
} else {
mColumnMapping[i] = columnIndex++;
}
}
}
@Override
public byte[] getBlob(int columnIndex) {
int realColumnIndex = mColumnMapping[columnIndex];
if (realColumnIndex < 0) {
throw new RuntimeException("Special column can only be retrieved as string.");
}
return super.getBlob(realColumnIndex);
}
@Override
public int getColumnCount() {
return mColumnMapping.length;
}
@Override
public int getColumnIndex(String columnName) {
for (int i = 0, len = mColumnNames.length; i < len; i++) {
if (mColumnNames[i].equals(columnName)) {
return i;
}
}
return super.getColumnIndex(columnName);
}
@Override
public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
int index = getColumnIndex(columnName);
if (index == -1) {
throw new IllegalArgumentException("Unknown column name");
}
return index;
}
@Override
public String getColumnName(int columnIndex) {
return mColumnNames[columnIndex];
}
@Override
public String[] getColumnNames() {
return mColumnNames.clone();
}
@Override
public double getDouble(int columnIndex) {
int realColumnIndex = mColumnMapping[columnIndex];
if (realColumnIndex < 0) {
throw new RuntimeException("Special column can only be retrieved as string.");
}
return super.getDouble(realColumnIndex);
}
@Override
public float getFloat(int columnIndex) {
int realColumnIndex = mColumnMapping[columnIndex];
if (realColumnIndex < 0) {
throw new RuntimeException("Special column can only be retrieved as string.");
}
return super.getFloat(realColumnIndex);
}
@Override
public int getInt(int columnIndex) {
int realColumnIndex = mColumnMapping[columnIndex];
if (realColumnIndex < 0) {
throw new RuntimeException("Special column can only be retrieved as string.");
}
return super.getInt(realColumnIndex);
}
@Override
public long getLong(int columnIndex) {
int realColumnIndex = mColumnMapping[columnIndex];
if (realColumnIndex < 0) {
throw new RuntimeException("Special column can only be retrieved as string.");
}
return super.getLong(realColumnIndex);
}
@Override
public short getShort(int columnIndex) {
int realColumnIndex = mColumnMapping[columnIndex];
if (realColumnIndex < 0) {
throw new RuntimeException("Special column can only be retrieved as string.");
}
return super.getShort(realColumnIndex);
}
@Override
public String getString(int columnIndex) {
int realColumnIndex = mColumnMapping[columnIndex];
if (realColumnIndex < 0) {
return mSpecialColumnValues[-realColumnIndex - 1];
}
return super.getString(realColumnIndex);
}
@TargetApi(11)
@Override
public int getType(int columnIndex) {
int realColumnIndex = mColumnMapping[columnIndex];
if (realColumnIndex < 0) {
return FIELD_TYPE_STRING;
}
return super.getType(realColumnIndex);
}
@Override
public boolean isNull(int columnIndex) {
int realColumnIndex = mColumnMapping[columnIndex];
if (realColumnIndex < 0) {
return (mSpecialColumnValues[-realColumnIndex - 1] == null);
}
return super.isNull(realColumnIndex);
}
}
}

View File

@ -21,23 +21,22 @@ import com.fsck.k9.Account;
import com.fsck.k9.AccountStats;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
import com.fsck.k9.SearchAccount;
import com.fsck.k9.activity.FolderInfoHolder;
import com.fsck.k9.activity.MessageInfoHolder;
import com.fsck.k9.activity.MessageList;
import com.fsck.k9.controller.MessagingController;
import com.fsck.k9.controller.MessagingListener;
import com.fsck.k9.fragment.MessageListFragment;
import com.fsck.k9.helper.MessageHelper;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Folder;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.store.LocalStore;
import com.fsck.k9.search.SearchAccount;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.concurrent.BlockingQueue;
@ -302,14 +301,13 @@ public class MessageProvider extends ContentProvider {
final SearchAccount integratedInboxAccount = SearchAccount.createUnifiedInboxAccount(getContext());
final MessagingController msgController = MessagingController.getInstance(K9.app);
msgController.searchLocalMessages(integratedInboxAccount, null,
msgController.searchLocalMessages(integratedInboxAccount.getRelatedSearch(),
new MesssageInfoHolderRetrieverListener(queue));
final List<MessageInfoHolder> holders = queue.take();
// TODO add sort order parameter
Collections.sort(holders, new MessageListFragment.ReverseComparator<MessageInfoHolder>(
new MessageListFragment.DateComparator()));
Collections.sort(holders, new ReverseDateComparator());
final String[] projectionToUse;
if (projection == null) {
@ -1025,7 +1023,8 @@ public class MessageProvider extends ContentProvider {
// launch command to delete the message
if ((myAccount != null) && (msg != null)) {
MessagingController.getInstance(K9.app).deleteMessages(new Message[] { msg }, null);
MessagingController controller = MessagingController.getInstance(K9.app);
controller.deleteMessages(Collections.singletonList(msg), null);
}
// FIXME return the actual number of deleted messages
@ -1123,4 +1122,16 @@ public class MessageProvider extends ContentProvider {
mUriMatcher.addURI(AUTHORITY, handler.getPath(), code);
}
public static class ReverseDateComparator implements Comparator<MessageInfoHolder> {
@Override
public int compare(MessageInfoHolder object2, MessageInfoHolder object1) {
if (object1.compareDate == null) {
return (object2.compareDate == null ? 0 : 1);
} else if (object2.compareDate == null) {
return -1;
} else {
return object1.compareDate.compareTo(object2.compareDate);
}
}
}
}

View File

@ -8,6 +8,7 @@ import com.fsck.k9.R;
import com.fsck.k9.activity.UnreadWidgetConfiguration;
import com.fsck.k9.activity.FolderList;
import com.fsck.k9.activity.MessageList;
import com.fsck.k9.search.LocalSearch;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
@ -62,8 +63,11 @@ public class UnreadWidgetProvider extends AppWidgetProvider {
clickIntent = FolderList.actionHandleAccountIntent(context, account, null,
false);
} else {
clickIntent = MessageList.actionHandleFolderIntent(context, account,
account.getAutoExpandFolderName());
LocalSearch search = new LocalSearch(account.getAutoExpandFolderName());
search.addAllowedFolder(account.getAutoExpandFolderName());
search.addAccountUuid(account.getUuid());
clickIntent = MessageList.intentDisplaySearch(context, search, false, true,
true);
}
clickIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
}

View File

@ -0,0 +1,431 @@
package com.fsck.k9.search;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Stack;
import android.database.Cursor;
import android.os.Parcel;
import android.os.Parcelable;
import com.fsck.k9.search.SearchSpecification.Attribute;
import com.fsck.k9.search.SearchSpecification.Searchfield;
import com.fsck.k9.search.SearchSpecification.SearchCondition;
/**
* This class stores search conditions. It's basically a boolean expression binary tree.
* The output will be SQL queries ( obtained by traversing inorder ).
*
* TODO removing conditions from the tree
* TODO implement NOT as a node again
*/
public class ConditionsTreeNode implements Parcelable {
public enum Operator {
AND, OR, CONDITION;
}
public ConditionsTreeNode mLeft;
public ConditionsTreeNode mRight;
public ConditionsTreeNode mParent;
/*
* If mValue isn't CONDITION then mCondition contains a real
* condition, otherwise it's null.
*/
public Operator mValue;
public SearchCondition mCondition;
/*
* Used for storing and retrieving the tree to/from the database.
* The algorithm is called "modified preorder tree traversal".
*/
public int mLeftMPTTMarker;
public int mRightMPTTMarker;
///////////////////////////////////////////////////////////////
// Static Helpers to restore a tree from a database cursor
///////////////////////////////////////////////////////////////
/**
* Builds a condition tree starting from a database cursor. The cursor
* should point to rows representing the nodes of the tree.
*
* @param cursor Cursor pointing to the first of a bunch or rows. Each rows
* should contains 1 tree node.
* @return A condition tree.
*/
public static ConditionsTreeNode buildTreeFromDB(Cursor cursor) {
Stack<ConditionsTreeNode> stack = new Stack<ConditionsTreeNode>();
ConditionsTreeNode tmp = null;
// root node
if (cursor.moveToFirst()) {
tmp = buildNodeFromRow(cursor);
stack.push(tmp);
}
// other nodes
while (cursor.moveToNext()) {
tmp = buildNodeFromRow(cursor);
if (tmp.mRightMPTTMarker < stack.peek().mRightMPTTMarker) {
stack.peek().mLeft = tmp;
stack.push(tmp);
} else {
while (stack.peek().mRightMPTTMarker < tmp.mRightMPTTMarker) {
stack.pop();
}
stack.peek().mRight = tmp;
}
}
return tmp;
}
/**
* Converts a single database row to a single condition node.
*
* @param cursor Cursor pointing to the row we want to convert.
* @return A single ConditionsTreeNode
*/
private static ConditionsTreeNode buildNodeFromRow(Cursor cursor) {
ConditionsTreeNode result = null;
SearchCondition condition = null;
Operator tmpValue = ConditionsTreeNode.Operator.valueOf(cursor.getString(5));
if (tmpValue == Operator.CONDITION) {
condition = new SearchCondition(Searchfield.valueOf(cursor.getString(0)),
Attribute.valueOf(cursor.getString(2)), cursor.getString(1));
}
result = new ConditionsTreeNode(condition);
result.mValue = tmpValue;
result.mLeftMPTTMarker = cursor.getInt(3);
result.mRightMPTTMarker = cursor.getInt(4);
return result;
}
///////////////////////////////////////////////////////////////
// Constructors
///////////////////////////////////////////////////////////////
public ConditionsTreeNode(SearchCondition condition) {
mParent = null;
mCondition = condition;
mValue = Operator.CONDITION;
}
public ConditionsTreeNode(ConditionsTreeNode parent, Operator op) {
mParent = parent;
mValue = op;
mCondition = null;
}
/* package */ ConditionsTreeNode cloneTree() {
if (mParent != null) {
throw new IllegalStateException("Can't call cloneTree() for a non-root node");
}
ConditionsTreeNode copy = new ConditionsTreeNode(mCondition.clone());
copy.mLeftMPTTMarker = mLeftMPTTMarker;
copy.mRightMPTTMarker = mRightMPTTMarker;
copy.mLeft = (mLeft == null) ? null : mLeft.cloneNode(copy);
copy.mRight = (mRight == null) ? null : mRight.cloneNode(copy);
return copy;
}
private ConditionsTreeNode cloneNode(ConditionsTreeNode parent) {
ConditionsTreeNode copy = new ConditionsTreeNode(parent, mValue);
copy.mCondition = mCondition.clone();
copy.mLeftMPTTMarker = mLeftMPTTMarker;
copy.mRightMPTTMarker = mRightMPTTMarker;
copy.mLeft = (mLeft == null) ? null : mLeft.cloneNode(copy);
copy.mRight = (mRight == null) ? null : mRight.cloneNode(copy);
return copy;
}
///////////////////////////////////////////////////////////////
// Public modifiers
///////////////////////////////////////////////////////////////
/**
* Adds the expression as the second argument of an AND
* clause to this node.
*
* @param expr Expression to 'AND' with.
* @return New top AND node.
* @throws Exception
*/
public ConditionsTreeNode and(ConditionsTreeNode expr) throws Exception {
return add(expr, Operator.AND);
}
/**
* Convenience method.
* Adds the provided condition as the second argument of an AND
* clause to this node.
*
* @param condition Condition to 'AND' with.
* @return New top AND node, new root.
*/
public ConditionsTreeNode and(SearchCondition condition) {
try {
ConditionsTreeNode tmp = new ConditionsTreeNode(condition);
return and(tmp);
} catch (Exception e) {
// impossible
return null;
}
}
/**
* Adds the expression as the second argument of an OR
* clause to this node.
*
* @param expr Expression to 'OR' with.
* @return New top OR node.
* @throws Exception
*/
public ConditionsTreeNode or(ConditionsTreeNode expr) throws Exception {
return add(expr, Operator.OR);
}
/**
* Convenience method.
* Adds the provided condition as the second argument of an OR
* clause to this node.
*
* @param condition Condition to 'OR' with.
* @return New top OR node, new root.
*/
public ConditionsTreeNode or(SearchCondition condition) {
try {
ConditionsTreeNode tmp = new ConditionsTreeNode(condition);
return or(tmp);
} catch (Exception e) {
// impossible
return null;
}
}
/**
* This applies the MPTT labeling to the subtree of which this node
* is the root node.
*
* For a description on MPTT see:
* http://www.sitepoint.com/hierarchical-data-database-2/
*/
public void applyMPTTLabel() {
applyMPTTLabel(1);
}
///////////////////////////////////////////////////////////////
// Public accessors
///////////////////////////////////////////////////////////////
/**
* Returns the condition stored in this node.
* @return Condition stored in the node.
*/
public SearchCondition getCondition() {
return mCondition;
}
/**
* Get a set of all the leaves in the tree.
* @return Set of all the leaves.
*/
public HashSet<ConditionsTreeNode> getLeafSet() {
HashSet<ConditionsTreeNode> leafSet = new HashSet<ConditionsTreeNode>();
return getLeafSet(leafSet);
}
/**
* Returns a list of all the nodes in the subtree of which this node
* is the root. The list contains the nodes in a pre traversal order.
*
* @return List of all nodes in subtree in preorder.
*/
public List<ConditionsTreeNode> preorder() {
ArrayList<ConditionsTreeNode> result = new ArrayList<ConditionsTreeNode>();
Stack<ConditionsTreeNode> stack = new Stack<ConditionsTreeNode>();
stack.push(this);
while (!stack.isEmpty()) {
ConditionsTreeNode current = stack.pop();
if (current.mLeft != null) {
stack.push(current.mLeft);
}
if (current.mRight != null) {
stack.push(current.mRight);
}
result.add(current);
}
return result;
}
///////////////////////////////////////////////////////////////
// Private class logic
///////////////////////////////////////////////////////////////
/**
* Adds two new ConditionTreeNodes, one for the operator and one for the
* new condition. The current node will end up on the same level as the
* one provided in the arguments, they will be siblings. Their common
* parent node will be one containing the operator provided in the arguments.
* The method will update all the required references so the tree ends up in
* a valid state.
*
* This method only supports node arguments with a null parent node.
*
* @param Node to add.
* @param Operator that will connect the new node with this one.
* @return New parent node, containing the operator.
* @throws Exception Throws when the provided new node does not have a null parent.
*/
private ConditionsTreeNode add(ConditionsTreeNode node, Operator op) throws Exception {
if (node.mParent != null) {
throw new Exception("Can only add new expressions from root node down.");
}
ConditionsTreeNode tmpNode = new ConditionsTreeNode(mParent, op);
tmpNode.mLeft = this;
tmpNode.mRight = node;
if (mParent != null) {
mParent.updateChild(this, tmpNode);
}
this.mParent = tmpNode;
node.mParent = tmpNode;
return tmpNode;
}
/**
* Helper method that replaces a child of the current node with a new node.
* If the provided old child node was the left one, left will be replaced with
* the new one. Same goes for the right one.
*
* @param oldChild Old child node to be replaced.
* @param newChild New child node.
*/
private void updateChild(ConditionsTreeNode oldChild, ConditionsTreeNode newChild) {
// we can compare objects id's because this is the desired behaviour in this case
if (mLeft == oldChild) {
mLeft = newChild;
} else if (mRight == oldChild) {
mRight = newChild;
}
}
/**
* Recursive function to gather all the leaves in the subtree of which
* this node is the root.
*
* @param leafSet Leafset that's being built.
* @return Set of leaves being completed.
*/
private HashSet<ConditionsTreeNode> getLeafSet(HashSet<ConditionsTreeNode> leafSet) {
if (mLeft == null && mRight == null) {
// if we ended up in a leaf, add ourself and return
leafSet.add(this);
return leafSet;
}
// we didn't end up in a leaf
if (mLeft != null) {
mLeft.getLeafSet(leafSet);
}
if (mRight != null) {
mRight.getLeafSet(leafSet);
}
return leafSet;
}
/**
* This applies the MPTT labeling to the subtree of which this node
* is the root node.
*
* For a description on MPTT see:
* http://www.sitepoint.com/hierarchical-data-database-2/
*/
private int applyMPTTLabel(int label) {
mLeftMPTTMarker = label;
if (mLeft != null) {
label = mLeft.applyMPTTLabel(label += 1);
}
if (mRight != null) {
label = mRight.applyMPTTLabel(label += 1);
}
++label;
mRightMPTTMarker = label;
return label;
}
///////////////////////////////////////////////////////////////
// Parcelable
//
// This whole class has to be parcelable because it's passed
// on through intents.
///////////////////////////////////////////////////////////////
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mValue.ordinal());
dest.writeParcelable(mCondition, flags);
dest.writeParcelable(mLeft, flags);
dest.writeParcelable(mRight, flags);
}
public static final Parcelable.Creator<ConditionsTreeNode> CREATOR =
new Parcelable.Creator<ConditionsTreeNode>() {
@Override
public ConditionsTreeNode createFromParcel(Parcel in) {
return new ConditionsTreeNode(in);
}
@Override
public ConditionsTreeNode[] newArray(int size) {
return new ConditionsTreeNode[size];
}
};
private ConditionsTreeNode(Parcel in) {
mValue = Operator.values()[in.readInt()];
mCondition = in.readParcelable(ConditionsTreeNode.class.getClassLoader());
mLeft = in.readParcelable(ConditionsTreeNode.class.getClassLoader());
mRight = in.readParcelable(ConditionsTreeNode.class.getClassLoader());
mParent = null;
if (mLeft != null) {
mLeft.mParent = this;
}
if (mRight != null) {
mRight.mParent = this;
}
}
}

View File

@ -0,0 +1,428 @@
package com.fsck.k9.search;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import android.os.Parcel;
import android.os.Parcelable;
import com.fsck.k9.mail.Flag;
/**
* This class represents a local search.
*
* Removing conditions could be done through matching there unique id in the leafset and then
* removing them from the tree.
*
* TODO implement a complete addAllowedFolder method
* TODO conflicting conditions check on add
* TODO duplicate condition checking?
* TODO assign each node a unique id that's used to retrieve it from the leaveset and remove.
*
*/
public class LocalSearch implements SearchSpecification {
private String mName;
private boolean mPredefined;
private boolean mManualSearch = false;
// since the uuid isn't in the message table it's not in the tree neither
private HashSet<String> mAccountUuids = new HashSet<String>();
private ConditionsTreeNode mConditions = null;
private HashSet<ConditionsTreeNode> mLeafSet = new HashSet<ConditionsTreeNode>();
///////////////////////////////////////////////////////////////
// Constructors
///////////////////////////////////////////////////////////////
/**
* Use this only if the search won't be saved. Saved searches need
* a name!
*/
public LocalSearch() {}
/**
*
* @param name
*/
public LocalSearch(String name) {
this.mName = name;
}
/**
* Use this constructor when you know what you'r doing. Normally it's only used
* when restoring these search objects from the database.
*
* @param name Name of the search
* @param searchConditions SearchConditions, may contains flags and folders
* @param accounts Relative Account's uuid's
* @param predefined Is this a predefined search or a user created one?
*/
protected LocalSearch(String name, ConditionsTreeNode searchConditions,
String accounts, boolean predefined) {
this(name);
mConditions = searchConditions;
mPredefined = predefined;
mLeafSet = new HashSet<ConditionsTreeNode>();
if (mConditions != null) {
mLeafSet.addAll(mConditions.getLeafSet());
}
// initialize accounts
if (accounts != null) {
for (String account : accounts.split(",")) {
mAccountUuids.add(account);
}
} else {
// impossible but still not unrecoverable
}
}
@Override
public LocalSearch clone() {
ConditionsTreeNode conditions = (mConditions == null) ? null : mConditions.cloneTree();
LocalSearch copy = new LocalSearch(mName, conditions, null, mPredefined);
copy.mManualSearch = mManualSearch;
copy.mAccountUuids = new HashSet<String>(mAccountUuids);
return copy;
}
///////////////////////////////////////////////////////////////
// Public manipulation methods
///////////////////////////////////////////////////////////////
/**
* Sets the name of the saved search. If one existed it will
* be overwritten.
*
* @param name Name to be set.
*/
public void setName(String name) {
this.mName = name;
}
/**
* Add a new account to the search. When no accounts are
* added manually we search all accounts on the device.
*
* @param uuid Uuid of the account to be added.
*/
public void addAccountUuid(String uuid) {
if (uuid.equals(ALL_ACCOUNTS)) {
mAccountUuids.clear();
return;
}
mAccountUuids.add(uuid);
}
/**
* Adds all the account uuids in the provided array to
* be matched by the seach.
*
* @param accountUuids
*/
public void addAccountUuids(String[] accountUuids) {
for (String acc : accountUuids) {
addAccountUuid(acc);
}
}
/**
* Removes an account UUID from the current search.
*
* @param uuid Account UUID to remove.
* @return True if removed, false otherwise.
*/
public boolean removeAccountUuid(String uuid) {
return mAccountUuids.remove(uuid);
}
/**
* Adds the provided node as the second argument of an AND
* clause to this node.
*
* @param field Message table field to match against.
* @param string Value to look for.
* @param contains Attribute to use when matching.
*
* @throws IllegalConditionException
*/
public void and(Searchfield field, String value, Attribute attribute) {
and(new SearchCondition(field, attribute, value));
}
/**
* Adds the provided condition as the second argument of an AND
* clause to this node.
*
* @param condition Condition to 'AND' with.
* @return New top AND node, new root.
*/
public ConditionsTreeNode and(SearchCondition condition) {
try {
ConditionsTreeNode tmp = new ConditionsTreeNode(condition);
return and(tmp);
} catch (Exception e) {
// impossible
return null;
}
}
/**
* Adds the provided node as the second argument of an AND
* clause to this node.
*
* @param node Node to 'AND' with.
* @return New top AND node, new root.
* @throws Exception
*/
public ConditionsTreeNode and(ConditionsTreeNode node) throws Exception {
mLeafSet.addAll(node.getLeafSet());
if (mConditions == null) {
mConditions = node;
return node;
}
mConditions = mConditions.and(node);
return mConditions;
}
/**
* Adds the provided condition as the second argument of an OR
* clause to this node.
*
* @param condition Condition to 'OR' with.
* @return New top OR node, new root.
*/
public ConditionsTreeNode or(SearchCondition condition) {
try {
ConditionsTreeNode tmp = new ConditionsTreeNode(condition);
return or(tmp);
} catch (Exception e) {
// impossible
return null;
}
}
/**
* Adds the provided node as the second argument of an OR
* clause to this node.
*
* @param node Node to 'OR' with.
* @return New top OR node, new root.
* @throws Exception
*/
public ConditionsTreeNode or(ConditionsTreeNode node) throws Exception {
mLeafSet.addAll(node.getLeafSet());
if (mConditions == null) {
mConditions = node;
return node;
}
mConditions = mConditions.or(node);
return mConditions;
}
/**
* Add all the flags to this node as required flags. The
* provided flags will be combined using AND with the root.
*
* @param requiredFlags Array of required flags.
*/
public void allRequiredFlags(Flag[] requiredFlags) {
if (requiredFlags != null) {
for (Flag f : requiredFlags) {
and(new SearchCondition(Searchfield.FLAG, Attribute.CONTAINS, f.name()));
}
}
}
/**
* Add all the flags to this node as forbidden flags. The
* provided flags will be combined using AND with the root.
*
* @param forbiddenFlags Array of forbidden flags.
*/
public void allForbiddenFlags(Flag[] forbiddenFlags) {
if (forbiddenFlags != null) {
for (Flag f : forbiddenFlags) {
and(new SearchCondition(Searchfield.FLAG, Attribute.NOT_CONTAINS, f.name()));
}
}
}
/**
* TODO
* FOR NOW: And the folder with the root.
*
* Add the folder as another folder to search in. The folder
* will be added AND to the root if no 'folder subtree' was found.
* Otherwise the folder will be added OR to that tree.
*
* @param name Name of the folder to add.
*/
public void addAllowedFolder(String name) {
/*
* TODO find folder sub-tree
* - do and on root of it & rest of search
* - do or between folder nodes
*/
mConditions = and(new SearchCondition(Searchfield.FOLDER, Attribute.EQUALS, name));
}
/*
* TODO make this more advanced!
* This is a temporarely solution that does NOT WORK for
* real searches because of possible extra conditions to a folder requirement.
*/
public List<String> getFolderNames() {
ArrayList<String> results = new ArrayList<String>();
for (ConditionsTreeNode node : mLeafSet) {
if (node.mCondition.field == Searchfield.FOLDER &&
node.mCondition.attribute == Attribute.EQUALS) {
results.add(node.mCondition.value);
}
}
return results;
}
/**
* Gets the leafset of the related condition tree.
*
* @return All the leaf conditions as a set.
*/
public Set<ConditionsTreeNode> getLeafSet() {
return mLeafSet;
}
///////////////////////////////////////////////////////////////
// Public accesor methods
///////////////////////////////////////////////////////////////
/**
* TODO THIS HAS TO GO!!!!
* very dirty fix for remotesearch support atm
*/
public String getRemoteSearchArguments() {
Set<ConditionsTreeNode> leafSet = getLeafSet();
if (leafSet == null) {
return null;
}
for (ConditionsTreeNode node : leafSet) {
if (node.getCondition().field == Searchfield.SUBJECT ||
node.getCondition().field == Searchfield.SENDER ) {
return node.getCondition().value;
}
}
return null;
}
/**
* Returns the name of the saved search.
*
* @return Name of the search.
*/
@Override
public String getName() {
return (mName == null) ? "" : mName;
}
/**
* Checks if this search was hard coded and shipped with K-9
*
* @return True is search was shipped with K-9
*/
public boolean isPredefined() {
return mPredefined;
}
public boolean isManualSearch() {
return mManualSearch;
}
public void setManualSearch(boolean manualSearch) {
mManualSearch = manualSearch;
}
/**
* Returns all the account uuids that this search will try to
* match against.
*
* @return Array of account uuids.
*/
@Override
public String[] getAccountUuids() {
if (mAccountUuids.size() == 0) {
return new String[] { SearchSpecification.ALL_ACCOUNTS };
}
String[] tmp = new String[mAccountUuids.size()];
mAccountUuids.toArray(tmp);
return tmp;
}
/**
* Returns whether or not to search all accounts.
*
* @return {@code true} if all accounts should be searched.
*/
public boolean searchAllAccounts() {
return (mAccountUuids.size() == 0);
}
/**
* Get the condition tree.
*
* @return The root node of the related conditions tree.
*/
@Override
public ConditionsTreeNode getConditions() {
return mConditions;
}
///////////////////////////////////////////////////////////////
// Parcelable
///////////////////////////////////////////////////////////////
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mName);
dest.writeByte((byte) (mPredefined ? 1 : 0));
dest.writeByte((byte) (mManualSearch ? 1 : 0));
dest.writeStringList(new ArrayList<String>(mAccountUuids));
dest.writeParcelable(mConditions, flags);
}
public static final Parcelable.Creator<LocalSearch> CREATOR =
new Parcelable.Creator<LocalSearch>() {
@Override
public LocalSearch createFromParcel(Parcel in) {
return new LocalSearch(in);
}
@Override
public LocalSearch[] newArray(int size) {
return new LocalSearch[size];
}
};
public LocalSearch(Parcel in) {
mName = in.readString();
mPredefined = (in.readByte() == 1);
mManualSearch = (in.readByte() == 1);
mAccountUuids.addAll(in.createStringArrayList());
mConditions = in.readParcelable(LocalSearch.class.getClassLoader());
mLeafSet = (mConditions == null) ? null : mConditions.getLeafSet();
}
}

View File

@ -0,0 +1,91 @@
package com.fsck.k9.search;
import java.util.UUID;
import android.content.Context;
import com.fsck.k9.BaseAccount;
import com.fsck.k9.R;
import com.fsck.k9.search.SearchSpecification.Attribute;
import com.fsck.k9.search.SearchSpecification.Searchfield;
/**
* This class is basically a wrapper around a LocalSearch. It allows to expose it as
* an account. This is a meta-account containing all the e-mail that matches the search.
*/
public class SearchAccount implements BaseAccount {
// create the all messages search ( all accounts is default when none specified )
public static SearchAccount createAllMessagesAccount(Context context) {
String name = context.getString(R.string.search_all_messages_title);
LocalSearch tmpSearch = new LocalSearch(name);
return new SearchAccount(tmpSearch, name,
context.getString(R.string.search_all_messages_detail));
}
// create the unified inbox meta account ( all accounts is default when none specified )
public static SearchAccount createUnifiedInboxAccount(Context context) {
String name = context.getString(R.string.integrated_inbox_title);
LocalSearch tmpSearch = new LocalSearch(name);
tmpSearch.and(Searchfield.INTEGRATE, "1", Attribute.EQUALS);
return new SearchAccount(tmpSearch, name,
context.getString(R.string.integrated_inbox_detail));
}
private String mEmail;
private String mDescription;
private LocalSearch mSearch;
private String mFakeUuid;
public SearchAccount(LocalSearch search, String description, String email)
throws IllegalArgumentException {
if (search == null) {
throw new IllegalArgumentException("Provided LocalSearch was null");
}
mSearch = search;
mDescription = description;
mEmail = email;
}
@Override
public synchronized String getEmail() {
return mEmail;
}
@Override
public synchronized void setEmail(String email) {
this.mEmail = email;
}
@Override
public String getDescription() {
return mDescription;
}
@Override
public void setDescription(String description) {
this.mDescription = description;
}
public LocalSearch getRelatedSearch() {
return mSearch;
}
/*
* This will only be used when accessed as an Account. If that
* is the case we don't want to return the uuid of a real account since
* this is posing as a fake meta-account. If this object is accesed as
* a Search then methods from LocalSearch will be called which do handle
* things nice.
*/
@Override
public String getUuid() {
if (mFakeUuid == null) {
mFakeUuid = UUID.randomUUID().toString();
}
return mFakeUuid;
}
}

View File

@ -0,0 +1,23 @@
package com.fsck.k9.search;
import com.fsck.k9.R;
import com.fsck.k9.mail.Flag;
/**
* This enum represents filtering parameters used by {@link com.fsck.k9.search.SearchAccount}.
*/
public enum SearchModifier {
FLAGGED(R.string.flagged_modifier, new Flag[] { Flag.FLAGGED }, null),
UNREAD(R.string.unread_modifier, null, new Flag[] { Flag.SEEN });
public final int resId;
public final Flag[] requiredFlags;
public final Flag[] forbiddenFlags;
SearchModifier(int nResId, Flag[] nRequiredFlags, Flag[] nForbiddenFlags) {
resId = nResId;
requiredFlags = nRequiredFlags;
forbiddenFlags = nForbiddenFlags;
}
}

View File

@ -0,0 +1,163 @@
package com.fsck.k9.search;
import android.os.Parcel;
import android.os.Parcelable;
public interface SearchSpecification extends Parcelable {
/**
* Get all the uuids of accounts this search acts on.
* @return Array of uuids.
*/
public String[] getAccountUuids();
/**
* Returns the search's name if it was named.
* @return Name of the search.
*/
public String getName();
/**
* Returns the root node of the condition tree accompanying
* the search.
*
* @return Root node of conditions tree.
*/
public ConditionsTreeNode getConditions();
/*
* Some meta names for certain conditions.
*/
public static final String ALL_ACCOUNTS = "allAccounts";
///////////////////////////////////////////////////////////////
// ATTRIBUTE enum
///////////////////////////////////////////////////////////////
public enum Attribute {
CONTAINS,
NOT_CONTAINS,
EQUALS,
NOT_EQUALS,
STARTSWITH,
NOT_STARTSWITH,
ENDSWITH,
NOT_ENDSWITH
}
///////////////////////////////////////////////////////////////
// SEARCHFIELD enum
///////////////////////////////////////////////////////////////
/*
* Using an enum in order to have more robust code. Users ( & coders )
* are prevented from passing illegal fields. No database overhead
* when invalid fields passed.
*
* By result, only the fields in here are searchable.
*
* Fields not in here at this moment ( and by effect not searchable ):
* id, html_content, internal_date, message_id,
* preview, mime_type
*
*/
public enum Searchfield {
SUBJECT,
DATE,
UID,
FLAG,
SENDER,
TO,
CC,
FOLDER,
BCC,
REPLY_TO,
MESSAGE_CONTENTS,
ATTACHMENT_COUNT,
DELETED,
THREAD_ROOT,
ID,
INTEGRATE
}
///////////////////////////////////////////////////////////////
// SearchCondition class
///////////////////////////////////////////////////////////////
/**
* This class represents 1 value for a certain search field. One
* value consists of three things:
* an attribute: equals, starts with, contains,...
* a searchfield: date, flags, sender, subject,...
* a value: "apple", "jesse",..
*
* @author dzan
*/
public class SearchCondition implements Parcelable {
public final String value;
public final Attribute attribute;
public final Searchfield field;
public SearchCondition(Searchfield field, Attribute attribute, String value) {
this.value = value;
this.attribute = attribute;
this.field = field;
}
private SearchCondition(Parcel in) {
this.value = in.readString();
this.attribute = Attribute.values()[in.readInt()];
this.field = Searchfield.values()[in.readInt()];
}
@Override
public SearchCondition clone() {
return new SearchCondition(field, attribute, value);
}
public String toHumanString() {
return field.toString() + attribute.toString();
}
@Override
public boolean equals(Object o) {
if (o instanceof SearchCondition) {
SearchCondition tmp = (SearchCondition) o;
if (tmp.attribute == attribute &&
tmp.field == field &&
tmp.value.equals(value)) {
return true;
}
}
return false;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(value);
dest.writeInt(attribute.ordinal());
dest.writeInt(field.ordinal());
}
public static final Parcelable.Creator<SearchCondition> CREATOR =
new Parcelable.Creator<SearchCondition>() {
@Override
public SearchCondition createFromParcel(Parcel in) {
return new SearchCondition(in);
}
@Override
public SearchCondition[] newArray(int size) {
return new SearchCondition[size];
}
};
}
}

View File

@ -0,0 +1,232 @@
package com.fsck.k9.search;
import java.util.List;
import com.fsck.k9.Account;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Folder.OpenMode;
import com.fsck.k9.mail.store.LocalStore;
import com.fsck.k9.mail.store.LocalStore.LocalFolder;
import com.fsck.k9.search.SearchSpecification.SearchCondition;
import com.fsck.k9.search.SearchSpecification.Searchfield;
public class SqlQueryBuilder {
public static void buildWhereClause(Account account, ConditionsTreeNode node,
StringBuilder query, List<String> selectionArgs) {
buildWhereClauseInternal(account, node, query, selectionArgs);
}
private static void buildWhereClauseInternal(Account account, ConditionsTreeNode node,
StringBuilder query, List<String> selectionArgs) {
if (node == null) {
return;
}
if (node.mLeft == null && node.mRight == null) {
SearchCondition condition = node.mCondition;
switch (condition.field) {
case FOLDER: {
String folderName = condition.value;
long folderId = getFolderId(account, folderName);
query.append("folder_id = ?");
selectionArgs.add(Long.toString(folderId));
break;
}
default: {
appendCondition(condition, query, selectionArgs);
}
}
} else {
query.append("(");
buildWhereClauseInternal(account, node.mLeft, query, selectionArgs);
query.append(") ");
query.append(node.mValue.name());
query.append(" (");
buildWhereClauseInternal(account, node.mRight, query, selectionArgs);
query.append(")");
}
}
private static void appendCondition(SearchCondition condition, StringBuilder query,
List<String> selectionArgs) {
query.append(getColumnName(condition));
appendExprRight(condition, query, selectionArgs);
}
private static long getFolderId(Account account, String folderName) {
long folderId = 0;
try {
LocalStore localStore = account.getLocalStore();
LocalFolder folder = localStore.getFolder(folderName);
folder.open(OpenMode.READ_ONLY);
folderId = folder.getId();
} catch (MessagingException e) {
//FIXME
e.printStackTrace();
}
return folderId;
}
private static String getColumnName(SearchCondition condition) {
String columnName = null;
switch (condition.field) {
case ATTACHMENT_COUNT: {
columnName = "attachment_count";
break;
}
case BCC: {
columnName = "bcc_list";
break;
}
case CC: {
columnName = "cc_list";
break;
}
case DATE: {
columnName = "date";
break;
}
case DELETED: {
columnName = "deleted";
break;
}
case FLAG: {
columnName = "flags";
break;
}
case FOLDER: {
columnName = "folder_id";
break;
}
case ID: {
columnName = "id";
break;
}
case MESSAGE_CONTENTS: {
columnName = "text_content";
break;
}
case REPLY_TO: {
columnName = "reply_to_list";
break;
}
case SENDER: {
columnName = "sender_list";
break;
}
case SUBJECT: {
columnName = "subject";
break;
}
case THREAD_ROOT: {
columnName = "thread_root";
break;
}
case TO: {
columnName = "to_list";
break;
}
case UID: {
columnName = "uid";
break;
}
case INTEGRATE: {
columnName = "integrate";
break;
}
}
if (columnName == null) {
throw new RuntimeException("Unhandled case");
}
return columnName;
}
private static void appendExprRight(SearchCondition condition, StringBuilder query,
List<String> selectionArgs) {
String value = condition.value;
Searchfield field = condition.field;
query.append(" ");
String selectionArg = null;
switch (condition.attribute) {
case NOT_CONTAINS:
query.append("NOT ");
//$FALL-THROUGH$
case CONTAINS: {
query.append("LIKE ?");
selectionArg = "%" + value + "%";
break;
}
case NOT_STARTSWITH:
query.append("NOT ");
//$FALL-THROUGH$
case STARTSWITH: {
query.append("LIKE ?");
selectionArg = "%" + value;
break;
}
case NOT_ENDSWITH:
query.append("NOT ");
//$FALL-THROUGH$
case ENDSWITH: {
query.append("LIKE ?");
selectionArg = value + "%";
break;
}
case NOT_EQUALS: {
if (isNumberColumn(field)) {
query.append("!= ?");
} else {
query.append("NOT LIKE ?");
}
selectionArg = value;
break;
}
case EQUALS: {
if (isNumberColumn(field)) {
query.append("= ?");
} else {
query.append("LIKE ?");
}
selectionArg = value;
break;
}
}
if (selectionArg == null) {
throw new RuntimeException("Unhandled case");
}
selectionArgs.add(selectionArg);
}
private static boolean isNumberColumn(Searchfield field) {
switch (field) {
case ATTACHMENT_COUNT:
case DATE:
case DELETED:
case FOLDER:
case ID:
case INTEGRATE:
case THREAD_ROOT: {
return true;
}
default: {
return false;
}
}
}
public static String addPrefixToSelection(String[] columnNames, String prefix, String selection) {
String result = selection;
for (String columnName : columnNames) {
result = result.replaceAll("\\b" + columnName + "\\b", prefix + columnName);
}
return result;
}
}