From df50ff50338356f9ca88a1b42fabfde8d286982c Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 8 Sep 2022 13:01:30 +0200 Subject: [PATCH 01/20] Prepare for version 6.304 --- app/k9mail/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/k9mail/build.gradle b/app/k9mail/build.gradle index 1c93303199..5f64c0aea6 100644 --- a/app/k9mail/build.gradle +++ b/app/k9mail/build.gradle @@ -49,7 +49,7 @@ android { testApplicationId "com.fsck.k9.tests" versionCode 33003 - versionName '6.303' + versionName '6.304-SNAPSHOT' // Keep in sync with the resource string array 'supported_languages' resConfigs "in", "br", "ca", "cs", "cy", "da", "de", "et", "en", "en_GB", "es", "eo", "eu", "fr", "gd", "gl", -- GitLab From f3c3b579fe130c59d531ff4b327dd2041a0e6bfe Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 9 Sep 2022 23:00:15 +0200 Subject: [PATCH 02/20] Reuse existing SearchView when recreating the toolbar menu --- .../java/com/fsck/k9/activity/MessageList.kt | 32 ++++++++++++++++--- .../fsck/k9/fragment/MessageListFragment.kt | 5 ++- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt index 59b24e05ab..71f668075d 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt @@ -17,6 +17,7 @@ import android.view.animation.AnimationUtils import android.widget.ProgressBar import android.widget.Toast import androidx.appcompat.app.ActionBar +import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.Toolbar import androidx.drawerlayout.widget.DrawerLayout @@ -708,7 +709,7 @@ open class MessageList : showMessageList() } } else if (this::searchView.isInitialized && !searchView.isIconified) { - searchView.isIconified = true + collapseSearchView() } else { if (isDrawerEnabled && account != null && supportFragmentManager.backStackEntryCount == 0) { if (K9.isShowUnifiedInbox) { @@ -918,6 +919,7 @@ open class MessageList : if (drawer!!.isOpen) { drawer!!.close() } else { + collapseSearchView() drawer!!.open() } } else { @@ -935,16 +937,28 @@ open class MessageList : override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.message_list_option, menu) - // setup search view val searchItem = menu.findItem(R.id.search) + initializeSearchMenuItem(searchItem) + + return true + } + + private fun initializeSearchMenuItem(searchItem: MenuItem) { + // Reuse existing SearchView if available + if (::searchView.isInitialized) { + searchItem.actionView = searchView + return + } + searchView = searchItem.actionView as SearchView searchView.maxWidth = Int.MAX_VALUE searchView.queryHint = resources.getString(R.string.search_action) - val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager + val searchManager = getSystemService(SEARCH_SERVICE) as SearchManager searchView.setSearchableInfo(searchManager.getSearchableInfo(componentName)) searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { messageListFragment?.onSearchRequested(query) + collapseSearchView() return true } @@ -952,8 +966,11 @@ open class MessageList : return false } }) + } - return true + private fun collapseSearchView() { + searchView.setQuery(null, false) + searchView.isIconified = true } fun setActionBarTitle(title: String, subtitle: String? = null) { @@ -1000,6 +1017,8 @@ open class MessageList : showMessageView() } } + + collapseSearchView() } override fun onForward(messageReference: MessageReference, decryptionResultForReply: Parcelable?) { @@ -1082,6 +1101,11 @@ open class MessageList : return true } + override fun startSupportActionMode(callback: ActionMode.Callback): ActionMode? { + collapseSearchView() + return super.startSupportActionMode(callback) + } + override fun showThread(account: Account, threadRootId: Long) { showMessageViewPlaceHolder() diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt index d27ff6752d..69759757b7 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt @@ -17,7 +17,6 @@ import android.widget.AdapterView.OnItemLongClickListener import android.widget.ListView import android.widget.TextView import android.widget.Toast -import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.ActionMode import androidx.core.os.bundleOf import androidx.fragment.app.Fragment @@ -1534,8 +1533,7 @@ class MessageListFragment : } private fun startAndPrepareActionMode() { - val activity = requireActivity() as AppCompatActivity - actionMode = activity.startSupportActionMode(actionModeCallback) + actionMode = fragmentListener.startSupportActionMode(actionModeCallback) actionMode?.invalidate() } @@ -2003,6 +2001,7 @@ class MessageListFragment : fun setMessageListTitle(title: String, subtitle: String?) fun onCompose(account: Account?) fun startSearch(query: String, account: Account?, folderId: Long?): Boolean + fun startSupportActionMode(callback: ActionMode.Callback): ActionMode? fun goBack() fun onFolderNotFoundError() -- GitLab From 60e33e529dc6dbe58628f980045db8fafd167594 Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 9 Sep 2022 23:27:54 +0200 Subject: [PATCH 03/20] Save/restore search view state in `MessageList` --- .../main/java/com/fsck/k9/activity/MessageList.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt index 71f668075d..8d1d39b736 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt @@ -101,6 +101,9 @@ open class MessageList : private lateinit var actionBar: ActionBar private lateinit var searchView: SearchView + private var initialSearchViewQuery: String? = null + private var initialSearchViewIconified: Boolean = true + private var drawer: K9Drawer? = null private var openFolderTransaction: FragmentTransaction? = null private var progressBar: ProgressBar? = null @@ -574,6 +577,8 @@ open class MessageList : outState.putSerializable(STATE_DISPLAY_MODE, displayMode) outState.putBoolean(STATE_MESSAGE_VIEW_ONLY, messageViewOnly) outState.putBoolean(STATE_MESSAGE_LIST_WAS_DISPLAYED, messageListWasDisplayed) + outState.putBoolean(STATE_SEARCH_VIEW_ICONIFIED, searchView.isIconified) + outState.putString(STATE_SEARCH_VIEW_QUERY, searchView.query?.toString()) } public override fun onRestoreInstanceState(savedInstanceState: Bundle) { @@ -581,6 +586,8 @@ open class MessageList : messageViewOnly = savedInstanceState.getBoolean(STATE_MESSAGE_VIEW_ONLY) messageListWasDisplayed = savedInstanceState.getBoolean(STATE_MESSAGE_LIST_WAS_DISPLAYED) + initialSearchViewIconified = savedInstanceState.getBoolean(STATE_SEARCH_VIEW_ICONIFIED) + initialSearchViewQuery = savedInstanceState.getString(STATE_SEARCH_VIEW_QUERY) } private fun initializeActionBar() { @@ -966,6 +973,9 @@ open class MessageList : return false } }) + + searchView.isIconified = initialSearchViewIconified + searchView.setQuery(initialSearchViewQuery, false) } private fun collapseSearchView() { @@ -1415,6 +1425,8 @@ open class MessageList : private const val STATE_DISPLAY_MODE = "displayMode" private const val STATE_MESSAGE_VIEW_ONLY = "messageViewOnly" private const val STATE_MESSAGE_LIST_WAS_DISPLAYED = "messageListWasDisplayed" + private const val STATE_SEARCH_VIEW_ICONIFIED = "searchViewIconified" + private const val STATE_SEARCH_VIEW_QUERY = "searchViewQuery" private const val FIRST_FRAGMENT_TRANSACTION = "first" private const val FRAGMENT_TAG_MESSAGE_VIEW_CONTAINER = "MessageViewContainerFragment" -- GitLab From 95cfc85858ee427ef9d300ee9b3be4810974a77f Mon Sep 17 00:00:00 2001 From: cketti Date: Sat, 10 Sep 2022 00:57:20 +0200 Subject: [PATCH 04/20] Use `MessageStore` to set folder status --- .../com/fsck/k9/controller/MessagingController.java | 13 ++++--------- .../java/com/fsck/k9/mailstore/LocalFolder.java | 9 --------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java b/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java index 03de5e215c..8999a76266 100644 --- a/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java +++ b/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java @@ -640,7 +640,7 @@ public class MessagingController { if (commandException != null && !syncListener.syncFailed) { String rootMessage = getRootCauseMessage(commandException); Timber.e("Root cause failure in %s:%s was '%s'", account, folderServerId, rootMessage); - updateFolderStatus(account, folderServerId, rootMessage); + updateFolderStatus(account, folderId, rootMessage); listener.synchronizeMailboxFailed(account, folderId, rootMessage); } } @@ -655,14 +655,9 @@ public class MessagingController { SYNC_FLAGS); } - private void updateFolderStatus(Account account, String folderServerId, String status) { - try { - LocalStore localStore = localStoreProvider.getInstance(account); - LocalFolder localFolder = localStore.getFolder(folderServerId); - localFolder.setStatus(status); - } catch (MessagingException e) { - Timber.w(e, "Couldn't update folder status for folder %s", folderServerId); - } + private void updateFolderStatus(Account account, long folderId, String status) { + MessageStore messageStore = messageStoreManager.getMessageStore(account); + messageStore.setStatus(folderId, status); } public void handleAuthenticationFailure(Account account, boolean incoming) { diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/LocalFolder.java b/app/core/src/main/java/com/fsck/k9/mailstore/LocalFolder.java index 406b29bcf1..98ba7ba9f3 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/LocalFolder.java +++ b/app/core/src/main/java/com/fsck/k9/mailstore/LocalFolder.java @@ -117,10 +117,6 @@ public class LocalFolder { return lastChecked; } - public String getStatus() { - return status; - } - public long getDatabaseId() { return databaseId; } @@ -297,11 +293,6 @@ public class LocalFolder { } } - public void setStatus(final String status) throws MessagingException { - this.status = status; - updateFolderColumn("status", status); - } - private void updateFolderColumn(final String column, final Object value) throws MessagingException { this.localStore.getDatabase().execute(false, new DbCallback() { @Override -- GitLab From 0591ff7822a917e9414aa339884f221fece27ba4 Mon Sep 17 00:00:00 2001 From: cketti Date: Sat, 10 Sep 2022 01:42:04 +0200 Subject: [PATCH 05/20] Rewrite `MessagingController.loadMessageRemoteSynchronous()` to not use `LocalStore` --- .../k9/controller/MessagingController.java | 50 +++++++------------ 1 file changed, 18 insertions(+), 32 deletions(-) diff --git a/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java b/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java index 8999a76266..5081bbad26 100644 --- a/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java +++ b/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java @@ -1233,51 +1233,37 @@ public class MessagingController { ); } - private void loadMessageRemoteSynchronous(Account account, long folderId, String uid, + private void loadMessageRemoteSynchronous(Account account, long folderId, String messageServerId, MessagingListener listener, boolean loadPartialFromSearch) { try { - LocalStore localStore = localStoreProvider.getInstance(account); - LocalFolder localFolder = localStore.getFolder(folderId); - localFolder.open(); - String folderServerId = localFolder.getServerId(); - - LocalMessage message = localFolder.getMessage(uid); + if (messageServerId.startsWith(K9.LOCAL_UID_PREFIX)) { + throw new IllegalArgumentException("Must not be called with a local UID"); + } - if (uid.startsWith(K9.LOCAL_UID_PREFIX)) { - Timber.w("Message has local UID so cannot download fully."); - // ASH move toast - android.widget.Toast.makeText(context, - "Message has local UID so cannot download fully", - android.widget.Toast.LENGTH_LONG).show(); - // TODO: Using X_DOWNLOADED_FULL is wrong because it's only a partial message. But - // one we can't download completely. Maybe add a new flag; X_PARTIAL_MESSAGE ? - message.setFlag(Flag.X_DOWNLOADED_FULL, true); - message.setFlag(Flag.X_DOWNLOADED_PARTIAL, false); - } else { - Backend backend = getBackend(account); + MessageStore messageStore = messageStoreManager.getMessageStore(account); - if (loadPartialFromSearch) { - SyncConfig syncConfig = createSyncConfig(account); - backend.downloadMessage(syncConfig, folderServerId, uid); - } else { - backend.downloadCompleteMessage(folderServerId, uid); - } + String folderServerId = messageStore.getFolderServerId(folderId); + if (folderServerId == null) { + throw new IllegalStateException("Folder not found (ID: " + folderId + ")"); + } - message = localFolder.getMessage(uid); + Backend backend = getBackend(account); - if (!loadPartialFromSearch) { - message.setFlag(Flag.X_DOWNLOADED_FULL, true); - } + if (loadPartialFromSearch) { + SyncConfig syncConfig = createSyncConfig(account); + backend.downloadMessage(syncConfig, folderServerId, messageServerId); + } else { + backend.downloadCompleteMessage(folderServerId, messageServerId); } - // now that we have the full message, refresh the headers for (MessagingListener l : getListeners(listener)) { - l.loadMessageRemoteFinished(account, folderId, uid); + l.loadMessageRemoteFinished(account, folderId, messageServerId); } } catch (Exception e) { for (MessagingListener l : getListeners(listener)) { - l.loadMessageRemoteFailed(account, folderId, uid, e); + l.loadMessageRemoteFailed(account, folderId, messageServerId, e); } + notifyUserIfCertificateProblem(account, e, true); Timber.e(e, "Error while loading remote message"); } -- GitLab From cd621d047ff00400f7a66545c81fb7ed7aef76b5 Mon Sep 17 00:00:00 2001 From: cketti Date: Sat, 10 Sep 2022 01:53:53 +0200 Subject: [PATCH 06/20] Rewrite `MessagingController.deleteDraft()` to not use `LocalStore` --- .../k9/controller/MessagingController.java | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java b/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java index 5081bbad26..8dea272689 100644 --- a/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java +++ b/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java @@ -1914,25 +1914,18 @@ public class MessagingController { }); } - public void deleteDraft(final Account account, long id) { - try { - Long folderId = account.getDraftsFolderId(); - if (folderId == null) { - Timber.w("No Drafts folder configured. Can't delete draft."); - return; - } - - LocalStore localStore = localStoreProvider.getInstance(account); - LocalFolder localFolder = localStore.getFolder(folderId); - localFolder.open(); - String uid = localFolder.getMessageUidById(id); - if (uid != null) { - MessageReference messageReference = new MessageReference(account.getUuid(), folderId, uid); - deleteMessage(messageReference); - } - } catch (MessagingException me) { - Timber.e(me, "Error deleting draft"); + public void deleteDraft(Account account, long messageId) { + Long folderId = account.getDraftsFolderId(); + if (folderId == null) { + Timber.w("No Drafts folder configured. Can't delete draft."); + return; } + + MessageStore messageStore = messageStoreManager.getMessageStore(account); + String messageServerId = messageStore.getMessageServerId(messageId); + MessageReference messageReference = new MessageReference(account.getUuid(), folderId, messageServerId); + + deleteMessage(messageReference); } public void deleteThreads(final List messages) { -- GitLab From af9c598f317b4ffec68ce2959ee28cafb5fc6ed2 Mon Sep 17 00:00:00 2001 From: cketti Date: Sun, 11 Sep 2022 16:10:43 +0200 Subject: [PATCH 07/20] Change the separator between message view pages --- .../fsck/k9/ui/messageview/MessageViewContainerFragment.kt | 7 +++++-- app/ui/legacy/src/main/res/values/dimensions.xml | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewContainerFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewContainerFragment.kt index 53d7592616..4501c25075 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewContainerFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewContainerFragment.kt @@ -8,8 +8,8 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.DividerItemDecoration import androidx.viewpager2.adapter.FragmentStateAdapter +import androidx.viewpager2.widget.MarginPageTransformer import androidx.viewpager2.widget.ViewPager2 import com.fsck.k9.controller.MessageReference import com.fsck.k9.ui.R @@ -88,10 +88,13 @@ class MessageViewContainerFragment : Fragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.message_view_container, container, false) + val resources = inflater.context.resources + val pageMargin = resources.getDimension(R.dimen.message_view_pager_page_margin).toInt() + viewPager = view.findViewById(R.id.message_viewpager) viewPager.isUserInputEnabled = true viewPager.offscreenPageLimit = ViewPager2.OFFSCREEN_PAGE_LIMIT_DEFAULT - viewPager.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.HORIZONTAL)) + viewPager.setPageTransformer(MarginPageTransformer(pageMargin)) viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { // The message list is updated each time the active message is changed. To avoid message list updates // during the animation, we only set the active message after the animation has finished. diff --git a/app/ui/legacy/src/main/res/values/dimensions.xml b/app/ui/legacy/src/main/res/values/dimensions.xml index 0a478eab7a..b57ef027f0 100644 --- a/app/ui/legacy/src/main/res/values/dimensions.xml +++ b/app/ui/legacy/src/main/res/values/dimensions.xml @@ -7,4 +7,6 @@ 8dp 4dp 12dp + + 16dp -- GitLab From 8a73c931349ab899d82e231c9e42980cc4b706d0 Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 9 Sep 2022 13:31:54 +0200 Subject: [PATCH 08/20] Move code to select message list items to `MessageListAdapter` --- .../fsck/k9/helper/CollectionExtensions.kt | 40 +++ .../fsck/k9/fragment/MessageListAdapter.kt | 74 +++++ .../fsck/k9/fragment/MessageListFragment.kt | 262 ++++++------------ 3 files changed, 201 insertions(+), 175 deletions(-) create mode 100644 app/core/src/main/java/com/fsck/k9/helper/CollectionExtensions.kt diff --git a/app/core/src/main/java/com/fsck/k9/helper/CollectionExtensions.kt b/app/core/src/main/java/com/fsck/k9/helper/CollectionExtensions.kt new file mode 100644 index 0000000000..b996406952 --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/helper/CollectionExtensions.kt @@ -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 Iterable.mapToSet(expectedSize: Int? = null, transform: (T) -> R): Set { + 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 Collection.mapCollectionToSet(transform: (T) -> R): Set { + 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) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListAdapter.kt b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListAdapter.kt index e06af5b0ec..4cb7974bb5 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListAdapter.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListAdapter.kt @@ -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 = emptySet() + private set(value) { + field = value + selectedCount = calculateSelectionCount() + notifyDataSetChanged() + } + + val selectedMessages: List + 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) { + 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 { diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt index 69759757b7..b38ca443a4 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt @@ -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 = 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? = 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,41 +846,20 @@ 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() - } + private fun selectAll() { + if (adapter.messages.isEmpty()) { + // Nothing to do if there are no messages + return + } - computeBatchDirection() - updateActionMode() - computeSelectAllVisibility() - } else { - this.selected.clear() - selectedCount = 0 + adapter.selectAll() - actionMode?.finish() - actionMode = null + if (actionMode == null) { + startAndPrepareActionMode() } - adapter.notifyDataSetChanged() + computeBatchDirection() + updateActionMode() } 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) - } + adapter.toggleSelection(messageListItem) - var selectedCountDelta = 1 - if (showingThreadedList) { - val threadCount = messageListItem.threadCount - if (threadCount > 1) { - selectedCountDelta = threadCount - } + if (adapter.selectedCount == 0) { + actionMode?.finish() + actionMode = null + return } - 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() } 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,25 +926,22 @@ class MessageListFragment : } private fun setFlagForSelected(flag: Flag, newState: Boolean) { - if (selected.isEmpty()) return + if (adapter.selected.isEmpty()) return - val messageMap: MutableMap> = mutableMapOf() - val threadMap: MutableMap> = mutableMapOf() - val accounts: MutableSet = mutableSetOf() + val messageMap = mutableMapOf>() + val threadMap = mutableMapOf>() + val accounts = mutableSetOf() - 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 +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 - get() { - return adapter.messages - .asSequence() - .filter { it.uniqueId in selected } - .map { MessageReference(it.account.uuid, it.folderId, it.messageUid) } - .toList() - } + private val selectedMessages: List + 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) { - 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 - 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 + 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() } -- GitLab From 20e70f8f2010d35319fd253cd0c006816f39704e Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 9 Sep 2022 16:11:09 +0200 Subject: [PATCH 09/20] Try to avoid using the list position whenever possible --- .../fsck/k9/fragment/MessageListAdapter.kt | 12 ++ .../fsck/k9/fragment/MessageListFragment.kt | 111 ++++-------------- .../fsck/k9/fragment/MessageListHandler.java | 14 --- 3 files changed, 37 insertions(+), 100 deletions(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListAdapter.kt b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListAdapter.kt index 4cb7974bb5..30cd1418bb 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListAdapter.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListAdapter.kt @@ -123,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) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt index b38ca443a4..061bd58d9b 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt @@ -376,7 +376,8 @@ class MessageListFragment : if (view === footerView) { handleFooterClick() } else { - handleListItemClick(position) + val messageListItem = adapter.getItem(position) + handleListItemClick(messageListItem) } } @@ -411,17 +412,14 @@ class MessageListFragment : } } - private fun handleListItemClick(position: Int) { + private fun handleListItemClick(messageListItem: MessageListItem) { if (adapter.selectedCount > 0) { - toggleMessageSelect(position) + 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) } } } @@ -429,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 } @@ -789,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 } } @@ -862,14 +854,6 @@ class MessageListFragment : updateActionMode() } - 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) { adapter.toggleSelection(messageListItem) @@ -1230,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) } @@ -1261,17 +1223,12 @@ class MessageListFragment : } private val selectedMessage: MessageReference? - get() { - val listViewPosition = listView.selectedItemPosition - val adapterPosition = listViewToAdapterPosition(listViewPosition) - if (adapterPosition == AdapterView.INVALID_POSITION) return null - return getReferenceForPosition(adapterPosition) - } + get() = selectedMessageListItem?.messageReference - private val adapterPositionForSelectedMessage: Int + private val selectedMessageListItem: MessageListItem? get() { - val listViewPosition = listView.selectedItemPosition - return listViewToAdapterPosition(listViewPosition) + val position = listView.selectedItemPosition + return if (position !in 0 until adapter.count) null else adapter.getItem(position) } private val selectedMessages: List @@ -1284,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() { @@ -1490,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) { @@ -1508,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) } } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListHandler.java b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListHandler.java index a6de78d14c..d62e7b138b 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListHandler.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListHandler.java @@ -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 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; - } } } } -- GitLab From ba9d9cd6129a6574a6d4512b01cf72203093ec77 Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 9 Sep 2022 21:56:51 +0200 Subject: [PATCH 10/20] Get rid of (deprecation) warnings --- .../ui/base/extensions/TextInputLayoutExtensions.kt | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/app/ui/base/src/main/java/com/fsck/k9/ui/base/extensions/TextInputLayoutExtensions.kt b/app/ui/base/src/main/java/com/fsck/k9/ui/base/extensions/TextInputLayoutExtensions.kt index 91b32d7d1d..ae0ff3a3de 100644 --- a/app/ui/base/src/main/java/com/fsck/k9/ui/base/extensions/TextInputLayoutExtensions.kt +++ b/app/ui/base/src/main/java/com/fsck/k9/ui/base/extensions/TextInputLayoutExtensions.kt @@ -13,11 +13,11 @@ import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL import androidx.biometric.BiometricPrompt import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleObserver -import androidx.lifecycle.OnLifecycleEvent +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.get import com.google.android.material.textfield.TextInputLayout /** @@ -29,7 +29,7 @@ fun TextInputLayout.configureAuthenticatedPasswordToggle( subtitle: String, needScreenLockMessage: String, ) { - val viewModel = ViewModelProvider(activity).get(AuthenticatedPasswordToggleViewModel::class.java) + val viewModel = ViewModelProvider(activity).get() viewModel.textInputLayout = this viewModel.activity = activity @@ -108,9 +108,8 @@ class AuthenticatedPasswordToggleViewModel : ViewModel() { set(value) { field = value - value?.lifecycle?.addObserver(object : LifecycleObserver { - @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) - fun removeReferences() { + value?.lifecycle?.addObserver(object : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { textInputLayout = null field = null } -- GitLab From a3be470b45a725e745861eb04202b09e73f1b98a Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 9 Sep 2022 22:03:13 +0200 Subject: [PATCH 11/20] Allow unmasking when the original password was removed/replaced --- .../base/extensions/TextInputLayoutExtensions.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/ui/base/src/main/java/com/fsck/k9/ui/base/extensions/TextInputLayoutExtensions.kt b/app/ui/base/src/main/java/com/fsck/k9/ui/base/extensions/TextInputLayoutExtensions.kt index ae0ff3a3de..8adf315e29 100644 --- a/app/ui/base/src/main/java/com/fsck/k9/ui/base/extensions/TextInputLayoutExtensions.kt +++ b/app/ui/base/src/main/java/com/fsck/k9/ui/base/extensions/TextInputLayoutExtensions.kt @@ -12,6 +12,7 @@ import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL import androidx.biometric.BiometricPrompt import androidx.core.content.ContextCompat +import androidx.core.widget.doOnTextChanged import androidx.fragment.app.FragmentActivity import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner @@ -69,9 +70,16 @@ fun TextInputLayout.configureAuthenticatedPasswordToggle( val editText = this.editText ?: error("TextInputLayout.editText == null") + editText.doOnTextChanged { text, _, before, count -> + // Check if the password field is empty or if all of the previous text was replaced + if (text != null && before > 0 && (text.isEmpty() || text.length - count == 0)) { + viewModel.isNewPassword = true + } + } + setEndIconOnClickListener { if (editText.isPasswordHidden) { - if (viewModel.isAuthenticated) { + if (viewModel.isShowPasswordAllowed) { activity.setSecure(true) editText.showPassword() } else { @@ -102,6 +110,10 @@ private fun FragmentActivity.setSecure(secure: Boolean) { @SuppressLint("StaticFieldLeak") class AuthenticatedPasswordToggleViewModel : ViewModel() { + val isShowPasswordAllowed: Boolean + get() = isAuthenticated || isNewPassword + + var isNewPassword = false var isAuthenticated = false var textInputLayout: TextInputLayout? = null var activity: FragmentActivity? = null -- GitLab From 029a8eb07e2e9f3838d4265949f1c678756efaf8 Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 12 Sep 2022 12:56:43 +0200 Subject: [PATCH 12/20] Don't allow showing the password without authentication after an orientation change --- .../extensions/TextInputLayoutExtensions.kt | 4 ++++ .../activity/setup/AccountSetupIncoming.java | 23 +++++++++++-------- .../activity/setup/AccountSetupOutgoing.java | 23 +++++++++++-------- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/app/ui/base/src/main/java/com/fsck/k9/ui/base/extensions/TextInputLayoutExtensions.kt b/app/ui/base/src/main/java/com/fsck/k9/ui/base/extensions/TextInputLayoutExtensions.kt index 8adf315e29..ca4f4305b3 100644 --- a/app/ui/base/src/main/java/com/fsck/k9/ui/base/extensions/TextInputLayoutExtensions.kt +++ b/app/ui/base/src/main/java/com/fsck/k9/ui/base/extensions/TextInputLayoutExtensions.kt @@ -23,6 +23,10 @@ import com.google.android.material.textfield.TextInputLayout /** * Configures a [TextInputLayout] so the password can only be revealed after authentication. + * + * **IMPORTANT**: Only call this after the instance state has been restored! Otherwise, restoring the previous state + * after the initial state has been set will be detected as replacing the whole text. In that case showing the password + * will be allowed without authentication. */ fun TextInputLayout.configureAuthenticatedPasswordToggle( activity: FragmentActivity, diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java index be25116dfa..4a9294eceb 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java @@ -89,6 +89,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener private CheckBox mSubscribedFoldersOnly; private AuthTypeAdapter mAuthTypeAdapter; private ConnectionSecurity[] mConnectionSecurityChoices = ConnectionSecurity.values(); + private boolean editSettings; public static void actionIncomingSettings(Activity context, Account account, boolean makeDefault) { Intent i = new Intent(context, AccountSetupIncoming.class); @@ -174,16 +175,8 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener mAuthTypeAdapter = AuthTypeAdapter.get(this, oAuthSupported); mAuthTypeView.setAdapter(mAuthTypeAdapter); - boolean editSettings = Intent.ACTION_EDIT.equals(getIntent().getAction()); + editSettings = Intent.ACTION_EDIT.equals(getIntent().getAction()); if (editSettings) { - TextInputLayoutHelper.configureAuthenticatedPasswordToggle( - mPasswordLayoutView, - this, - getString(R.string.account_setup_basics_show_password_biometrics_title), - getString(R.string.account_setup_basics_show_password_biometrics_subtitle), - getString(R.string.account_setup_basics_show_password_need_lock) - ); - if (getSupportActionBar() != null) { getSupportActionBar().setDisplayHomeAsUpEnabled(true); } @@ -386,6 +379,16 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener mPasswordView.addTextChangedListener(validationTextWatcher); mServerView.addTextChangedListener(validationTextWatcher); mPortView.addTextChangedListener(validationTextWatcher); + + if (editSettings) { + TextInputLayoutHelper.configureAuthenticatedPasswordToggle( + mPasswordLayoutView, + this, + getString(R.string.account_setup_basics_show_password_biometrics_title), + getString(R.string.account_setup_basics_show_password_biometrics_subtitle), + getString(R.string.account_setup_basics_show_password_need_lock) + ); + } } @Override @@ -538,7 +541,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener } if (resultCode == RESULT_OK) { - if (Intent.ACTION_EDIT.equals(getIntent().getAction())) { + if (editSettings) { Preferences.getPreferences().saveAccount(mAccount); finish(); } else { diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOutgoing.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOutgoing.java index 91a8f7cdcb..e62f977338 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOutgoing.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOutgoing.java @@ -73,6 +73,7 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener, private AuthTypeAdapter mAuthTypeAdapter; private Button mNextButton; private Account mAccount; + private boolean editSettings; public static void actionOutgoingSettings(Context context, Account account) { Intent i = new Intent(context, AccountSetupOutgoing.class); @@ -149,16 +150,8 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener, mAccount = Preferences.getPreferences().getAccount(accountUuid); } - boolean editSettings = Intent.ACTION_EDIT.equals(getIntent().getAction()); + editSettings = Intent.ACTION_EDIT.equals(getIntent().getAction()); if (editSettings) { - TextInputLayoutHelper.configureAuthenticatedPasswordToggle( - mPasswordLayoutView, - this, - getString(R.string.account_setup_basics_show_password_biometrics_title), - getString(R.string.account_setup_basics_show_password_biometrics_subtitle), - getString(R.string.account_setup_basics_show_password_need_lock) - ); - if (getSupportActionBar() != null) { getSupportActionBar().setDisplayHomeAsUpEnabled(true); } @@ -321,6 +314,16 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener, mPasswordView.addTextChangedListener(validationTextWatcher); mServerView.addTextChangedListener(validationTextWatcher); mPortView.addTextChangedListener(validationTextWatcher); + + if (editSettings) { + TextInputLayoutHelper.configureAuthenticatedPasswordToggle( + mPasswordLayoutView, + this, + getString(R.string.account_setup_basics_show_password_biometrics_title), + getString(R.string.account_setup_basics_show_password_biometrics_subtitle), + getString(R.string.account_setup_basics_show_password_need_lock) + ); + } } @Override @@ -494,7 +497,7 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener, } if (resultCode == RESULT_OK) { - if (Intent.ACTION_EDIT.equals(getIntent().getAction())) { + if (editSettings) { Preferences.getPreferences().saveAccount(mAccount); finish(); } else { -- GitLab From 00259046c6d83be85ae86b5fae4d4668399f85f2 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 14 Sep 2022 14:40:21 +0200 Subject: [PATCH 13/20] Add `MimeParameterDecoder` test for UTF-8 data in header value --- .../com/fsck/k9/mail/internet/MimeParameterDecoderTest.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mail/common/src/test/java/com/fsck/k9/mail/internet/MimeParameterDecoderTest.kt b/mail/common/src/test/java/com/fsck/k9/mail/internet/MimeParameterDecoderTest.kt index def1fe9890..4f16aba012 100644 --- a/mail/common/src/test/java/com/fsck/k9/mail/internet/MimeParameterDecoderTest.kt +++ b/mail/common/src/test/java/com/fsck/k9/mail/internet/MimeParameterDecoderTest.kt @@ -378,6 +378,14 @@ class MimeParameterDecoderTest { assertThat(mimeValue.ignoredParameters).isEmpty() } + @Test + fun `UTF-8 data in header value`() { + val mimeValue = MimeParameterDecoder.decode("application/x-stuff; name=\"filenäme.ext\"") + + assertThat(mimeValue.parameters).containsExactlyEntries("name" to "filenäme.ext") + assertThat(mimeValue.ignoredParameters).isEmpty() + } + private fun MapSubject.containsExactlyEntries(vararg values: Pair): Ordered { return containsExactlyEntriesIn(values.toMap()) } -- GitLab From dd3d103fd32274896ef7b86486d2eb1c68fc8965 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 14 Sep 2022 15:24:44 +0200 Subject: [PATCH 14/20] Add support for UTF-8 in quoted strings to `ImapResponseParser` --- mail/protocols/imap/build.gradle | 1 + .../fsck/k9/mail/store/imap/ImapResponseParser.java | 8 +++++--- .../k9/mail/store/imap/ImapResponseParserTest.java | 13 ++++++++++++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/mail/protocols/imap/build.gradle b/mail/protocols/imap/build.gradle index 3150d7d9a8..8585496e55 100644 --- a/mail/protocols/imap/build.gradle +++ b/mail/protocols/imap/build.gradle @@ -17,6 +17,7 @@ dependencies { implementation "com.jcraft:jzlib:1.0.7" implementation "com.beetstra.jutf7:jutf7:1.0.0" implementation "commons-io:commons-io:${versions.commonsIo}" + implementation "com.squareup.okio:okio:${versions.okio}" testImplementation project(":mail:testing") testImplementation "junit:junit:${versions.junit}" diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapResponseParser.java b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapResponseParser.java index 64c2058006..8a2e9a8994 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapResponseParser.java +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapResponseParser.java @@ -10,6 +10,7 @@ import com.fsck.k9.logging.Timber; import com.fsck.k9.mail.K9MailLib; import com.fsck.k9.mail.filter.FixedLengthInputStream; import com.fsck.k9.mail.filter.PeekableInputStream; +import okio.Buffer; import static com.fsck.k9.mail.K9MailLib.DEBUG_PROTOCOL_IMAP; @@ -396,7 +397,7 @@ class ImapResponseParser { private String parseQuoted() throws IOException { expect('"'); - StringBuilder sb = new StringBuilder(); + Buffer buffer = new Buffer(); int ch; boolean escape = false; while ((ch = inputStream.read()) != -1) { @@ -404,12 +405,13 @@ class ImapResponseParser { // Found the escape character escape = true; } else if (!escape && ch == '"') { - return sb.toString(); + return buffer.readUtf8(); } else { - sb.append((char) ch); + buffer.writeByte(ch); escape = false; } } + throw new IOException("parseQuoted(): end of stream reached"); } diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/ImapResponseParserTest.java b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/ImapResponseParserTest.java index f06eda131a..40feb704af 100644 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/ImapResponseParserTest.java +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/ImapResponseParserTest.java @@ -8,6 +8,7 @@ import java.util.List; import com.fsck.k9.mail.filter.FixedLengthInputStream; import com.fsck.k9.mail.filter.PeekableInputStream; +import kotlin.text.Charsets; import org.junit.Test; import static java.util.Arrays.asList; @@ -346,6 +347,16 @@ public class ImapResponseParserTest { assertEquals("qu\"oted", response.getString(0)); } + @Test + public void utf8InQuotedString() throws Exception { + ImapResponseParser parser = createParser("* \"quöted\"\r\n"); + + ImapResponse response = parser.readResponse(); + + assertEquals(1, response.size()); + assertEquals("quöted", response.getString(0)); + } + @Test(expected = IOException.class) public void testParseQuotedToEndOfStream() throws Exception { ImapResponseParser parser = createParser("* \"abc"); @@ -484,7 +495,7 @@ public class ImapResponseParserTest { } private ImapResponseParser createParser(String response) { - ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(response.getBytes()); + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(response.getBytes(Charsets.UTF_8)); peekableInputStream = new PeekableInputStream(byteArrayInputStream); return new ImapResponseParser(peekableInputStream); } -- GitLab From 037afd0596d869a1b8654575f966ced8ee08b923 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 14 Sep 2022 15:30:50 +0200 Subject: [PATCH 15/20] Add support for UTF-8 data in BODYSTRUCTURE response --- .../fsck/k9/mail/internet/MimeExtensions.kt | 1 + .../k9/mail/internet/MimeParameterEncoder.kt | 18 +++++++++++++++++- .../fsck/k9/mail/store/imap/RealImapFolder.kt | 8 +++----- .../k9/mail/store/imap/RealImapFolderTest.kt | 19 +++++++++++++++++++ 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeExtensions.kt b/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeExtensions.kt index 093a57cbfa..779518b901 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeExtensions.kt +++ b/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeExtensions.kt @@ -26,6 +26,7 @@ internal const val SEMICOLON = ';' internal const val EQUALS_SIGN = '=' internal const val ASTERISK = '*' internal const val SINGLE_QUOTE = '\'' +internal const val BACKSLASH = '\\' internal fun Char.isTSpecial() = this in TSPECIALS diff --git a/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeParameterEncoder.kt b/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeParameterEncoder.kt index 51e0505ac7..fe6b861a4a 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeParameterEncoder.kt +++ b/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeParameterEncoder.kt @@ -145,7 +145,7 @@ object MimeParameterEncoder { private fun String.isQuotable() = all { it.isQuotable() } - fun String.quoted(): String { + private fun String.quoted(): String { // quoted-string = [CFWS] DQUOTE *([FWS] qcontent) [FWS] DQUOTE [CFWS] // qcontent = qtext / quoted-pair // quoted-pair = ("\" (VCHAR / WSP)) @@ -165,6 +165,22 @@ object MimeParameterEncoder { } } + // RFC 6532-style header values + // Right now we only create such values for internal use (see IMAP BODYSTRUCTURE response parsing code) + fun String.quotedUtf8(): String { + return buildString(capacity = length + 16) { + append(DQUOTE) + for (c in this@quotedUtf8) { + if (c == DQUOTE || c == BACKSLASH) { + append('\\').append(c) + } else { + append(c) + } + } + append(DQUOTE) + } + } + private fun String.quotedLength(): Int { var length = 2 /* start and end quote */ for (c in this) { diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapFolder.kt b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapFolder.kt index c67605f645..58a888ddd3 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapFolder.kt +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapFolder.kt @@ -16,14 +16,12 @@ import com.fsck.k9.mail.internet.MimeHeader import com.fsck.k9.mail.internet.MimeMessageHelper import com.fsck.k9.mail.internet.MimeMultipart import com.fsck.k9.mail.internet.MimeParameterEncoder.isToken -import com.fsck.k9.mail.internet.MimeParameterEncoder.quoted +import com.fsck.k9.mail.internet.MimeParameterEncoder.quotedUtf8 import com.fsck.k9.mail.internet.MimeUtility import java.io.IOException import java.io.InputStream import java.text.SimpleDateFormat import java.util.Date -import java.util.HashMap -import java.util.LinkedHashSet import java.util.Locale import kotlin.math.max import kotlin.math.min @@ -891,7 +889,7 @@ internal class RealImapFolder( for (i in bodyParams.indices step 2) { val paramName = bodyParams.getString(i) val paramValue = bodyParams.getString(i + 1) - val encodedValue = if (paramValue.isToken()) paramValue else paramValue.quoted() + val encodedValue = if (paramValue.isToken()) paramValue else paramValue.quotedUtf8() contentType.append(String.format(";\r\n %s=%s", paramName, encodedValue)) } } @@ -918,7 +916,7 @@ internal class RealImapFolder( for (i in bodyDispositionParams.indices step 2) { val paramName = bodyDispositionParams.getString(i).lowercase() val paramValue = bodyDispositionParams.getString(i + 1) - val encodedValue = if (paramValue.isToken()) paramValue else paramValue.quoted() + val encodedValue = if (paramValue.isToken()) paramValue else paramValue.quotedUtf8() contentDisposition.append(String.format(";\r\n %s=%s", paramName, encodedValue)) } } diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapFolderTest.kt b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapFolderTest.kt index 73a7744e8f..c74fccfcba 100644 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapFolderTest.kt +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapFolderTest.kt @@ -770,6 +770,15 @@ class RealImapFolderTest { ) } + @Test + fun `fetch() with UTF-8 encoded content type parameter`() { + testHeaderFromBodyStructure( + bodyStructure = """("text" "plain" ("name" "filenäme.ext") NIL NIL "7bit" 42 23)""", + headerName = MimeHeader.HEADER_CONTENT_TYPE, + expectedHeaderValue = "text/plain;\r\n name=\"filenäme.ext\"" + ) + } + @Test fun `fetch() with simple content disposition parameter`() { testHeaderFromBodyStructure( @@ -810,6 +819,16 @@ class RealImapFolderTest { ) } + @Test + fun `fetch() with UTF-8 encoded content disposition parameter`() { + testHeaderFromBodyStructure( + bodyStructure = """("application" "octet-stream" NIL NIL NIL "8bit" 23 NIL """ + + """("attachment" ("filename" "filenäme.ext")) NIL NIL)""", + headerName = MimeHeader.HEADER_CONTENT_DISPOSITION, + expectedHeaderValue = "attachment;\r\n filename=\"filenäme.ext\";\r\n size=23" + ) + } + @Test fun fetch_withBodySaneFetchProfile_shouldIssueRespectiveCommand() { val folder = createFolder("Folder") -- GitLab From 1766bbe2ceef3350ab74a76d465e006c4890f46e Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 14 Sep 2022 16:35:48 +0200 Subject: [PATCH 16/20] Remove direct share support We (can) no longer use the contacts database to keep track on how often someone has been contacted. Without that information we can't make meaningful automatic suggestions for share targets. In the future we want to add support for app shortcuts to create a new message to a specific contact (see issue #2145). --- app/k9mail/src/main/AndroidManifest.xml | 14 --- .../directshare/K9ChooserTargetService.java | 93 ------------------- 2 files changed, 107 deletions(-) delete mode 100644 app/k9mail/src/main/java/com/fsck/k9/directshare/K9ChooserTargetService.java diff --git a/app/k9mail/src/main/AndroidManifest.xml b/app/k9mail/src/main/AndroidManifest.xml index c2f32660fd..dbf6fb714b 100644 --- a/app/k9mail/src/main/AndroidManifest.xml +++ b/app/k9mail/src/main/AndroidManifest.xml @@ -221,10 +221,6 @@ - - @@ -354,16 +350,6 @@ android:name=".service.DatabaseUpgradeService" android:exported="false"/> - - - - - - - diff --git a/app/k9mail/src/main/java/com/fsck/k9/directshare/K9ChooserTargetService.java b/app/k9mail/src/main/java/com/fsck/k9/directshare/K9ChooserTargetService.java deleted file mode 100644 index c74a5ec96f..0000000000 --- a/app/k9mail/src/main/java/com/fsck/k9/directshare/K9ChooserTargetService.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.fsck.k9.directshare; - - -import java.util.ArrayList; -import java.util.List; - -import android.annotation.TargetApi; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.graphics.Bitmap; -import android.graphics.drawable.Icon; -import android.os.Build; -import android.os.Bundle; -import android.service.chooser.ChooserTarget; -import android.service.chooser.ChooserTargetService; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.fsck.k9.activity.MessageCompose; -import com.fsck.k9.activity.compose.RecipientLoader; -import com.fsck.k9.activity.misc.ContactPicture; -import com.fsck.k9.contacts.ContactPictureLoader; -import com.fsck.k9.mail.Address; -import com.fsck.k9.view.RecipientSelectView.Recipient; - - -@TargetApi(Build.VERSION_CODES.M) -public class K9ChooserTargetService extends ChooserTargetService { - private static final int MAX_TARGETS = 5; - - private RecipientLoader recipientLoader; - private ContactPictureLoader contactPictureLoader; - - @Override - public void onCreate() { - super.onCreate(); - - Context applicationContext = getApplicationContext(); - recipientLoader = RecipientLoader.getMostContactedRecipientLoader(applicationContext, MAX_TARGETS); - contactPictureLoader = ContactPicture.getContactPictureLoader(); - } - - @Override - public List onGetChooserTargets(ComponentName targetActivityName, IntentFilter matchedFilter) { - List recipients = recipientLoader.loadInBackground(); - - return createChooserTargets(recipients); - } - - @NonNull - private List createChooserTargets(List recipients) { - float score = 1.0f; - - List targets = new ArrayList<>(); - ComponentName componentName = new ComponentName(this, MessageCompose.class); - for (Recipient recipient : recipients) { - Bundle intentExtras = prepareIntentExtras(recipient); - Icon icon = loadRecipientIcon(recipient); - - ChooserTarget chooserTarget = - new ChooserTarget(recipient.getDisplayNameOrAddress(), icon, score, componentName, intentExtras); - targets.add(chooserTarget); - - score -= 0.1; - } - - return targets; - } - - @NonNull - private Bundle prepareIntentExtras(Recipient recipient) { - Address address = recipient.address; - - Bundle extras = new Bundle(); - extras.putStringArray(Intent.EXTRA_EMAIL, new String[] { address.toString() }); - extras.putStringArray(Intent.EXTRA_CC, new String[0]); - extras.putStringArray(Intent.EXTRA_BCC, new String[0]); - - return extras; - } - - @Nullable - private Icon loadRecipientIcon(Recipient recipient) { - Bitmap bitmap = contactPictureLoader.getContactPicture(recipient); - if (bitmap == null) { - return null; - } - - return Icon.createWithBitmap(bitmap); - } -} -- GitLab From 54052990a87110a37605299f71d79ed9b9bbec8a Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 16 Sep 2022 13:27:45 +0200 Subject: [PATCH 17/20] Version 6.304 --- app/k9mail/build.gradle | 4 ++-- app/ui/legacy/src/main/res/raw/changelog_master.xml | 7 +++++++ fastlane/metadata/android/en-US/changelogs/33004.txt | 5 +++++ 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/33004.txt diff --git a/app/k9mail/build.gradle b/app/k9mail/build.gradle index 5f64c0aea6..5b78ccfa98 100644 --- a/app/k9mail/build.gradle +++ b/app/k9mail/build.gradle @@ -48,8 +48,8 @@ android { applicationId "com.fsck.k9" testApplicationId "com.fsck.k9.tests" - versionCode 33003 - versionName '6.304-SNAPSHOT' + versionCode 33004 + versionName '6.304' // Keep in sync with the resource string array 'supported_languages' resConfigs "in", "br", "ca", "cs", "cy", "da", "de", "et", "en", "en_GB", "es", "eo", "eu", "fr", "gd", "gl", diff --git a/app/ui/legacy/src/main/res/raw/changelog_master.xml b/app/ui/legacy/src/main/res/raw/changelog_master.xml index 02151b0f85..d75961eac6 100644 --- a/app/ui/legacy/src/main/res/raw/changelog_master.xml +++ b/app/ui/legacy/src/main/res/raw/changelog_master.xml @@ -5,6 +5,13 @@ Locale-specific versions are kept in res/raw-/changelog.xml. --> + + Allow unmasking the password field without authentication as soon as the original password in incoming/outgoing server settings has been overwritten completely + Don't crash when IMAP servers send attachment names that contain non-ASCII characters + Fixed a bug where the search input field would collapse back into its toolbar icon while it was being used + Removed support for Direct Share because the app can no longer use the number of times a person has been contacted to make useful suggestions + More internal changes + Fixed a bug that lead to search being broken Fixed error reporting for (old) send failures diff --git a/fastlane/metadata/android/en-US/changelogs/33004.txt b/fastlane/metadata/android/en-US/changelogs/33004.txt new file mode 100644 index 0000000000..08b6e2249b --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/33004.txt @@ -0,0 +1,5 @@ +- Allow unmasking the password field without authentication as soon as the original password in incoming/outgoing server settings has been overwritten completely +- Don't crash when IMAP servers send attachment names that contain non-ASCII characters +- Fixed a bug where the search input field would collapse back into its toolbar icon while it was being used +- Removed support for Direct Share because the app can no longer use the number of times a person has been contacted to make useful suggestions +- More internal changes -- GitLab From 293af97d1fba82b09cffd995be3cf522ddcfc07b Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 16 Sep 2022 13:43:00 +0200 Subject: [PATCH 18/20] Prepare for version 6.305 --- app/k9mail/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/k9mail/build.gradle b/app/k9mail/build.gradle index 5b78ccfa98..60c3fe402b 100644 --- a/app/k9mail/build.gradle +++ b/app/k9mail/build.gradle @@ -49,7 +49,7 @@ android { testApplicationId "com.fsck.k9.tests" versionCode 33004 - versionName '6.304' + versionName '6.305-SNAPSHOT' // Keep in sync with the resource string array 'supported_languages' resConfigs "in", "br", "ca", "cs", "cy", "da", "de", "et", "en", "en_GB", "es", "eo", "eu", "fr", "gd", "gl", -- GitLab From 165efa598a1e8711efa0e11e0c43b33e9437650a Mon Sep 17 00:00:00 2001 From: cketti Date: Sat, 17 Sep 2022 11:47:20 +0200 Subject: [PATCH 19/20] =?UTF-8?q?Fix=20crashes=20where=20`MessageList.sear?= =?UTF-8?q?chView=C2=B4=20is=20accessed=20before=20it=20was=20initialized?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/fsck/k9/activity/MessageList.kt | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt index 8d1d39b736..6425672393 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt @@ -100,7 +100,7 @@ open class MessageList : private val permissionUiHelper: PermissionUiHelper = K9PermissionUiHelper(this) private lateinit var actionBar: ActionBar - private lateinit var searchView: SearchView + private var searchView: SearchView? = null private var initialSearchViewQuery: String? = null private var initialSearchViewIconified: Boolean = true @@ -577,8 +577,10 @@ open class MessageList : outState.putSerializable(STATE_DISPLAY_MODE, displayMode) outState.putBoolean(STATE_MESSAGE_VIEW_ONLY, messageViewOnly) outState.putBoolean(STATE_MESSAGE_LIST_WAS_DISPLAYED, messageListWasDisplayed) - outState.putBoolean(STATE_SEARCH_VIEW_ICONIFIED, searchView.isIconified) - outState.putString(STATE_SEARCH_VIEW_QUERY, searchView.query?.toString()) + searchView?.let { searchView -> + outState.putBoolean(STATE_SEARCH_VIEW_ICONIFIED, searchView.isIconified) + outState.putString(STATE_SEARCH_VIEW_QUERY, searchView.query?.toString()) + } } public override fun onRestoreInstanceState(savedInstanceState: Bundle) { @@ -695,7 +697,7 @@ open class MessageList : override fun dispatchKeyEvent(event: KeyEvent): Boolean { var eventHandled = false - if (event.action == KeyEvent.ACTION_DOWN && ::searchView.isInitialized && searchView.isIconified) { + if (event.action == KeyEvent.ACTION_DOWN && isSearchViewCollapsed()) { eventHandled = onCustomKeyDown(event) } @@ -715,7 +717,7 @@ open class MessageList : } else { showMessageList() } - } else if (this::searchView.isInitialized && !searchView.isIconified) { + } else if (!isSearchViewCollapsed()) { collapseSearchView() } else { if (isDrawerEnabled && account != null && supportFragmentManager.backStackEntryCount == 0) { @@ -952,12 +954,12 @@ open class MessageList : private fun initializeSearchMenuItem(searchItem: MenuItem) { // Reuse existing SearchView if available - if (::searchView.isInitialized) { + searchView?.let { searchView -> searchItem.actionView = searchView return } - searchView = searchItem.actionView as SearchView + val searchView = searchItem.actionView as SearchView searchView.maxWidth = Int.MAX_VALUE searchView.queryHint = resources.getString(R.string.search_action) val searchManager = getSystemService(SEARCH_SERVICE) as SearchManager @@ -976,11 +978,17 @@ open class MessageList : searchView.isIconified = initialSearchViewIconified searchView.setQuery(initialSearchViewQuery, false) + + this.searchView = searchView } + private fun isSearchViewCollapsed(): Boolean = searchView?.isIconified == true + private fun collapseSearchView() { - searchView.setQuery(null, false) - searchView.isIconified = true + searchView?.let { searchView -> + searchView.setQuery(null, false) + searchView.isIconified = true + } } fun setActionBarTitle(title: String, subtitle: String? = null) { -- GitLab From f6d819761a35bfdf063da53325b67354a56620c2 Mon Sep 17 00:00:00 2001 From: cketti Date: Sat, 17 Sep 2022 12:14:15 +0200 Subject: [PATCH 20/20] Version 6.305 --- app/k9mail/build.gradle | 4 ++-- app/ui/legacy/src/main/res/raw/changelog_master.xml | 3 +++ fastlane/metadata/android/en-US/changelogs/33005.txt | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/33005.txt diff --git a/app/k9mail/build.gradle b/app/k9mail/build.gradle index 60c3fe402b..e0dae8bc25 100644 --- a/app/k9mail/build.gradle +++ b/app/k9mail/build.gradle @@ -48,8 +48,8 @@ android { applicationId "com.fsck.k9" testApplicationId "com.fsck.k9.tests" - versionCode 33004 - versionName '6.305-SNAPSHOT' + versionCode 33005 + versionName '6.305' // Keep in sync with the resource string array 'supported_languages' resConfigs "in", "br", "ca", "cs", "cy", "da", "de", "et", "en", "en_GB", "es", "eo", "eu", "fr", "gd", "gl", diff --git a/app/ui/legacy/src/main/res/raw/changelog_master.xml b/app/ui/legacy/src/main/res/raw/changelog_master.xml index d75961eac6..efa4f2abf1 100644 --- a/app/ui/legacy/src/main/res/raw/changelog_master.xml +++ b/app/ui/legacy/src/main/res/raw/changelog_master.xml @@ -5,6 +5,9 @@ Locale-specific versions are kept in res/raw-/changelog.xml. --> + + Fixed a bug that could lead to a crash when opening the message list + Allow unmasking the password field without authentication as soon as the original password in incoming/outgoing server settings has been overwritten completely Don't crash when IMAP servers send attachment names that contain non-ASCII characters diff --git a/fastlane/metadata/android/en-US/changelogs/33005.txt b/fastlane/metadata/android/en-US/changelogs/33005.txt new file mode 100644 index 0000000000..6d1d2d1b97 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/33005.txt @@ -0,0 +1 @@ +- Fixed a bug that could lead to a crash when opening the message list -- GitLab