Switch MessageListFragment away from CursorLoader

This commit is contained in:
cketti 2020-01-12 15:53:23 +01:00
parent 4fa2fd7094
commit ab61e80bc3
16 changed files with 376 additions and 310 deletions

View file

@ -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()) }

View file

@ -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());

View file

@ -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,

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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()

View file

@ -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()) }
}

View file

@ -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)
}
}
}

View 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()) }
}

View file

@ -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()) }
}

View file

@ -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?
)

View file

@ -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()
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}

View file

@ -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()
)
}
}

View file

@ -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)
}
}
}