Merge pull request #6287 from thundernest/move_selection_to_adapter

Move code to select message list items to `MessageListAdapter`
This commit is contained in:
cketti 2022-09-12 14:06:35 +02:00 committed by GitHub
commit ed88e505b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 245 additions and 282 deletions

View file

@ -0,0 +1,40 @@
package com.fsck.k9.helper
/**
* Returns a [Set] containing the results of applying the given [transform] function to each element in the original
* collection.
*
* If you know the size of the output or can make an educated guess, specify [expectedSize] as an optimization.
* The initial capacity of the `Set` will be derived from this value.
*/
inline fun <T, R> Iterable<T>.mapToSet(expectedSize: Int? = null, transform: (T) -> R): Set<R> {
return if (expectedSize != null) {
mapTo(LinkedHashSet(setCapacity(expectedSize)), transform)
} else {
mapTo(mutableSetOf(), transform)
}
}
/**
* Returns a [Set] containing the results of applying the given [transform] function to each element in the original
* collection.
*
* The size of the output is expected to be equal to the size of the input. If that's not the case, please use
* [mapToSet] instead.
*/
inline fun <T, R> Collection<T>.mapCollectionToSet(transform: (T) -> R): Set<R> {
return mapToSet(expectedSize = size, transform)
}
// A copy of Kotlin's internal mapCapacity() for the JVM
fun setCapacity(expectedSize: Int): Int = when {
// We are not coercing the value to a valid one and not throwing an exception. It is up to the caller to
// properly handle negative values.
expectedSize < 0 -> expectedSize
expectedSize < 3 -> expectedSize + 1
expectedSize < INT_MAX_POWER_OF_TWO -> ((expectedSize / 0.75F) + 1.0F).toInt()
// any large value
else -> Int.MAX_VALUE
}
private const val INT_MAX_POWER_OF_TWO: Int = 1 shl (Int.SIZE_BITS - 2)

View file

