package com.fsck.k9.fragment;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Future;
import android.app.Activity;
import android.app.DialogFragment;
import android.app.Fragment;
import android.app.LoaderManager;
import android.app.LoaderManager.LoaderCallbacks;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.CursorLoader;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.Loader;
import android.database.Cursor;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.widget.SwipeRefreshLayout;
import android.text.TextUtils;
import timber.log.Timber;
import android.view.ActionMode;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.fsck.k9.Account;
import com.fsck.k9.Account.SortType;
import com.fsck.k9.BuildConfig;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
import com.fsck.k9.activity.ActivityListener;
import com.fsck.k9.activity.ChooseFolder;
import com.fsck.k9.activity.FolderInfoHolder;
import com.fsck.k9.activity.MessageReference;
import com.fsck.k9.activity.misc.ContactPictureLoader;
import com.fsck.k9.cache.EmailProviderCache;
import com.fsck.k9.controller.MessagingController;
import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener;
import com.fsck.k9.fragment.MessageListFragmentComparators.ArrivalComparator;
import com.fsck.k9.fragment.MessageListFragmentComparators.AttachmentComparator;
import com.fsck.k9.fragment.MessageListFragmentComparators.ComparatorChain;
import com.fsck.k9.fragment.MessageListFragmentComparators.DateComparator;
import com.fsck.k9.fragment.MessageListFragmentComparators.FlaggedComparator;
import com.fsck.k9.fragment.MessageListFragmentComparators.ReverseComparator;
import com.fsck.k9.fragment.MessageListFragmentComparators.ReverseIdComparator;
import com.fsck.k9.fragment.MessageListFragmentComparators.SenderComparator;
import com.fsck.k9.fragment.MessageListFragmentComparators.SubjectComparator;
import com.fsck.k9.fragment.MessageListFragmentComparators.UnreadComparator;
import com.fsck.k9.helper.ContactPicture;
import com.fsck.k9.helper.MergeCursorWithUniqueId;
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.MessagingException;
import com.fsck.k9.mailstore.LocalFolder;
import com.fsck.k9.preferences.StorageEditor;
import com.fsck.k9.provider.EmailProvider;
import com.fsck.k9.provider.EmailProvider.MessageColumns;
import com.fsck.k9.provider.EmailProvider.SpecialColumns;
import com.fsck.k9.search.ConditionsTreeNode;
import com.fsck.k9.search.LocalSearch;
import com.fsck.k9.search.SearchSpecification;
import com.fsck.k9.search.SearchSpecification.SearchCondition;
import com.fsck.k9.search.SearchSpecification.SearchField;
import com.fsck.k9.search.SqlQueryBuilder;
import static com.fsck.k9.fragment.MLFProjectionInfo.ACCOUNT_UUID_COLUMN;
import static com.fsck.k9.fragment.MLFProjectionInfo.FLAGGED_COLUMN;
import static com.fsck.k9.fragment.MLFProjectionInfo.FOLDER_NAME_COLUMN;
import static com.fsck.k9.fragment.MLFProjectionInfo.ID_COLUMN;
import static com.fsck.k9.fragment.MLFProjectionInfo.PROJECTION;
import static com.fsck.k9.fragment.MLFProjectionInfo.READ_COLUMN;
import static com.fsck.k9.fragment.MLFProjectionInfo.SUBJECT_COLUMN;
import static com.fsck.k9.fragment.MLFProjectionInfo.THREADED_PROJECTION;
import static com.fsck.k9.fragment.MLFProjectionInfo.THREAD_COUNT_COLUMN;
import static com.fsck.k9.fragment.MLFProjectionInfo.THREAD_ROOT_COLUMN;
import static com.fsck.k9.fragment.MLFProjectionInfo.UID_COLUMN;
public class MessageListFragment extends Fragment implements OnItemClickListener,
ConfirmationDialogFragmentListener, LoaderCallbacks<Cursor> {
public static MessageListFragment newInstance(
LocalSearch search, boolean isThreadDisplay, boolean threadedList) {
MessageListFragment fragment = new MessageListFragment();
Bundle args = new Bundle();
args.putParcelable(ARG_SEARCH, search);
args.putBoolean(ARG_IS_THREAD_DISPLAY, isThreadDisplay);
args.putBoolean(ARG_THREADED_LIST, threadedList);
fragment.setArguments(args);
return fragment;
}
private static final int ACTIVITY_CHOOSE_FOLDER_MOVE = 1;
private static final int ACTIVITY_CHOOSE_FOLDER_COPY = 2;
private static final String ARG_SEARCH = "searchObject";
private static final String ARG_THREADED_LIST = "showingThreadedList";
private static final String ARG_IS_THREAD_DISPLAY = "isThreadedDisplay";
private static final String STATE_SELECTED_MESSAGES = "selectedMessages";
private static final String STATE_ACTIVE_MESSAGE = "activeMessage";
private static final String STATE_REMOTE_SEARCH_PERFORMED = "remoteSearchPerformed";
private static final String STATE_MESSAGE_LIST = "listState";
/**
* Maps a {@link SortType} to a {@link Comparator} implementation.
*/
private static final Map<SortType, Comparator<Cursor>> SORT_COMPARATORS;
static {
// fill the mapping at class time loading
final Map<SortType, Comparator<Cursor>> 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_SUBJECT, new SubjectComparator());
map.put(SortType.SORT_SENDER, new SenderComparator());
map.put(SortType.SORT_UNREAD, new UnreadComparator());
// make it immutable to prevent accidental alteration (content is immutable already)
SORT_COMPARATORS = Collections.unmodifiableMap(map);
}
ListView listView;
private SwipeRefreshLayout swipeRefreshLayout;
Parcelable savedListState;
int previewLines = 0;
private MessageListAdapter adapter;
private View footerView;
private FolderInfoHolder currentFolder;
private LayoutInflater layoutInflater;
private MessagingController messagingController;
private Account account;
private String[] accountUuids;
private int unreadMessageCount = 0;
private Cursor[] cursors;
private boolean[] cursorValid;
int uniqueIdColumn;
/**
* Stores the name of the folder that we want to open as soon as possible after load.
*/
private String folderName;
private boolean remoteSearchPerformed = false;
private Future<?> remoteSearchFuture = null;
private List<Message> extraSearchResults;
private String title;
private LocalSearch search = null;
private boolean singleAccountMode;
private boolean singleFolderMode;
private boolean allAccounts;
private final MessageListHandler handler = new MessageListHandler(this);
private SortType sortType = SortType.SORT_DATE;
private boolean sortAscending = true;
private boolean sortDateAscending = false;
boolean senderAboveSubject = false;
boolean checkboxes = true;
boolean stars = true;
private int selectedCount = 0;
Set<Long> selected = new HashSet<>();
private ActionMode actionMode;
private Boolean hasConnectivity;
/**
* Relevant messages for the current context when we have to remember the chosen messages
* between user interactions (e.g. selecting a folder for move operation).
*/
private List<MessageReference> activeMessages;
/* package visibility for faster inner class access */
MessageHelper messageHelper;
private final ActionModeCallback actionModeCallback = new ActionModeCallback();
MessageListFragmentListener fragmentListener;
boolean showingThreadedList;
private boolean isThreadDisplay;
private Context context;
private final ActivityListener activityListener = new MessageListActivityListener();
private Preferences preferences;
private boolean loaderJustInitialized;
MessageReference activeMessage;
/**
* {@code true} after {@link #onCreate(Bundle)} was executed. Used in {@link #updateTitle()} to
* make sure we don't access member variables before initialization is complete.
*/
private boolean initialized = false;
ContactPictureLoader contactsPictureLoader;
private LocalBroadcastManager localBroadcastManager;
private BroadcastReceiver cacheBroadcastReceiver;
private IntentFilter cacheIntentFilter;
/**
* Stores the unique ID of the message the context menu was opened for.
*
* We have to save this because the message list might change between the time the menu was
* opened and when the user clicks on a menu item. When this happens the 'adapter position' that
* is accessible via the {@code ContextMenu} object might correspond to another list item and we
* would end up using/modifying the wrong message.
*
* The value of this field is {@code 0} when no context menu is currently open.
*/
private long contextMenuUniqueId = 0;
/**
* @return The comparator to use to display messages in an ordered
* fashion. Never {@code null}.
*/
private Comparator<Cursor> getComparator() {
final List<Comparator<Cursor>> chain =
new ArrayList<>(3 /* we add 3 comparators at most */);
// Add the specified comparator
final Comparator<Cursor> comparator = SORT_COMPARATORS.get(sortType);
if (sortAscending) {
chain.add(comparator);
} else {
chain.add(new ReverseComparator<>(comparator));
}
// Add the date comparator if not already specified
if (sortType != SortType.SORT_DATE && sortType != SortType.SORT_ARRIVAL) {
final Comparator<Cursor> dateComparator = SORT_COMPARATORS.get(SortType.SORT_DATE);
if (sortDateAscending) {
chain.add(dateComparator);
} else {
chain.add(new ReverseComparator<>(dateComparator));
}
}
// Add the id comparator
chain.add(new ReverseIdComparator());
// Build the comparator chain
return new ComparatorChain<>(chain);
}
void folderLoading(String folder, boolean loading) {
if (currentFolder != null && currentFolder.name.equals(folder)) {
currentFolder.loading = loading;
}
updateMoreMessagesOfCurrentFolder();
updateFooterView();
}
public void updateTitle() {
if (!initialized) {
return;
}
setWindowTitle();
if (!search.isManualSearch()) {
setWindowProgress();
}
}
private void setWindowProgress() {
int level = Window.PROGRESS_END;
if (currentFolder != null && currentFolder.loading && activityListener.getFolderTotal() > 0) {
int divisor = activityListener.getFolderTotal();
if (divisor != 0) {
level = (Window.PROGRESS_END / divisor) * (activityListener.getFolderCompleted()) ;
if (level > Window.PROGRESS_END) {
level = Window.PROGRESS_END;
}
}
}
fragmentListener.setMessageListProgress(level);
}
private void setWindowTitle() {
// regular folder content display
if (!isManualSearch() && singleFolderMode) {
Activity activity = getActivity();
String displayName = FolderInfoHolder.getDisplayName(activity, account,
folderName);
fragmentListener.setMessageListTitle(displayName);
String operation = activityListener.getOperation(activity);
if (operation.length() < 1) {
fragmentListener.setMessageListSubTitle(account.getEmail());
} else {
fragmentListener.setMessageListSubTitle(operation);
}
} else {
// query result display. This may be for a search folder as opposed to a user-initiated search.
if (title != null) {
// This was a search folder; the search folder has overridden our title.
fragmentListener.setMessageListTitle(title);
} else {
// This is a search result; set it to the default search result line.
fragmentListener.setMessageListTitle(getString(R.string.search_results));
}
fragmentListener.setMessageListSubTitle(null);
}
// set unread count
if (unreadMessageCount <= 0) {
fragmentListener.setUnreadCount(0);
} else {
if (!singleFolderMode && title == null) {
// The unread message count is easily confused
// with total number of messages in the search result, so let's hide it.
fragmentListener.setUnreadCount(0);
} else {
fragmentListener.setUnreadCount(unreadMessageCount);
}
}
}
void progress(final boolean progress) {
fragmentListener.enableActionBarProgress(progress);
if (swipeRefreshLayout != null && !progress) {
swipeRefreshLayout.setRefreshing(false);
}
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (view == footerView) {
if (currentFolder != null && !search.isManualSearch() && currentFolder.moreMessages) {
messagingController.loadMoreMessages(account, folderName, null);
} else if (currentFolder != null && isRemoteSearch() &&
extraSearchResults != null && extraSearchResults.size() > 0) {
int numResults = extraSearchResults.size();
int limit = account.getRemoteSearchNumResults();
List<Message> toProcess = extraSearchResults;
if (limit > 0 && numResults > limit) {
toProcess = toProcess.subList(0, limit);
extraSearchResults = extraSearchResults.subList(limit,
extraSearchResults.size());
} else {
extraSearchResults = null;
updateFooter(null);
}
messagingController.loadSearchResults(account, currentFolder.name, toProcess, activityListener);
}
return;
}
Cursor cursor = (Cursor) parent.getItemAtPosition(position);
if (cursor == null) {
return;
}
if (selectedCount > 0) {
toggleMessageSelect(position);
} else {
if (showingThreadedList && cursor.getInt(THREAD_COUNT_COLUMN) > 1) {
Account account = getAccountFromCursor(cursor);
String folderName = cursor.getString(FOLDER_NAME_COLUMN);
// If threading is enabled and this item represents a thread, display the thread contents.
long rootId = cursor.getLong(THREAD_ROOT_COLUMN);
fragmentListener.showThread(account, folderName, rootId);
} else {
// This item represents a message; just display the message.
openMessageAtPosition(listViewToAdapterPosition(position));
}
}
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
context = activity.getApplicationContext();
try {
fragmentListener = (MessageListFragmentListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.getClass() +
" must implement MessageListFragmentListener");
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Context appContext = getActivity().getApplicationContext();
preferences = Preferences.getPreferences(appContext);
messagingController = MessagingController.getInstance(getActivity().getApplication());
previewLines = K9.messageListPreviewLines();
checkboxes = K9.messageListCheckboxes();
stars = K9.messageListStars();
if (K9.showContactPicture()) {
contactsPictureLoader = ContactPicture.getContactPictureLoader(getActivity());
}
restoreInstanceState(savedInstanceState);
decodeArguments();
createCacheBroadcastReceiver(appContext);
initialized = true;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
layoutInflater = inflater;
View view = inflater.inflate(R.layout.message_list_fragment, container, false);
initializePullToRefresh(view);
initializeLayout();
listView.setVerticalFadingEdgeEnabled(false);
return view;
}
@Override
public void onDestroyView() {
savedListState = listView.onSaveInstanceState();
super.onDestroyView();
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
messageHelper = MessageHelper.getInstance(getActivity());
initializeMessageList();
// This needs to be done before initializing the cursor loader below
initializeSortSettings();
loaderJustInitialized = true;
LoaderManager loaderManager = getLoaderManager();
int len = accountUuids.length;
cursors = new Cursor[len];
cursorValid = new boolean[len];
for (int i = 0; i < len; i++) {
loaderManager.initLoader(i, null, this);
cursorValid[i] = false;
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
saveSelectedMessages(outState);
saveListState(outState);
outState.putBoolean(STATE_REMOTE_SEARCH_PERFORMED, remoteSearchPerformed);
if (activeMessage != null) {
outState.putString(STATE_ACTIVE_MESSAGE, activeMessage.toIdentityString());
}
}
/**
* Restore the state of a previous {@link MessageListFragment} instance.
*
* @see #onSaveInstanceState(Bundle)
*/
private void restoreInstanceState(Bundle savedInstanceState) {
if (savedInstanceState == null) {
return;
}
restoreSelectedMessages(savedInstanceState);
remoteSearchPerformed = savedInstanceState.getBoolean(STATE_REMOTE_SEARCH_PERFORMED);
savedListState = savedInstanceState.getParcelable(STATE_MESSAGE_LIST);
String messageReferenceString = savedInstanceState.getString(STATE_ACTIVE_MESSAGE);
activeMessage = MessageReference.parse(messageReferenceString);
}
/**
* Write the unique IDs of selected messages to a {@link Bundle}.
*/
private void saveSelectedMessages(Bundle outState) {
long[] selected = new long[this.selected.size()];
int i = 0;
for (Long id : this.selected) {
selected[i++] = id;
}
outState.putLongArray(STATE_SELECTED_MESSAGES, selected);
}
/**
* Restore selected messages from a {@link Bundle}.
*/
private void restoreSelectedMessages(Bundle savedInstanceState) {
long[] selected = savedInstanceState.getLongArray(STATE_SELECTED_MESSAGES);
if (selected != null) {
for (long id : selected) {
this.selected.add(id);
}
}
}
private void saveListState(Bundle outState) {
if (savedListState != null) {
// The previously saved state was never restored, so just use that.
outState.putParcelable(STATE_MESSAGE_LIST, savedListState);
} else if (listView != null) {
outState.putParcelable(STATE_MESSAGE_LIST, listView.onSaveInstanceState());
}
}
private void initializeSortSettings() {
if (singleAccountMode) {
sortType = account.getSortType();
sortAscending = account.isSortAscending(sortType);
sortDateAscending = account.isSortAscending(SortType.SORT_DATE);
} else {
sortType = K9.getSortType();
sortAscending = K9.isSortAscending(sortType);
sortDateAscending = K9.isSortAscending(SortType.SORT_DATE);
}
}
private void decodeArguments() {
Bundle args = getArguments();
showingThreadedList = args.getBoolean(ARG_THREADED_LIST, false);
isThreadDisplay = args.getBoolean(ARG_IS_THREAD_DISPLAY, false);
search = args.getParcelable(ARG_SEARCH);
title = search.getName();
String[] accountUuids = search.getAccountUuids();
singleAccountMode = false;
if (accountUuids.length == 1 && !search.searchAllAccounts()) {
singleAccountMode = true;
account = preferences.getAccount(accountUuids[0]);
}
singleFolderMode = false;
if (singleAccountMode && (search.getFolderNames().size() == 1)) {
singleFolderMode = true;
folderName = search.getFolderNames().get(0);
currentFolder = getFolderInfoHolder(folderName, account);
}
allAccounts = false;
if (singleAccountMode) {
this.accountUuids = new String[] { account.getUuid() };
} else {
if (accountUuids.length == 1 &&
accountUuids[0].equals(SearchSpecification.ALL_ACCOUNTS)) {
allAccounts = true;
List<Account> accounts = preferences.getAccounts();
this.accountUuids = new String[accounts.size()];
for (int i = 0, len = accounts.size(); i < len; i++) {
this.accountUuids[i] = accounts.get(i).getUuid();
}
if (this.accountUuids.length == 1) {
singleAccountMode = true;
account = accounts.get(0);
}
} else {
this.accountUuids = accountUuids;
}
}
}
private void initializeMessageList() {
adapter = new MessageListAdapter(this);
if (folderName != null) {
currentFolder = getFolderInfoHolder(folderName, account);
}
if (singleFolderMode) {
listView.addFooterView(getFooterView(listView));
updateFooterView();
}
listView.setAdapter(adapter);
}
private void createCacheBroadcastReceiver(Context appContext) {
localBroadcastManager = LocalBroadcastManager.getInstance(appContext);
cacheBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
adapter.notifyDataSetChanged();
}
};
cacheIntentFilter = new IntentFilter(EmailProviderCache.ACTION_CACHE_UPDATED);
}
private FolderInfoHolder getFolderInfoHolder(String folderName, Account account) {
try {
LocalFolder localFolder = MlfUtils.getOpenFolder(folderName, account);
return new FolderInfoHolder(context, localFolder, account);
} catch (MessagingException e) {
throw new RuntimeException(e);
}
}
@Override
public void onPause() {
super.onPause();
localBroadcastManager.unregisterReceiver(cacheBroadcastReceiver);
activityListener.onPause(getActivity());
messagingController.removeListener(activityListener);
}
/**
* 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();
senderAboveSubject = K9.messageListSenderAboveSubject();
if (!loaderJustInitialized) {
restartLoader();
} else {
loaderJustInitialized = false;
}
// Check if we have connectivity. Cache the value.
if (hasConnectivity == null) {
hasConnectivity = Utility.hasConnectivity(getActivity().getApplication());
}
localBroadcastManager.registerReceiver(cacheBroadcastReceiver, cacheIntentFilter);
activityListener.onResume(getActivity());
messagingController.addListener(activityListener);
//Cancel pending new mail notifications when we open an account
List<Account> accountsWithNotification;
Account account = this.account;
if (account != null) {
accountsWithNotification = Collections.singletonList(account);
} else {
accountsWithNotification = preferences.getAccounts();
}
for (Account accountWithNotification : accountsWithNotification) {
messagingController.cancelNotificationsForAccount(accountWithNotification);
}
if (this.account != null && folderName != null && !search.isManualSearch()) {
messagingController.getFolderUnreadMessageCount(this.account, folderName, activityListener);
}
updateTitle();
}
private void restartLoader() {
if (cursorValid == null) {
return;
}
// Refresh the message list
LoaderManager loaderManager = getLoaderManager();
for (int i = 0; i < accountUuids.length; i++) {
loaderManager.restartLoader(i, null, this);
cursorValid[i] = false;
}
}
private void initializePullToRefresh(View layout) {
swipeRefreshLayout = (SwipeRefreshLayout) layout.findViewById(R.id.swiperefresh);
listView = (ListView) layout.findViewById(R.id.message_list);
if (isRemoteSearchAllowed()) {
swipeRefreshLayout.setOnRefreshListener(
new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
onRemoteSearchRequested();
}
}
);
} else if (isCheckMailSupported()) {
swipeRefreshLayout.setOnRefreshListener(
new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
checkMail();
}
}
);
}
// Disable pull-to-refresh until the message list has been loaded
swipeRefreshLayout.setEnabled(false);
}
private void initializeLayout() {
listView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
listView.setLongClickable(true);
listView.setFastScrollEnabled(true);
listView.setScrollingCacheEnabled(false);
listView.setOnItemClickListener(this);
registerForContextMenu(listView);
}
public void onCompose() {
if (!singleAccountMode) {
/*
* If we have a query string, we don't have an account to let
* compose start the default action.
*/
fragmentListener.onCompose(null);
} else {
fragmentListener.onCompose(account);
}
}
private void onReply(MessageReference messageReference) {
fragmentListener.onReply(messageReference);
}
private void onReplyAll(MessageReference messageReference) {
fragmentListener.onReplyAll(messageReference);
}
private void onForward(MessageReference messageReference) {
fragmentListener.onForward(messageReference);
}
private void onResendMessage(MessageReference messageReference) {
fragmentListener.onResendMessage(messageReference);
}
public void changeSort(SortType sortType) {
Boolean sortAscending = (this.sortType == sortType) ? !this.sortAscending : null;
changeSort(sortType, sortAscending);
}
/**
* User has requested a remote search. Setup the bundle and start the intent.
*/
private void onRemoteSearchRequested() {
String searchAccount;
String searchFolder;
searchAccount = account.getUuid();
searchFolder = currentFolder.name;
String queryString = search.getRemoteSearchArguments();
remoteSearchPerformed = true;
remoteSearchFuture = messagingController.searchRemoteMessages(searchAccount, searchFolder,
queryString, null, null, activityListener);
swipeRefreshLayout.setEnabled(false);
fragmentListener.remoteSearchStarted();
}
/**
* 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) {
this.sortType = sortType;
Account account = this.account;
if (account != null) {
account.setSortType(this.sortType);
if (sortAscending == null) {
this.sortAscending = account.isSortAscending(this.sortType);
} else {
this.sortAscending = sortAscending;
}
account.setSortAscending(this.sortType, this.sortAscending);
sortDateAscending = account.isSortAscending(SortType.SORT_DATE);
account.save(preferences);
} else {
K9.setSortType(this.sortType);
if (sortAscending == null) {
this.sortAscending = K9.isSortAscending(this.sortType);
} else {
this.sortAscending = sortAscending;
}
K9.setSortAscending(this.sortType, this.sortAscending);
sortDateAscending = K9.isSortAscending(SortType.SORT_DATE);
StorageEditor editor = preferences.getStorage().edit();
K9.save(editor);
editor.commit();
}
reSort();
}
private void reSort() {
int toastString = sortType.getToast(sortAscending);
Toast toast = Toast.makeText(getActivity(), toastString, Toast.LENGTH_SHORT);
toast.show();
LoaderManager loaderManager = getLoaderManager();
for (int i = 0, len = accountUuids.length; i < len; i++) {
loaderManager.restartLoader(i, null, this);
}
}
public void onCycleSort() {
SortType[] sorts = SortType.values();
int curIndex = 0;
for (int i = 0; i < sorts.length; i++) {
if (sorts[i] == sortType) {
curIndex = i;
break;
}
}
curIndex++;
if (curIndex == sorts.length) {
curIndex = 0;
}
changeSort(sorts[curIndex]);
}
private void onDelete(MessageReference message) {
onDelete(Collections.singletonList(message));
}
private void onDelete(List<MessageReference> messages) {
if (K9.confirmDelete()) {
// remember the message selection for #onCreateDialog(int)
activeMessages = messages;
showDialog(R.id.dialog_confirm_delete);
} else {
onDeleteConfirmed(messages);
}
}
private void onDeleteConfirmed(List<MessageReference> messages) {
if (showingThreadedList) {
messagingController.deleteThreads(messages);
} else {
messagingController.deleteMessages(messages, null);
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode != Activity.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<MessageReference> messages = activeMessages;
if (destFolderName != null) {
activeMessages = null; // don't need it any more
if (messages.size() > 0) {
MlfUtils.setLastSelectedFolderName(preferences, messages, destFolderName);
}
switch (requestCode) {
case ACTIVITY_CHOOSE_FOLDER_MOVE:
move(messages, destFolderName);
break;
case ACTIVITY_CHOOSE_FOLDER_COPY:
copy(messages, destFolderName);
break;
}
}
break;
}
}
}
public void onExpunge() {
if (currentFolder != null) {
onExpunge(account, currentFolder.name);
}
}
private void onExpunge(final Account account, String folderName) {
messagingController.expunge(account, folderName);
}
private void showDialog(int dialogId) {
DialogFragment fragment;
switch (dialogId) {
case R.id.dialog_confirm_spam: {
String title = getString(R.string.dialog_confirm_spam_title);
int selectionSize = activeMessages.size();
String message = getResources().getQuantityString(
R.plurals.dialog_confirm_spam_message, selectionSize, selectionSize);
String confirmText = getString(R.string.dialog_confirm_spam_confirm_button);
String cancelText = getString(R.string.dialog_confirm_spam_cancel_button);
fragment = ConfirmationDialogFragment.newInstance(dialogId, title, message,
confirmText, cancelText);
break;
}
case R.id.dialog_confirm_delete: {
String title = getString(R.string.dialog_confirm_delete_title);
int selectionSize = activeMessages.size();
String message = getResources().getQuantityString(
R.plurals.dialog_confirm_delete_messages, selectionSize,
selectionSize);
String confirmText = getString(R.string.dialog_confirm_delete_confirm_button);
String cancelText = getString(R.string.dialog_confirm_delete_cancel_button);
fragment = ConfirmationDialogFragment.newInstance(dialogId, title, message,
confirmText, cancelText);
break;
}
case R.id.dialog_confirm_mark_all_as_read: {
String title = getString(R.string.dialog_confirm_mark_all_as_read_title);
String message = getString(R.string.dialog_confirm_mark_all_as_read_message);
String confirmText = getString(R.string.dialog_confirm_mark_all_as_read_confirm_button);
String cancelText = getString(R.string.dialog_confirm_mark_all_as_read_cancel_button);
fragment = ConfirmationDialogFragment.newInstance(dialogId, title, message, confirmText, cancelText);
break;
}
default: {
throw new RuntimeException("Called showDialog(int) with unknown dialog id.");
}
}
fragment.setTargetFragment(this, dialogId);
fragment.show(getFragmentManager(), getDialogTag(dialogId));
}
private String getDialogTag(int dialogId) {
return "dialog-" + dialogId;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId();
switch (itemId) {
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: {
selectAll();
return true;
}
}
if (!singleAccountMode) {
// 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: {
onSendPendingMessages();
return true;
}
case R.id.expunge: {
if (currentFolder != null) {
onExpunge(account, currentFolder.name);
}
return true;
}
default: {
return super.onOptionsItemSelected(item);
}
}
}
public void onSendPendingMessages() {
messagingController.sendPendingMessages(account, null);
}
@Override
public boolean onContextItemSelected(android.view.MenuItem item) {
if (contextMenuUniqueId == 0) {
return false;
}
int adapterPosition = getPositionForUniqueId(contextMenuUniqueId);
if (adapterPosition == AdapterView.INVALID_POSITION) {
return false;
}
switch (item.getItemId()) {
case R.id.deselect:
case R.id.select: {
toggleMessageSelectWithAdapterPosition(adapterPosition);
break;
}
case R.id.reply: {
onReply(getMessageAtPosition(adapterPosition));
break;
}
case R.id.reply_all: {
onReplyAll(getMessageAtPosition(adapterPosition));
break;
}
case R.id.forward: {
onForward(getMessageAtPosition(adapterPosition));
break;
}
case R.id.send_again: {
onResendMessage(getMessageAtPosition(adapterPosition));
selectedCount = 0;
break;
}
case R.id.same_sender: {
Cursor cursor = (Cursor) adapter.getItem(adapterPosition);
String senderAddress = MlfUtils.getSenderAddressFromCursor(cursor);
if (senderAddress != null) {
fragmentListener.showMoreFromSameSender(senderAddress);
}
break;
}
case R.id.delete: {
MessageReference message = getMessageAtPosition(adapterPosition);
onDelete(message);
break;
}
case R.id.mark_as_read: {
setFlag(adapterPosition, Flag.SEEN, true);
break;
}
case R.id.mark_as_unread: {
setFlag(adapterPosition, Flag.SEEN, false);
break;
}
case R.id.flag: {
setFlag(adapterPosition, Flag.FLAGGED, true);
break;
}
case R.id.unflag: {
setFlag(adapterPosition, Flag.FLAGGED, false);
break;
}
// only if the account supports this
case R.id.archive: {
onArchive(getMessageAtPosition(adapterPosition));
break;
}
case R.id.spam: {
onSpam(getMessageAtPosition(adapterPosition));
break;
}
case R.id.move: {
onMove(getMessageAtPosition(adapterPosition));
break;
}
case R.id.copy: {
onCopy(getMessageAtPosition(adapterPosition));
break;
}
// debug options
case R.id.debug_delete_locally: {
onDebugClearLocally(getMessageAtPosition(adapterPosition));
break;
}
}
contextMenuUniqueId = 0;
return true;
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
Cursor cursor = (Cursor) listView.getItemAtPosition(info.position);
if (cursor == null) {
return;
}
getActivity().getMenuInflater().inflate(R.menu.message_list_item_context, menu);
menu.findItem(R.id.debug_delete_locally).setVisible(BuildConfig.DEBUG);
contextMenuUniqueId = cursor.getLong(uniqueIdColumn);
Account account = getAccountFromCursor(cursor);
String subject = cursor.getString(SUBJECT_COLUMN);
boolean read = (cursor.getInt(READ_COLUMN) == 1);
boolean flagged = (cursor.getInt(FLAGGED_COLUMN) == 1);
menu.setHeaderTitle(subject);
if (selected.contains(contextMenuUniqueId)) {
menu.findItem(R.id.select).setVisible(false);
} else {
menu.findItem(R.id.deselect).setVisible(false);
}
if (read) {
menu.findItem(R.id.mark_as_read).setVisible(false);
} else {
menu.findItem(R.id.mark_as_unread).setVisible(false);
}
if (flagged) {
menu.findItem(R.id.flag).setVisible(false);
} else {
menu.findItem(R.id.unflag).setVisible(false);
}
if (!messagingController.isCopyCapable(account)) {
menu.findItem(R.id.copy).setVisible(false);
}
if (!messagingController.isMoveCapable(account)) {
menu.findItem(R.id.move).setVisible(false);
menu.findItem(R.id.archive).setVisible(false);
menu.findItem(R.id.spam).setVisible(false);
}
if (!account.hasArchiveFolder()) {
menu.findItem(R.id.archive).setVisible(false);
}
if (!account.hasSpamFolder()) {
menu.findItem(R.id.spam).setVisible(false);
}
}
public void onSwipeRightToLeft(final MotionEvent e1, final MotionEvent e2) {
// Handle right-to-left as an un-select
handleSwipe(e1, false);
}
public void onSwipeLeftToRight(final MotionEvent e1, final MotionEvent e2) {
// Handle left-to-right as a select.
handleSwipe(e1, true);
}
/**
* Handle a select or unselect swipe event.
*
* @param downMotion
* Event that started the swipe
* @param selected
* {@code true} if this was an attempt to select (i.e. left to right).
*/
private void handleSwipe(final MotionEvent downMotion, final boolean selected) {
int x = (int) downMotion.getRawX();
int y = (int) downMotion.getRawY();
Rect headerRect = new Rect();
listView.getGlobalVisibleRect(headerRect);
// Only handle swipes in the visible area of the message list
if (headerRect.contains(x, y)) {
int[] listPosition = new int[2];
listView.getLocationOnScreen(listPosition);
int listX = x - listPosition[0];
int listY = y - listPosition[1];
int listViewPosition = listView.pointToPosition(listX, listY);
toggleMessageSelect(listViewPosition);
}
}
private int listViewToAdapterPosition(int position) {
if (position >= 0 && position < adapter.getCount()) {
return position;
}
return AdapterView.INVALID_POSITION;
}
private int adapterToListViewPosition(int position) {
if (position >= 0 && position < adapter.getCount()) {
return position;
}
return AdapterView.INVALID_POSITION;
}
class MessageListActivityListener extends ActivityListener {
@Override
public void remoteSearchFailed(String folder, final String err) {
handler.post(new Runnable() {
@Override
public void run() {
Activity activity = getActivity();
if (activity != null) {
Toast.makeText(activity, R.string.remote_search_error,
Toast.LENGTH_LONG).show();
}
}
});
}
@Override
public void remoteSearchStarted(String folder) {
handler.progress(true);
handler.updateFooter(context.getString(R.string.remote_search_sending_query));
}
@Override
public void enableProgressIndicator(boolean enable) {
handler.progress(enable);
}
@Override
public void remoteSearchFinished(String folder, int numResults, int maxResults, List<Message> extraResults) {
handler.progress(false);
handler.remoteSearchFinished();
extraSearchResults = extraResults;
if (extraResults != null && extraResults.size() > 0) {
handler.updateFooter(String.format(context.getString(R.string.load_more_messages_fmt), maxResults));
} else {
handler.updateFooter(null);
}
fragmentListener.setMessageListProgress(Window.PROGRESS_END);
}
@Override
public void remoteSearchServerQueryComplete(String folderName, int numResults, int maxResults) {
handler.progress(true);
if (maxResults != 0 && numResults > maxResults) {
handler.updateFooter(context.getString(R.string.remote_search_downloading_limited,
maxResults, numResults));
} else {
handler.updateFooter(context.getString(R.string.remote_search_downloading, numResults));
}
fragmentListener.setMessageListProgress(Window.PROGRESS_START);
}
@Override
public void informUserOfStatus() {
handler.refreshTitle();
}
@Override
public void synchronizeMailboxStarted(Account account, String folder) {
if (updateForMe(account, folder)) {
handler.progress(true);
handler.folderLoading(folder, true);
}
super.synchronizeMailboxStarted(account, folder);
}
@Override
public void synchronizeMailboxFinished(Account account, String folder,
int totalMessagesInMailbox, int numNewMessages) {
if (updateForMe(account, folder)) {
handler.progress(false);
handler.folderLoading(folder, false);
}
super.synchronizeMailboxFinished(account, folder, totalMessagesInMailbox, numNewMessages);
}
@Override
public void synchronizeMailboxFailed(Account account, String folder, String message) {
if (updateForMe(account, folder)) {
handler.progress(false);
handler.folderLoading(folder, false);
}
super.synchronizeMailboxFailed(account, folder, message);
}
@Override
public void folderStatusChanged(Account account, String folder, int unreadMessageCount) {
if (isSingleAccountMode() && isSingleFolderMode() && MessageListFragment.this.account.equals(account) &&
folderName.equals(folder)) {
MessageListFragment.this.unreadMessageCount = unreadMessageCount;
}
super.folderStatusChanged(account, folder, unreadMessageCount);
}
private boolean updateForMe(Account account, String folder) {
if (account == null || folder == null) {
return false;
}
if (!Utility.arrayContains(accountUuids, account.getUuid())) {
return false;
}
List<String> folderNames = search.getFolderNames();
return (folderNames.isEmpty() || folderNames.contains(folder));
}
}
private View getFooterView(ViewGroup parent) {
if (footerView == null) {
footerView = layoutInflater.inflate(R.layout.message_list_item_footer, parent, false);
FooterViewHolder holder = new FooterViewHolder();
holder.main = (TextView) footerView.findViewById(R.id.main_text);
footerView.setTag(holder);
}
return footerView;
}
private void updateFooterView() {
if (!search.isManualSearch() && currentFolder != null && account != null) {
if (currentFolder.loading) {
updateFooter(context.getString(R.string.status_loading_more));
} else if (!currentFolder.moreMessages) {
updateFooter(null);
} else {
String message;
if (!currentFolder.lastCheckFailed) {
if (account.getDisplayCount() == 0) {
message = context.getString(R.string.message_list_load_more_messages_action);
} else {
message = String.format(context.getString(R.string.load_more_messages_fmt),
account.getDisplayCount());
}
} else {
message = context.getString(R.string.status_loading_more_failed);
}
updateFooter(message);
}
} else {
updateFooter(null);
}
}
public void updateFooter(final String text) {
if (footerView == null) {
return;
}
FooterViewHolder holder = (FooterViewHolder) footerView.getTag();
if (text != null) {
holder.main.setText(text);
holder.main.setVisibility(View.VISIBLE);
} else {
holder.main.setVisibility(View.GONE);
}
}
static class FooterViewHolder {
public TextView main;
}
/**
* Set selection state for all messages.
*
* @param selected
* If {@code true} all messages get selected. Otherwise, all messages get deselected and
* action mode is finished.
*/
private void setSelectionState(boolean selected) {
if (selected) {
if (adapter.getCount() == 0) {
// Nothing to do if there are no messages
return;
}
selectedCount = 0;
for (int i = 0, end = adapter.getCount(); i < end; i++) {
Cursor cursor = (Cursor) adapter.getItem(i);
long uniqueId = cursor.getLong(uniqueIdColumn);
this.selected.add(uniqueId);
if (showingThreadedList) {
int threadCount = cursor.getInt(THREAD_COUNT_COLUMN);
selectedCount += (threadCount > 1) ? threadCount : 1;
} else {
selectedCount++;
}
}
if (actionMode == null) {
startAndPrepareActionMode();
}
computeBatchDirection();
updateActionModeTitle();
computeSelectAllVisibility();
} else {
this.selected.clear();
selectedCount = 0;
if (actionMode != null) {
actionMode.finish();
actionMode = null;
}
}
adapter.notifyDataSetChanged();
}
private void toggleMessageSelect(int listViewPosition) {
int adapterPosition = listViewToAdapterPosition(listViewPosition);
if (adapterPosition == AdapterView.INVALID_POSITION) {
return;
}
toggleMessageSelectWithAdapterPosition(adapterPosition);
}
void toggleMessageFlagWithAdapterPosition(int adapterPosition) {
Cursor cursor = (Cursor) adapter.getItem(adapterPosition);
boolean flagged = (cursor.getInt(FLAGGED_COLUMN) == 1);
setFlag(adapterPosition,Flag.FLAGGED, !flagged);
}
void toggleMessageSelectWithAdapterPosition(int adapterPosition) {
Cursor cursor = (Cursor) adapter.getItem(adapterPosition);
long uniqueId = cursor.getLong(uniqueIdColumn);
boolean selected = this.selected.contains(uniqueId);
if (!selected) {
this.selected.add(uniqueId);
} else {
this.selected.remove(uniqueId);
}
int selectedCountDelta = 1;
if (showingThreadedList) {
int threadCount = cursor.getInt(THREAD_COUNT_COLUMN);
if (threadCount > 1) {
selectedCountDelta = threadCount;
}
}
if (actionMode != null) {
if (selectedCount == selectedCountDelta && selected) {
actionMode.finish();
actionMode = null;
return;
}
} else {
startAndPrepareActionMode();
}
if (selected) {
selectedCount -= selectedCountDelta;
} else {
selectedCount += selectedCountDelta;
}
computeBatchDirection();
updateActionModeTitle();
computeSelectAllVisibility();
adapter.notifyDataSetChanged();
}
private void updateActionModeTitle() {
actionMode.setTitle(String.format(getString(R.string.actionbar_selected), selectedCount));
}
private void computeSelectAllVisibility() {
actionModeCallback.showSelectAll(selected.size() != adapter.getCount());
}
private void computeBatchDirection() {
boolean isBatchFlag = false;
boolean isBatchRead = false;
for (int i = 0, end = adapter.getCount(); i < end; i++) {
Cursor cursor = (Cursor) adapter.getItem(i);
long uniqueId = cursor.getLong(uniqueIdColumn);
if (selected.contains(uniqueId)) {
boolean read = (cursor.getInt(READ_COLUMN) == 1);
boolean flagged = (cursor.getInt(FLAGGED_COLUMN) == 1);
if (!flagged) {
isBatchFlag = true;
}
if (!read) {
isBatchRead = true;
}
if (isBatchFlag && isBatchRead) {
break;
}
}
}
actionModeCallback.showMarkAsRead(isBatchRead);
actionModeCallback.showFlag(isBatchFlag);
}
private void setFlag(int adapterPosition, final Flag flag, final boolean newState) {
if (adapterPosition == AdapterView.INVALID_POSITION) {
return;
}
Cursor cursor = (Cursor) adapter.getItem(adapterPosition);
Account account = preferences.getAccount(cursor.getString(ACCOUNT_UUID_COLUMN));
if (showingThreadedList && cursor.getInt(THREAD_COUNT_COLUMN) > 1) {
long threadRootId = cursor.getLong(THREAD_ROOT_COLUMN);
messagingController.setFlagForThreads(account,
Collections.singletonList(threadRootId), flag, newState);
} else {
long id = cursor.getLong(ID_COLUMN);
messagingController.setFlag(account, Collections.singletonList(id), flag,
newState);
}
computeBatchDirection();
}
private void setFlagForSelected(final Flag flag, final boolean newState) {
if (selected.isEmpty()) {
return;
}
Map<Account, List<Long>> messageMap = new HashMap<>();
Map<Account, List<Long>> threadMap = new HashMap<>();
Set<Account> accounts = new HashSet<>();
for (int position = 0, end = adapter.getCount(); position < end; position++) {
Cursor cursor = (Cursor) adapter.getItem(position);
long uniqueId = cursor.getLong(uniqueIdColumn);
if (selected.contains(uniqueId)) {
String uuid = cursor.getString(ACCOUNT_UUID_COLUMN);
Account account = preferences.getAccount(uuid);
accounts.add(account);
if (showingThreadedList && cursor.getInt(THREAD_COUNT_COLUMN) > 1) {
List<Long> threadRootIdList = threadMap.get(account);
if (threadRootIdList == null) {
threadRootIdList = new ArrayList<>();
threadMap.put(account, threadRootIdList);
}
threadRootIdList.add(cursor.getLong(THREAD_ROOT_COLUMN));
} else {
List<Long> messageIdList = messageMap.get(account);
if (messageIdList == null) {
messageIdList = new ArrayList<>();
messageMap.put(account, messageIdList);
}
messageIdList.add(cursor.getLong(ID_COLUMN));
}
}
}
for (Account account : accounts) {
List<Long> messageIds = messageMap.get(account);
List<Long> threadRootIds = threadMap.get(account);
if (messageIds != null) {
messagingController.setFlag(account, messageIds, flag, newState);
}
if (threadRootIds != null) {
messagingController.setFlagForThreads(account, threadRootIds, flag, newState);
}
}
computeBatchDirection();
}
private void onMove(MessageReference message) {
onMove(Collections.singletonList(message));
}
/**
* Display the message move activity.
*
* @param messages
* Never {@code null}.
*/
private void onMove(List<MessageReference> messages) {
if (!checkCopyOrMovePossible(messages, FolderOperation.MOVE)) {
return;
}
String folderName;
if (isThreadDisplay) {
folderName = messages.get(0).getFolderName();
} else if (singleFolderMode) {
folderName = currentFolder.folder.getName();
} else {
folderName = null;
}
displayFolderChoice(ACTIVITY_CHOOSE_FOLDER_MOVE, folderName,
messages.get(0).getAccountUuid(), null,
messages);
}
private void onCopy(MessageReference message) {
onCopy(Collections.singletonList(message));
}
/**
* Display the message copy activity.
*
* @param messages
* Never {@code null}.
*/
private void onCopy(List<MessageReference> messages) {
if (!checkCopyOrMovePossible(messages, FolderOperation.COPY)) {
return;
}
String folderName;
if (isThreadDisplay) {
folderName = messages.get(0).getFolderName();
} else if (singleFolderMode) {
folderName = currentFolder.folder.getName();
} else {
folderName = null;
}
displayFolderChoice(ACTIVITY_CHOOSE_FOLDER_COPY, folderName,
messages.get(0).getAccountUuid(),
null,
messages);
}
private void onDebugClearLocally(MessageReference message) {
messagingController.debugClearMessagesLocally(Collections.singletonList(message));
}
/**
* 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 {@code >= 0}, this code will be returned in {@code onActivityResult()} when the
* activity exits.
*
* @see #startActivityForResult(Intent, int)
*/
private void displayFolderChoice(int requestCode, String sourceFolderName,
String accountUuid, String lastSelectedFolderName,
List<MessageReference> messages) {
Intent intent = new Intent(getActivity(), ChooseFolder.class);
intent.putExtra(ChooseFolder.EXTRA_ACCOUNT, accountUuid);
intent.putExtra(ChooseFolder.EXTRA_SEL_FOLDER, lastSelectedFolderName);
if (sourceFolderName == null) {
intent.putExtra(ChooseFolder.EXTRA_SHOW_CURRENT, "yes");
} else {
intent.putExtra(ChooseFolder.EXTRA_CUR_FOLDER, sourceFolderName);
}
// remember the selected messages for #onActivityResult
activeMessages = messages;
startActivityForResult(intent, requestCode);
}
private void onArchive(MessageReference message) {
onArchive(Collections.singletonList(message));
}
private void onArchive(final List<MessageReference> messages) {
Map<Account, List<MessageReference>> messagesByAccount = groupMessagesByAccount(messages);
for (Entry<Account, List<MessageReference>> entry : messagesByAccount.entrySet()) {
Account account = entry.getKey();
String archiveFolder = account.getArchiveFolderName();
if (!K9.FOLDER_NONE.equals(archiveFolder)) {
move(entry.getValue(), archiveFolder);
}
}
}
private Map<Account, List<MessageReference>> groupMessagesByAccount(final List<MessageReference> messages) {
Map<Account, List<MessageReference>> messagesByAccount = new HashMap<>();
for (MessageReference message : messages) {
Account account = preferences.getAccount(message.getAccountUuid());
List<MessageReference> msgList = messagesByAccount.get(account);
if (msgList == null) {
msgList = new ArrayList<>();
messagesByAccount.put(account, msgList);
}
msgList.add(message);
}
return messagesByAccount;
}
private void onSpam(MessageReference message) {
onSpam(Collections.singletonList(message));
}
/**
* Move messages to the spam folder.
*
* @param messages
* The messages to move to the spam folder. Never {@code null}.
*/
private void onSpam(List<MessageReference> messages) {
if (K9.confirmSpam()) {
// remember the message selection for #onCreateDialog(int)
activeMessages = messages;
showDialog(R.id.dialog_confirm_spam);
} else {
onSpamConfirmed(messages);
}
}
private void onSpamConfirmed(List<MessageReference> messages) {
Map<Account, List<MessageReference>> messagesByAccount = groupMessagesByAccount(messages);
for (Entry<Account, List<MessageReference>> entry : messagesByAccount.entrySet()) {
Account account = entry.getKey();
String spamFolder = account.getSpamFolderName();
if (!K9.FOLDER_NONE.equals(spamFolder)) {
move(entry.getValue(), spamFolder);
}
}
}
private enum FolderOperation {
COPY, MOVE
}
/**
* Display a Toast message if any message isn't synchronized
*
* @param messages
* The messages to copy or move. Never {@code null}.
* @param operation
* The type of operation to perform. Never {@code null}.
*
* @return {@code true}, if operation is possible.
*/
private boolean checkCopyOrMovePossible(final List<MessageReference> messages,
final FolderOperation operation) {
if (messages.isEmpty()) {
return false;
}
boolean first = true;
for (MessageReference message : messages) {
if (first) {
first = false;
Account account = preferences.getAccount(message.getAccountUuid());
if ((operation == FolderOperation.MOVE && !messagingController.isMoveCapable(account)) ||
(operation == FolderOperation.COPY && !messagingController.isCopyCapable(account))) {
return false;
}
}
// message check
if ((operation == FolderOperation.MOVE && !messagingController.isMoveCapable(message)) ||
(operation == FolderOperation.COPY && !messagingController.isCopyCapable(message))) {
final Toast toast = Toast.makeText(getActivity(), R.string.move_copy_cannot_copy_unsynced_message,
Toast.LENGTH_LONG);
toast.show();
return false;
}
}
return true;
}
/**
* Copy the specified messages to the specified folder.
*
* @param messages
* List of messages to copy. Never {@code null}.
* @param destination
* The name of the destination folder. Never {@code null}.
*/
private void copy(List<MessageReference> messages, final String destination) {
copyOrMove(messages, destination, FolderOperation.COPY);
}
/**
* Move the specified messages to the specified folder.
*
* @param messages
* The list of messages to move. Never {@code null}.
* @param destination
* The name of the destination folder. Never {@code null}.
*/
private void move(List<MessageReference> messages, final String destination) {
copyOrMove(messages, 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.
*
* @param messages
* The list of messages to copy or move. Never {@code null}.
* @param destination
* The name of the destination folder. Never {@code null} or {@link K9#FOLDER_NONE}.
* @param operation
* Specifies what operation to perform. Never {@code null}.
*/
private void copyOrMove(List<MessageReference> messages, final String destination,
final FolderOperation operation) {
Map<String, List<MessageReference>> folderMap = new HashMap<>();
for (MessageReference message : messages) {
if ((operation == FolderOperation.MOVE && !messagingController.isMoveCapable(message)) ||
(operation == FolderOperation.COPY && !messagingController.isCopyCapable(message))) {
Toast.makeText(getActivity(), R.string.move_copy_cannot_copy_unsynced_message,
Toast.LENGTH_LONG).show();
// XXX return meaningful error value?
// message isn't synchronized
return;
}
String folderName = message.getFolderName();
if (folderName.equals(destination)) {
// Skip messages already in the destination folder
continue;
}
List<MessageReference> outMessages = folderMap.get(folderName);
if (outMessages == null) {
outMessages = new ArrayList<>();
folderMap.put(folderName, outMessages);
}
outMessages.add(message);
}
for (Map.Entry<String, List<MessageReference>> entry : folderMap.entrySet()) {
String folderName = entry.getKey();
List<MessageReference> outMessages = entry.getValue();
Account account = preferences.getAccount(outMessages.get(0).getAccountUuid());
if (operation == FolderOperation.MOVE) {
if (showingThreadedList) {
messagingController.moveMessagesInThread(account, folderName, outMessages, destination);
} else {
messagingController.moveMessages(account, folderName, outMessages, destination);
}
} else {
if (showingThreadedList) {
messagingController.copyMessagesInThread(account, folderName, outMessages, destination);
} else {
messagingController.copyMessages(account, folderName, outMessages, destination);
}
}
}
}
class ActionModeCallback implements ActionMode.Callback {
private MenuItem mSelectAll;
private MenuItem mMarkAsRead;
private MenuItem mMarkAsUnread;
private MenuItem mFlag;
private MenuItem mUnflag;
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
mSelectAll = menu.findItem(R.id.select_all);
mMarkAsRead = menu.findItem(R.id.mark_as_read);
mMarkAsUnread = menu.findItem(R.id.mark_as_unread);
mFlag = menu.findItem(R.id.flag);
mUnflag = menu.findItem(R.id.unflag);
// we don't support cross account actions atm
if (!singleAccountMode) {
// 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);
Set<String> accountUuids = getAccountUuidsForSelected();
for (String accountUuid : accountUuids) {
Account account = preferences.getAccount(accountUuid);
if (account != null) {
setContextCapabilities(account, menu);
}
}
}
return true;
}
/**
* Get the set of account UUIDs for the selected messages.
*/
private Set<String> getAccountUuidsForSelected() {
int maxAccounts = accountUuids.length;
Set<String> accountUuids = new HashSet<>(maxAccounts);
for (int position = 0, end = adapter.getCount(); position < end; position++) {
Cursor cursor = (Cursor) adapter.getItem(position);
long uniqueId = cursor.getLong(uniqueIdColumn);
if (selected.contains(uniqueId)) {
String accountUuid = cursor.getString(ACCOUNT_UUID_COLUMN);
accountUuids.add(accountUuid);
if (accountUuids.size() == MessageListFragment.this.accountUuids.length) {
break;
}
}
}
return accountUuids;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
actionMode = null;
mSelectAll = null;
mMarkAsRead = null;
mMarkAsUnread = null;
mFlag = null;
mUnflag = null;
setSelectionState(false);
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.message_list_context, menu);
// check capabilities
setContextCapabilities(account, menu);
return true;
}
/**
* Disables menu options not supported by the account type or current "search view".
*
* @param account
* The account to query for its capabilities.
* @param menu
* The menu to adapt.
*/
private void setContextCapabilities(Account account, Menu menu) {
if (!singleAccountMode) {
// We don't support cross-account copy/move operations right now
menu.findItem(R.id.move).setVisible(false);
menu.findItem(R.id.copy).setVisible(false);
//TODO: we could support the archive and spam operations if all selected messages
// belong to non-POP3 accounts
menu.findItem(R.id.archive).setVisible(false);
menu.findItem(R.id.spam).setVisible(false);
} else {
// hide unsupported
if (!messagingController.isCopyCapable(account)) {
menu.findItem(R.id.copy).setVisible(false);
}
if (!messagingController.isMoveCapable(account)) {
menu.findItem(R.id.move).setVisible(false);
menu.findItem(R.id.archive).setVisible(false);
menu.findItem(R.id.spam).setVisible(false);
}
if (!account.hasArchiveFolder()) {
menu.findItem(R.id.archive).setVisible(false);
}
if (!account.hasSpamFolder()) {
menu.findItem(R.id.spam).setVisible(false);
}
}
}
public void showSelectAll(boolean show) {
if (actionMode != null) {
mSelectAll.setVisible(show);
}
}
public void showMarkAsRead(boolean show) {
if (actionMode != null) {
mMarkAsRead.setVisible(show);
mMarkAsUnread.setVisible(!show);
}
}
public void showFlag(boolean show) {
if (actionMode != null) {
mFlag.setVisible(show);
mUnflag.setVisible(!show);
}
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
/*
* 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: {
List<MessageReference> messages = getCheckedMessages();
onDelete(messages);
selectedCount = 0;
break;
}
case R.id.mark_as_read: {
setFlagForSelected(Flag.SEEN, true);
break;
}
case R.id.mark_as_unread: {
setFlagForSelected(Flag.SEEN, false);
break;
}
case R.id.flag: {
setFlagForSelected(Flag.FLAGGED, true);
break;
}
case R.id.unflag: {
setFlagForSelected(Flag.FLAGGED, false);
break;
}
case R.id.select_all: {
selectAll();
break;
}
// only if the account supports this
case R.id.archive: {
onArchive(getCheckedMessages());
selectedCount = 0;
break;
}
case R.id.spam: {
onSpam(getCheckedMessages());
selectedCount = 0;
break;
}
case R.id.move: {
onMove(getCheckedMessages());
selectedCount = 0;
break;
}
case R.id.copy: {
onCopy(getCheckedMessages());
selectedCount = 0;
break;
}
}
if (selectedCount == 0) {
actionMode.finish();
}
return true;
}
}
@Override
public void doPositiveClick(int dialogId) {
switch (dialogId) {
case R.id.dialog_confirm_spam: {
onSpamConfirmed(activeMessages);
// No further need for this reference
activeMessages = null;
break;
}
case R.id.dialog_confirm_delete: {
onDeleteConfirmed(activeMessages);
activeMessage = null;
break;
}
case R.id.dialog_confirm_mark_all_as_read: {
markAllAsRead();
break;
}
}
}
@Override
public void doNegativeClick(int dialogId) {
switch (dialogId) {
case R.id.dialog_confirm_spam:
case R.id.dialog_confirm_delete: {
// No further need for this reference
activeMessages = null;
break;
}
}
}
@Override
public void dialogCancelled(int dialogId) {
doNegativeClick(dialogId);
}
public void checkMail() {
if (isSingleAccountMode() && isSingleFolderMode()) {
messagingController.synchronizeMailbox(account, folderName, activityListener, null);
messagingController.sendPendingMessages(account, activityListener);
} else if (allAccounts) {
messagingController.checkMail(context, null, true, true, activityListener);
} else {
for (String accountUuid : accountUuids) {
Account account = preferences.getAccount(accountUuid);
messagingController.checkMail(context, account, true, true, activityListener);
}
}
}
/**
* We need to do some special clean up when leaving a remote search result screen. If no
* remote search is in progress, this method does nothing special.
*/
@Override
public void onStop() {
// If we represent a remote search, then kill that before going back.
if (isRemoteSearch() && remoteSearchFuture != null) {
try {
Timber.i("Remote search in progress, attempting to abort...");
// Canceling the future stops any message fetches in progress.
final boolean cancelSuccess = remoteSearchFuture.cancel(true); // mayInterruptIfRunning = true
if (!cancelSuccess) {
Timber.e("Could not cancel remote search future.");
}
// Closing the folder will kill off the connection if we're mid-search.
final Account searchAccount = account;
final Folder remoteFolder = currentFolder.folder;
remoteFolder.close();
// Send a remoteSearchFinished() message for good measure.
activityListener
.remoteSearchFinished(currentFolder.name, 0, searchAccount.getRemoteSearchNumResults(), null);
} catch (Exception e) {
// Since the user is going back, log and squash any exceptions.
Timber.e(e, "Could not abort remote search before going back");
}
}
super.onStop();
}
public void selectAll() {
setSelectionState(true);
}
public void onMoveUp() {
int currentPosition = listView.getSelectedItemPosition();
if (currentPosition == AdapterView.INVALID_POSITION || listView.isInTouchMode()) {
currentPosition = listView.getFirstVisiblePosition();
}
if (currentPosition > 0) {
listView.setSelection(currentPosition - 1);
}
}
public void onMoveDown() {
int currentPosition = listView.getSelectedItemPosition();
if (currentPosition == AdapterView.INVALID_POSITION || listView.isInTouchMode()) {
currentPosition = listView.getFirstVisiblePosition();
}
if (currentPosition < listView.getCount()) {
listView.setSelection(currentPosition + 1);
}
}
public boolean openPrevious(MessageReference messageReference) {
int position = getPosition(messageReference);
if (position <= 0) {
return false;
}
openMessageAtPosition(position - 1);
return true;
}
public boolean openNext(MessageReference messageReference) {
int position = getPosition(messageReference);
if (position < 0 || position == adapter.getCount() - 1) {
return false;
}
openMessageAtPosition(position + 1);
return true;
}
public boolean isFirst(MessageReference messageReference) {
return adapter.isEmpty() || messageReference.equals(getReferenceForPosition(0));
}
public boolean isLast(MessageReference messageReference) {
return adapter.isEmpty() || messageReference.equals(getReferenceForPosition(adapter.getCount() - 1));
}
private MessageReference getReferenceForPosition(int position) {
Cursor cursor = (Cursor) adapter.getItem(position);
String accountUuid = cursor.getString(ACCOUNT_UUID_COLUMN);
String folderName = cursor.getString(FOLDER_NAME_COLUMN);
String messageUid = cursor.getString(UID_COLUMN);
return new MessageReference(accountUuid, folderName, messageUid, null);
}
private void openMessageAtPosition(int position) {
// Scroll message into view if necessary
int listViewPosition = adapterToListViewPosition(position);
if (listViewPosition != AdapterView.INVALID_POSITION &&
(listViewPosition < listView.getFirstVisiblePosition() ||
listViewPosition > listView.getLastVisiblePosition())) {
listView.setSelection(listViewPosition);
}
MessageReference ref = getReferenceForPosition(position);
// For some reason the listView.setSelection() above won't do anything when we call
// onOpenMessage() (and consequently adapter.notifyDataSetChanged()) right away. So we
// defer the call using MessageListHandler.
handler.openMessage(ref);
}
private int getPosition(MessageReference messageReference) {
for (int i = 0, len = adapter.getCount(); i < len; i++) {
Cursor cursor = (Cursor) adapter.getItem(i);
String accountUuid = cursor.getString(ACCOUNT_UUID_COLUMN);
String folderName = cursor.getString(FOLDER_NAME_COLUMN);
String uid = cursor.getString(UID_COLUMN);
if (accountUuid.equals(messageReference.getAccountUuid()) &&
folderName.equals(messageReference.getFolderName()) &&
uid.equals(messageReference.getUid())) {
return i;
}
}
return -1;
}
public interface MessageListFragmentListener {
void enableActionBarProgress(boolean enable);
void setMessageListProgress(int level);
void showThread(Account account, String folderName, long rootId);
void showMoreFromSameSender(String senderAddress);
void onResendMessage(MessageReference message);
void onForward(MessageReference message);
void onReply(MessageReference message);
void onReplyAll(MessageReference message);
void openMessage(MessageReference messageReference);
void setMessageListTitle(String title);
void setMessageListSubTitle(String subTitle);
void setUnreadCount(int unread);
void onCompose(Account account);
boolean startSearch(Account account, String folderName);
void remoteSearchStarted();
void goBack();
void updateMenu();
}
public void onReverseSort() {
changeSort(sortType);
}
private MessageReference getSelectedMessage() {
int listViewPosition = listView.getSelectedItemPosition();
int adapterPosition = listViewToAdapterPosition(listViewPosition);
return getMessageAtPosition(adapterPosition);
}
private int getAdapterPositionForSelectedMessage() {
int listViewPosition = listView.getSelectedItemPosition();
return listViewToAdapterPosition(listViewPosition);
}
private int getPositionForUniqueId(long uniqueId) {
for (int position = 0, end = adapter.getCount(); position < end; position++) {
Cursor cursor = (Cursor) adapter.getItem(position);
if (cursor.getLong(uniqueIdColumn) == uniqueId) {
return position;
}
}
return AdapterView.INVALID_POSITION;
}
private MessageReference getMessageAtPosition(int adapterPosition) {
if (adapterPosition == AdapterView.INVALID_POSITION) {
return null;
}
Cursor cursor = (Cursor) adapter.getItem(adapterPosition);
String accountUuid = cursor.getString(ACCOUNT_UUID_COLUMN);
String folderName = cursor.getString(FOLDER_NAME_COLUMN);
String messageUid = cursor.getString(UID_COLUMN);
return new MessageReference(accountUuid, folderName, messageUid, null);
}
private List<MessageReference> getCheckedMessages() {
List<MessageReference> messages = new ArrayList<>(selected.size());
for (int position = 0, end = adapter.getCount(); position < end; position++) {
Cursor cursor = (Cursor) adapter.getItem(position);
long uniqueId = cursor.getLong(uniqueIdColumn);
if (selected.contains(uniqueId)) {
MessageReference message = getMessageAtPosition(position);
if (message != null) {
messages.add(message);
}
}
}
return messages;
}
public void onDelete() {
MessageReference message = getSelectedMessage();
if (message != null) {
onDelete(Collections.singletonList(message));
}
}
public void toggleMessageSelect() {
toggleMessageSelect(listView.getSelectedItemPosition());
}
public void onToggleFlagged() {
onToggleFlag(Flag.FLAGGED, FLAGGED_COLUMN);
}
public void onToggleRead() {
onToggleFlag(Flag.SEEN, READ_COLUMN);
}
private void onToggleFlag(Flag flag, int flagColumn) {
int adapterPosition = getAdapterPositionForSelectedMessage();
if (adapterPosition == ListView.INVALID_POSITION) {
return;
}
Cursor cursor = (Cursor) adapter.getItem(adapterPosition);
boolean flagState = (cursor.getInt(flagColumn) == 1);
setFlag(adapterPosition, flag, !flagState);
}
public void onMove() {
MessageReference message = getSelectedMessage();
if (message != null) {
onMove(message);
}
}
public void onArchive() {
MessageReference message = getSelectedMessage();
if (message != null) {
onArchive(message);
}
}
public void onCopy() {
MessageReference message = getSelectedMessage();
if (message != null) {
onCopy(message);
}
}
public boolean isOutbox() {
return (folderName != null && folderName.equals(account.getOutboxFolderName()));
}
private boolean isErrorFolder() {
return K9.ERROR_FOLDER_NAME.equals(folderName);
}
public boolean isRemoteFolder() {
if (search.isManualSearch() || isOutbox() || isErrorFolder()) {
return false;
}
if (!messagingController.isMoveCapable(account)) {
// For POP3 accounts only the Inbox is a remote folder.
return (folderName != null && folderName.equals(account.getInboxFolderName()));
}
return true;
}
public boolean isManualSearch() {
return search.isManualSearch();
}
public boolean isAccountExpungeCapable() {
try {
return (account != null && account.getRemoteStore().isExpungeCapable());
} catch (Exception e) {
return false;
}
}
public void onRemoteSearch() {
// Remote search is useless without the network.
if (hasConnectivity) {
onRemoteSearchRequested();
} else {
Toast.makeText(getActivity(), getText(R.string.remote_search_unavailable_no_network),
Toast.LENGTH_SHORT).show();
}
}
public boolean isRemoteSearch() {
return remoteSearchPerformed;
}
public boolean isRemoteSearchAllowed() {
if (!search.isManualSearch() || remoteSearchPerformed || !singleFolderMode) {
return false;
}
boolean allowRemoteSearch = false;
final Account searchAccount = account;
if (searchAccount != null) {
allowRemoteSearch = searchAccount.allowRemoteSearch();
}
return allowRemoteSearch;
}
public boolean onSearchRequested() {
String folderName = (currentFolder != null) ? currentFolder.name : null;
return fragmentListener.startSearch(account, folderName);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
String accountUuid = accountUuids[id];
Account account = preferences.getAccount(accountUuid);
String threadId = getThreadId(search);
Uri uri;
String[] projection;
boolean needConditions;
if (threadId != null) {
uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + accountUuid + "/thread/" + threadId);
projection = PROJECTION;
needConditions = false;
} else if (showingThreadedList) {
uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + accountUuid + "/messages/threaded");
projection = THREADED_PROJECTION;
needConditions = true;
} else {
uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + accountUuid + "/messages");
projection = PROJECTION;
needConditions = true;
}
StringBuilder query = new StringBuilder();
List<String> queryArgs = new ArrayList<>();
if (needConditions) {
boolean selectActive = activeMessage != null && activeMessage.getAccountUuid().equals(accountUuid);
if (selectActive) {
query.append("(" + MessageColumns.UID + " = ? AND " + SpecialColumns.FOLDER_NAME + " = ?) OR (");
queryArgs.add(activeMessage.getUid());
queryArgs.add(activeMessage.getFolderName());
}
SqlQueryBuilder.buildWhereClause(account, search.getConditions(), query, queryArgs);
if (selectActive) {
query.append(')');
}
}
String selection = query.toString();
String[] selectionArgs = queryArgs.toArray(new String[0]);
String sortOrder = buildSortOrder();
return new CursorLoader(getActivity(), uri, projection, selection, selectionArgs,
sortOrder);
}
private String getThreadId(LocalSearch search) {
for (ConditionsTreeNode node : search.getLeafSet()) {
SearchCondition condition = node.mCondition;
if (condition.field == SearchField.THREAD_ID) {
return condition.value;
}
}
return null;
}
private String buildSortOrder() {
String sortColumn;
switch (sortType) {
case SORT_ARRIVAL: {
sortColumn = MessageColumns.INTERNAL_DATE;
break;
}
case SORT_ATTACHMENT: {
sortColumn = "(" + MessageColumns.ATTACHMENT_COUNT + " < 1)";
break;
}
case SORT_FLAGGED: {
sortColumn = "(" + MessageColumns.FLAGGED + " != 1)";
break;
}
case SORT_SENDER: {
//FIXME
sortColumn = MessageColumns.SENDER_LIST;
break;
}
case SORT_SUBJECT: {
sortColumn = MessageColumns.SUBJECT + " COLLATE NOCASE";
break;
}
case SORT_UNREAD: {
sortColumn = MessageColumns.READ;
break;
}
case SORT_DATE:
default: {
sortColumn = MessageColumns.DATE;
}
}
String sortDirection = (sortAscending) ? " ASC" : " DESC";
String secondarySort;
if (sortType == SortType.SORT_DATE || sortType == SortType.SORT_ARRIVAL) {
secondarySort = "";
} else {
secondarySort = MessageColumns.DATE + ((sortDateAscending) ? " ASC, " : " DESC, ");
}
return sortColumn + sortDirection + ", " + secondarySort + MessageColumns.ID + " DESC";
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
if (isThreadDisplay && data.getCount() == 0) {
handler.goBack();
return;
}
swipeRefreshLayout.setRefreshing(false);
swipeRefreshLayout.setEnabled(isPullToRefreshAllowed());
final int loaderId = loader.getId();
cursors[loaderId] = data;
cursorValid[loaderId] = true;
Cursor cursor;
if (cursors.length > 1) {
cursor = new MergeCursorWithUniqueId(cursors, getComparator());
uniqueIdColumn = cursor.getColumnIndex("_id");
} else {
cursor = data;
uniqueIdColumn = ID_COLUMN;
}
if (isThreadDisplay) {
if (cursor.moveToFirst()) {
title = cursor.getString(SUBJECT_COLUMN);
if (!TextUtils.isEmpty(title)) {
title = Utility.stripSubject(title);
}
if (TextUtils.isEmpty(title)) {
title = getString(R.string.general_no_subject);
}
updateTitle();
} else {
//TODO: empty thread view -> return to full message list
}
}
cleanupSelected(cursor);
updateContextMenu(cursor);
adapter.swapCursor(cursor);
resetActionMode();
computeBatchDirection();
if (isLoadFinished()) {
if (savedListState != null) {
handler.restoreListPosition();
}
fragmentListener.updateMenu();
}
}
private void updateMoreMessagesOfCurrentFolder() {
if (folderName != null) {
try {
LocalFolder folder = MlfUtils.getOpenFolder(folderName, account);
currentFolder.setMoreMessagesFromFolder(folder);
} catch (MessagingException e) {
throw new RuntimeException(e);
}
}
}
public boolean isLoadFinished() {
if (cursorValid == null) {
return false;
}
for (boolean cursorValid : this.cursorValid) {
if (!cursorValid) {
return false;
}
}
return true;
}
/**
* Close the context menu when the message it was opened for is no longer in the message list.
*/
private void updateContextMenu(Cursor cursor) {
if (contextMenuUniqueId == 0) {
return;
}
for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
long uniqueId = cursor.getLong(uniqueIdColumn);
if (uniqueId == contextMenuUniqueId) {
return;
}
}
contextMenuUniqueId = 0;
Activity activity = getActivity();
if (activity != null) {
activity.closeContextMenu();
}
}
private void cleanupSelected(Cursor cursor) {
if (selected.isEmpty()) {
return;
}
Set<Long> selected = new HashSet<>();
for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
long uniqueId = cursor.getLong(uniqueIdColumn);
if (this.selected.contains(uniqueId)) {
selected.add(uniqueId);
}
}
this.selected = selected;
}
/**
* Starts or finishes the action mode when necessary.
*/
private void resetActionMode() {
if (selected.isEmpty()) {
if (actionMode != null) {
actionMode.finish();
}
return;
}
if (actionMode == null) {
startAndPrepareActionMode();
}
recalculateSelectionCount();
updateActionModeTitle();
}
private void startAndPrepareActionMode() {
actionMode = getActivity().startActionMode(actionModeCallback);
actionMode.invalidate();
}
/**
* Recalculates the selection count.
*
* <p>
* For non-threaded lists this is simply the number of visibly selected messages. If threaded
* view is enabled this method counts the number of messages in the selected threads.
* </p>
*/
private void recalculateSelectionCount() {
if (!showingThreadedList) {
selectedCount = selected.size();
return;
}
selectedCount = 0;
for (int i = 0, end = adapter.getCount(); i < end; i++) {
Cursor cursor = (Cursor) adapter.getItem(i);
long uniqueId = cursor.getLong(uniqueIdColumn);
if (selected.contains(uniqueId)) {
int threadCount = cursor.getInt(THREAD_COUNT_COLUMN);
selectedCount += (threadCount > 1) ? threadCount : 1;
}
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
selected.clear();
adapter.swapCursor(null);
}
Account getAccountFromCursor(Cursor cursor) {
String accountUuid = cursor.getString(ACCOUNT_UUID_COLUMN);
return preferences.getAccount(accountUuid);
}
void remoteSearchFinished() {
remoteSearchFuture = null;
}
/**
* Mark a message as 'active'.
*
* <p>
* The active message is the one currently displayed in the message view portion of the split
* view.
* </p>
*
* @param messageReference
* {@code null} to not mark any message as being 'active'.
*/
public void setActiveMessage(MessageReference messageReference) {
activeMessage = messageReference;
// Reload message list with modified query that always includes the active message
if (isAdded()) {
restartLoader();
}
// Redraw list immediately
if (adapter != null) {
adapter.notifyDataSetChanged();
}
}
public boolean isSingleAccountMode() {
return singleAccountMode;
}
public boolean isSingleFolderMode() {
return singleFolderMode;
}
public boolean isInitialized() {
return initialized;
}
public boolean isMarkAllAsReadSupported() {
return (isSingleAccountMode() && isSingleFolderMode());
}
public void confirmMarkAllAsRead() {
if (K9.confirmMarkAllRead()) {
showDialog(R.id.dialog_confirm_mark_all_as_read);
} else {
markAllAsRead();
}
}
private void markAllAsRead() {
if (isMarkAllAsReadSupported()) {
messagingController.markAllMessagesRead(account, folderName);
}
}
public boolean isCheckMailSupported() {
return (allAccounts || !isSingleAccountMode() || !isSingleFolderMode() ||
isRemoteFolder());
}
private boolean isCheckMailAllowed() {
return (!isManualSearch() && isCheckMailSupported());
}
private boolean isPullToRefreshAllowed() {
return (isRemoteSearchAllowed() || isCheckMailAllowed());
}
LayoutInflater getLayoutInflater() {
return layoutInflater;
}
}