package com.fsck.k9.activity; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.EnumMap; import java.util.Iterator; import java.util.List; import java.util.Map; import android.app.AlertDialog; import android.app.Dialog; import android.app.SearchManager; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences.Editor; import android.graphics.Color; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Handler; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.style.AbsoluteSizeSpan; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.util.Log; import android.util.TypedValue; import android.view.GestureDetector; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemLongClickListener; import android.widget.BaseAdapter; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.ImageButton; import android.widget.ListView; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import com.actionbarsherlock.app.ActionBar; import com.actionbarsherlock.internal.view.menu.MenuBuilder; import com.actionbarsherlock.view.ActionMode; import com.actionbarsherlock.view.Menu; import com.actionbarsherlock.view.MenuInflater; import com.actionbarsherlock.view.MenuItem; import com.actionbarsherlock.view.Window; import com.fsck.k9.Account; import com.fsck.k9.Account.SortType; import com.fsck.k9.AccountStats; 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.setup.AccountSettings; import com.fsck.k9.activity.setup.FolderSettings; import com.fsck.k9.activity.setup.Prefs; import com.fsck.k9.controller.MessagingController; import com.fsck.k9.controller.MessagingListener; import com.fsck.k9.helper.MenuPopup; import com.fsck.k9.helper.MessageHelper; import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.store.LocalStore; import com.fsck.k9.mail.store.LocalStore.LocalFolder; import com.fsck.k9.mail.store.StorageManager; import com.handmark.pulltorefresh.library.PullToRefreshBase; import com.handmark.pulltorefresh.library.PullToRefreshListView; /** * MessageList is the primary user interface for the program. This Activity * shows a list of messages. * From this Activity the user can perform all standard message operations. */ public class MessageList extends K9ListActivity implements OnItemClickListener { /** * Reverses the result of a {@link Comparator}. * * @param */ public static class ReverseComparator implements Comparator { private Comparator mDelegate; /** * @param delegate * Never null. */ public ReverseComparator(final Comparator delegate) { mDelegate = delegate; } @Override public int compare(final T object1, final T object2) { // arg1 & 2 are mixed up, this is done on purpose return mDelegate.compare(object2, object1); } } /** * Chains comparator to find a non-0 result. * * @param */ public static class ComparatorChain implements Comparator { private List> mChain; /** * @param chain * Comparator chain. Never null. */ public ComparatorChain(final List> chain) { mChain = chain; } @Override public int compare(T object1, T object2) { int result = 0; for (final Comparator comparator : mChain) { result = comparator.compare(object1, object2); if (result != 0) { break; } } return result; } } public static class AttachmentComparator implements Comparator { @Override public int compare(MessageInfoHolder object1, MessageInfoHolder object2) { return (object1.message.hasAttachments() ? 0 : 1) - (object2.message.hasAttachments() ? 0 : 1); } } public static class FlaggedComparator implements Comparator { @Override public int compare(MessageInfoHolder object1, MessageInfoHolder object2) { return (object1.flagged ? 0 : 1) - (object2.flagged ? 0 : 1); } } public static class UnreadComparator implements Comparator { @Override public int compare(MessageInfoHolder object1, MessageInfoHolder object2) { return (object1.read ? 1 : 0) - (object2.read ? 1 : 0); } } public static class SenderComparator implements Comparator { @Override public int compare(MessageInfoHolder object1, MessageInfoHolder object2) { if (object1.compareCounterparty == null) { return (object2.compareCounterparty == null ? 0 : 1); } else if (object2.compareCounterparty == null) { return -1; } else { return object1.compareCounterparty.toLowerCase().compareTo(object2.compareCounterparty.toLowerCase()); } } } public static class DateComparator implements Comparator { @Override public int compare(MessageInfoHolder object1, MessageInfoHolder object2) { if (object1.compareDate == null) { return (object2.compareDate == null ? 0 : 1); } else if (object2.compareDate == null) { return -1; } else { return object1.compareDate.compareTo(object2.compareDate); } } } public static class ArrivalComparator implements Comparator { @Override public int compare(MessageInfoHolder object1, MessageInfoHolder object2) { return object1.compareArrival.compareTo(object2.compareArrival); } } public static class SubjectComparator implements Comparator { @Override public int compare(MessageInfoHolder arg0, MessageInfoHolder arg1) { // XXX doesn't respect the Comparator contract since it alters the compared object if (arg0.compareSubject == null) { arg0.compareSubject = Utility.stripSubject(arg0.message.getSubject()); } if (arg1.compareSubject == null) { arg1.compareSubject = Utility.stripSubject(arg1.message.getSubject()); } return arg0.compareSubject.compareToIgnoreCase(arg1.compareSubject); } } /** * Immutable empty {@link Message} array */ private static final Message[] EMPTY_MESSAGE_ARRAY = new Message[0]; private static final int ACTIVITY_CHOOSE_FOLDER_MOVE = 1; private static final int ACTIVITY_CHOOSE_FOLDER_COPY = 2; private static final String EXTRA_ACCOUNT = "account"; private static final String EXTRA_FOLDER = "folder"; private static final String EXTRA_REMOTE_SEARCH = "com.fsck.k9.remote_search"; private static final String EXTRA_SEARCH_ACCOUNT = "com.fsck.k9.search_account"; private static final String EXTRA_SEARCH_FOLDER = "com.fsck.k9.search_folder"; private static final String EXTRA_QUERY_FLAGS = "queryFlags"; private static final String EXTRA_FORBIDDEN_FLAGS = "forbiddenFlags"; private static final String EXTRA_INTEGRATE = "integrate"; private static final String EXTRA_ACCOUNT_UUIDS = "accountUuids"; private static final String EXTRA_FOLDER_NAMES = "folderNames"; private static final String EXTRA_TITLE = "title"; private static final String EXTRA_LIST_POSITION = "listPosition"; private static final String EXTRA_RETURN_FROM_MESSAGE_VIEW = "returnFromMessageView"; /** * Maps a {@link SortType} to a {@link Comparator} implementation. */ private static final Map> SORT_COMPARATORS; static { // fill the mapping at class time loading final Map> map = new EnumMap>(SortType.class); map.put(SortType.SORT_ATTACHMENT, new AttachmentComparator()); map.put(SortType.SORT_DATE, new DateComparator()); map.put(SortType.SORT_ARRIVAL, new ArrivalComparator()); map.put(SortType.SORT_FLAGGED, new FlaggedComparator()); map.put(SortType.SORT_SENDER, new SenderComparator()); map.put(SortType.SORT_SUBJECT, new SubjectComparator()); map.put(SortType.SORT_UNREAD, new UnreadComparator()); // make it immutable to prevent accidental alteration (content is immutable already) SORT_COMPARATORS = Collections.unmodifiableMap(map); } private ListView mListView; private PullToRefreshListView mPullToRefreshView; private int mPreviewLines = 0; private MessageListAdapter mAdapter; private View mFooterView; private FolderInfoHolder mCurrentFolder; private LayoutInflater mInflater; private MessagingController mController; private Account mAccount; private int mUnreadMessageCount = 0; /** * Stores the name of the folder that we want to open as soon as possible * after load. */ private String mFolderName; /** * If we're doing a search, this contains the query string. */ private String mQueryString; private Flag[] mQueryFlags = null; private Flag[] mForbiddenFlags = null; private boolean mRemoteSearch = false; private String mSearchAccount = null; private String mSearchFolder = null; private boolean mIntegrate = false; private String[] mAccountUuids = null; private String[] mFolderNames = null; private String mTitle; private MessageListHandler mHandler = new MessageListHandler(); private SortType mSortType = SortType.SORT_DATE; private boolean mSortAscending = true; private boolean mSortDateAscending = false; private boolean mStars = true; private boolean mCheckboxes = true; private int mSelectedCount = 0; private FontSizes mFontSizes = K9.getFontSizes(); private MenuItem mRefreshMenuItem; private ActionBar mActionBar; private ActionMode mActionMode; private View mActionBarProgressView; private Bundle mState = null; /** * Relevant messages for the current context when we have to remember the * chosen messages between user interactions (eg. Selecting a folder for * move operation) */ private List mActiveMessages; private Context context; /* package visibility for faster inner class access */ MessageHelper mMessageHelper = MessageHelper.getInstance(this); private StorageManager.StorageListener mStorageListener = new StorageListenerImplementation(); private TextView mActionBarTitle; private TextView mActionBarSubTitle; private TextView mActionBarUnread; private ActionModeCallback mActionModeCallback = new ActionModeCallback(); private final class StorageListenerImplementation implements StorageManager.StorageListener { @Override public void onUnmount(String providerId) { if (mAccount != null && providerId.equals(mAccount.getLocalStorageProviderId())) { runOnUiThread(new Runnable() { @Override public void run() { onAccountUnavailable(); } }); } } @Override public void onMount(String providerId) { // no-op } } /** * This class is used to run operations that modify UI elements in the UI thread. * *

We are using convenience methods that add a {@link android.os.Message} instance or a * {@link Runnable} to the message queue.

* *

Note: If you add a method to this class make sure you don't accidentally * perform the operation in the calling thread.

*/ class MessageListHandler extends Handler { private static final int ACTION_REMOVE_MESSAGE = 1; private static final int ACTION_RESET_UNREAD_COUNT = 2; private static final int ACTION_SORT_MESSAGES = 3; private static final int ACTION_FOLDER_LOADING = 4; private static final int ACTION_REFRESH_TITLE = 5; private static final int ACTION_PROGRESS = 6; public void removeMessage(MessageReference messageReference) { android.os.Message msg = android.os.Message.obtain(this, ACTION_REMOVE_MESSAGE, messageReference); sendMessage(msg); } public void sortMessages() { android.os.Message msg = android.os.Message.obtain(this, ACTION_SORT_MESSAGES); sendMessage(msg); } public void folderLoading(String folder, boolean loading) { android.os.Message msg = android.os.Message.obtain(this, ACTION_FOLDER_LOADING, (loading) ? 1 : 0, 0, folder); sendMessage(msg); } public void refreshTitle() { android.os.Message msg = android.os.Message.obtain(this, ACTION_REFRESH_TITLE); sendMessage(msg); } public void progress(final boolean progress) { android.os.Message msg = android.os.Message.obtain(this, ACTION_PROGRESS, (progress) ? 1 : 0, 0); sendMessage(msg); } public void updateFooter(final String message, final boolean showProgress) { runOnUiThread(new Runnable() { @Override public void run() { MessageList.this.updateFooter(message, showProgress); } }); } public void changeMessageUid(final MessageReference ref, final String newUid) { // Instead of explicitly creating a container to be able to pass both arguments in a // Message we post a Runnable to the message queue. post(new Runnable() { @Override public void run() { mAdapter.changeMessageUid(ref, newUid); } }); } public void addOrUpdateMessages(final Account account, final String folderName, final List providedMessages, final boolean verifyAgainstSearch) { // We copy the message list because it's later modified by MessagingController final List messages = new ArrayList(providedMessages); post(new Runnable() { @Override public void run() { mAdapter.addOrUpdateMessages(account, folderName, messages, verifyAgainstSearch); } }); } @Override public void handleMessage(android.os.Message msg) { switch (msg.what) { case ACTION_REMOVE_MESSAGE: { MessageReference messageReference = (MessageReference) msg.obj; mAdapter.removeMessage(messageReference); break; } case ACTION_RESET_UNREAD_COUNT: { mAdapter.resetUnreadCount(); break; } case ACTION_SORT_MESSAGES: { mAdapter.sortMessages(); break; } case ACTION_FOLDER_LOADING: { String folder = (String) msg.obj; boolean loading = (msg.arg1 == 1); MessageList.this.folderLoading(folder, loading); break; } case ACTION_REFRESH_TITLE: { MessageList.this.refreshTitle(); break; } case ACTION_PROGRESS: { boolean progress = (msg.arg1 == 1); MessageList.this.progress(progress); break; } } } } /** * @return The comparator to use to display messages in an ordered * fashion. Never null. */ protected Comparator getComparator() { final List> chain = new ArrayList>(2 /* we add 2 comparators at most */); { // add the specified comparator final Comparator comparator = SORT_COMPARATORS.get(mSortType); if (mSortAscending) { chain.add(comparator); } else { chain.add(new ReverseComparator(comparator)); } } { // add the date comparator if not already specified if (mSortType != SortType.SORT_DATE && mSortType != SortType.SORT_ARRIVAL) { final Comparator comparator = SORT_COMPARATORS.get(SortType.SORT_DATE); if (mSortDateAscending) { chain.add(comparator); } else { chain.add(new ReverseComparator(comparator)); } } } return new ComparatorChain(chain); } private void folderLoading(String folder, boolean loading) { if (mCurrentFolder != null && mCurrentFolder.name.equals(folder)) { mCurrentFolder.loading = loading; } updateFooterView(); } private void refreshTitle() { setWindowTitle(); if (!mRemoteSearch) { setWindowProgress(); } } private void setWindowProgress() { int level = Window.PROGRESS_END; if (mCurrentFolder != null && mCurrentFolder.loading && mAdapter.mListener.getFolderTotal() > 0) { int divisor = mAdapter.mListener.getFolderTotal(); if (divisor != 0) { level = (Window.PROGRESS_END / divisor) * (mAdapter.mListener.getFolderCompleted()) ; if (level > Window.PROGRESS_END) { level = Window.PROGRESS_END; } } } setSupportProgress(level); } private void setWindowTitle() { // regular folder content display if (mFolderName != null) { String displayName = FolderInfoHolder.getDisplayName(MessageList.this, mAccount, mFolderName); mActionBarTitle.setText(displayName); String operation = mAdapter.mListener.getOperation(MessageList.this, getTimeFormat()).trim(); if (operation.length() < 1) { mActionBarSubTitle.setText(mAccount.getEmail()); } else { mActionBarSubTitle.setText(operation); } } else if (mQueryString != null) { // query result display. This may be for a search folder as opposed to a user-initiated search. if (mTitle != null) { // This was a search folder; the search folder has overridden our title. mActionBarTitle.setText(mTitle); } else { // This is a search result; set it to the default search result line. mActionBarTitle.setText(getString(R.string.search_results)); } } // set unread count if (mUnreadMessageCount == 0) { mActionBarUnread.setVisibility(View.GONE); } else { if (mQueryString != null && mTitle == null) { // This is a search result. The unread message count is easily confused // with total number of messages in the search result, so let's hide it. mActionBarUnread.setVisibility(View.GONE); } else { mActionBarUnread.setText(Integer.toString(mUnreadMessageCount)); mActionBarUnread.setVisibility(View.VISIBLE); } } } private void progress(final boolean progress) { // Make sure we don't try this before the menu is initialized // this could happen while the activity is initialized. if (mRefreshMenuItem != null) { if (progress) { mRefreshMenuItem.setActionView(mActionBarProgressView); } else { mRefreshMenuItem.setActionView(null); } } if (mPullToRefreshView != null && !progress) { mPullToRefreshView.onRefreshComplete(); } } /** * Show the message list that was used to open the {@link MessageView} for a message. * *

* Note: * The {@link MessageList} instance should still be around and all we do is bring it back to * the front (see the activity flags).
* Out of sheer paranoia we also set the extras that were used to create the original * {@code MessageList} instance. Using those, the activity can be recreated in the unlikely * case of it having been killed by the OS. *

* * @param context * The {@link Context} instance to invoke the {@link Context#startActivity(Intent)} * method on. * @param extras * The extras used to create the original {@code MessageList} instance. * * @see MessageView#actionView(Context, MessageReference, ArrayList, Bundle) */ public static void actionHandleFolder(Context context, Bundle extras) { Intent intent = new Intent(context, MessageList.class); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); intent.putExtras(extras); intent.putExtra(EXTRA_RETURN_FROM_MESSAGE_VIEW, true); context.startActivity(intent); } public static void actionHandleFolder(Context context, Account account, String folder) { Intent intent = new Intent(context, MessageList.class); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); intent.putExtra(EXTRA_ACCOUNT, account.getUuid()); if (folder != null) { intent.putExtra(EXTRA_FOLDER, folder); } context.startActivity(intent); } public static 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); 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); } @Override public void onItemClick(AdapterView parent, View view, int position, long id) { if (view == mFooterView) { if (mCurrentFolder != null && !mRemoteSearch) { mController.loadMoreMessages(mAccount, mFolderName, mAdapter.mListener); } else if (mRemoteSearch && mAdapter.mExtraSearchResults != null && mAdapter.mExtraSearchResults.size() > 0 && mSearchAccount != null) { int numResults = mAdapter.mExtraSearchResults.size(); Account account = Preferences.getPreferences(this).getAccount(mSearchAccount); if (account == null) { mHandler.updateFooter("", false); return; } int limit = account.getRemoteSearchNumResults(); List toProcess = mAdapter.mExtraSearchResults; if (limit > 0 && numResults > limit) { toProcess = toProcess.subList(0, limit); mAdapter.mExtraSearchResults = mAdapter.mExtraSearchResults.subList(limit, mAdapter.mExtraSearchResults.size()); } else { mAdapter.mExtraSearchResults = null; mHandler.updateFooter("", false); } mController.loadSearchResults(account, mSearchFolder, toProcess, mAdapter.mListener); } return; } final MessageInfoHolder message = (MessageInfoHolder) parent.getItemAtPosition(position); if (mSelectedCount > 0) { toggleMessageSelect(message); } else { onOpenMessage(message); } } @Override public void onCreate(Bundle savedInstanceState) { context = this; super.onCreate(savedInstanceState); setContentView(R.layout.message_list); mActionBarProgressView = getLayoutInflater().inflate(R.layout.actionbar_indeterminate_progress_actionview, null); // need this for actionbar initialization mQueryString = getIntent().getStringExtra(SearchManager.QUERY); mPullToRefreshView = (PullToRefreshListView) findViewById(R.id.message_list); mInflater = getLayoutInflater(); mActionBar = getSupportActionBar(); initializeActionBar(); initializeLayout(); mPreviewLines = K9.messageListPreviewLines(); initializeMessageList(getIntent(), true); mListView.setVerticalFadingEdgeEnabled(false); // Enable context action bar behaviour mListView.setOnItemLongClickListener(new OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { final MessageInfoHolder message = (MessageInfoHolder) parent.getItemAtPosition(position); toggleMessageSelect(message); return true; }}); // Correcting for screen rotation when in ActionMode mSelectedCount = getSelectionFromCheckboxes().size(); if (mSelectedCount > 0) { mActionMode = MessageList.this.startActionMode(mActionModeCallback); updateActionModeTitle(); } } @Override public void onNewIntent(Intent intent) { setIntent(intent); // onNewIntent doesn't autoset our "internal" intent initializeMessageList(intent, false); } private void initializeMessageList(Intent intent, boolean create) { boolean returnFromMessageView = intent.getBooleanExtra( EXTRA_RETURN_FROM_MESSAGE_VIEW, false); if (!create && returnFromMessageView) { // We're returning from the MessageView activity with "Manage back button" enabled. // So just leave the activity in the state it was left in. return; } mQueryString = intent.getStringExtra(SearchManager.QUERY); mFolderName = null; mRemoteSearch = false; mSearchAccount = null; mSearchFolder = null; if (mQueryString != null) { if (Intent.ACTION_SEARCH.equals(intent.getAction())) { //Query was received from Search Dialog Bundle appData = getIntent().getBundleExtra(SearchManager.APP_DATA); if (appData != null) { mSearchAccount = appData.getString(EXTRA_SEARCH_ACCOUNT); mSearchFolder = appData.getString(EXTRA_SEARCH_FOLDER); mRemoteSearch = appData.getBoolean(EXTRA_REMOTE_SEARCH); } } else { mSearchAccount = intent.getStringExtra(EXTRA_SEARCH_ACCOUNT); mSearchFolder = intent.getStringExtra(EXTRA_SEARCH_FOLDER); } } String accountUuid = intent.getStringExtra(EXTRA_ACCOUNT); mFolderName = intent.getStringExtra(EXTRA_FOLDER); mAccount = Preferences.getPreferences(this).getAccount(accountUuid); if (mAccount != null && !mAccount.isAvailable(this)) { 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]); } } 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(); } mAdapter = new MessageListAdapter(); restorePreviousData(); if (mFolderName != null) { mCurrentFolder = mAdapter.getFolder(mFolderName, mAccount); } // Hide "Load up to x more" footer for local search views mFooterView.setVisibility((mQueryString != null && !mRemoteSearch) ? View.GONE : View.VISIBLE); mController = MessagingController.getInstance(getApplication()); mListView.setAdapter(mAdapter); } private void restorePreviousData() { final ActivityState previousData = getLastNonConfigurationInstance(); if (previousData != null) { mAdapter.restoreMessages(previousData.messages); mActiveMessages = previousData.activeMessages; } } @Override public void onPause() { super.onPause(); mController.removeListener(mAdapter.mListener); saveListState(); StorageManager.getInstance(getApplication()).removeListener(mStorageListener); } public void saveListState() { mState = new Bundle(); mState.putInt(EXTRA_LIST_POSITION, mListView.getSelectedItemPosition()); } public void restoreListState() { if (mState == null) { return; } int pos = mState.getInt(EXTRA_LIST_POSITION, ListView.INVALID_POSITION); if (pos >= mListView.getCount()) { pos = mListView.getCount() - 1; } if (pos == ListView.INVALID_POSITION) { mListView.setSelected(false); } else { mListView.setSelection(pos); } } /** * On resume we refresh messages for the folder that is currently open. * This guarantees that things like unread message count and read status * are updated. */ @Override public void onResume() { super.onResume(); if (mAccount != null && !mAccount.isAvailable(this)) { onAccountUnavailable(); return; } if (!(this instanceof Search)) { //necessary b/c no guarantee Search.onStop will be called before MessageList.onResume //when returning from search results Search.setActive(false); } StorageManager.getInstance(getApplication()).addListener(mStorageListener); mStars = K9.messageListStars(); mCheckboxes = K9.messageListCheckboxes(); // TODO Add support for pull to fresh on searches. if(mQueryString == null) { mPullToRefreshView.setOnRefreshListener(new PullToRefreshBase.OnRefreshListener() { @Override public void onRefresh(PullToRefreshBase refreshView) { checkMail(mAccount, mFolderName); } }); } else { mPullToRefreshView.setMode(PullToRefreshBase.Mode.DISABLED); } mController.addListener(mAdapter.mListener); //Cancel pending new mail notifications when we open an account Account[] accountsWithNotification; Preferences prefs = Preferences.getPreferences(getApplicationContext()); Account account = getCurrentAccount(prefs); if (account != null) { accountsWithNotification = new Account[] { account }; mSortType = account.getSortType(); mSortAscending = account.isSortAscending(mSortType); mSortDateAscending = account.isSortAscending(SortType.SORT_DATE); } else { accountsWithNotification = prefs.getAccounts(); mSortType = K9.getSortType(); mSortAscending = K9.isSortAscending(mSortType); mSortDateAscending = K9.isSortAscending(SortType.SORT_DATE); } for (Account accountWithNotification : accountsWithNotification) { mController.notifyAccountCancel(this, accountWithNotification); } if (mAdapter.isEmpty()) { if (mRemoteSearch) { //TODO: Support flag based search mController.searchRemoteMessages(mSearchAccount, mSearchFolder, mQueryString, null, null, mAdapter.mListener); } else if (mFolderName != null) { mController.listLocalMessages(mAccount, mFolderName, mAdapter.mListener); // Hide the archive button if we don't have an archive folder. if (!mAccount.hasArchiveFolder()) { // mBatchArchiveButton.setVisibility(View.GONE); } } else if (mQueryString != null) { mController.searchLocalMessages(mAccountUuids, mFolderNames, null, mQueryString, mIntegrate, mQueryFlags, mForbiddenFlags, mAdapter.mListener); // Don't show the archive button if this is a search. // mBatchArchiveButton.setVisibility(View.GONE); } } else { // reread the selected date format preference in case it has changed mMessageHelper.refresh(); mAdapter.markAllMessagesAsDirty(); new Thread() { @Override public void run() { if (!mRemoteSearch) { if (mFolderName != null) { mController.listLocalMessagesSynchronous(mAccount, mFolderName, mAdapter.mListener); } else if (mQueryString != null) { mController.searchLocalMessagesSynchronous(mAccountUuids, mFolderNames, null, mQueryString, mIntegrate, mQueryFlags, mForbiddenFlags, mAdapter.mListener); } runOnUiThread(new Runnable() { @Override public void run() { mAdapter.pruneDirtyMessages(); mAdapter.notifyDataSetChanged(); restoreListState(); } }); } } } .start(); } if (mAccount != null && mFolderName != null && !mRemoteSearch) { mController.getFolderUnreadMessageCount(mAccount, mFolderName, mAdapter.mListener); } refreshTitle(); } private void initializeActionBar() { mActionBar.setDisplayShowCustomEnabled(true); mActionBar.setCustomView(R.layout.actionbar_custom); View customView = mActionBar.getCustomView(); mActionBarTitle = (TextView) customView.findViewById(R.id.actionbar_title_first); 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); } private void initializeLayout() { mListView = mPullToRefreshView.getRefreshableView(); mListView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY); mListView.setLongClickable(true); mListView.setFastScrollEnabled(true); mListView.setScrollingCacheEnabled(false); mListView.setOnItemClickListener(this); mListView.addFooterView(getFooterView(mListView)); registerForContextMenu(mListView); } /** * Container for values to be kept while the device configuration is * modified at runtime (keyboard, orientation, etc.) and Android restarts * this activity. * * @see MessageList#onRetainNonConfigurationInstance() * @see MessageList#getLastNonConfigurationInstance() */ static class ActivityState { public List messages; public List activeMessages; } /* (non-Javadoc) * * Method overridden for proper typing within this class (the return type is * more specific than the super implementation) * * @see android.app.Activity#onRetainNonConfigurationInstance() */ @Override public ActivityState onRetainNonConfigurationInstance() { final ActivityState state = new ActivityState(); state.messages = mAdapter.getMessages(); state.activeMessages = mActiveMessages; return state; } /* * (non-Javadoc) * * Method overridden for proper typing within this class (the return type is * more specific than the super implementation) * * @see android.app.Activity#getLastNonConfigurationInstance() */ @Override public ActivityState getLastNonConfigurationInstance() { return (ActivityState) super.getLastNonConfigurationInstance(); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { // Shortcuts that work no matter what is selected switch (keyCode) { // messagelist is actually a K9Activity, not a K9ListActivity // This saddens me greatly, but to support volume key navigation // in MessageView, we implement this bit of wrapper code case KeyEvent.KEYCODE_VOLUME_UP: { if (K9.useVolumeKeysForListNavigationEnabled()) { int currentPosition = mListView.getSelectedItemPosition(); if (currentPosition == AdapterView.INVALID_POSITION || mListView.isInTouchMode()) { currentPosition = mListView.getFirstVisiblePosition(); } if (currentPosition > 0) { mListView.setSelection(currentPosition - 1); } return true; } return false; } case KeyEvent.KEYCODE_VOLUME_DOWN: { if (K9.useVolumeKeysForListNavigationEnabled()) { int currentPosition = mListView.getSelectedItemPosition(); if (currentPosition == AdapterView.INVALID_POSITION || mListView.isInTouchMode()) { currentPosition = mListView.getFirstVisiblePosition(); } if (currentPosition < mListView.getCount()) { mListView.setSelection(currentPosition + 1); } return true; } return false; } case KeyEvent.KEYCODE_C: { onCompose(); return true; } case KeyEvent.KEYCODE_Q: { onShowFolderList(); return true; } case KeyEvent.KEYCODE_O: { onCycleSort(); return true; } case KeyEvent.KEYCODE_I: { changeSort(mSortType); return true; } case KeyEvent.KEYCODE_H: { Toast toast = Toast.makeText(this, R.string.message_list_help_key, Toast.LENGTH_LONG); toast.show(); return true; } } //Shortcuts that only work when a message is selected boolean retval = true; int position = mListView.getSelectedItemPosition(); try { if (position >= 0) { MessageInfoHolder message = (MessageInfoHolder) mAdapter.getItem(position); final List selection = getSelectionFromMessage(message); if (message != null) { switch (keyCode) { case KeyEvent.KEYCODE_DEL: { onDelete(selection); return true; } case KeyEvent.KEYCODE_S: { toggleMessageSelect(message); return true; } case KeyEvent.KEYCODE_D: { onDelete(selection); return true; } case KeyEvent.KEYCODE_G: { setFlag(selection, Flag.FLAGGED, !message.flagged); return true; } case KeyEvent.KEYCODE_M: { onMove(selection); return true; } case KeyEvent.KEYCODE_V: { onArchive(selection); return true; } case KeyEvent.KEYCODE_Y: { onCopy(selection); return true; } case KeyEvent.KEYCODE_Z: { setFlag(selection, Flag.SEEN, !message.read); return true; } } } } } finally { retval = super.onKeyDown(keyCode, event); } return retval; } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { // Swallow these events too to avoid the audible notification of a volume change if (K9.useVolumeKeysForListNavigationEnabled()) { if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP) || (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)) { if (K9.DEBUG) Log.v(K9.LOG_TAG, "Swallowed key up."); return true; } } return super.onKeyUp(keyCode, event); } private void onOpenMessage(MessageInfoHolder message) { if (message.folder.name.equals(message.message.getFolder().getAccount().getDraftsFolderName())) { MessageCompose.actionEditDraft(this, message.message.getFolder().getAccount(), message.message); } else { // Need to get the list before the sort starts ArrayList messageRefs = new ArrayList(); for (MessageInfoHolder holder : mAdapter.getMessages()) { MessageReference ref = holder.message.makeMessageReference(); messageRefs.add(ref); } MessageReference ref = message.message.makeMessageReference(); Log.i(K9.LOG_TAG, "MessageList sending message " + ref); MessageView.actionView(this, ref, messageRefs, getIntent().getExtras()); } /* * We set read=true here for UI performance reasons. The actual value * will get picked up on the refresh when the Activity is resumed but * that may take a second or so and we don't want this to show and * then go away. I've gone back and forth on this, and this gives a * better UI experience, so I am putting it back in. */ if (!message.read) { message.read = true; } } private void onAccounts() { Accounts.listAccounts(this); finish(); } private void onShowFolderList() { FolderList.actionHandleAccount(this, mAccount); finish(); } private void onCompose() { if (mQueryString != null) { /* * If we have a query string, we don't have an account to let * compose start the default action. */ MessageCompose.actionCompose(this, null); } else { MessageCompose.actionCompose(this, mAccount); } } private void onReply(MessageInfoHolder holder) { MessageCompose.actionReply(this, holder.message.getFolder().getAccount(), holder.message, false, null); } private void onReplyAll(MessageInfoHolder holder) { MessageCompose.actionReply(this, holder.message.getFolder().getAccount(), holder.message, true, null); } private void onForward(MessageInfoHolder holder) { MessageCompose.actionForward(this, holder.message.getFolder().getAccount(), holder.message, null); } private void onResendMessage(MessageInfoHolder message) { MessageCompose.actionEditDraft(this, message.message.getFolder().getAccount(), message.message); } private void onEditPrefs() { Prefs.actionPrefs(this); } private void onEditAccount() { AccountSettings.actionSettings(this, mAccount); } @Override public boolean onSearchRequested() { if (mAccount != null && mCurrentFolder != null && mAccount.allowRemoteSearch()) { //if in a remote searchable folder, ask user what they want. //TODO: Add ability to remember selection? final CharSequence[] items = new CharSequence[2]; items[0] = getString(R.string.search_mode_local_all); items[1] = getString(R.string.search_mode_remote); AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(getString(R.string.search_mode_title)); builder.setItems(items, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int item) { Bundle appData = null; if (item == 1) { appData = new Bundle(); appData.putString(EXTRA_SEARCH_ACCOUNT, mAccount.getUuid()); appData.putString(EXTRA_SEARCH_FOLDER, mCurrentFolder.name); appData.putBoolean(EXTRA_REMOTE_SEARCH, true); } //else do regular search, which doesn't require any special parameter setup startSearch(null, false, appData, false); } }); AlertDialog alert = builder.create(); alert.show(); return true; } startSearch(null, false, null, false); return true; } private void changeSort(SortType sortType) { Boolean sortAscending = (mSortType == sortType) ? !mSortAscending : null; changeSort(sortType, sortAscending); } /** * Change the sort type and sort order used for the message list. * * @param sortType * Specifies which field to use for sorting the message list. * @param sortAscending * Specifies the sort order. If this argument is {@code null} the default search order * for the sort type is used. */ // FIXME: Don't save the changes in the UI thread private void changeSort(SortType sortType, Boolean sortAscending) { mSortType = sortType; Preferences prefs = Preferences.getPreferences(getApplicationContext()); Account account = getCurrentAccount(prefs); if (account != null) { account.setSortType(mSortType); if (sortAscending == null) { mSortAscending = account.isSortAscending(mSortType); } else { mSortAscending = sortAscending; } account.setSortAscending(mSortType, mSortAscending); mSortDateAscending = account.isSortAscending(SortType.SORT_DATE); account.save(prefs); } else { K9.setSortType(mSortType); if (sortAscending == null) { mSortAscending = K9.isSortAscending(mSortType); } else { mSortAscending = sortAscending; } K9.setSortAscending(mSortType, mSortAscending); mSortDateAscending = K9.isSortAscending(SortType.SORT_DATE); Editor editor = prefs.getPreferences().edit(); K9.save(editor); editor.commit(); } reSort(); } private void reSort() { int toastString = mSortType.getToast(mSortAscending); Toast toast = Toast.makeText(this, toastString, Toast.LENGTH_SHORT); toast.show(); mAdapter.sortMessages(); } private void onCycleSort() { SortType[] sorts = SortType.values(); int curIndex = 0; for (int i = 0; i < sorts.length; i++) { if (sorts[i] == mSortType) { curIndex = i; break; } } curIndex++; if (curIndex == sorts.length) { curIndex = 0; } changeSort(sorts[curIndex]); } /** * @param holders * Never {@code null}. */ private void onDelete(final List holders) { final List messagesToRemove = new ArrayList(); for (MessageInfoHolder holder : holders) { messagesToRemove.add(holder.message); } mAdapter.removeMessages(holders); mController.deleteMessages(messagesToRemove.toArray(EMPTY_MESSAGE_ARRAY), null); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode != RESULT_OK) { return; } switch (requestCode) { case ACTIVITY_CHOOSE_FOLDER_MOVE: case ACTIVITY_CHOOSE_FOLDER_COPY: { if (data == null) { return; } final String destFolderName = data.getStringExtra(ChooseFolder.EXTRA_NEW_FOLDER); final List holders = mActiveMessages; if (destFolderName != null) { mActiveMessages = null; // don't need it any more final Account account = holders.get(0).message.getFolder().getAccount(); account.setLastSelectedFolderName(destFolderName); switch (requestCode) { case ACTIVITY_CHOOSE_FOLDER_MOVE: move(holders, destFolderName); break; case ACTIVITY_CHOOSE_FOLDER_COPY: copy(holders, destFolderName); break; } } break; } } } private void onExpunge(final Account account, String folderName) { mController.expunge(account, folderName, null); } @Override public Dialog onCreateDialog(int id) { switch (id) { case R.id.dialog_confirm_spam: return ConfirmationDialog.create(this, id, R.string.dialog_confirm_spam_title, "" /* text is refreshed by #onPrepareDialog(int, Dialog) below */, R.string.dialog_confirm_spam_confirm_button, R.string.dialog_confirm_spam_cancel_button, new Runnable() { @Override public void run() { onSpamConfirmed(mActiveMessages); // No further need for this reference mActiveMessages = null; } }, new Runnable() { @Override public void run() { // event for cancel, we don't need this reference any more mActiveMessages = null; } }); } return super.onCreateDialog(id); } /* * (non-Javadoc) * * Android happens to invoke this method even if the given dialog is not * shown (eg. a dismissed dialog) as part of the automatic activity * reloading following a configuration change (orientation, keyboard, * locale, etc.). */ @Override public void onPrepareDialog(final int id, final Dialog dialog) { switch (id) { case R.id.dialog_confirm_spam: { // mActiveMessages can be null if Android restarts the activity // while this dialog is not actually shown (but was displayed at // least once) if (mActiveMessages != null) { final int selectionSize = mActiveMessages.size(); final String message; message = getResources().getQuantityString(R.plurals.dialog_confirm_spam_message, selectionSize, Integer.valueOf(selectionSize)); ((AlertDialog) dialog).setMessage(message); } break; } default: { super.onPrepareDialog(id, dialog); } } } private void onToggleRead(final List holders) { Message message; Folder folder; Account account; String folderName; int i = 0; for (final Iterator iterator = holders.iterator(); iterator.hasNext(); i++) { final MessageInfoHolder messageInfo = iterator.next(); message = messageInfo.message; folder = message.getFolder(); account = folder.getAccount(); folderName = message.getFolder().getName(); mController.setFlag(account, folderName, new Message[]{message}, Flag.SEEN, !messageInfo.read); messageInfo.read = !messageInfo.read; mAdapter.sortMessages(); } } private void onToggleFlag(final List holders) { Message message; Folder folder; Account account; String folderName; int i = 0; for (final Iterator iterator = holders.iterator(); iterator.hasNext(); i++) { final MessageInfoHolder messageInfo = iterator.next(); message = messageInfo.message; folder = message.getFolder(); account = folder.getAccount(); folderName = message.getFolder().getName(); mController.setFlag(account, folderName, new Message[]{message}, Flag.FLAGGED, !messageInfo.flagged); messageInfo.flagged = !messageInfo.flagged; mAdapter.sortMessages(); } } private void checkMail(Account account, String folderName) { mController.synchronizeMailbox(account, folderName, mAdapter.mListener, null); mController.sendPendingMessages(account, mAdapter.mListener); } @Override public boolean onOptionsItemSelected(MenuItem item) { int itemId = item.getItemId(); switch (itemId) { case android.R.id.home: { if (mQueryString == null) { onShowFolderList(); } else { onAccounts(); } return true; } case R.id.compose: { onCompose(); return true; } case R.id.check_mail: { checkMail(mAccount, mFolderName); return true; } case R.id.set_sort_date: { changeSort(SortType.SORT_DATE); return true; } case R.id.set_sort_arrival: { changeSort(SortType.SORT_ARRIVAL); return true; } case R.id.set_sort_subject: { changeSort(SortType.SORT_SUBJECT); return true; } case R.id.set_sort_sender: { changeSort(SortType.SORT_SENDER); return true; } case R.id.set_sort_flag: { changeSort(SortType.SORT_FLAGGED); return true; } case R.id.set_sort_unread: { changeSort(SortType.SORT_UNREAD); return true; } case R.id.set_sort_attach: { changeSort(SortType.SORT_ATTACHMENT); return true; } case R.id.select_all: toggleAllSelected(); return true; case R.id.app_settings: { onEditPrefs(); return true; } } if (mQueryString != null) { // 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; } switch (itemId) { case R.id.send_messages: { mController.sendPendingMessages(mAccount, mAdapter.mListener); return true; } case R.id.folder_settings: { if (mFolderName != null) { FolderSettings.actionSettings(this, mAccount, mFolderName); } return true; } case R.id.account_settings: { onEditAccount(); return true; } case R.id.expunge: { if (mCurrentFolder != null) { onExpunge(mAccount, mCurrentFolder.name); } return true; } default: { return super.onOptionsItemSelected(item); } } } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); getSupportMenuInflater().inflate(R.menu.message_list_option, menu); mRefreshMenuItem = menu.findItem(R.id.check_mail); return true; } @Override public boolean onPrepareOptionsMenu(Menu menu) { if (mQueryString != null || mIntegrate) { menu.findItem(R.id.expunge).setVisible(false); menu.findItem(R.id.check_mail).setVisible(false); menu.findItem(R.id.send_messages).setVisible(false); menu.findItem(R.id.folder_settings).setVisible(false); menu.findItem(R.id.account_settings).setVisible(false); } else { if (mCurrentFolder != null && mCurrentFolder.name.equals(mAccount.getOutboxFolderName())) { menu.findItem(R.id.check_mail).setVisible(false); } else { menu.findItem(R.id.send_messages).setVisible(false); } if (mCurrentFolder != null && K9.ERROR_FOLDER_NAME.equals(mCurrentFolder.name)) { menu.findItem(R.id.expunge).setVisible(false); } if (!mController.isMoveCapable(mAccount)) { // FIXME: Really we want to do this for all local-only folders if (mCurrentFolder != null && !mAccount.getInboxFolderName().equals(mCurrentFolder.name)) { menu.findItem(R.id.check_mail).setVisible(false); } menu.findItem(R.id.expunge).setVisible(false); } } return true; } class MessageListAdapter extends BaseAdapter { private final List mMessages = Collections.synchronizedList(new ArrayList()); public List mExtraSearchResults; private final ActivityListener mListener = new ActivityListener() { // TODO achen - may need to add a setSupportProgress to mHandler to run on the UI thread. @Override public void remoteSearchAddMessage(Account account, String folderName, Message message, final int numDone, final int numTotal) { if (numTotal > 0 && numDone < numTotal) { setSupportProgress(Window.PROGRESS_END / numTotal * numDone); } else { setSupportProgress(Window.PROGRESS_END); } addOrUpdateMessages(account, folderName, Collections.singletonList(message), false); } @Override public void remoteSearchFailed(Account acct, String folder, final String err) { //TODO: Better error handling runOnUiThread(new Runnable() { public void run() { Toast.makeText(getApplication(), err, Toast.LENGTH_LONG).show(); } }); } @Override public void remoteSearchStarted(Account acct, String folder) { mHandler.progress(true); mHandler.updateFooter(getString(R.string.remote_search_sending_query), true); } @Override public void remoteSearchFinished(Account acct, String folder, int numResults, List extraResults) { mHandler.progress(false); if (extraResults != null && extraResults.size() > 0) { mExtraSearchResults = extraResults; mHandler.updateFooter(String.format(getString(R.string.load_more_messages_fmt), acct.getRemoteSearchNumResults()), false); } else { mHandler.updateFooter("", false); } setSupportProgress(Window.PROGRESS_END); } @Override public void remoteSearchServerQueryComplete(Account account, String folderName, int numResults) { mHandler.progress(true); if (account != null && account.getRemoteSearchNumResults() != 0 && numResults > account.getRemoteSearchNumResults()) { mHandler.updateFooter(getString(R.string.remote_search_downloading_limited, account.getRemoteSearchNumResults(), numResults), true); } else { mHandler.updateFooter(getString(R.string.remote_search_downloading, numResults), true); } setSupportProgress(Window.PROGRESS_START); } @Override public void informUserOfStatus() { mHandler.refreshTitle(); } @Override public void synchronizeMailboxStarted(Account account, String folder) { if (updateForMe(account, folder)) { mHandler.progress(true); mHandler.folderLoading(folder, true); } super.synchronizeMailboxStarted(account, folder); } @Override public void synchronizeMailboxFinished(Account account, String folder, int totalMessagesInMailbox, int numNewMessages) { if (updateForMe(account, folder)) { mHandler.progress(false); mHandler.folderLoading(folder, false); mHandler.sortMessages(); } super.synchronizeMailboxFinished(account, folder, totalMessagesInMailbox, numNewMessages); } @Override public void synchronizeMailboxFailed(Account account, String folder, String message) { if (updateForMe(account, folder)) { mHandler.progress(false); mHandler.folderLoading(folder, false); mHandler.sortMessages(); } super.synchronizeMailboxFailed(account, folder, message); } @Override public void synchronizeMailboxAddOrUpdateMessage(Account account, String folder, Message message) { mHandler.addOrUpdateMessages(account, folder, Collections.singletonList(message), true); } @Override public void synchronizeMailboxRemovedMessage(Account account, String folder, Message message) { mHandler.removeMessage(message.makeMessageReference()); } @Override public void listLocalMessagesStarted(Account account, String folder) { if ((mQueryString != null && folder == null) || (account != null && account.equals(mAccount))) { mHandler.progress(true); if (folder != null) { mHandler.folderLoading(folder, true); } } } @Override public void listLocalMessagesFailed(Account account, String folder, String message) { if ((mQueryString != null && folder == null) || (account != null && account.equals(mAccount))) { mHandler.sortMessages(); mHandler.progress(false); if (folder != null) { mHandler.folderLoading(folder, false); } } } @Override public void listLocalMessagesFinished(Account account, String folder) { if ((mQueryString != null && folder == null) || (account != null && account.equals(mAccount))) { mHandler.sortMessages(); mHandler.progress(false); if (folder != null) { mHandler.folderLoading(folder, false); } } } @Override public void listLocalMessagesRemoveMessage(Account account, String folder, Message message) { mHandler.removeMessage(message.makeMessageReference()); } @Override public void listLocalMessagesAddMessages(Account account, String folder, List messages) { mHandler.addOrUpdateMessages(account, folder, messages, false); } @Override public void listLocalMessagesUpdateMessage(Account account, String folder, Message message) { mHandler.addOrUpdateMessages(account, folder, Collections.singletonList(message), false); } @Override public void searchStats(AccountStats stats) { mUnreadMessageCount = stats.unreadMessageCount; super.searchStats(stats); } @Override public void folderStatusChanged(Account account, String folder, int unreadMessageCount) { if (updateForMe(account, folder)) { mUnreadMessageCount = unreadMessageCount; } super.folderStatusChanged(account, folder, unreadMessageCount); } @Override public void messageUidChanged(Account account, String folder, String oldUid, String newUid) { MessageReference ref = new MessageReference(); ref.accountUuid = account.getUuid(); ref.folderName = folder; ref.uid = oldUid; mHandler.changeMessageUid(ref, newUid); } }; private boolean updateForMe(Account account, String folder) { if ((account.equals(mAccount) && mFolderName != null && folder.equals(mFolderName))) { return true; } else { return false; } } public List getMessages() { return mMessages; } public void restoreMessages(List messages) { mMessages.addAll(messages); } private Drawable mAttachmentIcon; private Drawable mForwardedIcon; private Drawable mAnsweredIcon; private Drawable mForwardedAnsweredIcon; MessageListAdapter() { mAttachmentIcon = getResources().getDrawable(R.drawable.ic_email_attachment_small); mAnsweredIcon = getResources().getDrawable(R.drawable.ic_email_answered_small); mForwardedIcon = getResources().getDrawable(R.drawable.ic_email_forwarded_small); mForwardedAnsweredIcon = getResources().getDrawable(R.drawable.ic_email_forwarded_answered_small); } public void markAllMessagesAsDirty() { for (MessageInfoHolder holder : mMessages) { holder.dirty = true; } } public void pruneDirtyMessages() { List messagesToRemove = new ArrayList(); for (MessageInfoHolder holder : mMessages) { if (holder.dirty) { messagesToRemove.add(holder); } } removeMessages(messagesToRemove); } public void removeMessage(MessageReference messageReference) { MessageInfoHolder holder = getMessage(messageReference); if (holder == null) { Log.w(K9.LOG_TAG, "Got callback to remove non-existent message with UID " + messageReference.uid); } else { removeMessages(Collections.singletonList(holder)); } } public void removeMessages(final List messages) { if (messages.isEmpty()) { return; } for (MessageInfoHolder message : messages) { if (message != null && (mFolderName == null || ( message.folder != null && message.folder.name.equals(mFolderName)))) { if (message.selected && mSelectedCount > 0) { mSelectedCount--; } mMessages.remove(message); } } resetUnreadCount(); notifyDataSetChanged(); computeSelectAllVisibility(); } /** * Set the selection state for all messages at once. * @param selected Selection state to set. */ public void setSelectionForAllMesages(final boolean selected) { for (MessageInfoHolder message : mMessages) { message.selected = selected; } notifyDataSetChanged(); } public void addMessages(final List messages) { if (messages.isEmpty()) { return; } final boolean wasEmpty = mMessages.isEmpty(); for (final MessageInfoHolder message : messages) { if (mFolderName == null || (message.folder != null && message.folder.name.equals(mFolderName))) { int index = Collections.binarySearch(mMessages, message, getComparator()); if (index < 0) { index = (index * -1) - 1; } mMessages.add(index, message); } } if (wasEmpty) { mListView.setSelection(0); } resetUnreadCount(); notifyDataSetChanged(); computeSelectAllVisibility(); } public void changeMessageUid(MessageReference ref, String newUid) { MessageInfoHolder holder = getMessage(ref); if (holder != null) { holder.uid = newUid; holder.message.setUid(newUid); } } public void resetUnreadCount() { if (mQueryString != null) { int unreadCount = 0; for (MessageInfoHolder holder : mMessages) { unreadCount += holder.read ? 0 : 1; } mUnreadMessageCount = unreadCount; refreshTitle(); } } public void sortMessages() { final Comparator chainComparator = getComparator(); Collections.sort(mMessages, chainComparator); notifyDataSetChanged(); } public void addOrUpdateMessages(final Account account, final String folderName, final List messages, final boolean verifyAgainstSearch) { boolean needsSort = false; final List messagesToAdd = new ArrayList(); List messagesToRemove = new ArrayList(); List messagesToSearch = new ArrayList(); // cache field into local variable for faster access for JVM without JIT final MessageHelper messageHelper = mMessageHelper; for (Message message : messages) { MessageInfoHolder m = getMessage(message); if (message.isSet(Flag.DELETED)) { if (m != null) { messagesToRemove.add(m); } } else { final Folder messageFolder = message.getFolder(); final Account messageAccount = messageFolder.getAccount(); if (m == null) { if (updateForMe(account, folderName)) { m = new MessageInfoHolder(); FolderInfoHolder folderInfoHolder = new FolderInfoHolder( MessageList.this, messageFolder, messageAccount); messageHelper.populate(m, message, folderInfoHolder, messageAccount); messagesToAdd.add(m); } else { if (mQueryString != null) { if (verifyAgainstSearch) { messagesToSearch.add(message); } else { m = new MessageInfoHolder(); FolderInfoHolder folderInfoHolder = new FolderInfoHolder( MessageList.this, messageFolder, messageAccount); messageHelper.populate(m, message, folderInfoHolder, messageAccount); messagesToAdd.add(m); } } } } else { m.dirty = false; // as we reload the message, unset its dirty flag FolderInfoHolder folderInfoHolder = new FolderInfoHolder(MessageList.this, messageFolder, account); messageHelper.populate(m, message, folderInfoHolder, account); needsSort = true; } } } if (!messagesToSearch.isEmpty()) { mController.searchLocalMessages(mAccountUuids, mFolderNames, messagesToSearch.toArray(EMPTY_MESSAGE_ARRAY), mQueryString, mIntegrate, mQueryFlags, mForbiddenFlags, new MessagingListener() { @Override public void listLocalMessagesAddMessages(Account account, String folder, List messages) { mHandler.addOrUpdateMessages(account, folder, messages, false); } }); } if (!messagesToRemove.isEmpty()) { removeMessages(messagesToRemove); } if (!messagesToAdd.isEmpty()) { addMessages(messagesToAdd); } if (needsSort) { sortMessages(); resetUnreadCount(); } } /** * Find a specific message in the message list. * *

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

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

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

* * @param messageReference * A {@link MessageReference} instance describing the message to look for. * * @return The corresponding {@link MessageInfoHolder} instance if the message was found in * the message list. {@code null} otherwise. */ private MessageInfoHolder getMessage(MessageReference messageReference) { String uid; for (MessageInfoHolder holder : mMessages) { uid = messageReference.uid; if ((holder.uid == uid || uid.equals(holder.uid)) && holder.folder.name.equals(messageReference.folderName) && holder.account.equals(messageReference.accountUuid)) { return holder; } } return null; } public FolderInfoHolder getFolder(String folder, Account account) { LocalFolder local_folder = null; try { LocalStore localStore = account.getLocalStore(); local_folder = localStore.getFolder(folder); return new FolderInfoHolder(context, local_folder, account); } catch (Exception e) { Log.e(K9.LOG_TAG, "getFolder(" + folder + ") goes boom: ", e); return null; } finally { if (local_folder != null) { local_folder.close(); } } } private final OnClickListener flagClickListener = new OnClickListener() { @Override public void onClick(View v) { // Perform action on clicks MessageInfoHolder message = (MessageInfoHolder) getItem((Integer)v.getTag()); onToggleFlag(Arrays.asList(new MessageInfoHolder[]{message})); } }; private final OnClickListener itemMenuClickListener = new OnClickListener() { @Override public void onClick(View v) { // Perform action on clicks final MessageInfoHolder message = (MessageInfoHolder) getItem((Integer)v.getTag()); final MenuBuilder menu = new MenuBuilder(MessageList.this); getSupportMenuInflater().inflate(R.menu.message_list_item_context, menu); MenuPopup popup = new MenuPopup(MessageList.this, menu, v); popup.setOnMenuItemClickListener( new MenuPopup.OnMenuItemClickListener() { @Override public void onMenuItemClick(int itemId){ MenuItem item = menu.getItem(itemId); final List selection = getSelectionFromMessage(message); switch (item.getItemId()) { case R.id.reply: { onReply(message); break; } case R.id.reply_all: { onReplyAll(message); break; } case R.id.forward: { onForward(message); break; } case R.id.send_again: { onResendMessage(message); mSelectedCount = 0; break; } case R.id.same_sender: { MessageList.actionHandle(MessageList.this, "From " + message.sender, message.senderAddress, false, null, null); break; } case R.id.delete: { onDelete(selection); break; } case R.id.read_toggle: { onToggleRead(selection); break; } case R.id.flag_toggle: { onToggleFlag(selection); break; } // only if the account supports this case R.id.archive: { onArchive(selection); break; } case R.id.spam: { onSpam(selection); break; } case R.id.move: { onMove(selection); break; } case R.id.copy: { onCopy(selection); break; } } } } ); popup.show(); } }; @Override public int getCount() { return mMessages.size(); } @Override public long getItemId(int position) { try { MessageInfoHolder messageHolder = (MessageInfoHolder) getItem(position); if (messageHolder != null) { return messageHolder.message.getId(); } } catch (Exception e) { Log.i(K9.LOG_TAG, "getItemId(" + position + ") ", e); } return -1; } @Override public Object getItem(int position) { try { if (position < mMessages.size()) { return mMessages.get(position); } } catch (Exception e) { Log.e(K9.LOG_TAG, "getItem(" + position + "), but folder.messages.size() = " + mMessages.size(), e); } return null; } @Override public View getView(int position, View convertView, ViewGroup parent) { MessageInfoHolder message = (MessageInfoHolder) getItem(position); View view; if ((convertView != null) && (convertView.getId() == R.layout.message_list_item)) { view = convertView; } else { view = mInflater.inflate(R.layout.message_list_item, parent, false); view.setId(R.layout.message_list_item); } MessageViewHolder holder = (MessageViewHolder) view.getTag(); if (holder == null) { holder = new MessageViewHolder(); holder.subject = (TextView) view.findViewById(R.id.subject); holder.from = (TextView) view.findViewById(R.id.from); holder.date = (TextView) view.findViewById(R.id.date); holder.chip = view.findViewById(R.id.chip); holder.preview = (TextView) view.findViewById(R.id.preview); holder.selected = (CheckBox) view.findViewById(R.id.selected_checkbox); holder.flagged = (CheckBox) view.findViewById(R.id.flagged); holder.itemMenu = (ImageButton) view.findViewById(R.id.item_menu); holder.flagged.setOnClickListener(flagClickListener); holder.itemMenu.setOnClickListener(itemMenuClickListener); if (!mStars) { holder.flagged.setVisibility(View.GONE); } if (mCheckboxes) { holder.selected.setVisibility(View.VISIBLE); } if (holder.selected != null) { holder.selected.setOnCheckedChangeListener(holder); } holder.subject.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getMessageListSubject()); holder.date.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getMessageListDate()); holder.preview.setLines(mPreviewLines); holder.preview.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getMessageListPreview()); holder.itemMenu.setFocusable(false); view.setTag(holder); } if (message != null) { bindView(position, view, holder, message); } else { // This branch code is triggered when the local store // hands us an invalid message holder.chip.getBackground().setAlpha(0); holder.subject.setText(getString(R.string.general_no_subject)); holder.subject.setTypeface(null, Typeface.NORMAL); String noSender = getString(R.string.general_no_sender); if (holder.preview != null) { holder.preview.setText(noSender, TextView.BufferType.SPANNABLE); Spannable str = (Spannable) holder.preview.getText(); str.setSpan(new StyleSpan(Typeface.NORMAL), 0, noSender.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); str.setSpan(new AbsoluteSizeSpan(mFontSizes.getMessageListSender(), true), 0, noSender.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } else { holder.from.setText(noSender); holder.from.setTypeface(null, Typeface.NORMAL); holder.from.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); } holder.date.setText(getString(R.string.general_no_date)); //WARNING: Order of the next 2 lines matter holder.position = -1; holder.selected.setChecked(false); if (!mCheckboxes) { holder.selected.setVisibility(View.GONE); } holder.flagged.setChecked(false); } return view; } /** * Associate model data to view object. * * @param position * The position of the item within the adapter's data set of * the item whose view we want. * @param view * Main view component to alter. Never null. * @param holder * Convenience view holder - eases access to view * child views. Never null. * @param message * Never null. */ private void bindView(final int position, final View view, final MessageViewHolder holder, final MessageInfoHolder message) { holder.subject.setTypeface(null, message.read ? Typeface.NORMAL : Typeface.BOLD); // XXX TODO there has to be some way to walk our view hierarchy and get this holder.flagged.setTag(position); holder.itemMenu.setTag(position); holder.flagged.setChecked(message.flagged); // So that the mSelectedCount is only incremented/decremented // when a user checks the checkbox (vs code) holder.position = -1; holder.selected.setChecked(message.selected); if (!mCheckboxes) { holder.selected.setVisibility(message.selected ? View.VISIBLE : View.GONE); } holder.chip.setBackgroundDrawable(message.message.getFolder().getAccount().generateColorChip(message.read).drawable()); // TODO: Make these colors part of the theme // if (K9.getK9Theme() == K9.THEME_LIGHT) { // // Light theme: light grey background for read messages // view.setBackgroundColor(message.read ? Color.rgb(229, 229, 229) : Color.rgb(255, 255, 255)); // } else { // // Dark theme: dark grey background for unread messages // view.setBackgroundColor(message.read ? 0 : Color.rgb(45, 45, 45)); // } if ((message.message.getSubject() == null) || message.message.getSubject().equals("")) { holder.subject.setText(getText(R.string.general_no_subject)); } else { holder.subject.setText(message.message.getSubject()); } int senderTypeface = message.read ? Typeface.NORMAL : Typeface.BOLD; if (holder.preview != null) { /* * Because text views can't wrap around each other(?) we * compose a custom view containing the preview and the * from. */ holder.preview.setText(new SpannableStringBuilder(recipientSigil(message)) .append(message.sender).append(" ").append(message.message.getPreview()), TextView.BufferType.SPANNABLE); Spannable str = (Spannable)holder.preview.getText(); // Create a span section for the sender, and assign the correct font size and weight. str.setSpan(new StyleSpan(senderTypeface), 0, message.sender.length() + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); str.setSpan(new AbsoluteSizeSpan(mFontSizes.getMessageListSender(), true), 0, message.sender.length() + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); // TODO: Make these colors part of the theme int color = (K9.getK9Theme() == K9.THEME_LIGHT) ? Color.rgb(105, 105, 105) : Color.rgb(160, 160, 160); // set span for preview message. str.setSpan(new ForegroundColorSpan(color), // How do I can specify the android.R.attr.textColorTertiary message.sender.length() + 1, str.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } else { holder.from.setText(new SpannableStringBuilder(recipientSigil(message)).append(message.sender)); holder.from.setTypeface(null, senderTypeface); } holder.date.setText(message.getDate(mMessageHelper)); Drawable statusHolder = null; if (message.forwarded && message.answered) { statusHolder = mForwardedAnsweredIcon; } else if (message.answered) { statusHolder = mAnsweredIcon; } else if (message.forwarded) { statusHolder = mForwardedIcon; } holder.subject.setCompoundDrawablesWithIntrinsicBounds(statusHolder, // left null, // top message.message.hasAttachments() ? mAttachmentIcon : null, // right null); // bottom holder.position = position; } private String recipientSigil(MessageInfoHolder message) { if (message.message.toMe()) { return getString(R.string.messagelist_sent_to_me_sigil); } else if (message.message.ccMe()) { return getString(R.string.messagelist_sent_cc_me_sigil); } else { return ""; } } @Override public boolean hasStableIds() { return true; } } class MessageViewHolder implements OnCheckedChangeListener { public TextView subject; public TextView preview; public TextView from; public TextView time; public TextView date; public CheckBox flagged; public View chip; public CheckBox selected; public ImageButton itemMenu; public int position = -1; @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (position != -1) { MessageInfoHolder message = (MessageInfoHolder) mAdapter.getItem(position); toggleMessageSelect(message); if (!mCheckboxes) { selected.setVisibility(isChecked ? View.VISIBLE : View.GONE); } } } } private View getFooterView(ViewGroup parent) { if (mFooterView == null) { mFooterView = mInflater.inflate(R.layout.message_list_item_footer, parent, false); mFooterView.setId(R.layout.message_list_item_footer); FooterViewHolder holder = new FooterViewHolder(); holder.progress = (ProgressBar) mFooterView.findViewById(R.id.message_list_progress); holder.progress.setIndeterminate(true); holder.main = (TextView) mFooterView.findViewById(R.id.main_text); mFooterView.setTag(holder); } return mFooterView; } private void updateFooterView() { if (mCurrentFolder != null && mAccount != null) { if (mCurrentFolder.loading) { final boolean showProgress = true; updateFooter(getString(R.string.status_loading_more), showProgress); } else { String message; if (!mCurrentFolder.lastCheckFailed) { if (mAccount.getDisplayCount() == 0) { message = getString(R.string.message_list_load_more_messages_action); } else { message = String.format(getString(R.string.load_more_messages_fmt), mAccount.getDisplayCount()); } } else { message = getString(R.string.status_loading_more_failed); } final boolean showProgress = false; updateFooter(message, showProgress); } } else { final boolean showProgress = false; updateFooter(null, showProgress); } } public void updateFooter(final String text, final boolean progressVisible) { FooterViewHolder holder = (FooterViewHolder) mFooterView.getTag(); holder.progress.setVisibility(progressVisible ? ProgressBar.VISIBLE : ProgressBar.INVISIBLE); if (text != null) { holder.main.setText(text); } if (progressVisible || holder.main.getText().length() > 0) { holder.main.setVisibility(View.VISIBLE); } else { holder.main.setVisibility(View.GONE); } } static class FooterViewHolder { public ProgressBar progress; public TextView main; } private void setAllSelected(boolean isSelected) { mSelectedCount = 0; for (MessageInfoHolder holder : mAdapter.getMessages()) { holder.selected = isSelected; mSelectedCount += (isSelected ? 1 : 0); } mAdapter.notifyDataSetChanged(); if (isSelected) { updateActionModeTitle(); computeSelectAllVisibility(); } } /** * Toggle all selected message states. Sort of. If anything selected, unselect everything. If nothing is * selected, select everything. */ private void toggleAllSelected() { boolean newState = true; // If there was anything selected, unselect everything. if (mSelectedCount > 0) { newState = false; } mAdapter.setSelectionForAllMesages(newState); if (newState) { mSelectedCount = mAdapter.getCount(); mActionMode = MessageList.this.startActionMode(mActionModeCallback); updateActionModeTitle(); computeSelectAllVisibility(); } else { mSelectedCount = 0; mActionMode.finish(); } } private void toggleMessageSelect(final MessageInfoHolder holder){ if (mActionMode != null) { if (mSelectedCount == 1 && holder.selected) { mActionMode.finish(); return; } } else { mActionMode = MessageList.this.startActionMode(mActionModeCallback); } if (holder.selected) { holder.selected = false; mSelectedCount -= 1; } else { holder.selected = true; mSelectedCount += 1; } mAdapter.notifyDataSetChanged(); updateActionModeTitle(); // make sure the onPrepareActionMode is called mActionMode.invalidate(); computeSelectAllVisibility(); } private void updateActionModeTitle() { mActionMode.setTitle(String.format(getString(R.string.actionbar_selected), mSelectedCount)); } private void computeSelectAllVisibility() { mActionModeCallback.showSelectAll(mSelectedCount != mAdapter.getCount()); } /** * @param holders * Messages to update. Never {@code null}. * @param flag * Flag to be updated on the specified messages. Never * {@code null}. * @param newState * State to set for the given flag. */ private void setFlag(final List holders, final Flag flag, final boolean newState) { if (holders.isEmpty()) { return; } final Message[] messageList = new Message[holders.size()]; int i = 0; for (final Iterator iterator = holders.iterator(); iterator.hasNext(); i++) { final MessageInfoHolder holder = iterator.next(); messageList[i] = holder.message; if (flag == Flag.SEEN) { holder.read = newState; } else if (flag == Flag.FLAGGED) { holder.flagged = newState; } } mController.setFlag(messageList, flag, newState); mAdapter.sortMessages(); } /** * Display the message move activity. * * @param holders * Never {@code null}. */ private void onMove(final List holders) { if (!checkCopyOrMovePossible(holders, FolderOperation.MOVE)) { return; } final Folder folder = holders.size() == 1 ? holders.get(0).message.getFolder() : mCurrentFolder.folder; displayFolderChoice(ACTIVITY_CHOOSE_FOLDER_MOVE, folder, holders); } /** * Display the message copy activity. * * @param holders * Never {@code null}. */ private void onCopy(final List holders) { if (!checkCopyOrMovePossible(holders, FolderOperation.COPY)) { return; } final Folder folder = holders.size() == 1 ? holders.get(0).message.getFolder() : mCurrentFolder.folder; displayFolderChoice(ACTIVITY_CHOOSE_FOLDER_COPY, folder, holders); } /** * Helper method to manage the invocation of * {@link #startActivityForResult(Intent, int)} for a folder operation * ({@link ChooseFolder} activity), while saving a list of associated * messages. * * @param requestCode * If >= 0, this code will be returned in onActivityResult() when * the activity exits. * @param folder * Never {@code null}. * @param holders * Messages to be affected by the folder operation. Never * {@code null}. * @see #startActivityForResult(Intent, int) */ private void displayFolderChoice(final int requestCode, final Folder folder, final List holders) { final Intent intent = new Intent(this, ChooseFolder.class); intent.putExtra(ChooseFolder.EXTRA_ACCOUNT, folder.getAccount().getUuid()); intent.putExtra(ChooseFolder.EXTRA_CUR_FOLDER, folder.getName()); intent.putExtra(ChooseFolder.EXTRA_SEL_FOLDER, folder.getAccount().getLastSelectedFolderName()); // remember the selected messages for #onActivityResult mActiveMessages = holders; startActivityForResult(intent, requestCode); } /** * @param holders * Never {@code null}. */ private void onArchive(final List holders) { final String folderName = holders.get(0).message.getFolder().getAccount().getArchiveFolderName(); if (K9.FOLDER_NONE.equalsIgnoreCase(folderName)) { return; } // TODO one should separate messages by account and call move afterwards // (because each account might have a specific Archive folder name) move(holders, folderName); } /** * @param holders * Never {@code null}. */ private void onSpam(final List holders) { if (K9.confirmSpam()) { // remember the message selection for #onCreateDialog(int) mActiveMessages = holders; showDialog(R.id.dialog_confirm_spam); } else { onSpamConfirmed(holders); } } /** * @param holders * Never {@code null}. */ private void onSpamConfirmed(final List holders) { final String folderName = holders.get(0).message.getFolder().getAccount().getSpamFolderName(); if (K9.FOLDER_NONE.equalsIgnoreCase(folderName)) { return; } // TODO one should separate messages by account and call move afterwards // (because each account might have a specific Spam folder name) move(holders, folderName); } private static enum FolderOperation { COPY, MOVE } /** * Display an Toast message if any message isn't synchronized * * @param holders * Never null. * @param operation * Never {@code null}. * * @return true if operation is possible */ private boolean checkCopyOrMovePossible(final List holders, final FolderOperation operation) { if (holders.isEmpty()) { return false; } boolean first = true; for (final MessageInfoHolder holder : holders) { final Message message = holder.message; if (first) { first = false; // account check final Account account = message.getFolder().getAccount(); if ((operation == FolderOperation.MOVE && !mController.isMoveCapable(account)) || (operation == FolderOperation.COPY && !mController.isCopyCapable(account))) { return false; } } // message check if ((operation == FolderOperation.MOVE && !mController.isMoveCapable(message)) || (operation == FolderOperation.COPY && !mController.isCopyCapable(message))) { final Toast toast = Toast.makeText(this, R.string.move_copy_cannot_copy_unsynced_message, Toast.LENGTH_LONG); toast.show(); return false; } } return true; } /** * Helper method to get a List of message ready to be processed. This implementation will return a list containing the sole argument. * * @param holder Never {@code null}. * @return Never {@code null}. */ private List getSelectionFromMessage(final MessageInfoHolder holder) { final List selection = Collections.singletonList(holder); return selection; } /** * Helper method to get a List of message ready to be processed. This implementation will iterate over messages and choose the checked ones. * * @return Never {@code null}. */ private List getSelectionFromCheckboxes() { final List selection = new ArrayList(); for (final MessageInfoHolder holder : mAdapter.getMessages()) { if (holder.selected) { selection.add(holder); } } return selection; } /** * Copy the specified messages to the specified folder. * * @param holders Never {@code null}. * @param destination Never {@code null}. */ private void copy(final List holders, final String destination) { copyOrMove(holders, destination, FolderOperation.COPY); } /** * Move the specified messages to the specified folder. * * @param holders Never {@code null}. * @param destination Never {@code null}. */ private void move(final List holders, final String destination) { copyOrMove(holders, destination, FolderOperation.MOVE); } /** * The underlying implementation for {@link #copy(List, String)} and * {@link #move(List, String)}. This method was added mainly because those 2 * methods share common behavior. * * Note: Must be called from the UI thread! * * @param holders * Never {@code null}. * @param destination * Never {@code null}. * @param operation * Never {@code null}. */ private void copyOrMove(final List holders, final String destination, final FolderOperation operation) { if (K9.FOLDER_NONE.equalsIgnoreCase(destination)) { return; } boolean first = true; Account account = null; String folderName = null; final List messages = new ArrayList(holders.size()); for (final MessageInfoHolder holder : holders) { final Message message = holder.message; if (first) { first = false; folderName = message.getFolder().getName(); account = message.getFolder().getAccount(); if ((operation == FolderOperation.MOVE && !mController.isMoveCapable(account)) || (operation == FolderOperation.COPY && !mController.isCopyCapable(account))) { // account is not copy/move capable return; } } else if (!account.equals(message.getFolder().getAccount()) || !folderName.equals(message.getFolder().getName())) { // make sure all messages come from the same account/folder? return; } if ((operation == FolderOperation.MOVE && !mController.isMoveCapable(message)) || (operation == FolderOperation.COPY && !mController.isCopyCapable(message))) { final Toast toast = Toast.makeText(this, R.string.move_copy_cannot_copy_unsynced_message, Toast.LENGTH_LONG); toast.show(); // XXX return meaningful error value? // message isn't synchronized return; } messages.add(message); } if (operation == FolderOperation.MOVE) { mController.moveMessages(account, folderName, messages.toArray(new Message[messages.size()]), destination, null); mAdapter.removeMessages(holders); } else { mController.copyMessages(account, folderName, messages.toArray(new Message[messages.size()]), destination, null); } } protected void onAccountUnavailable() { finish(); // TODO inform user about account unavailability using Toast Accounts.listAccounts(this); } /** * Return the currently "open" account if available. * * @param prefs * A {@link Preferences} instance that might be used to retrieve the current * {@link Account}. * * @return The {@code Account} all displayed messages belong to. */ private Account getCurrentAccount(Preferences prefs) { Account account = null; if (mQueryString != null && !mIntegrate && mAccountUuids != null && mAccountUuids.length == 1) { String uuid = mAccountUuids[0]; account = prefs.getAccount(uuid); } else if (mAccount != null) { account = mAccount; } return account; } class ActionModeCallback implements ActionMode.Callback { private MenuItem mSelectAll; @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { mSelectAll = menu.findItem(R.id.select_all); if (mQueryString != null) { // show all menu.findItem(R.id.move).setVisible(true); menu.findItem(R.id.archive).setVisible(true); menu.findItem(R.id.spam).setVisible(true); menu.findItem(R.id.copy).setVisible(true); // hide uncapable /* * TODO think of a better way then looping over all * messages. */ final List selection = getSelectionFromCheckboxes(); Account account; for (MessageInfoHolder holder : selection) { account = holder.message.getFolder().getAccount(); setContextCapabilities(account, menu); } } return true; } @Override public void onDestroyActionMode(ActionMode mode) { mActionMode = null; mSelectAll = null; setAllSelected(false); } @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { MenuInflater inflater = mode.getMenuInflater(); inflater.inflate(R.menu.message_list_context, menu); // check capabilities if (mQueryString == null) { setContextCapabilities(mAccount, menu); } return true; } /** * Disables menu options based on if the account supports it or not. * It also checks the controller and for now the 'mode' the messagelist * is operation in ( query or not ). * * @param mAccount Account to check capabilities of. * @param menu Menu to adapt. */ private void setContextCapabilities(Account mAccount, Menu menu) { /* * TODO get rid of this when we finally split the messagelist into * a folder content display and a search result display */ if (mQueryString != null) { menu.findItem(R.id.move).setVisible(false); menu.findItem(R.id.copy).setVisible(false); menu.findItem(R.id.archive).setVisible(false); menu.findItem(R.id.spam).setVisible(false); return; } // hide unsupported if (!mController.isCopyCapable(mAccount)) { menu.findItem(R.id.copy).setVisible(false); } if (!mController.isMoveCapable(mAccount)) { menu.findItem(R.id.move).setVisible(false); menu.findItem(R.id.archive).setVisible(false); menu.findItem(R.id.spam).setVisible(false); } if (!mAccount.hasArchiveFolder()) { menu.findItem(R.id.archive).setVisible(false); } if (!mAccount.hasSpamFolder()) { menu.findItem(R.id.spam).setVisible(false); } } public void showSelectAll(boolean show) { if (mActionMode != null) { mSelectAll.setVisible(show); } } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { final List selection = getSelectionFromCheckboxes(); /* * In the following we assume that we can't move or copy * mails to the same folder. Also that spam isn't available if we are * in the spam folder,same for archive. * * This is the case currently so safe assumption. */ switch (item.getItemId()) { case R.id.delete: { onDelete(selection); mSelectedCount = 0; break; } case R.id.read_toggle: { onToggleRead(selection); break; } case R.id.flag_toggle: { onToggleFlag(selection); break; } case R.id.select_all: { setAllSelected(true); break; } // only if the account supports this case R.id.archive: { onArchive(selection); mSelectedCount = 0; break; } case R.id.spam: { onSpam(selection); mSelectedCount = 0; break; } case R.id.move: { onMove(selection); mSelectedCount = 0; break; } case R.id.copy: { onCopy(selection); mSelectedCount = 0; break; } } if (mSelectedCount == 0) { mActionMode.finish(); } return true; } }; }