mirror of
https://github.com/moparisthebest/k-9
synced 2024-11-23 18:02:15 -05:00
Merge branch 'content_provider'
This commit is contained in:
commit
08615f205f
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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"/>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -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() };
|
||||
search = new LocalSearch(description);
|
||||
search.addAccountUuid(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.allRequiredFlags(searchModifier.requiredFlags);
|
||||
search.allForbiddenFlags(searchModifier.forbiddenFlags);
|
||||
MessageList.actionDisplaySearch(Accounts.this, search, true, false);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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() };
|
||||
LocalSearch search = new LocalSearch(description);
|
||||
try {
|
||||
search.allRequiredFlags(searchModifier.requiredFlags);
|
||||
search.allForbiddenFlags(searchModifier.forbiddenFlags);
|
||||
} catch (Exception e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flag[] getForbiddenFlags() {
|
||||
return searchModifier.forbiddenFlags;
|
||||
search.addAllowedFolder(folderName);
|
||||
search.addAccountUuid(account.getUuid());
|
||||
MessageList.actionDisplaySearch(FolderList.this, search, true, false);
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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() };
|
||||
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();
|
||||
}
|
||||
|
||||
//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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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,84 +46,40 @@ 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 actionDisplaySearch(Context context, SearchSpecification search,
|
||||
boolean noThreading, boolean newTask) {
|
||||
actionDisplaySearch(context, search, noThreading, newTask, true);
|
||||
}
|
||||
|
||||
public static void actionHandleFolder(Context context, Account account, String folder) {
|
||||
public static void actionDisplaySearch(Context context, SearchSpecification search,
|
||||
boolean noThreading, boolean newTask, boolean clearTop) {
|
||||
context.startActivity(
|
||||
intentDisplaySearch(context, search, noThreading, newTask, clearTop));
|
||||
}
|
||||
|
||||
public static Intent intentDisplaySearch(Context context, SearchSpecification search,
|
||||
boolean noThreading, boolean newTask, boolean clearTop) {
|
||||
Intent intent = new Intent(context, MessageList.class);
|
||||
intent.putExtra(EXTRA_SEARCH, search);
|
||||
intent.putExtra(EXTRA_NO_THREADING, noThreading);
|
||||
|
||||
if (clearTop) {
|
||||
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 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 actionHandle(Context context, String title, String queryString, boolean integrate, Flag[] flags, Flag[] forbiddenFlags) {
|
||||
Intent intent = new Intent(context, MessageList.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
intent.putExtra(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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (searchSpecification.getForbiddenFlags() != null) {
|
||||
intent.putExtra(EXTRA_FORBIDDEN_FLAGS, Utility.combine(searchSpecification.getForbiddenFlags(), ','));
|
||||
}
|
||||
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);
|
||||
if (newTask) {
|
||||
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);
|
||||
return intent;
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
// regular LocalSearch object was passed
|
||||
mSearch = intent.getParcelableExtra(EXTRA_SEARCH);
|
||||
mNoThreading = intent.getBooleanExtra(EXTRA_NO_THREADING, false);
|
||||
}
|
||||
|
||||
String accountUuid = intent.getStringExtra(EXTRA_ACCOUNT);
|
||||
mFolderName = intent.getStringExtra(EXTRA_FOLDER);
|
||||
String[] accountUuids = mSearch.getAccountUuids();
|
||||
mSingleAccountMode = (accountUuids.length == 1 && !mSearch.searchAllAccounts());
|
||||
mSingleFolderMode = mSingleAccountMode && (mSearch.getFolderNames().size() == 1);
|
||||
|
||||
mAccount = Preferences.getPreferences(this).getAccount(accountUuid);
|
||||
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 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]);
|
||||
if (mSingleFolderMode) {
|
||||
mFolderName = mSearch.getFolderNames().get(0);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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(
|
||||
|
@ -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());
|
||||
|
@ -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,
|
||||
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);
|
||||
}
|
||||
|
||||
public void copyMessages(final Account account, final String srcFolder, final Message[] messages, final String destFolder,
|
||||
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 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) {
|
||||
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
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
463
src/com/fsck/k9/helper/MergeCursor.java
Normal file
463
src/com/fsck/k9/helper/MergeCursor.java
Normal 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");
|
||||
}
|
||||
}
|
83
src/com/fsck/k9/helper/MergeCursorWithUniqueId.java
Normal file
83
src/com/fsck/k9/helper/MergeCursorWithUniqueId.java
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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?
|
||||
}
|
||||
|
@ -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
@ -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 {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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,
|
||||
|
579
src/com/fsck/k9/provider/EmailProvider.java
Normal file
579
src/com/fsck/k9/provider/EmailProvider.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
431
src/com/fsck/k9/search/ConditionsTreeNode.java
Normal file
431
src/com/fsck/k9/search/ConditionsTreeNode.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
428
src/com/fsck/k9/search/LocalSearch.java
Normal file
428
src/com/fsck/k9/search/LocalSearch.java
Normal 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();
|
||||
}
|
||||
}
|
91
src/com/fsck/k9/search/SearchAccount.java
Normal file
91
src/com/fsck/k9/search/SearchAccount.java
Normal 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;
|
||||
}
|
||||
}
|
23
src/com/fsck/k9/search/SearchModifier.java
Normal file
23
src/com/fsck/k9/search/SearchModifier.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
163
src/com/fsck/k9/search/SearchSpecification.java
Normal file
163
src/com/fsck/k9/search/SearchSpecification.java
Normal 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];
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
232
src/com/fsck/k9/search/SqlQueryBuilder.java
Normal file
232
src/com/fsck/k9/search/SqlQueryBuilder.java
Normal 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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user