Merge pull request #4438 from k9mail/bye_CursorAdapter

Switch MessageListAdapter away from CursorAdapter
This commit is contained in:
cketti 2020-01-12 13:59:52 +01:00 committed by GitHub
commit 4fa2fd7094
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 231 additions and 235 deletions

View file

@ -2,7 +2,6 @@ package com.fsck.k9.fragment
import android.content.Context
import android.content.res.Resources
import android.database.Cursor
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.Drawable
@ -15,7 +14,7 @@ import android.text.style.StyleSpan
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CursorAdapter
import android.widget.BaseAdapter
import android.widget.TextView
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.isVisible
@ -26,20 +25,18 @@ import com.fsck.k9.mail.Address
import com.fsck.k9.ui.ContactBadge
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.MessageListItem
import kotlin.math.max
class MessageListAdapter internal constructor(
context: Context,
private val context: Context,
theme: Resources.Theme,
private val res: Resources,
private val layoutInflater: LayoutInflater,
private val contactsPictureLoader: ContactPictureLoader,
private val messageListExtractor: MessageListExtractor,
private val listItemListener: MessageListItemActionListener,
private val appearance: MessageListAppearance
) : CursorAdapter(context, null, 0) {
) : BaseAdapter() {
private val forwardedIcon: Drawable
private val answeredIcon: Drawable
@ -77,9 +74,13 @@ class MessageListAdapter internal constructor(
array.recycle()
}
var activeMessage: MessageReference? = null
var messages: List<MessageListItem> = emptyList()
set(value) {
field = value
notifyDataSetChanged()
}
var uniqueIdColumn: Int = 0
var activeMessage: MessageReference? = null
var selected: Set<Long> = emptySet()
@ -100,7 +101,23 @@ class MessageListAdapter internal constructor(
}
}
override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
override fun hasStableIds(): Boolean = true
override fun getCount(): Int = messages.size
override fun getItemId(position: Int): Long = messages[position].uniqueId
override fun getItem(position: Int): MessageListItem = messages[position]
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val message = getItem(position)
val view: View = convertView ?: newView(parent)
bindView(view, context, message)
return view
}
private fun newView(parent: ViewGroup?): View {
val view = layoutInflater.inflate(R.layout.message_list_item, parent, false)
val holder = MessageViewHolder(view, listItemListener)
@ -125,14 +142,13 @@ class MessageListAdapter internal constructor(
return view
}
override fun bindView(view: View, context: Context, cursor: Cursor) {
val item = messageListExtractor.extractMessageListItem(cursor, uniqueIdColumn)
val isSelected = selected.contains(item.uniqueId)
val isActive = isActiveMessage(item)
private fun bindView(view: View, context: Context, message: MessageListItem) {
val isSelected = selected.contains(message.uniqueId)
val isActive = isActiveMessage(message)
val holder = view.tag as MessageViewHolder
with(item) {
with(message) {
val maybeBoldTypeface = if (isRead) Typeface.NORMAL else Typeface.BOLD
val displayDate = DateUtils.getRelativeTimeSpanString(context, messageDate)
val displayThreadCount = if (appearance.showingThreadedList) threadCount else 0

View file

@ -92,20 +92,13 @@ 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.MessageListItem;
import timber.log.Timber;
import static com.fsck.k9.Account.Expunge.EXPUNGE_MANUALLY;
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_SERVER_ID_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,
@ -174,7 +167,6 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
private Cursor[] cursors;
private boolean[] cursorValid;
private int uniqueIdColumn;
/**
* Stores the server ID of the folder that we want to open as soon as possible after load.
@ -358,24 +350,22 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
return;
}
Cursor cursor = (Cursor) parent.getItemAtPosition(position);
if (cursor == null) {
return;
}
int adapterPosition = listViewToAdapterPosition(position);
MessageListItem messageListItem = adapter.getItem(adapterPosition);
if (selectedCount > 0) {
toggleMessageSelect(position);
} else {
if (showingThreadedList && cursor.getInt(THREAD_COUNT_COLUMN) > 1) {
Account account = getAccountFromCursor(cursor);
String folderServerId = cursor.getString(FOLDER_SERVER_ID_COLUMN);
if (showingThreadedList && messageListItem.getThreadCount() > 1) {
Account account = messageListItem.getAccount();
String folderServerId = messageListItem.getFolderServerId();
// If threading is enabled and this item represents a thread, display the thread contents.
long rootId = cursor.getLong(THREAD_ROOT_COLUMN);
long rootId = messageListItem.getThreadRoot();
fragmentListener.showThread(account, folderServerId, rootId);
} else {
// This item represents a message; just display the message.
openMessageAtPosition(listViewToAdapterPosition(position));
openMessageAtPosition(adapterPosition);
}
}
}
@ -585,7 +575,6 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
getResources(),
layoutInflater,
ContactPicture.getContactPictureLoader(),
messageListExtractor,
this,
getMessageListAppearance()
);
@ -1094,8 +1083,8 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
onResendMessage(getMessageAtPosition(adapterPosition));
selectedCount = 0;
} else if (id == R.id.same_sender) {
Cursor cursor = (Cursor) adapter.getItem(adapterPosition);
String senderAddress = MlfUtils.getSenderAddressFromCursor(cursor);
MessageListItem messageListItem = adapter.getItem(adapterPosition);
String senderAddress = messageListItem.getSenderAddress();
if (senderAddress != null) {
fragmentListener.showMoreFromSameSender(senderAddress);
}
@ -1131,21 +1120,18 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
super.onCreateContextMenu(menu, v, menuInfo);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
Cursor cursor = (Cursor) listView.getItemAtPosition(info.position);
if (cursor == null) {
return;
}
int adapterPosition = listViewToAdapterPosition(info.position);
MessageListItem messageListItem = adapter.getItem(adapterPosition);
getActivity().getMenuInflater().inflate(R.menu.message_list_item_context, menu);
menu.findItem(R.id.debug_delete_locally).setVisible(K9.DEVELOPER_MODE);
contextMenuUniqueId = cursor.getLong(uniqueIdColumn);
Account account = getAccountFromCursor(cursor);
contextMenuUniqueId = messageListItem.getUniqueId();
Account account = messageListItem.getAccount();
String subject = cursor.getString(SUBJECT_COLUMN);
boolean read = (cursor.getInt(READ_COLUMN) == 1);
boolean flagged = (cursor.getInt(FLAGGED_COLUMN) == 1);
String subject = messageListItem.getSubject();
boolean read = messageListItem.isRead();
boolean flagged = messageListItem.isStarred();
menu.setHeaderTitle(subject);
@ -1416,12 +1402,12 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
selectedCount = 0;
for (int i = 0, end = adapter.getCount(); i < end; i++) {
Cursor cursor = (Cursor) adapter.getItem(i);
long uniqueId = cursor.getLong(uniqueIdColumn);
MessageListItem messageListItem = adapter.getItem(i);
long uniqueId = messageListItem.getUniqueId();
this.selected.add(uniqueId);
if (showingThreadedList) {
int threadCount = cursor.getInt(THREAD_COUNT_COLUMN);
int threadCount = messageListItem.getThreadCount();
selectedCount += (threadCount > 1) ? threadCount : 1;
} else {
selectedCount++;
@ -1457,15 +1443,15 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
@Override
public void toggleMessageFlagWithAdapterPosition(int adapterPosition) {
Cursor cursor = (Cursor) adapter.getItem(adapterPosition);
boolean flagged = (cursor.getInt(FLAGGED_COLUMN) == 1);
MessageListItem messageListItem = adapter.getItem(adapterPosition);
boolean flagged = messageListItem.isStarred();
setFlag(adapterPosition,Flag.FLAGGED, !flagged);
}
private void toggleMessageSelectWithAdapterPosition(int adapterPosition) {
Cursor cursor = (Cursor) adapter.getItem(adapterPosition);
long uniqueId = cursor.getLong(uniqueIdColumn);
MessageListItem messageListItem = adapter.getItem(adapterPosition);
long uniqueId = messageListItem.getUniqueId();
boolean selected = this.selected.contains(uniqueId);
if (!selected) {
@ -1476,7 +1462,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
int selectedCountDelta = 1;
if (showingThreadedList) {
int threadCount = cursor.getInt(THREAD_COUNT_COLUMN);
int threadCount = messageListItem.getThreadCount();
if (threadCount > 1) {
selectedCountDelta = threadCount;
}
@ -1519,12 +1505,12 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
boolean isBatchRead = false;
for (int i = 0, end = adapter.getCount(); i < end; i++) {
Cursor cursor = (Cursor) adapter.getItem(i);
long uniqueId = cursor.getLong(uniqueIdColumn);
MessageListItem messageListItem = adapter.getItem(i);
long uniqueId = messageListItem.getUniqueId();
if (selected.contains(uniqueId)) {
boolean read = (cursor.getInt(READ_COLUMN) == 1);
boolean flagged = (cursor.getInt(FLAGGED_COLUMN) == 1);
boolean read = messageListItem.isRead();
boolean flagged = messageListItem.isStarred();
if (!flagged) {
isBatchFlag = true;
@ -1548,15 +1534,15 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
return;
}
Cursor cursor = (Cursor) adapter.getItem(adapterPosition);
Account account = preferences.getAccount(cursor.getString(ACCOUNT_UUID_COLUMN));
MessageListItem messageListItem = adapter.getItem(adapterPosition);
Account account = messageListItem.getAccount();
if (showingThreadedList && cursor.getInt(THREAD_COUNT_COLUMN) > 1) {
long threadRootId = cursor.getLong(THREAD_ROOT_COLUMN);
if (showingThreadedList && messageListItem.getThreadCount() > 1) {
long threadRootId = messageListItem.getThreadRoot();
messagingController.setFlagForThreads(account,
Collections.singletonList(threadRootId), flag, newState);
} else {
long id = cursor.getLong(ID_COLUMN);
long id = messageListItem.getDatabaseId();
messagingController.setFlag(account, Collections.singletonList(id), flag,
newState);
}
@ -1574,22 +1560,21 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
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);
MessageListItem messageListItem = adapter.getItem(position);
long uniqueId = messageListItem.getUniqueId();
if (selected.contains(uniqueId)) {
String uuid = cursor.getString(ACCOUNT_UUID_COLUMN);
Account account = preferences.getAccount(uuid);
Account account = messageListItem.getAccount();
accounts.add(account);
if (showingThreadedList && cursor.getInt(THREAD_COUNT_COLUMN) > 1) {
if (showingThreadedList && messageListItem.getThreadCount() > 1) {
List<Long> threadRootIdList = threadMap.get(account);
if (threadRootIdList == null) {
threadRootIdList = new ArrayList<>();
threadMap.put(account, threadRootIdList);
}
threadRootIdList.add(cursor.getLong(THREAD_ROOT_COLUMN));
threadRootIdList.add(messageListItem.getThreadRoot());
} else {
List<Long> messageIdList = messageMap.get(account);
if (messageIdList == null) {
@ -1597,7 +1582,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
messageMap.put(account, messageIdList);
}
messageIdList.add(cursor.getLong(ID_COLUMN));
messageIdList.add(messageListItem.getDatabaseId());
}
}
}
@ -1947,11 +1932,11 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
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);
MessageListItem messageListItem = adapter.getItem(position);
long uniqueId = messageListItem.getUniqueId();
if (selected.contains(uniqueId)) {
String accountUuid = cursor.getString(ACCOUNT_UUID_COLUMN);
String accountUuid = messageListItem.getAccount().getUuid();
accountUuids.add(accountUuid);
if (accountUuids.size() == MessageListFragment.this.accountUuids.length) {
@ -2226,11 +2211,11 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
}
private MessageReference getReferenceForPosition(int position) {
Cursor cursor = (Cursor) adapter.getItem(position);
MessageListItem messageListItem = adapter.getItem(position);
String accountUuid = cursor.getString(ACCOUNT_UUID_COLUMN);
String folderServerId = cursor.getString(FOLDER_SERVER_ID_COLUMN);
String messageUid = cursor.getString(UID_COLUMN);
String accountUuid = messageListItem.getAccount().getUuid();
String folderServerId = messageListItem.getFolderServerId();
String messageUid = messageListItem.getMessageUid();
return new MessageReference(accountUuid, folderServerId, messageUid, null);
}
@ -2253,11 +2238,11 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
private int getPosition(MessageReference messageReference) {
for (int i = 0, len = adapter.getCount(); i < len; i++) {
Cursor cursor = (Cursor) adapter.getItem(i);
MessageListItem messageListItem = adapter.getItem(i);
String accountUuid = cursor.getString(ACCOUNT_UUID_COLUMN);
String folderServerId = cursor.getString(FOLDER_SERVER_ID_COLUMN);
String uid = cursor.getString(UID_COLUMN);
String accountUuid = messageListItem.getAccount().getUuid();
String folderServerId = messageListItem.getFolderServerId();
String uid = messageListItem.getMessageUid();
if (accountUuid.equals(messageReference.getAccountUuid()) &&
folderServerId.equals(messageReference.getFolderServerId()) &&
@ -2305,8 +2290,8 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
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) {
MessageListItem messageListItem = adapter.getItem(position);
if (messageListItem.getUniqueId() == uniqueId) {
return position;
}
}
@ -2319,11 +2304,11 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
return null;
}
Cursor cursor = (Cursor) adapter.getItem(adapterPosition);
MessageListItem messageListItem = adapter.getItem(adapterPosition);
String accountUuid = cursor.getString(ACCOUNT_UUID_COLUMN);
String folderServerId = cursor.getString(FOLDER_SERVER_ID_COLUMN);
String messageUid = cursor.getString(UID_COLUMN);
String accountUuid = messageListItem.getAccount().getUuid();
String folderServerId = messageListItem.getFolderServerId();
String messageUid = messageListItem.getMessageUid();
return new MessageReference(accountUuid, folderServerId, messageUid, null);
}
@ -2331,8 +2316,8 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
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);
MessageListItem messageListItem = adapter.getItem(position);
long uniqueId = messageListItem.getUniqueId();
if (selected.contains(uniqueId)) {
MessageReference message = getMessageAtPosition(position);
@ -2357,21 +2342,26 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
}
public void onToggleFlagged() {
onToggleFlag(Flag.FLAGGED, FLAGGED_COLUMN);
onToggleFlag(Flag.FLAGGED);
}
public void onToggleRead() {
onToggleFlag(Flag.SEEN, READ_COLUMN);
onToggleFlag(Flag.SEEN);
}
private void onToggleFlag(Flag flag, int flagColumn) {
private void onToggleFlag(Flag flag) {
int adapterPosition = getAdapterPositionForSelectedMessage();
if (adapterPosition == ListView.INVALID_POSITION) {
return;
}
Cursor cursor = (Cursor) adapter.getItem(adapterPosition);
boolean flagState = (cursor.getInt(flagColumn) == 1);
MessageListItem messageListItem = adapter.getItem(adapterPosition);
boolean flagState = false;
if (flag == Flag.SEEN) {
flagState = messageListItem.isRead();
} else if (flag == Flag.FLAGGED) {
flagState = messageListItem.isStarred();
}
setFlag(adapterPosition, flag, !flagState);
}
@ -2577,6 +2567,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
cursorValid[loaderId] = true;
Cursor cursor;
int uniqueIdColumn;
if (cursors.length > 1) {
cursor = new MergeCursorWithUniqueId(cursors, getComparator());
uniqueIdColumn = cursor.getColumnIndex("_id");
@ -2584,11 +2575,13 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
cursor = data;
uniqueIdColumn = ID_COLUMN;
}
adapter.setUniqueIdColumn(uniqueIdColumn);
List<MessageListItem> messageListItems = messageListExtractor.extractMessageList(cursor, uniqueIdColumn);
if (isThreadDisplay) {
if (cursor.moveToFirst()) {
title = cursor.getString(SUBJECT_COLUMN);
if (!messageListItems.isEmpty()) {
MessageListItem messageListItem = messageListItems.get(0);
title = messageListItem.getSubject();
if (!TextUtils.isEmpty(title)) {
title = Utility.stripSubject(title);
}
@ -2601,12 +2594,12 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
}
}
cleanupSelected(cursor);
cleanupSelected(messageListItems);
adapter.setSelected(selected);
updateContextMenu(cursor);
updateContextMenu(messageListItems);
adapter.swapCursor(cursor);
adapter.setMessages(messageListItems);
resetActionMode();
computeBatchDirection();
@ -2648,14 +2641,13 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
/**
* Close the context menu when the message it was opened for is no longer in the message list.
*/
private void updateContextMenu(Cursor cursor) {
private void updateContextMenu(List<MessageListItem> messageListItems) {
if (contextMenuUniqueId == 0) {
return;
}
for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
long uniqueId = cursor.getLong(uniqueIdColumn);
if (uniqueId == contextMenuUniqueId) {
for (MessageListItem messageListItem : messageListItems) {
if (messageListItem.getUniqueId() == contextMenuUniqueId) {
return;
}
}
@ -2667,14 +2659,14 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
}
}
private void cleanupSelected(Cursor cursor) {
private void cleanupSelected(List<MessageListItem> messageListItems) {
if (selected.isEmpty()) {
return;
}
Set<Long> selected = new HashSet<>();
for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
long uniqueId = cursor.getLong(uniqueIdColumn);
for (MessageListItem messageListItem : messageListItems) {
long uniqueId = messageListItem.getUniqueId();
if (this.selected.contains(uniqueId)) {
selected.add(uniqueId);
}
@ -2727,11 +2719,11 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
selectedCount = 0;
for (int i = 0, end = adapter.getCount(); i < end; i++) {
Cursor cursor = (Cursor) adapter.getItem(i);
long uniqueId = cursor.getLong(uniqueIdColumn);
MessageListItem messageListItem = adapter.getItem(i);
long uniqueId = messageListItem.getUniqueId();
if (selected.contains(uniqueId)) {
int threadCount = cursor.getInt(THREAD_COUNT_COLUMN);
int threadCount = messageListItem.getThreadCount();
selectedCount += (threadCount > 1) ? threadCount : 1;
}
}
@ -2740,12 +2732,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
@Override
public void onLoaderReset(Loader<Cursor> loader) {
selected.clear();
adapter.swapCursor(null);
}
private Account getAccountFromCursor(Cursor cursor) {
String accountUuid = cursor.getString(ACCOUNT_UUID_COLUMN);
return preferences.getAccount(accountUuid);
adapter.setMessages(Collections.<MessageListItem>emptyList());
}
void remoteSearchFinished() {

View file

@ -11,7 +11,11 @@ class MessageListExtractor(
private val preferences: Preferences,
private val messageHelper: MessageHelper
) {
fun extractMessageListItem(cursor: Cursor, uniqueIdColumn: Int): MessageListItem {
fun extractMessageList(cursor: Cursor, uniqueIdColumn: Int): List<MessageListItem> {
return cursor.map { extractMessageListItem(it, uniqueIdColumn) }
}
private fun extractMessageListItem(cursor: Cursor, uniqueIdColumn: Int): MessageListItem {
val position = cursor.position
val accountUuid = cursor.getString(MLFProjectionInfo.ACCOUNT_UUID_COLUMN)
val account = preferences.getAccount(accountUuid)
@ -41,6 +45,9 @@ class MessageListExtractor(
val uniqueId = cursor.getLong(uniqueIdColumn)
val folderServerId = cursor.getString(MLFProjectionInfo.FOLDER_SERVER_ID_COLUMN)
val messageUid = cursor.getString(MLFProjectionInfo.UID_COLUMN)
val databaseId = cursor.getLong(MLFProjectionInfo.ID_COLUMN)
val senderAddress = fromAddresses.getOrNull(0)?.address
val threadRoot = cursor.getLong(MLFProjectionInfo.THREAD_ROOT_COLUMN)
return MessageListItem(
position,
@ -62,7 +69,10 @@ class MessageListExtractor(
hasAttachments,
uniqueId,
folderServerId,
messageUid
messageUid,
databaseId,
senderAddress,
threadRoot
)
}
@ -93,4 +103,11 @@ class MessageListExtractor(
private fun Cursor.getIntIfColumnPresent(columnIndex: Int): Int? {
return if (columnCount >= columnIndex + 1) getInt(columnIndex) else null
}
private inline fun <T> Cursor.map(block: (Cursor) -> T): List<T> {
return List(count) { index ->
moveToPosition(index)
block(this)
}
}
}

View file

@ -23,5 +23,8 @@ data class MessageListItem(
val hasAttachments: Boolean,
val uniqueId: Long,
val folderServerId: String,
val messageUid: String
val messageUid: String,
val databaseId: Long,
val senderAddress: String?,
val threadRoot: Long
)

View file

@ -1,8 +1,6 @@
package com.fsck.k9.fragment
import android.content.Context
import android.database.Cursor
import android.database.MatrixCursor
import android.text.Spannable
import android.text.style.AbsoluteSizeSpan
import android.view.ContextThemeWrapper
@ -17,49 +15,31 @@ import com.fsck.k9.Account
import com.fsck.k9.FontSizes
import com.fsck.k9.FontSizes.FONT_DEFAULT
import com.fsck.k9.FontSizes.LARGE
import com.fsck.k9.Preferences
import com.fsck.k9.RobolectricTest
import com.fsck.k9.contacts.ContactPictureLoader
import com.fsck.k9.helper.MessageHelper
import com.fsck.k9.mail.Address
import com.fsck.k9.provider.EmailProvider.MessageColumns
import com.fsck.k9.provider.EmailProvider.SpecialColumns
import com.fsck.k9.provider.EmailProvider.ThreadColumns
import com.fsck.k9.textString
import com.fsck.k9.ui.ContactBadge
import com.fsck.k9.ui.R
import com.fsck.k9.ui.messagelist.MessageListAppearance
import com.fsck.k9.ui.messagelist.MessageListExtractor
import com.nhaarman.mockito_kotlin.anyArray
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.eq
import com.fsck.k9.ui.messagelist.MessageListItem
import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.whenever
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Ignore
import org.junit.Test
import org.mockito.AdditionalMatchers.aryEq
import org.robolectric.RuntimeEnvironment
private const val SOME_ACCOUNT_UUID = "6b84207b-25de-4dab-97c3-953bbf03fec6"
private const val DISPLAY_NAME = "Display Name"
private const val FIRST_LINE_DEFAULT_FONT_SIZE = 18f
private const val SECOND_LINE_DEFAULT_FONT_SIZE = 14f
private const val DATE_DEFAULT_FONT_SIZE = 14f
class MessageListAdapterTest : RobolectricTest() {
val context: Context = ContextThemeWrapper(RuntimeEnvironment.application, R.style.Theme_K9_Light)
val testAccount = Account(SOME_ACCOUNT_UUID)
val messageHelper: MessageHelper = mock {
on { getDisplayName(eq(testAccount), anyArray(), anyArray()) } doReturn DISPLAY_NAME
}
val preferences: Preferences = mock {
on { getAccount(SOME_ACCOUNT_UUID) } doReturn testAccount
}
val contactsPictureLoader: ContactPictureLoader = mock()
val listItemListener: MessageListItemActionListener = mock()
@ -100,21 +80,21 @@ class MessageListAdapterTest : RobolectricTest() {
}
@Test
fun withStarsAndFlaggedMessage_shouldCheckStarCheckBox() {
fun withStarsAndStarredMessage_shouldCheckStarCheckBox() {
val adapter = createAdapter(stars = true)
val cursor = createCursor(flagged = 1)
val messageListItem = createMessageListItem(isStarred = true)
val view = adapter.createAndBindView(cursor)
val view = adapter.createAndBindView(messageListItem)
assertTrue(view.starView.isChecked)
}
@Test
fun withStarsAndUnflaggedMessage_shouldNotCheckStarCheckBox() {
fun withStarsAndUnstarredMessage_shouldNotCheckStarCheckBox() {
val adapter = createAdapter(stars = true)
val cursor = createCursor(flagged = 0)
val messageListItem = createMessageListItem(isStarred = false)
val view = adapter.createAndBindView(cursor)
val view = adapter.createAndBindView(messageListItem)
assertFalse(view.starView.isChecked)
}
@ -140,9 +120,9 @@ class MessageListAdapterTest : RobolectricTest() {
@Test
fun withThreadCountOne_shouldHideThreadCountView() {
val adapter = createAdapter()
val cursor = createCursor(threadCount = 1)
val messageListItem = createMessageListItem(threadCount = 1)
val view = adapter.createAndBindView(cursor)
val view = adapter.createAndBindView(messageListItem)
assertTrue(view.threadCountView.isGone)
}
@ -150,9 +130,9 @@ class MessageListAdapterTest : RobolectricTest() {
@Test
fun withThreadCountGreaterOne_shouldShowThreadCountViewWithExpectedValue() {
val adapter = createAdapter()
val cursor = createCursor(threadCount = 13)
val messageListItem = createMessageListItem(threadCount = 13)
val view = adapter.createAndBindView(cursor)
val view = adapter.createAndBindView(messageListItem)
assertTrue(view.threadCountView.isVisible)
assertEquals("13", view.threadCountView.textString)
@ -161,9 +141,9 @@ class MessageListAdapterTest : RobolectricTest() {
@Test
fun withoutSenderAboveSubject_shouldShowSubjectInFirstLine() {
val adapter = createAdapter(senderAboveSubject = false)
val cursor = createCursor(subject = "Subject")
val messageListItem = createMessageListItem(subject = "Subject")
val view = adapter.createAndBindView(cursor)
val view = adapter.createAndBindView(messageListItem)
assertEquals("Subject", view.firstLineView.textString)
}
@ -171,37 +151,39 @@ class MessageListAdapterTest : RobolectricTest() {
@Test
fun withSenderAboveSubject_shouldShowDisplayNameInFirstLine() {
val adapter = createAdapter(senderAboveSubject = true)
val messageListItem = createMessageListItem(displayName = "Display Name")
val view = adapter.createAndBindView()
val view = adapter.createAndBindView(messageListItem)
assertEquals(DISPLAY_NAME, view.firstLineView.textString)
assertEquals("Display Name", view.firstLineView.textString)
}
@Test
fun withoutSenderAboveSubjectAndZeroPreviewLines_shouldShowDisplayNameInSecondLine() {
val adapter = createAdapter(senderAboveSubject = false, previewLines = 0)
val messageListItem = createMessageListItem(displayName = "Display Name")
val view = adapter.createAndBindView()
val view = adapter.createAndBindView(messageListItem)
assertEquals(DISPLAY_NAME, view.secondLineView.textString)
assertEquals("Display Name", view.secondLineView.textString)
}
@Test
fun withoutSenderAboveSubjectAndPreviewLines_shouldShowDisplayNameAndPreviewInSecondLine() {
val adapter = createAdapter(senderAboveSubject = false, previewLines = 1)
val cursor = createCursor(preview = "Preview")
val messageListItem = createMessageListItem(displayName = "Display Name", previewText = "Preview")
val view = adapter.createAndBindView(cursor)
val view = adapter.createAndBindView(messageListItem)
assertEquals(secondLine(DISPLAY_NAME, "Preview"), view.secondLineView.textString)
assertEquals(secondLine("Display Name", "Preview"), view.secondLineView.textString)
}
@Test
fun withSenderAboveSubjectAndZeroPreviewLines_shouldShowSubjectInSecondLine() {
val adapter = createAdapter(senderAboveSubject = true, previewLines = 0)
val cursor = createCursor(subject = "Subject")
val messageListItem = createMessageListItem(subject = "Subject")
val view = adapter.createAndBindView(cursor)
val view = adapter.createAndBindView(messageListItem)
assertEquals("Subject", view.secondLineView.textString)
}
@ -209,9 +191,9 @@ class MessageListAdapterTest : RobolectricTest() {
@Test
fun withSenderAboveSubjectAndPreviewLines_shouldShowSubjectAndPreviewInSecondLine() {
val adapter = createAdapter(senderAboveSubject = true, previewLines = 1)
val cursor = createCursor(subject = "Subject", preview = "Preview")
val messageListItem = createMessageListItem(subject = "Subject", previewText = "Preview")
val view = adapter.createAndBindView(cursor)
val view = adapter.createAndBindView(messageListItem)
assertEquals(secondLine("Subject", "Preview"), view.secondLineView.textString)
}
@ -220,10 +202,9 @@ class MessageListAdapterTest : RobolectricTest() {
@Ignore("Currently failing. See issue #4152.")
fun withSenderAboveSubjectAndMessageToMe_shouldDisplayIndicatorInFirstLine() {
val adapter = createAdapter(senderAboveSubject = true)
val cursor = createCursor(to = "to@domain.example")
configureMessageHelperMockToMe("to@domain.example")
val messageListItem = createMessageListItem(toMe = true)
val view = adapter.createAndBindView(cursor)
val view = adapter.createAndBindView(messageListItem)
assertTrue(view.firstLineView.containsToMeIndicator())
}
@ -232,10 +213,9 @@ class MessageListAdapterTest : RobolectricTest() {
@Ignore("Currently failing. See issue #4152.")
fun withSenderAboveSubjectAndMessageCcMe_shouldDisplayIndicatorInFirstLine() {
val adapter = createAdapter(senderAboveSubject = true)
val cursor = createCursor(cc = "cc@domain.example")
configureMessageHelperMockToMe("cc@domain.example")
val messageListItem = createMessageListItem(ccMe = true)
val view = adapter.createAndBindView(cursor)
val view = adapter.createAndBindView(messageListItem)
assertTrue(view.firstLineView.containsCcMeIndicator())
}
@ -243,10 +223,9 @@ class MessageListAdapterTest : RobolectricTest() {
@Test
fun withoutSenderAboveSubjectAndMessageToMe_shouldDisplayIndicatorInSecondLine() {
val adapter = createAdapter(senderAboveSubject = false)
val cursor = createCursor(to = "to@domain.example")
configureMessageHelperMockToMe("to@domain.example")
val messageListItem = createMessageListItem(toMe = true)
val view = adapter.createAndBindView(cursor)
val view = adapter.createAndBindView(messageListItem)
assertTrue(view.secondLineView.containsToMeIndicator())
}
@ -254,30 +233,29 @@ class MessageListAdapterTest : RobolectricTest() {
@Test
fun withoutSenderAboveSubjectAndMessageCcMe_shouldDisplayIndicatorInSecondLine() {
val adapter = createAdapter(senderAboveSubject = false)
val cursor = createCursor(cc = "cc@domain.example")
configureMessageHelperMockToMe("cc@domain.example")
val messageListItem = createMessageListItem(ccMe = true)
val view = adapter.createAndBindView(cursor)
val view = adapter.createAndBindView(messageListItem)
assertTrue(view.secondLineView.containsCcMeIndicator())
}
@Test
fun withAttachmentCountZero_shouldHideAttachmentCountView() {
fun withoutAttachments_shouldHideAttachmentCountView() {
val adapter = createAdapter()
val cursor = createCursor(attachmentCount = 0)
val messageListItem = createMessageListItem(hasAttachments = false)
val view = adapter.createAndBindView(cursor)
val view = adapter.createAndBindView(messageListItem)
assertTrue(view.attachmentCountView.isGone)
}
@Test
fun withNonZeroAttachmentCount_shouldShowAttachmentCountView() {
fun withAttachments_shouldShowAttachmentCountView() {
val adapter = createAdapter()
val cursor = createCursor(attachmentCount = 3)
val messageListItem = createMessageListItem(hasAttachments = true)
val view = adapter.createAndBindView(cursor)
val view = adapter.createAndBindView(messageListItem)
assertTrue(view.attachmentCountView.isVisible)
}
@ -420,11 +398,6 @@ class MessageListAdapterTest : RobolectricTest() {
assertEquals(22f, view.secondLineView.textSize)
}
fun configureMessageHelperMockToMe(address: String) {
val addresses = Address.parse(address)
whenever(messageHelper.toMe(eq(testAccount), aryEq(addresses))).thenReturn(true)
}
fun createFontSizes(
subject: Int = FONT_DEFAULT,
sender: Int = FONT_DEFAULT,
@ -466,66 +439,66 @@ class MessageListAdapterTest : RobolectricTest() {
res = context.resources,
layoutInflater = LayoutInflater.from(context),
contactsPictureLoader = contactsPictureLoader,
messageListExtractor = MessageListExtractor(preferences, messageHelper),
listItemListener = listItemListener,
appearance = appearance
)
}
fun createCursor(
id: Long = 0L,
uid: String = "irrelevant",
internalDate: Long = 0L,
fun createMessageListItem(
position: Int = 0,
account: Account = Account(SOME_ACCOUNT_UUID),
subject: String = "irrelevant",
date: Long = 0L,
sender: String = "irrelevant@domain.example",
to: String = "irrelevant@domain.example",
cc: String = "irrelevant@domain.example",
read: Int = 0,
flagged: Int = 0,
answered: Int = 0,
forwarded: Int = 0,
attachmentCount: Int = 0,
folderId: String = "irrelevant",
previewType: String = "text",
preview: String = "irrelevant",
threadRoot: Long = 0L,
accountUuid: String = SOME_ACCOUNT_UUID,
threadCount: Int = 0,
messageDate: Long = 0L,
displayName: CharSequence = "irrelevant",
counterPartyAddress: Address? = Address.parse("irrelevant@domain.example").first(),
fromMe: Boolean = false,
toMe: Boolean = false,
ccMe: Boolean = false,
previewText: String = "irrelevant",
isMessageEncrypted: Boolean = false,
isRead: Boolean = false,
isStarred: Boolean = false,
isAnswered: Boolean = false,
isForwarded: Boolean = false,
hasAttachments: Boolean = false,
uniqueId: Long = 0L,
folderServerId: String = "irrelevant",
threadCount: Int = 0
): Cursor {
val mapping = mapOf(
MessageColumns.ID to id,
MessageColumns.UID to uid,
MessageColumns.INTERNAL_DATE to internalDate,
MessageColumns.SUBJECT to subject,
MessageColumns.DATE to date,
MessageColumns.SENDER_LIST to Address.pack(Address.parse(sender)),
MessageColumns.TO_LIST to Address.pack(Address.parse(to)),
MessageColumns.CC_LIST to Address.pack(Address.parse(cc)),
MessageColumns.READ to read,
MessageColumns.FLAGGED to flagged,
MessageColumns.ANSWERED to answered,
MessageColumns.FORWARDED to forwarded,
MessageColumns.ATTACHMENT_COUNT to attachmentCount,
MessageColumns.FOLDER_ID to folderId,
MessageColumns.PREVIEW_TYPE to previewType,
MessageColumns.PREVIEW to preview,
ThreadColumns.ROOT to threadRoot,
SpecialColumns.ACCOUNT_UUID to accountUuid,
SpecialColumns.FOLDER_SERVER_ID to folderServerId,
SpecialColumns.THREAD_COUNT to threadCount
messageUid: String = "irrelevant",
databaseId: Long = 0L,
senderAddress: String? = null,
threadRoot: Long = 0L
): MessageListItem {
return MessageListItem(
position,
account,
subject,
threadCount,
messageDate,
displayName,
counterPartyAddress,
fromMe,
toMe,
ccMe,
previewText,
isMessageEncrypted,
isRead,
isStarred,
isAnswered,
isForwarded,
hasAttachments,
uniqueId,
folderServerId,
messageUid,
databaseId,
senderAddress,
threadRoot
)
return MatrixCursor(mapping.keys.toTypedArray())
.apply { addRow(mapping.values.toTypedArray()) }
.also { it.moveToFirst() }
}
fun MessageListAdapter.createAndBindView(cursor: Cursor = createCursor()): View {
val view = newView(context, cursor, LinearLayout(context))
bindView(view, context, cursor)
return view
fun MessageListAdapter.createAndBindView(item: MessageListItem = createMessageListItem()): View {
messages = listOf(item)
return getView(0, null, LinearLayout(context))
}
fun secondLine(senderOrSubject: String, preview: String) = "$senderOrSubject $preview"