@ -56,6 +56,12 @@ class MessageListAdapter internal constructor(
set(value) {
field = value
messagesMap = value.associateBy { it.uniqueId }
if (selected.isNotEmpty()) {
val uniqueIds = messagesMap.keys
selected = selected.intersect(uniqueIds)
}
notifyDataSetChanged()
}
@ -64,6 +70,20 @@ class MessageListAdapter internal constructor(
var activeMessage: MessageReference? = null
var selected: Set<Long> = emptySet()
private set(value) {
field = value
selectedCount = calculateSelectionCount()
notifyDataSetChanged()
}
val selectedMessages: List<MessageListItem>
get() = selected.map { messagesMap[it]!! }
val isAllSelected: Boolean
get() = selected.isNotEmpty() && selected.size == messages.size
var selectedCount: Int = 0
private set
private inline val subjectViewFontSize: Int
get() = if (appearance.senderAboveSubject) {
@ -103,6 +123,18 @@ class MessageListAdapter internal constructor(
return messagesMap[uniqueId]!!
}
fun getItem(messageReference: MessageReference): MessageListItem? {
return messages.firstOrNull {
it.account.uuid == messageReference.accountUuid &&
it.folderId == messageReference.folderId &&
it.messageUid == messageReference.uid
}
}
fun getPosition(messageListItem: MessageListItem): Int? {
return messages.indexOf(messageListItem).takeIf { it != -1 }
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val message = getItem(position)
val view: View = convertView ?: newView(parent)
@ -302,6 +334,60 @@ class MessageListAdapter internal constructor(
item.folderId == activeMessage.folderId &&
item.messageUid == activeMessage.uid
}
fun toggleSelection(item: MessageListItem) {
if (messagesMap[item.uniqueId] == null) {
// MessageListItem is no longer in the list
return
}
if (item.uniqueId in selected) {
deselectMessage(item)
} else {
selectMessage(item)
}
}
private fun selectMessage(item: MessageListItem) {
selected = selected + item.uniqueId
}
private fun deselectMessage(item: MessageListItem) {
selected = selected - item.uniqueId
}
fun selectAll() {
val uniqueIds = messagesMap.keys.toSet()
selected = uniqueIds
}
fun clearSelected() {
selected = emptySet()
}
fun restoreSelected(selectedIds: Set<Long>) {
if (selectedIds.isEmpty()) {
clearSelected()
} else {
val uniqueIds = messagesMap.keys
selected = selectedIds.intersect(uniqueIds)
}
}
private fun calculateSelectionCount(): Int {
if (selected.isEmpty()) {
return 0
}
if (!appearance.showingThreadedList) {
return selected.size
}
return messages
.asSequence()
.filter { it.uniqueId in selected }
.sumOf { it.threadCount.coerceAtLeast(1) }
}
}
interface MessageListItemActionListener {

View file

@ -36,6 +36,7 @@ import com.fsck.k9.controller.SimpleMessagingListener
import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener
import com.fsck.k9.fragment.MessageListFragment.MessageListFragmentListener.Companion.MAX_PROGRESS
import com.fsck.k9.helper.Utility
import com.fsck.k9.helper.mapToSet
import com.fsck.k9.mail.Flag
import com.fsck.k9.mail.MessagingException
import com.fsck.k9.search.LocalSearch
@ -52,7 +53,6 @@ import com.fsck.k9.ui.messagelist.MessageListInfo
import com.fsck.k9.ui.messagelist.MessageListItem
import com.fsck.k9.ui.messagelist.MessageListViewModel
import com.fsck.k9.ui.messagelist.MessageSortOverride
import java.util.HashSet
import java.util.concurrent.Future
import net.jcip.annotations.GuardedBy
import org.koin.android.ext.android.inject
@ -99,8 +99,6 @@ class MessageListFragment :
private var sortType = SortType.SORT_DATE
private var sortAscending = true
private var sortDateAscending = false
private var selectedCount = 0
private var selected: MutableSet<Long> = HashSet()
private var actionMode: ActionMode? = null
private var hasConnectivity: Boolean? = null
@ -112,6 +110,7 @@ class MessageListFragment :
private var showingThreadedList = false
private var isThreadDisplay = false
private var activeMessage: MessageReference? = null
private var rememberedSelected: Set<Long>? = null
lateinit var localSearch: LocalSearch
private set
@ -180,10 +179,7 @@ class MessageListFragment :
}
private fun restoreSelectedMessages(savedInstanceState: Bundle) {
val selectedIds = savedInstanceState.getLongArray(STATE_SELECTED_MESSAGES) ?: return
for (id in selectedIds) {
selected.add(id)
}
rememberedSelected = savedInstanceState.getLongArray(STATE_SELECTED_MESSAGES)?.toSet()
}
fun restoreListState(savedListState: Parcelable) {
@ -380,7 +376,8 @@ class MessageListFragment :
if (view === footerView) {
handleFooterClick()
} else {
handleListItemClick(position)
val messageListItem = adapter.getItem(position)
handleListItemClick(messageListItem)
}
}
@ -415,17 +412,14 @@ class MessageListFragment :
}
}
private fun handleListItemClick(position: Int) {
if (selectedCount > 0) {
toggleMessageSelect(position)
private fun handleListItemClick(messageListItem: MessageListItem) {
if (adapter.selectedCount > 0) {
toggleMessageSelect(messageListItem)
} else {
val adapterPosition = listViewToAdapterPosition(position)
val messageListItem = adapter.getItem(adapterPosition)
if (showingThreadedList && messageListItem.threadCount > 1) {
fragmentListener.showThread(messageListItem.account, messageListItem.threadRoot)
} else {
openMessageAtPosition(adapterPosition)
openMessage(messageListItem.messageReference)
}
}
}
@ -433,7 +427,9 @@ class MessageListFragment :
override fun onItemLongClick(parent: AdapterView<*>?, view: View, position: Int, id: Long): Boolean {
if (view === footerView) return false
toggleMessageSelect(position)
val messageListItem = adapter.getItem(position)
toggleMessageSelect(messageListItem)
return true
}
@ -450,7 +446,7 @@ class MessageListFragment :
super.onSaveInstanceState(outState)
saveListState(outState)
outState.putLongArray(STATE_SELECTED_MESSAGES, selected.toLongArray())
outState.putLongArray(STATE_SELECTED_MESSAGES, adapter.selected.toLongArray())
outState.putBoolean(STATE_REMOTE_SEARCH_PERFORMED, isRemoteSearch)
outState.putStringArray(
STATE_ACTIVE_MESSAGES,
@ -793,14 +789,6 @@ class MessageListFragment :
messagingController.sendPendingMessages(account, null)
}
private fun listViewToAdapterPosition(position: Int): Int {
return if (position in 0 until adapter.count) position else AdapterView.INVALID_POSITION
}
private fun adapterToListViewPosition(position: Int): Int {
return if (position in 0 until adapter.count) position else AdapterView.INVALID_POSITION
}
private fun getFooterView(parent: ViewGroup?): View? {
return footerView ?: createFooterView(parent).also { footerView = it }
}
@ -850,89 +838,37 @@ class MessageListFragment :
holder.main.text = text
}
private fun setSelectionState(selected: Boolean) {
if (selected) {
if (adapter.count == 0) {
// Nothing to do if there are no messages
return
}
selectedCount = 0
for (i in 0 until adapter.count) {
val messageListItem = adapter.getItem(i)
this.selected.add(messageListItem.uniqueId)
if (showingThreadedList) {
selectedCount += messageListItem.threadCount.coerceAtLeast(1)
} else {
selectedCount++
}
}
if (actionMode == null) {
startAndPrepareActionMode()
}
computeBatchDirection()
updateActionMode()
computeSelectAllVisibility()
} else {
this.selected.clear()
selectedCount = 0
actionMode?.finish()
actionMode = null
private fun selectAll() {
if (adapter.messages.isEmpty()) {
// Nothing to do if there are no messages
return
}
adapter.notifyDataSetChanged()
}
adapter.selectAll()
private fun toggleMessageSelect(listViewPosition: Int) {
val adapterPosition = listViewToAdapterPosition(listViewPosition)
if (adapterPosition == AdapterView.INVALID_POSITION) return
val messageListItem = adapter.getItem(adapterPosition)
toggleMessageSelect(messageListItem)
}
private fun toggleMessageSelect(messageListItem: MessageListItem) {
val uniqueId = messageListItem.uniqueId
val selected = selected.contains(uniqueId)
if (!selected) {
this.selected.add(uniqueId)
} else {
this.selected.remove(uniqueId)
}
var selectedCountDelta = 1
if (showingThreadedList) {
val threadCount = messageListItem.threadCount
if (threadCount > 1) {
selectedCountDelta = threadCount
}
}
if (actionMode != null) {
if (selected && selectedCount - selectedCountDelta == 0) {
actionMode?.finish()
actionMode = null
return
}
} else {
if (actionMode == null) {
startAndPrepareActionMode()
}
if (selected) {
selectedCount -= selectedCountDelta
} else {
selectedCount += selectedCountDelta
}
computeBatchDirection()
updateActionMode()
computeSelectAllVisibility()
}
adapter.notifyDataSetChanged()
private fun toggleMessageSelect(messageListItem: MessageListItem) {
adapter.toggleSelection(messageListItem)
if (adapter.selectedCount == 0) {
actionMode?.finish()
actionMode = null
return
}
if (actionMode == null) {
startAndPrepareActionMode()
}
computeBatchDirection()
updateActionMode()
}
override fun onToggleMessageSelection(item: MessageListItem) {
@ -945,36 +881,19 @@ class MessageListFragment :
private fun updateActionMode() {
val actionMode = actionMode ?: error("actionMode == null")
actionMode.title = getString(R.string.actionbar_selected, selectedCount)
actionMode.title = getString(R.string.actionbar_selected, adapter.selectedCount)
actionModeCallback.showSelectAll(!adapter.isAllSelected)
actionMode.invalidate()
}
private fun computeSelectAllVisibility() {
actionModeCallback.showSelectAll(selected.size != adapter.count)
}
private fun computeBatchDirection() {
var isBatchFlag = false
var isBatchRead = false
for (i in 0 until adapter.count) {
val messageListItem = adapter.getItem(i)
if (selected.contains(messageListItem.uniqueId)) {
if (!messageListItem.isStarred) {
isBatchFlag = true
}
val selectedMessages = adapter.selectedMessages
val notAllRead = !selectedMessages.all { it.isRead }
val notAllStarred = !selectedMessages.all { it.isStarred }
if (!messageListItem.isRead) {
isBatchRead = true
}
if (isBatchFlag && isBatchRead) {
break
}
}
}
actionModeCallback.showMarkAsRead(isBatchRead)
actionModeCallback.showFlag(isBatchFlag)
actionModeCallback.showMarkAsRead(notAllRead)
actionModeCallback.showFlag(notAllStarred)
}
private fun setFlag(messageListItem: MessageListItem, flag: Flag, newState: Boolean) {
@ -991,25 +910,22 @@ class MessageListFragment :
}
private fun setFlagForSelected(flag: Flag, newState: Boolean) {
if (selected.isEmpty()) return
if (adapter.selected.isEmpty()) return
val messageMap: MutableMap<Account, MutableList<Long>> = mutableMapOf()
val threadMap: MutableMap<Account, MutableList<Long>> = mutableMapOf()
val accounts: MutableSet<Account> = mutableSetOf()
val messageMap = mutableMapOf<Account, MutableList<Long>>()
val threadMap = mutableMapOf<Account, MutableList<Long>>()
val accounts = mutableSetOf<Account>()
for (position in 0 until adapter.count) {
val messageListItem = adapter.getItem(position)
for (messageListItem in adapter.selectedMessages) {
val account = messageListItem.account
if (messageListItem.uniqueId in selected) {
accounts.add(account)
accounts.add(account)
if (showingThreadedList && messageListItem.threadCount > 1) {
val threadRootIdList = threadMap.getOrPut(account) { mutableListOf() }
threadRootIdList.add(messageListItem.threadRoot)
} else {
val messageIdList = messageMap.getOrPut(account) { mutableListOf() }
messageIdList.add(messageListItem.databaseId)
}
if (showingThreadedList && messageListItem.threadCount > 1) {
val threadRootIdList = threadMap.getOrPut(account) { mutableListOf() }
threadRootIdList.add(messageListItem.threadRoot)
} else {
val messageIdList = messageMap.getOrPut(account) { mutableListOf() }
messageIdList.add(messageListItem.databaseId)
}
}
@ -1276,10 +1192,6 @@ class MessageListFragment :
super.onStop()
}
fun selectAll() {
setSelectionState(true)
}
fun onMoveUp() {
var currentPosition = listView.selectedItemPosition
if (currentPosition == AdapterView.INVALID_POSITION || listView.isInTouchMode) {
@ -1302,28 +1214,6 @@ class MessageListFragment :
}
}
private fun getReferenceForPosition(position: Int): MessageReference {
val item = adapter.getItem(position)
return MessageReference(item.account.uuid, item.folderId, item.messageUid)
}
private fun openMessageAtPosition(position: Int) {
// Scroll message into view if necessary
val listViewPosition = adapterToListViewPosition(position)
if (listViewPosition != AdapterView.INVALID_POSITION &&
(listViewPosition < listView.firstVisiblePosition || listViewPosition > listView.lastVisiblePosition)
) {
listView.setSelection(listViewPosition)
}
val messageReference = getReferenceForPosition(position)
// For some reason the listView.setSelection() above won't do anything when we call
// onOpenMessage() (and consequently adapter.notifyDataSetChanged()) right away. So we
// defer the call using MessageListHandler.
handler.openMessage(messageReference)
}
fun openMessage(messageReference: MessageReference) {
fragmentListener.openMessage(messageReference)
}
@ -1333,27 +1223,16 @@ class MessageListFragment :
}
private val selectedMessage: MessageReference?
get() = selectedMessageListItem?.messageReference
private val selectedMessageListItem: MessageListItem?
get() {
val listViewPosition = listView.selectedItemPosition
val adapterPosition = listViewToAdapterPosition(listViewPosition)
if (adapterPosition == AdapterView.INVALID_POSITION) return null
return getReferenceForPosition(adapterPosition)
val position = listView.selectedItemPosition
return if (position !in 0 until adapter.count) null else adapter.getItem(position)
}
private val adapterPositionForSelectedMessage: Int
get() {
val listViewPosition = listView.selectedItemPosition
return listViewToAdapterPosition(listViewPosition)
}
private val checkedMessages: List<MessageReference>
get() {
return adapter.messages
.asSequence()
.filter { it.uniqueId in selected }
.map { MessageReference(it.account.uuid, it.folderId, it.messageUid) }
.toList()
}
private val selectedMessages: List<MessageReference>
get() = adapter.selectedMessages.map { it.messageReference }
fun onDelete() {
selectedMessage?.let { message ->
@ -1362,29 +1241,21 @@ class MessageListFragment :
}
fun toggleMessageSelect() {
toggleMessageSelect(listView.selectedItemPosition)
selectedMessageListItem?.let { messageListItem ->
toggleMessageSelect(messageListItem)
}
}
fun onToggleFlagged() {
toggleFlag(Flag.FLAGGED)
selectedMessageListItem?.let { messageListItem ->
setFlag(messageListItem, Flag.FLAGGED, !messageListItem.isStarred)
}
}
fun onToggleRead() {
toggleFlag(Flag.SEEN)
}
private fun toggleFlag(flag: Flag) {
val adapterPosition = adapterPositionForSelectedMessage
if (adapterPosition == ListView.INVALID_POSITION) return
val messageListItem = adapter.getItem(adapterPosition)
val flagState = when (flag) {
Flag.SEEN -> messageListItem.isRead
Flag.FLAGGED -> messageListItem.isStarred
else -> false
selectedMessageListItem?.let { messageListItem ->
setFlag(messageListItem, Flag.SEEN, !messageListItem.isRead)
}
setFlag(messageListItem, flag, !flagState)
}
fun onMove() {
@ -1484,14 +1355,15 @@ class MessageListFragment :
}
}
cleanupSelected(messageListItems)
adapter.selected = selected
adapter.messages = messageListItems
rememberedSelected?.let {
rememberedSelected = null
adapter.restoreSelected(it)
}
resetActionMode()
computeBatchDirection()
computeSelectAllVisibility()
if (savedListState != null) {
handler.restoreListPosition(savedListState)
@ -1506,19 +1378,10 @@ class MessageListFragment :
}
}
private fun cleanupSelected(messageListItems: List<MessageListItem>) {
if (selected.isEmpty()) return
selected = messageListItems.asSequence()
.map { it.uniqueId }
.filter { it in selected }
.toMutableSet()
}
private fun resetActionMode() {
if (!isResumed) return
if (!isActive || selected.isEmpty()) {
if (!isActive || adapter.selected.isEmpty()) {
actionMode?.finish()
actionMode = null
return
@ -1528,7 +1391,6 @@ class MessageListFragment :
startAndPrepareActionMode()
}
recalculateSelectionCount()
updateActionMode()
}
@ -1537,18 +1399,6 @@ class MessageListFragment :
actionMode?.invalidate()
}
private fun recalculateSelectionCount() {
if (!showingThreadedList) {
selectedCount = selected.size
return
}
selectedCount = adapter.messages
.asSequence()
.filter { it.uniqueId in selected }
.sumOf { it.threadCount.coerceAtLeast(1) }
}
fun remoteSearchFinished() {
remoteSearchFuture = null
}
@ -1589,8 +1439,7 @@ class MessageListFragment :
if (sortType != SortType.SORT_UNREAD && sortType != SortType.SORT_FLAGGED) return
val position = getPosition(messageReference)
val messageListItem = adapter.getItem(position)
val messageListItem = adapter.getItem(messageReference) ?: return
val existingEntry = messageSortOverrides.firstOrNull { it.first == messageReference }
if (existingEntry != null) {
@ -1607,20 +1456,11 @@ class MessageListFragment :
}
private fun scrollToMessage(messageReference: MessageReference) {
val position = getPosition(messageReference)
val viewPosition = adapterToListViewPosition(position)
if (viewPosition != AdapterView.INVALID_POSITION &&
(viewPosition <= listView.firstVisiblePosition || viewPosition >= listView.lastVisiblePosition)
) {
listView.smoothScrollToPosition(viewPosition)
}
}
val messageListItem = adapter.getItem(messageReference) ?: return
val position = adapter.getPosition(messageListItem) ?: return
private fun getPosition(messageReference: MessageReference): Int {
return adapter.messages.indexOfFirst { messageListItem ->
messageListItem.account.uuid == messageReference.accountUuid &&
messageListItem.folderId == messageReference.folderId &&
messageListItem.messageUid == messageReference.uid
if (position <= listView.firstVisiblePosition || position >= listView.lastVisiblePosition) {
listView.smoothScrollToPosition(position)
}
}
@ -1846,12 +1686,7 @@ class MessageListFragment :
}
private val accountUuidsForSelected: Set<String>
get() {
return adapter.messages.asSequence()
.filter { it.uniqueId in selected }
.map { it.account.uuid }
.toSet()
}
get() = adapter.selectedMessages.mapToSet { it.account.uuid }
override fun onDestroyActionMode(mode: ActionMode) {
actionMode = null
@ -1861,7 +1696,7 @@ class MessageListFragment :
flag = null
unflag = null
setSelectionState(false)
adapter.clearSelected()
}
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
@ -1942,42 +1777,58 @@ class MessageListFragment :
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
// In the following we assume that we can't move or copy mails to the same folder. Also that spam isn't
// available if we are in the spam folder, same for archive.
when (item.itemId) {
val endSelectionMode = when (item.itemId) {
R.id.delete -> {
val messages = checkedMessages
onDelete(messages)
selectedCount = 0
onDelete(selectedMessages)
true
}
R.id.mark_as_read -> {
setFlagForSelected(Flag.SEEN, true)
false
}
R.id.mark_as_unread -> {
setFlagForSelected(Flag.SEEN, false)
false
}
R.id.flag -> {
setFlagForSelected(Flag.FLAGGED, true)
false
}
R.id.unflag -> {
setFlagForSelected(Flag.FLAGGED, false)
false
}
R.id.select_all -> {
selectAll()
false
}
R.id.mark_as_read -> setFlagForSelected(Flag.SEEN, true)
R.id.mark_as_unread -> setFlagForSelected(Flag.SEEN, false)
R.id.flag -> setFlagForSelected(Flag.FLAGGED, true)
R.id.unflag -> setFlagForSelected(Flag.FLAGGED, false)
R.id.select_all -> selectAll()
R.id.archive -> {
onArchive(checkedMessages)
onArchive(selectedMessages)
// TODO: Only finish action mode if all messages have been moved.
selectedCount = 0
true
}
R.id.spam -> {
onSpam(checkedMessages)
onSpam(selectedMessages)
// TODO: Only finish action mode if all messages have been moved.
selectedCount = 0
true
}
R.id.move -> {
onMove(checkedMessages)
selectedCount = 0
onMove(selectedMessages)
true
}
R.id.move_to_drafts -> {
onMoveToDraftsFolder(checkedMessages)
selectedCount = 0
onMoveToDraftsFolder(selectedMessages)
true
}
R.id.copy -> {
onCopy(checkedMessages)
selectedCount = 0
onCopy(selectedMessages)
true
}
else -> return false
}
if (selectedCount == 0) {
if (endSelectionMode) {
mode.finish()
}

View file

@ -7,8 +7,6 @@ import android.app.Activity;
import android.os.Handler;
import android.os.Parcelable;
import com.fsck.k9.controller.MessageReference;
/**
* This class is used to run operations that modify UI elements in the UI thread.
*
@ -25,7 +23,6 @@ public class MessageListHandler extends Handler {
private static final int ACTION_REMOTE_SEARCH_FINISHED = 4;
private static final int ACTION_GO_BACK = 5;
private static final int ACTION_RESTORE_LIST_POSITION = 6;
private static final int ACTION_OPEN_MESSAGE = 7;
private WeakReference<MessageListFragment> mFragment;
@ -79,12 +76,6 @@ public class MessageListHandler extends Handler {
}
}
public void openMessage(MessageReference messageReference) {
android.os.Message msg = android.os.Message.obtain(this, ACTION_OPEN_MESSAGE,
messageReference);
sendMessage(msg);
}
@Override
public void handleMessage(android.os.Message msg) {
MessageListFragment fragment = mFragment.get();
@ -131,11 +122,6 @@ public class MessageListHandler extends Handler {
fragment.restoreListState(savedListState);
break;
}
case ACTION_OPEN_MESSAGE: {
MessageReference messageReference = (MessageReference) msg.obj;
fragment.openMessage(messageReference);
break;
}
}
}
}