Switch MessageListFragment away from CursorLoader
This commit is contained in:
parent
4fa2fd7094
commit
ab61e80bc3
16 changed files with 376 additions and 310 deletions
|
@ -15,6 +15,7 @@ import org.koin.dsl.module
|
|||
val mainModule = module {
|
||||
single { Preferences.getPreferences(get()) }
|
||||
single { get<Context>().resources }
|
||||
single { get<Context>().contentResolver }
|
||||
single { LocalStoreProvider() }
|
||||
single<PowerManager> { TracingPowerManager.getPowerManager(get()) }
|
||||
single { Contacts.getInstance(get()) }
|
||||
|
|
|
@ -48,6 +48,10 @@ public class EmailProvider extends ContentProvider {
|
|||
public static String AUTHORITY;
|
||||
public static Uri CONTENT_URI;
|
||||
|
||||
public static Uri getNotificationUri(String accountUuid) {
|
||||
return Uri.withAppendedPath(CONTENT_URI, "account/" + accountUuid + "/messages");
|
||||
}
|
||||
|
||||
private UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
|
||||
|
||||
|
@ -238,8 +242,7 @@ public class EmailProvider extends ContentProvider {
|
|||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
|
||||
Uri notificationUri = Uri.withAppendedPath(CONTENT_URI, "account/" + accountUuid + "/messages");
|
||||
cursor.setNotificationUri(contentResolver, notificationUri);
|
||||
cursor.setNotificationUri(contentResolver, getNotificationUri(accountUuid));
|
||||
|
||||
cursor = new SpecialColumnsCursor(new IdTrickeryCursor(cursor), projection, specialColumns);
|
||||
cursor = new EmailProviderCacheCursor(accountUuid, cursor, getContext());
|
||||
|
|
|
@ -7,6 +7,7 @@ import com.fsck.k9.contacts.contactsModule
|
|||
import com.fsck.k9.fragment.fragmentModule
|
||||
import com.fsck.k9.ui.choosefolder.chooseFolderUiModule
|
||||
import com.fsck.k9.ui.endtoend.endToEndUiModule
|
||||
import com.fsck.k9.ui.folders.foldersUiModule
|
||||
import com.fsck.k9.ui.managefolders.manageFoldersUiModule
|
||||
import com.fsck.k9.ui.messagelist.messageListUiModule
|
||||
import com.fsck.k9.ui.settings.settingsUiModule
|
||||
|
@ -18,6 +19,7 @@ val uiModules = listOf(
|
|||
uiModule,
|
||||
settingsUiModule,
|
||||
endToEndUiModule,
|
||||
foldersUiModule,
|
||||
messageListUiModule,
|
||||
manageFoldersUiModule,
|
||||
chooseFolderUiModule,
|
||||
|
|
|
@ -10,7 +10,7 @@ import com.fsck.k9.provider.EmailProvider.ThreadColumns;
|
|||
|
||||
public final class MLFProjectionInfo {
|
||||
|
||||
static final String[] THREADED_PROJECTION = {
|
||||
public static final String[] THREADED_PROJECTION = {
|
||||
MessageColumns.ID,
|
||||
MessageColumns.UID,
|
||||
MessageColumns.INTERNAL_DATE,
|
||||
|
@ -55,6 +55,6 @@ public final class MLFProjectionInfo {
|
|||
public static final int FOLDER_SERVER_ID_COLUMN = 18;
|
||||
public static final int THREAD_COUNT_COLUMN = 19;
|
||||
|
||||
static final String[] PROJECTION = Arrays.copyOf(THREADED_PROJECTION,
|
||||
public static final String[] PROJECTION = Arrays.copyOf(THREADED_PROJECTION,
|
||||
THREAD_COUNT_COLUMN);
|
||||
}
|
||||
|
|
|
@ -3,8 +3,6 @@ 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;
|
||||
|
@ -18,9 +16,7 @@ import android.content.BroadcastReceiver;
|
|||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
@ -45,10 +41,6 @@ import androidx.appcompat.app.AppCompatActivity;
|
|||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
||||
import androidx.loader.content.CursorLoader;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
import com.fsck.k9.Account;
|
||||
|
@ -64,45 +56,27 @@ import com.fsck.k9.cache.EmailProviderCache;
|
|||
import com.fsck.k9.controller.MessageReference;
|
||||
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.MergeCursorWithUniqueId;
|
||||
import com.fsck.k9.helper.Utility;
|
||||
import com.fsck.k9.mail.Flag;
|
||||
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 com.fsck.k9.ui.R;
|
||||
import com.fsck.k9.ui.messagelist.MessageListAppearance;
|
||||
import com.fsck.k9.ui.messagelist.MessageListExtractor;
|
||||
import com.fsck.k9.ui.messagelist.MessageListConfig;
|
||||
import com.fsck.k9.ui.messagelist.MessageListFragmentDiContainer;
|
||||
import com.fsck.k9.ui.messagelist.MessageListItem;
|
||||
import com.fsck.k9.ui.messagelist.MessageListViewModel;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
import static com.fsck.k9.Account.Expunge.EXPUNGE_MANUALLY;
|
||||
import static com.fsck.k9.fragment.MLFProjectionInfo.ID_COLUMN;
|
||||
import static com.fsck.k9.fragment.MLFProjectionInfo.PROJECTION;
|
||||
import static com.fsck.k9.fragment.MLFProjectionInfo.THREADED_PROJECTION;
|
||||
|
||||
|
||||
public class MessageListFragment extends Fragment implements OnItemClickListener,
|
||||
ConfirmationDialogFragmentListener, LoaderCallbacks<Cursor>, MessageListItemActionListener {
|
||||
ConfirmationDialogFragmentListener, MessageListItemActionListener {
|
||||
|
||||
public static MessageListFragment newInstance(
|
||||
LocalSearch search, boolean isThreadDisplay, boolean threadedList) {
|
||||
|
@ -127,36 +101,15 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
|
|||
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);
|
||||
}
|
||||
|
||||
private final SortTypeToastProvider sortTypeToastProvider = DI.get(SortTypeToastProvider.class);
|
||||
private final MessageListExtractor messageListExtractor = DI.get(MessageListExtractor.class);
|
||||
private final MessageListFragmentDiContainer diContainer = new MessageListFragmentDiContainer(this);
|
||||
|
||||
ListView listView;
|
||||
private SwipeRefreshLayout swipeRefreshLayout;
|
||||
Parcelable savedListState;
|
||||
|
||||
private MessageListAdapter adapter;
|
||||
private boolean messageListLoaded;
|
||||
private View footerView;
|
||||
private FolderInfoHolder currentFolder;
|
||||
private LayoutInflater layoutInflater;
|
||||
|
@ -165,9 +118,6 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
|
|||
private Account account;
|
||||
private String[] accountUuids;
|
||||
|
||||
private Cursor[] cursors;
|
||||
private boolean[] cursorValid;
|
||||
|
||||
/**
|
||||
* Stores the server ID of the folder that we want to open as soon as possible after load.
|
||||
*/
|
||||
|
@ -205,7 +155,6 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
|
|||
private Context context;
|
||||
private final ActivityListener activityListener = new MessageListActivityListener();
|
||||
private Preferences preferences;
|
||||
private boolean loaderJustInitialized;
|
||||
private MessageReference activeMessage;
|
||||
/**
|
||||
* {@code true} after {@link #onCreate(Bundle)} was executed. Used in {@link #updateTitle()} to
|
||||
|
@ -228,39 +177,8 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
|
|||
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);
|
||||
private MessageListViewModel getViewModel() {
|
||||
return diContainer.getViewModel();
|
||||
}
|
||||
|
||||
void folderLoading(String folder, boolean loading) {
|
||||
|
@ -398,6 +316,8 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
|
|||
|
||||
createCacheBroadcastReceiver(appContext);
|
||||
|
||||
getViewModel().getMessageListLiveData().observe(this, this::setMessageList);
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
|
@ -429,18 +349,10 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
|
|||
|
||||
initializeMessageList();
|
||||
|
||||
// This needs to be done before initializing the cursor loader below
|
||||
// This needs to be done before loading the message list 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;
|
||||
}
|
||||
loadMessageList();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -645,12 +557,6 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
|
|||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (!loaderJustInitialized) {
|
||||
restartLoader();
|
||||
} else {
|
||||
loaderJustInitialized = false;
|
||||
}
|
||||
|
||||
// Check if we have connectivity. Cache the value.
|
||||
if (hasConnectivity == null) {
|
||||
hasConnectivity = Utility.hasConnectivity(getActivity().getApplication());
|
||||
|
@ -681,19 +587,6 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
|
|||
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 = layout.findViewById(R.id.swiperefresh);
|
||||
listView = layout.findViewById(R.id.message_list);
|
||||
|
@ -842,10 +735,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
|
|||
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);
|
||||
}
|
||||
loadMessageList();
|
||||
}
|
||||
|
||||
public void onCycleSort() {
|
||||
|
@ -2445,116 +2335,8 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
|
|||
return fragmentListener.startSearch(account, folderServerId);
|
||||
}
|
||||
|
||||
@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_SERVER_ID + " = ?) OR (");
|
||||
queryArgs.add(activeMessage.getUid());
|
||||
queryArgs.add(activeMessage.getFolderServerId());
|
||||
}
|
||||
|
||||
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) {
|
||||
public void setMessageList(List<MessageListItem> messageListItems) {
|
||||
if (isThreadDisplay && messageListItems.isEmpty()) {
|
||||
handler.goBack();
|
||||
return;
|
||||
}
|
||||
|
@ -2562,22 +2344,6 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
|
|||
swipeRefreshLayout.setRefreshing(false);
|
||||
swipeRefreshLayout.setEnabled(isPullToRefreshAllowed());
|
||||
|
||||
final int loaderId = loader.getId();
|
||||
cursors[loaderId] = data;
|
||||
cursorValid[loaderId] = true;
|
||||
|
||||
Cursor cursor;
|
||||
int uniqueIdColumn;
|
||||
if (cursors.length > 1) {
|
||||
cursor = new MergeCursorWithUniqueId(cursors, getComparator());
|
||||
uniqueIdColumn = cursor.getColumnIndex("_id");
|
||||
} else {
|
||||
cursor = data;
|
||||
uniqueIdColumn = ID_COLUMN;
|
||||
}
|
||||
|
||||
List<MessageListItem> messageListItems = messageListExtractor.extractMessageList(cursor, uniqueIdColumn);
|
||||
|
||||
if (isThreadDisplay) {
|
||||
if (!messageListItems.isEmpty()) {
|
||||
MessageListItem messageListItem = messageListItems.get(0);
|
||||
|
@ -2604,13 +2370,13 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
|
|||
resetActionMode();
|
||||
computeBatchDirection();
|
||||
|
||||
if (isLoadFinished()) {
|
||||
if (savedListState != null) {
|
||||
handler.restoreListPosition();
|
||||
}
|
||||
messageListLoaded = true;
|
||||
|
||||
fragmentListener.updateMenu();
|
||||
if (savedListState != null) {
|
||||
handler.restoreListPosition();
|
||||
}
|
||||
|
||||
fragmentListener.updateMenu();
|
||||
}
|
||||
|
||||
private void updateMoreMessagesOfCurrentFolder() {
|
||||
|
@ -2625,17 +2391,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
|
|||
}
|
||||
|
||||
public boolean isLoadFinished() {
|
||||
if (cursorValid == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (boolean cursorValid : this.cursorValid) {
|
||||
if (!cursorValid) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return messageListLoaded;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2729,12 +2485,6 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
selected.clear();
|
||||
adapter.setMessages(Collections.<MessageListItem>emptyList());
|
||||
}
|
||||
|
||||
void remoteSearchFinished() {
|
||||
remoteSearchFuture = null;
|
||||
}
|
||||
|
@ -2755,7 +2505,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
|
|||
|
||||
// Reload message list with modified query that always includes the active message
|
||||
if (isAdded()) {
|
||||
restartLoader();
|
||||
loadMessageList();
|
||||
}
|
||||
|
||||
// Redraw list immediately
|
||||
|
@ -2811,4 +2561,11 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
|
|||
public LocalSearch getLocalSearch() {
|
||||
return search;
|
||||
}
|
||||
|
||||
private void loadMessageList() {
|
||||
MessageListConfig config = new MessageListConfig(search, showingThreadedList, sortType, sortAscending,
|
||||
sortDateAscending, activeMessage);
|
||||
|
||||
getViewModel().loadMessageList(config);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ import com.fsck.k9.mailstore.DisplayFolder
|
|||
import com.fsck.k9.mailstore.Folder
|
||||
import com.fsck.k9.ui.folders.FolderIconProvider
|
||||
import com.fsck.k9.ui.folders.FolderNameFormatter
|
||||
import com.fsck.k9.ui.messagelist.MessageListViewModel
|
||||
import com.fsck.k9.ui.folders.FoldersViewModel
|
||||
import com.fsck.k9.ui.settings.SettingsActivity
|
||||
import com.mikepenz.iconics.IconicsColor
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
|
@ -40,7 +40,7 @@ import org.koin.core.KoinComponent
|
|||
import org.koin.core.inject
|
||||
|
||||
class K9Drawer(private val parent: MessageList, savedInstanceState: Bundle?) : KoinComponent {
|
||||
private val viewModel: MessageListViewModel by parent.viewModel()
|
||||
private val viewModel: FoldersViewModel by parent.viewModel()
|
||||
private val folderNameFormatter: FolderNameFormatter by inject()
|
||||
private val preferences: Preferences by inject()
|
||||
private val themeManager: ThemeManager by inject()
|
||||
|
|
|
@ -1,19 +1,13 @@
|
|||
package com.fsck.k9.ui
|
||||
|
||||
import com.fsck.k9.ui.folders.FolderNameFormatter
|
||||
import com.fsck.k9.ui.folders.FoldersLiveDataFactory
|
||||
import com.fsck.k9.ui.helper.DisplayHtmlUiFactory
|
||||
import com.fsck.k9.ui.helper.HtmlSettingsProvider
|
||||
import com.fsck.k9.ui.helper.HtmlToSpanned
|
||||
import com.fsck.k9.ui.messagelist.MessageListExtractor
|
||||
import org.koin.dsl.module
|
||||
|
||||
val uiModule = module {
|
||||
single { FolderNameFormatter(get()) }
|
||||
single { HtmlToSpanned() }
|
||||
single { ThemeManager(get()) }
|
||||
single { HtmlSettingsProvider(get()) }
|
||||
single { DisplayHtmlUiFactory(get()) }
|
||||
single { FoldersLiveDataFactory(get(), get(), get()) }
|
||||
single { MessageListExtractor(get(), get()) }
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
package com.fsck.k9.ui.folders
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.mailstore.DisplayFolder
|
||||
|
||||
class FoldersViewModel(private val foldersLiveDataFactory: FoldersLiveDataFactory) : ViewModel() {
|
||||
private var currentFoldersLiveData: FoldersLiveData? = null
|
||||
private val foldersLiveData = MediatorLiveData<List<DisplayFolder>>()
|
||||
|
||||
fun getFolderListLiveData(): LiveData<List<DisplayFolder>> {
|
||||
return foldersLiveData
|
||||
}
|
||||
|
||||
fun loadFolders(account: Account) {
|
||||
if (currentFoldersLiveData?.accountUuid == account.uuid) return
|
||||
|
||||
removeCurrentFoldersLiveData()
|
||||
|
||||
val liveData = foldersLiveDataFactory.create(account)
|
||||
currentFoldersLiveData = liveData
|
||||
|
||||
foldersLiveData.addSource(liveData) { items ->
|
||||
foldersLiveData.value = items
|
||||
}
|
||||
}
|
||||
|
||||
fun stopLoadingFolders() {
|
||||
removeCurrentFoldersLiveData()
|
||||
foldersLiveData.value = null
|
||||
}
|
||||
|
||||
private fun removeCurrentFoldersLiveData() {
|
||||
currentFoldersLiveData?.let {
|
||||
currentFoldersLiveData = null
|
||||
foldersLiveData.removeSource(it)
|
||||
}
|
||||
}
|
||||
}
|
10
app/ui/src/main/java/com/fsck/k9/ui/folders/KoinModule.kt
Normal file
10
app/ui/src/main/java/com/fsck/k9/ui/folders/KoinModule.kt
Normal file
|
@ -0,0 +1,10 @@
|
|||
package com.fsck.k9.ui.folders
|
||||
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
|
||||
val foldersUiModule = module {
|
||||
single { FolderNameFormatter(get()) }
|
||||
single { FoldersLiveDataFactory(get(), get(), get()) }
|
||||
viewModel { FoldersViewModel(get()) }
|
||||
}
|
|
@ -6,4 +6,7 @@ import org.koin.dsl.module
|
|||
val messageListUiModule = module {
|
||||
viewModel { MessageListViewModel(get()) }
|
||||
factory { DefaultFolderProvider() }
|
||||
factory { MessageListExtractor(get(), get()) }
|
||||
factory { MessageListLoader(get(), get(), get()) }
|
||||
factory { MessageListLiveDataFactory(get(), get()) }
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
package com.fsck.k9.ui.messagelist
|
||||
|
||||
import com.fsck.k9.Account.SortType
|
||||
import com.fsck.k9.controller.MessageReference
|
||||
import com.fsck.k9.search.LocalSearch
|
||||
|
||||
data class MessageListConfig(
|
||||
val search: LocalSearch,
|
||||
val showingThreadedList: Boolean,
|
||||
val sortType: SortType,
|
||||
val sortAscending: Boolean,
|
||||
val sortDateAscending: Boolean,
|
||||
val activeMessage: MessageReference?
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
package com.fsck.k9.ui.messagelist
|
||||
|
||||
import com.fsck.k9.fragment.MessageListFragment
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
class MessageListFragmentDiContainer(fragment: MessageListFragment) {
|
||||
val viewModel: MessageListViewModel by fragment.viewModel()
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package com.fsck.k9.ui.messagelist
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.database.ContentObserver
|
||||
import android.os.Handler
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.fsck.k9.provider.EmailProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class MessageListLiveData(
|
||||
private val messageListLoader: MessageListLoader,
|
||||
private val contentResolver: ContentResolver,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
val config: MessageListConfig
|
||||
) : LiveData<List<MessageListItem>>() {
|
||||
private val notificationUris = config.search.accountUuids.map { accountUuid ->
|
||||
EmailProvider.getNotificationUri(accountUuid)
|
||||
}
|
||||
|
||||
private val contentObserver = object : ContentObserver(Handler()) {
|
||||
override fun onChange(selfChange: Boolean) {
|
||||
loadMessageListAsync()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadMessageListAsync() {
|
||||
coroutineScope.launch(Dispatchers.Main) {
|
||||
value = withContext(Dispatchers.IO) {
|
||||
messageListLoader.getMessageList(config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActive() {
|
||||
super.onActive()
|
||||
|
||||
for (notificationUri in notificationUris) {
|
||||
contentResolver.registerContentObserver(notificationUri, false, contentObserver)
|
||||
}
|
||||
|
||||
loadMessageListAsync()
|
||||
}
|
||||
|
||||
override fun onInactive() {
|
||||
super.onInactive()
|
||||
|
||||
for (notificationUri in notificationUris) {
|
||||
contentResolver.unregisterContentObserver(contentObserver)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package com.fsck.k9.ui.messagelist
|
||||
|
||||
import android.content.ContentResolver
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class MessageListLiveDataFactory(
|
||||
private val messageListLoader: MessageListLoader,
|
||||
private val contentResolver: ContentResolver
|
||||
) {
|
||||
fun create(coroutineScope: CoroutineScope, config: MessageListConfig): MessageListLiveData {
|
||||
return MessageListLiveData(messageListLoader, contentResolver, coroutineScope, config)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
package com.fsck.k9.ui.messagelist
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.Account.SortType
|
||||
import com.fsck.k9.Preferences
|
||||
import com.fsck.k9.fragment.MLFProjectionInfo
|
||||
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.MergeCursorWithUniqueId
|
||||
import com.fsck.k9.provider.EmailProvider
|
||||
import com.fsck.k9.provider.EmailProvider.SpecialColumns
|
||||
import com.fsck.k9.search.LocalSearch
|
||||
import com.fsck.k9.search.SearchSpecification.SearchField
|
||||
import com.fsck.k9.search.SqlQueryBuilder
|
||||
import java.util.ArrayList
|
||||
import java.util.Comparator
|
||||
|
||||
class MessageListLoader(
|
||||
private val preferences: Preferences,
|
||||
private val contentResolver: ContentResolver,
|
||||
private val messageListExtractor: MessageListExtractor
|
||||
) {
|
||||
|
||||
fun getMessageList(config: MessageListConfig): List<MessageListItem> {
|
||||
val accountUuids = config.search.accountUuids
|
||||
val cursors = accountUuids.mapNotNull { preferences.getAccount(it) }
|
||||
.mapNotNull { loadMessageListForAccount(it, config) }
|
||||
.toTypedArray()
|
||||
|
||||
val cursor: Cursor
|
||||
val uniqueIdColumn: Int
|
||||
if (cursors.size > 1) {
|
||||
cursor = MergeCursorWithUniqueId(cursors, getComparator(config))
|
||||
uniqueIdColumn = cursor.getColumnIndex("_id")
|
||||
} else {
|
||||
cursor = cursors[0]
|
||||
uniqueIdColumn = MLFProjectionInfo.ID_COLUMN
|
||||
}
|
||||
|
||||
return messageListExtractor.extractMessageList(cursor, uniqueIdColumn)
|
||||
}
|
||||
|
||||
private fun loadMessageListForAccount(account: Account, config: MessageListConfig): Cursor? {
|
||||
val accountUuid = account.uuid
|
||||
val threadId: String? = getThreadId(config.search)
|
||||
|
||||
val uri: Uri
|
||||
val projection: Array<String>
|
||||
val needConditions: Boolean
|
||||
when {
|
||||
threadId != null -> {
|
||||
uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/$accountUuid/thread/$threadId")
|
||||
projection = MLFProjectionInfo.PROJECTION
|
||||
needConditions = false
|
||||
}
|
||||
config.showingThreadedList -> {
|
||||
uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/$accountUuid/messages/threaded")
|
||||
projection = MLFProjectionInfo.THREADED_PROJECTION
|
||||
needConditions = true
|
||||
}
|
||||
else -> {
|
||||
uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/$accountUuid/messages")
|
||||
projection = MLFProjectionInfo.PROJECTION
|
||||
needConditions = true
|
||||
}
|
||||
}
|
||||
|
||||
val query = StringBuilder()
|
||||
val queryArgs: MutableList<String> = ArrayList()
|
||||
if (needConditions) {
|
||||
val activeMessage = config.activeMessage
|
||||
val selectActive = activeMessage != null && activeMessage.accountUuid == accountUuid
|
||||
if (selectActive && activeMessage != null) {
|
||||
query.append("(${EmailProvider.MessageColumns.UID} = ? AND ${SpecialColumns.FOLDER_SERVER_ID} = ?) OR (")
|
||||
queryArgs.add(activeMessage.uid)
|
||||
queryArgs.add(activeMessage.folderServerId)
|
||||
}
|
||||
|
||||
SqlQueryBuilder.buildWhereClause(account, config.search.conditions, query, queryArgs)
|
||||
|
||||
if (selectActive) {
|
||||
query.append(')')
|
||||
}
|
||||
}
|
||||
|
||||
val selection = query.toString()
|
||||
val selectionArgs = queryArgs.toTypedArray()
|
||||
|
||||
val sortOrder: String = buildSortOrder(config)
|
||||
|
||||
return contentResolver.query(uri, projection, selection, selectionArgs, sortOrder)
|
||||
}
|
||||
|
||||
private fun getThreadId(search: LocalSearch): String? {
|
||||
return search.leafSet.firstOrNull { it.condition.field == SearchField.THREAD_ID }?.condition?.value
|
||||
}
|
||||
|
||||
private fun buildSortOrder(config: MessageListConfig): String {
|
||||
val sortColumn = when (config.sortType) {
|
||||
SortType.SORT_ARRIVAL -> EmailProvider.MessageColumns.INTERNAL_DATE
|
||||
SortType.SORT_ATTACHMENT -> "(${EmailProvider.MessageColumns.ATTACHMENT_COUNT} < 1)"
|
||||
SortType.SORT_FLAGGED -> "(${EmailProvider.MessageColumns.FLAGGED} != 1)"
|
||||
SortType.SORT_SENDER -> EmailProvider.MessageColumns.SENDER_LIST // FIXME
|
||||
SortType.SORT_SUBJECT -> "${EmailProvider.MessageColumns.SUBJECT} COLLATE NOCASE"
|
||||
SortType.SORT_UNREAD -> EmailProvider.MessageColumns.READ
|
||||
SortType.SORT_DATE -> EmailProvider.MessageColumns.DATE
|
||||
else -> EmailProvider.MessageColumns.DATE
|
||||
}
|
||||
|
||||
val sortDirection = if (config.sortAscending) " ASC" else " DESC"
|
||||
val secondarySort = if (config.sortType == SortType.SORT_DATE || config.sortType == SortType.SORT_ARRIVAL) {
|
||||
""
|
||||
} else {
|
||||
if (config.sortDateAscending) {
|
||||
"${EmailProvider.MessageColumns.DATE} ASC, "
|
||||
} else {
|
||||
"${EmailProvider.MessageColumns.DATE} DESC, "
|
||||
}
|
||||
}
|
||||
|
||||
return "$sortColumn$sortDirection, $secondarySort${EmailProvider.MessageColumns.ID} DESC"
|
||||
}
|
||||
|
||||
private fun getComparator(config: MessageListConfig): Comparator<Cursor>? {
|
||||
val chain: MutableList<Comparator<Cursor>> = ArrayList(3 /* we add 3 comparators at most */)
|
||||
|
||||
// Add the specified comparator
|
||||
val comparator = SORT_COMPARATORS.getValue(config.sortType)
|
||||
if (config.sortAscending) {
|
||||
chain.add(comparator)
|
||||
} else {
|
||||
chain.add(ReverseComparator(comparator))
|
||||
}
|
||||
|
||||
// Add the date comparator if not already specified
|
||||
if (config.sortType != SortType.SORT_DATE && config.sortType != SortType.SORT_ARRIVAL) {
|
||||
val dateComparator = SORT_COMPARATORS.getValue(SortType.SORT_DATE)
|
||||
if (config.sortDateAscending) {
|
||||
chain.add(dateComparator)
|
||||
} else {
|
||||
chain.add(ReverseComparator(dateComparator))
|
||||
}
|
||||
}
|
||||
|
||||
// Add the id comparator
|
||||
chain.add(ReverseIdComparator())
|
||||
|
||||
// Build the comparator chain
|
||||
return ComparatorChain(chain)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val SORT_COMPARATORS = mapOf(
|
||||
SortType.SORT_ATTACHMENT to AttachmentComparator(),
|
||||
SortType.SORT_DATE to DateComparator(),
|
||||
SortType.SORT_ARRIVAL to ArrivalComparator(),
|
||||
SortType.SORT_FLAGGED to FlaggedComparator(),
|
||||
SortType.SORT_SUBJECT to SubjectComparator(),
|
||||
SortType.SORT_SENDER to SenderComparator(),
|
||||
SortType.SORT_UNREAD to UnreadComparator()
|
||||
)
|
||||
}
|
||||
}
|
|
@ -3,41 +3,33 @@ package com.fsck.k9.ui.messagelist
|
|||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.mailstore.DisplayFolder
|
||||
import com.fsck.k9.ui.folders.FoldersLiveData
|
||||
import com.fsck.k9.ui.folders.FoldersLiveDataFactory
|
||||
import androidx.lifecycle.viewModelScope
|
||||
|
||||
class MessageListViewModel(private val foldersLiveDataFactory: FoldersLiveDataFactory) : ViewModel() {
|
||||
private var currentFoldersLiveData: FoldersLiveData? = null
|
||||
private val foldersLiveData = MediatorLiveData<List<DisplayFolder>>()
|
||||
class MessageListViewModel(private val messageListLiveDataFactory: MessageListLiveDataFactory) : ViewModel() {
|
||||
private var currentMessageListLiveData: MessageListLiveData? = null
|
||||
private val messageListLiveData = MediatorLiveData<List<MessageListItem>>()
|
||||
|
||||
fun getFolderListLiveData(): LiveData<List<DisplayFolder>> {
|
||||
return foldersLiveData
|
||||
fun getMessageListLiveData(): LiveData<List<MessageListItem>> {
|
||||
return messageListLiveData
|
||||
}
|
||||
|
||||
fun loadFolders(account: Account) {
|
||||
if (currentFoldersLiveData?.accountUuid == account.uuid) return
|
||||
fun loadMessageList(config: MessageListConfig) {
|
||||
if (currentMessageListLiveData?.config == config) return
|
||||
|
||||
removeCurrentFoldersLiveData()
|
||||
removeCurrentMessageListLiveData()
|
||||
|
||||
val liveData = foldersLiveDataFactory.create(account)
|
||||
currentFoldersLiveData = liveData
|
||||
val liveData = messageListLiveDataFactory.create(viewModelScope, config)
|
||||
currentMessageListLiveData = liveData
|
||||
|
||||
foldersLiveData.addSource(liveData) { items ->
|
||||
foldersLiveData.value = items
|
||||
messageListLiveData.addSource(liveData) { items ->
|
||||
messageListLiveData.value = items
|
||||
}
|
||||
}
|
||||
|
||||
fun stopLoadingFolders() {
|
||||
removeCurrentFoldersLiveData()
|
||||
foldersLiveData.value = null
|
||||
}
|
||||
|
||||
private fun removeCurrentFoldersLiveData() {
|
||||
currentFoldersLiveData?.let {
|
||||
currentFoldersLiveData = null
|
||||
foldersLiveData.removeSource(it)
|
||||
private fun removeCurrentMessageListLiveData() {
|
||||
currentMessageListLiveData?.let {
|
||||
currentMessageListLiveData = null
|
||||
messageListLiveData.removeSource(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue