Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8a73c931 authored by cketti's avatar cketti
Browse files

Move code to select message list items to `MessageListAdapter`

parent cc9f2fa8
Loading
Loading
Loading
Loading
+40 −0
Original line number Diff line number Diff line
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)
+74 −0
Original line number Diff line number Diff line
@@ -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) {
@@ -302,6 +322,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 {
+87 −175
Original line number Diff line number Diff line
@@ -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) {
@@ -416,7 +412,7 @@ class MessageListFragment :
    }

    private fun handleListItemClick(position: Int) {
        if (selectedCount > 0) {
        if (adapter.selectedCount > 0) {
            toggleMessageSelect(position)
        } else {
            val adapterPosition = listViewToAdapterPosition(position)
@@ -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,
@@ -850,24 +846,13 @@ class MessageListFragment :
        holder.main.text = text
    }

    private fun setSelectionState(selected: Boolean) {
        if (selected) {
            if (adapter.count == 0) {
    private fun selectAll() {
        if (adapter.messages.isEmpty()) {
            // 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++
                }
            }
        adapter.selectAll()

        if (actionMode == null) {
            startAndPrepareActionMode()
@@ -875,16 +860,6 @@ class MessageListFragment :

        computeBatchDirection()
        updateActionMode()
            computeSelectAllVisibility()
        } else {
            this.selected.clear()
            selectedCount = 0

            actionMode?.finish()
            actionMode = null
        }

        adapter.notifyDataSetChanged()
    }

    private fun toggleMessageSelect(listViewPosition: Int) {
@@ -896,43 +871,20 @@ class MessageListFragment :
    }

    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
            }
        }
        adapter.toggleSelection(messageListItem)

        if (actionMode != null) {
            if (selected && selectedCount - selectedCountDelta == 0) {
        if (adapter.selectedCount == 0) {
            actionMode?.finish()
            actionMode = null
            return
        }
        } else {
            startAndPrepareActionMode()
        }

        if (selected) {
            selectedCount -= selectedCountDelta
        } else {
            selectedCount += selectedCountDelta
        if (actionMode == null) {
            startAndPrepareActionMode()
        }

        computeBatchDirection()
        updateActionMode()
        computeSelectAllVisibility()

        adapter.notifyDataSetChanged()
    }

    override fun onToggleMessageSelection(item: MessageListItem) {
@@ -945,36 +897,19 @@ class MessageListFragment :

    private fun updateActionMode() {
        val actionMode = actionMode ?: error("actionMode == null")
        actionMode.title = getString(R.string.actionbar_selected, selectedCount)
        actionMode.invalidate()
    }
        actionMode.title = getString(R.string.actionbar_selected, adapter.selectedCount)
        actionModeCallback.showSelectAll(!adapter.isAllSelected)

    private fun computeSelectAllVisibility() {
        actionModeCallback.showSelectAll(selected.size != adapter.count)
        actionMode.invalidate()
    }

    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,16 +926,14 @@ 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)

            if (showingThreadedList && messageListItem.threadCount > 1) {
@@ -1011,7 +944,6 @@ class MessageListFragment :
                messageIdList.add(messageListItem.databaseId)
            }
        }
        }

        for (account in accounts) {
            messageMap[account]?.let { messageIds ->
@@ -1276,10 +1208,6 @@ class MessageListFragment :
        super.onStop()
    }

    fun selectAll() {
        setSelectionState(true)
    }

    fun onMoveUp() {
        var currentPosition = listView.selectedItemPosition
        if (currentPosition == AdapterView.INVALID_POSITION || listView.isInTouchMode) {
@@ -1346,14 +1274,8 @@ class MessageListFragment :
            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 ->
@@ -1484,14 +1406,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 +1429,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 +1442,6 @@ class MessageListFragment :
            startAndPrepareActionMode()
        }

        recalculateSelectionCount()
        updateActionMode()
    }

@@ -1537,18 +1450,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
    }
@@ -1846,12 +1747,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 +1757,7 @@ class MessageListFragment :
            flag = null
            unflag = null

            setSelectionState(false)
            adapter.clearSelected()
        }

        override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
@@ -1942,42 +1838,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
                }
                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()
                    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.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()
            }