From 4371972654d53226bc84519bf972b93592e12327 Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 18 Jul 2022 19:39:53 +0200 Subject: [PATCH 01/47] Prepare for version 6.300 --- app/k9mail/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/k9mail/build.gradle b/app/k9mail/build.gradle index 85780b1332..f893dc7a1b 100644 --- a/app/k9mail/build.gradle +++ b/app/k9mail/build.gradle @@ -47,8 +47,8 @@ android { applicationId "com.fsck.k9" testApplicationId "com.fsck.k9.tests" - versionCode 32001 - versionName '6.201' + versionCode 33000 + versionName '6.300-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 52b22ba2ca7486a98be92aa73bf92cd6044984b3 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 19 Jul 2022 12:54:44 +0200 Subject: [PATCH 02/47] Call `invalidateMenu()` instead of directly updating the current `Menu` --- .../src/main/java/com/fsck/k9/activity/MessageList.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 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 0a533dc741..0aa635a108 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 @@ -1309,7 +1309,7 @@ open class MessageList : showMessageViewPlaceHolder() } - configureMenu(menu) + invalidateMenu() } private fun addMessageListFragment(fragment: MessageListFragment) { @@ -1402,7 +1402,7 @@ open class MessageList : override fun remoteSearchStarted() { // Remove action button for remote search - configureMenu(menu) + invalidateMenu() } override fun goBack() { @@ -1477,7 +1477,7 @@ open class MessageList : setDrawerLockState() showDefaultTitleView() - configureMenu(menu) + invalidateMenu() onMessageListDisplayed() } @@ -1506,7 +1506,7 @@ open class MessageList : } showMessageTitleView() - configureMenu(menu) + invalidateMenu() } override fun updateMenu() { -- GitLab From 185a08de0b5a46d7eca55bc3a8c9ce4299646f1e Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 19 Jul 2022 13:00:23 +0200 Subject: [PATCH 03/47] Remove custom `updateMenu()` mechanism --- .../src/main/java/com/fsck/k9/activity/MessageList.kt | 4 ---- .../java/com/fsck/k9/fragment/MessageListFragment.kt | 7 +++++-- .../fsck/k9/ui/messageview/MessageViewFragment.java | 11 +++++++---- 3 files changed, 12 insertions(+), 10 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 0aa635a108..fa430b8724 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 @@ -1509,10 +1509,6 @@ open class MessageList : invalidateMenu() } - override fun updateMenu() { - invalidateOptionsMenu() - } - override fun disableDeleteAction() { menu!!.findItem(R.id.delete).isEnabled = false } 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 f0e8625e7e..157c6c0d9c 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 @@ -1493,7 +1493,7 @@ class MessageListFragment : savedListState = null } - fragmentListener.updateMenu() + invalidateMenu() currentFolder?.let { currentFolder -> currentFolder.moreMessages = messageListInfo.hasMoreMessages @@ -1591,6 +1591,10 @@ class MessageListFragment : resetActionMode() } + private fun invalidateMenu() { + requireActivity().invalidateMenu() + } + val isCheckMailSupported: Boolean get() = allAccounts || !isSingleAccountMode || !isSingleFolderMode || isRemoteFolder @@ -1949,7 +1953,6 @@ class MessageListFragment : fun startSearch(query: String, account: Account?, folderId: Long?): Boolean fun remoteSearchStarted() fun goBack() - fun updateMenu() fun onFolderNotFoundError() companion object { diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java index e1ce84d49d..81b1fd019e 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java @@ -240,7 +240,7 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF mAccount = Preferences.getPreferences(getApplicationContext()).getAccount(mMessageReference.getAccountUuid()); messageLoaderHelper.asyncStartOrResumeLoadingMessage(messageReference, null); - mFragmentListener.updateMenu(); + invalidateMenu(); } private void hideKeyboard() { @@ -281,7 +281,7 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF mMessageView.getMessageHeaderView().setCryptoStatusLoading(); } displaySubject(message.getSubject()); - mFragmentListener.updateMenu(); + invalidateMenu(); } private void displaySubject(String subject) { @@ -517,7 +517,7 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF Collections.singletonList(mMessage), Flag.SEEN, !mMessage.isSet(Flag.SEEN)); mMessageView.setHeaders(mMessage, mAccount, true); - mFragmentListener.updateMenu(); + invalidateMenu(); } } @@ -773,7 +773,6 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF void onReply(MessageReference messageReference, @Nullable Parcelable decryptionResultForReply); void setProgress(boolean b); void showNextMessageOrReturn(); - void updateMenu(); } public boolean isInitialized() { @@ -884,6 +883,10 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF return new AttachmentController(mController, this, attachment); } + private void invalidateMenu() { + requireActivity().invalidateMenu(); + } + private enum FolderOperation { COPY, MOVE } -- GitLab From 1de5fa8cf25f224d8c7a7c54a81558f3a1e3503d Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 19 Jul 2022 14:54:13 +0200 Subject: [PATCH 04/47] Mark message list as visible when listing messages in a thread --- app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt | 1 + 1 file changed, 1 insertion(+) 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 fa430b8724..04fbdf60fd 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 @@ -1326,6 +1326,7 @@ open class MessageList : } messageListFragment = fragment + fragment.onListVisible() if (isDrawerEnabled) { lockDrawer() -- GitLab From 4486782166076d9184ed2e53f4ce682bedbe6e56 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 19 Jul 2022 13:56:23 +0200 Subject: [PATCH 05/47] Move code to prepare the message list menu to `MessageListFragment` --- .../java/com/fsck/k9/activity/MessageList.kt | 45 --------------- .../fsck/k9/fragment/MessageListFragment.kt | 57 ++++++++++++++++--- 2 files changed, 49 insertions(+), 53 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 04fbdf60fd..dde63fa0d2 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 @@ -1185,51 +1185,6 @@ open class MessageList : menu.findItem(R.id.unsubscribe).isVisible = messageViewFragment!!.canMessageBeUnsubscribed() } - - // Set visibility of menu items related to the message list - - // Hide search menu items by default and enable one when appropriate - menu.findItem(R.id.search).isVisible = false - menu.findItem(R.id.search_remote).isVisible = false - menu.findItem(R.id.search_everywhere).isVisible = false - - if (displayMode == DisplayMode.MESSAGE_VIEW || messageListFragment == null || - !messageListFragment!!.isInitialized - ) { - menu.findItem(R.id.set_sort).isVisible = false - menu.findItem(R.id.select_all).isVisible = false - menu.findItem(R.id.send_messages).isVisible = false - menu.findItem(R.id.expunge).isVisible = false - menu.findItem(R.id.empty_trash).isVisible = false - menu.findItem(R.id.mark_all_as_read).isVisible = false - } else { - menu.findItem(R.id.set_sort).isVisible = true - menu.findItem(R.id.select_all).isVisible = true - menu.findItem(R.id.compose).isVisible = true - menu.findItem(R.id.mark_all_as_read).isVisible = messageListFragment!!.isMarkAllAsReadSupported - - if (!messageListFragment!!.isSingleAccountMode) { - menu.findItem(R.id.expunge).isVisible = false - menu.findItem(R.id.send_messages).isVisible = false - } else { - menu.findItem(R.id.send_messages).isVisible = messageListFragment!!.isOutbox - menu.findItem(R.id.expunge).isVisible = messageListFragment!!.isRemoteFolder && - messageListFragment!!.shouldShowExpungeAction() - } - menu.findItem(R.id.empty_trash).isVisible = messageListFragment!!.isShowingTrashFolder - - // If this is an explicit local search, show the option to search on the server - if (!messageListFragment!!.isRemoteSearch && messageListFragment!!.isRemoteSearchAllowed) { - menu.findItem(R.id.search_remote).isVisible = true - } else if (!messageListFragment!!.isManualSearch) { - menu.findItem(R.id.search).isVisible = true - } - - val messageListFragment = messageListFragment!! - if (messageListFragment.isManualSearch && !messageListFragment.localSearch.searchAllAccounts()) { - menu.findItem(R.id.search_everywhere).isVisible = true - } - } } fun setActionBarTitle(title: String, subtitle: String? = null) { 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 157c6c0d9c..b23bb8426a 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 @@ -147,6 +147,7 @@ class MessageListFragment : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + setHasOptionsMenu(true) restoreInstanceState(savedInstanceState) decodeArguments() ?: return @@ -644,7 +645,7 @@ class MessageListFragment : } } - val isShowingTrashFolder: Boolean + private val isShowingTrashFolder: Boolean get() { if (!isSingleFolderMode) return false return currentFolder!!.databaseId == account!!.trashFolderId @@ -703,6 +704,46 @@ class MessageListFragment : return "dialog-$dialogId" } + override fun onPrepareOptionsMenu(menu: Menu) { + if (isListVisible) { + prepareMenu(menu) + } else { + hideMenu(menu) + } + } + + private fun prepareMenu(menu: Menu) { + menu.findItem(R.id.set_sort).isVisible = true + menu.findItem(R.id.select_all).isVisible = true + menu.findItem(R.id.compose).isVisible = true + menu.findItem(R.id.mark_all_as_read).isVisible = isMarkAllAsReadSupported + menu.findItem(R.id.empty_trash).isVisible = isShowingTrashFolder + + if (isSingleAccountMode) { + menu.findItem(R.id.send_messages).isVisible = isOutbox + menu.findItem(R.id.expunge).isVisible = isRemoteFolder && shouldShowExpungeAction() + } else { + menu.findItem(R.id.send_messages).isVisible = false + menu.findItem(R.id.expunge).isVisible = false + } + + menu.findItem(R.id.search).isVisible = !isManualSearch + menu.findItem(R.id.search_remote).isVisible = !isRemoteSearch && isRemoteSearchAllowed + menu.findItem(R.id.search_everywhere).isVisible = isManualSearch && !localSearch.searchAllAccounts() + } + + private fun hideMenu(menu: Menu) { + menu.findItem(R.id.search).isVisible = false + menu.findItem(R.id.search_remote).isVisible = false + menu.findItem(R.id.set_sort).isVisible = false + menu.findItem(R.id.select_all).isVisible = false + menu.findItem(R.id.mark_all_as_read).isVisible = false + menu.findItem(R.id.send_messages).isVisible = false + menu.findItem(R.id.empty_trash).isVisible = false + menu.findItem(R.id.expunge).isVisible = false + menu.findItem(R.id.search_everywhere).isVisible = false + } + override fun onOptionsItemSelected(item: MenuItem): Boolean { val id = item.itemId if (id == R.id.set_sort_date) { @@ -1416,7 +1457,7 @@ class MessageListFragment : return currentFolder.databaseId == folderId } - val isRemoteFolder: Boolean + private val isRemoteFolder: Boolean get() { if (localSearch.isManualSearch || isOutbox) return false @@ -1428,10 +1469,10 @@ class MessageListFragment : } } - val isManualSearch: Boolean + private val isManualSearch: Boolean get() = localSearch.isManualSearch - fun shouldShowExpungeAction(): Boolean { + private fun shouldShowExpungeAction(): Boolean { val account = this.account ?: return false return account.expungePolicy == Expunge.EXPUNGE_MANUALLY && messagingController.supportsExpunge(account) } @@ -1445,7 +1486,7 @@ class MessageListFragment : } } - val isRemoteSearchAllowed: Boolean + private val isRemoteSearchAllowed: Boolean get() = isManualSearch && !isRemoteSearch && isSingleFolderMode && messagingController.isPushCapable(account) fun onSearchRequested(query: String): Boolean { @@ -1453,7 +1494,7 @@ class MessageListFragment : return fragmentListener.startSearch(query, account, folderId) } - fun setMessageList(messageListInfo: MessageListInfo) { + private fun setMessageList(messageListInfo: MessageListInfo) { val messageListItems = messageListInfo.messageListItems if (isThreadDisplay && messageListItems.isEmpty()) { handler.goBack() @@ -1564,7 +1605,7 @@ class MessageListFragment : } } - val isMarkAllAsReadSupported: Boolean + private val isMarkAllAsReadSupported: Boolean get() = isSingleAccountMode && isSingleFolderMode && !isOutbox fun confirmMarkAllAsRead() { @@ -1595,7 +1636,7 @@ class MessageListFragment : requireActivity().invalidateMenu() } - val isCheckMailSupported: Boolean + private val isCheckMailSupported: Boolean get() = allAccounts || !isSingleAccountMode || !isSingleFolderMode || isRemoteFolder private val isCheckMailAllowed: Boolean -- GitLab From 9f45fe1d460c9a5c7a01a9f41f8087b38bf5c15d Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 19 Jul 2022 15:13:29 +0200 Subject: [PATCH 06/47] Refactor `MessageListFragment.onOptionsItemSelected()` --- .../fsck/k9/fragment/MessageListFragment.kt | 55 +++++-------------- 1 file changed, 13 insertions(+), 42 deletions(-) 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 b23bb8426a..16a204640b 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 @@ -745,50 +745,21 @@ class MessageListFragment : } override fun onOptionsItemSelected(item: MenuItem): Boolean { - val id = item.itemId - if (id == R.id.set_sort_date) { - changeSort(SortType.SORT_DATE) - return true - } else if (id == R.id.set_sort_arrival) { - changeSort(SortType.SORT_ARRIVAL) - return true - } else if (id == R.id.set_sort_subject) { - changeSort(SortType.SORT_SUBJECT) - return true - } else if (id == R.id.set_sort_sender) { - changeSort(SortType.SORT_SENDER) - return true - } else if (id == R.id.set_sort_flag) { - changeSort(SortType.SORT_FLAGGED) - return true - } else if (id == R.id.set_sort_unread) { - changeSort(SortType.SORT_UNREAD) - return true - } else if (id == R.id.set_sort_attach) { - changeSort(SortType.SORT_ATTACHMENT) - return true - } else if (id == R.id.select_all) { - selectAll() - return true - } - - if (!isSingleAccountMode) { - // None of the options after this point are "safe" for search results - // TODO: This is not true for "unread" and "starred" searches in regular folders - return false + when (item.itemId) { + R.id.set_sort_date -> changeSort(SortType.SORT_DATE) + R.id.set_sort_arrival -> changeSort(SortType.SORT_ARRIVAL) + R.id.set_sort_subject -> changeSort(SortType.SORT_SUBJECT) + R.id.set_sort_sender -> changeSort(SortType.SORT_SENDER) + R.id.set_sort_flag -> changeSort(SortType.SORT_FLAGGED) + R.id.set_sort_unread -> changeSort(SortType.SORT_UNREAD) + R.id.set_sort_attach -> changeSort(SortType.SORT_ATTACHMENT) + R.id.select_all -> selectAll() + R.id.send_messages -> onSendPendingMessages() + R.id.expunge -> onExpunge() + else -> return super.onOptionsItemSelected(item) } - if (id == R.id.send_messages) { - onSendPendingMessages() - return true - } else if (id == R.id.expunge) { - currentFolder?.let { folderInfoHolder -> - onExpunge(account, folderInfoHolder.databaseId) - } - return true - } else { - return super.onOptionsItemSelected(item) - } + return true } fun onSendPendingMessages() { -- GitLab From 42daa056fd0a9f1a62a67fe5c594bbd57e0072e5 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 19 Jul 2022 15:32:27 +0200 Subject: [PATCH 07/47] Move message list menu item handling code to `MessageListFragment` --- .../java/com/fsck/k9/activity/MessageList.kt | 58 +------------------ .../fsck/k9/fragment/MessageListFragment.kt | 31 +++++----- 2 files changed, 15 insertions(+), 74 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 dde63fa0d2..b56b7ecc98 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 @@ -26,7 +26,6 @@ import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.commit import androidx.lifecycle.Observer import com.fsck.k9.Account -import com.fsck.k9.Account.SortType import com.fsck.k9.K9 import com.fsck.k9.K9.SplitViewMode import com.fsck.k9.Preferences @@ -917,45 +916,12 @@ open class MessageList : goBack() } return true - } else if (id == R.id.compose) { - messageListFragment!!.onCompose() - return true } else if (id == R.id.toggle_message_view_theme) { onToggleTheme() return true - } else if (id == R.id.set_sort_date) { // MessageList - messageListFragment!!.changeSort(SortType.SORT_DATE) - return true - } else if (id == R.id.set_sort_arrival) { - messageListFragment!!.changeSort(SortType.SORT_ARRIVAL) - return true - } else if (id == R.id.set_sort_subject) { - messageListFragment!!.changeSort(SortType.SORT_SUBJECT) - return true - } else if (id == R.id.set_sort_sender) { - messageListFragment!!.changeSort(SortType.SORT_SENDER) - return true - } else if (id == R.id.set_sort_flag) { - messageListFragment!!.changeSort(SortType.SORT_FLAGGED) - return true - } else if (id == R.id.set_sort_unread) { - messageListFragment!!.changeSort(SortType.SORT_UNREAD) - return true - } else if (id == R.id.set_sort_attach) { - messageListFragment!!.changeSort(SortType.SORT_ATTACHMENT) - return true - } else if (id == R.id.select_all) { - messageListFragment!!.selectAll() - return true - } else if (id == R.id.search_remote) { - messageListFragment!!.onRemoteSearch() - return true } else if (id == R.id.search_everywhere) { searchEverywhere() return true - } else if (id == R.id.mark_all_as_read) { - messageListFragment!!.confirmMarkAllAsRead() - return true } else if (id == R.id.next_message) { // MessageView showNextMessage() return true @@ -1009,29 +975,7 @@ open class MessageList : return true } - if (!singleFolderMode) { - // None of the options after this point are "safe" for search results - // TODO: This is not true for "unread" and "starred" searches in regular folders - return false - } - - return when (id) { - R.id.send_messages -> { - messageListFragment!!.onSendPendingMessages() - true - } - R.id.expunge -> { - messageListFragment!!.onExpunge() - true - } - R.id.empty_trash -> { - messageListFragment!!.onEmptyTrash() - true - } - else -> { - super.onOptionsItemSelected(item) - } - } + return super.onOptionsItemSelected(item) } private fun searchEverywhere() { 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 16a204640b..ed72925398 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 @@ -115,10 +115,8 @@ class MessageListFragment : private set var isSingleAccountMode = false private set - var isSingleFolderMode = false - private set - var isRemoteSearch = false - private set + private var isSingleFolderMode = false + private var isRemoteSearch = false private val isUnifiedInbox: Boolean get() = localSearch.id == SearchAccount.UNIFIED_INBOX @@ -130,8 +128,7 @@ class MessageListFragment : * `true` after [.onCreate] was executed. Used in [.updateTitle] to * make sure we don't access member variables before initialization is complete. */ - var isInitialized = false - private set + private var isInitialized = false private var isListVisible = false @@ -507,7 +504,7 @@ class MessageListFragment : } } - fun changeSort(sortType: SortType) { + private fun changeSort(sortType: SortType) { val sortAscending = if (this.sortType == sortType) !sortAscending else null changeSort(sortType, sortAscending) } @@ -629,17 +626,13 @@ class MessageListFragment : } } - fun onExpunge() { + private fun onExpunge() { currentFolder?.let { folderInfoHolder -> - onExpunge(account, folderInfoHolder.databaseId) + messagingController.expunge(account, folderInfoHolder.databaseId) } } - private fun onExpunge(account: Account?, folderId: Long) { - messagingController.expunge(account, folderId) - } - - fun onEmptyTrash() { + private fun onEmptyTrash() { if (isShowingTrashFolder) { showDialog(R.id.dialog_confirm_empty_trash) } @@ -746,6 +739,8 @@ class MessageListFragment : override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { + R.id.search_remote -> onRemoteSearch() + R.id.compose -> onCompose() R.id.set_sort_date -> changeSort(SortType.SORT_DATE) R.id.set_sort_arrival -> changeSort(SortType.SORT_ARRIVAL) R.id.set_sort_subject -> changeSort(SortType.SORT_SUBJECT) @@ -754,7 +749,9 @@ class MessageListFragment : R.id.set_sort_unread -> changeSort(SortType.SORT_UNREAD) R.id.set_sort_attach -> changeSort(SortType.SORT_ATTACHMENT) R.id.select_all -> selectAll() + R.id.mark_all_as_read -> confirmMarkAllAsRead() R.id.send_messages -> onSendPendingMessages() + R.id.empty_trash -> onEmptyTrash() R.id.expunge -> onExpunge() else -> return super.onOptionsItemSelected(item) } @@ -762,7 +759,7 @@ class MessageListFragment : return true } - fun onSendPendingMessages() { + private fun onSendPendingMessages() { messagingController.sendPendingMessages(account, null) } @@ -1448,7 +1445,7 @@ class MessageListFragment : return account.expungePolicy == Expunge.EXPUNGE_MANUALLY && messagingController.supportsExpunge(account) } - fun onRemoteSearch() { + private fun onRemoteSearch() { // Remote search is useless without the network. if (hasConnectivity == true) { onRemoteSearchRequested() @@ -1579,7 +1576,7 @@ class MessageListFragment : private val isMarkAllAsReadSupported: Boolean get() = isSingleAccountMode && isSingleFolderMode && !isOutbox - fun confirmMarkAllAsRead() { + private fun confirmMarkAllAsRead() { if (K9.isConfirmMarkAllRead) { showDialog(R.id.dialog_confirm_mark_all_as_read) } else { -- GitLab From 48847e17eaecc519e31ca84ed91e270b368cf17f Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 19 Jul 2022 17:48:42 +0200 Subject: [PATCH 08/47] Rename .java to .kt --- .../{MessageViewFragment.java => MessageViewFragment.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/{MessageViewFragment.java => MessageViewFragment.kt} (100%) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt similarity index 100% rename from app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java rename to app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt -- GitLab From 87dbccdb51bd7421cae96b57f7647e0dade7d9be Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 19 Jul 2022 17:48:42 +0200 Subject: [PATCH 09/47] Convert `MessageViewFragment` to Kotlin --- .../ui/messageview/AttachmentController.java | 4 +- .../k9/ui/messageview/MessageViewFragment.kt | 1233 ++++++++--------- 2 files changed, 576 insertions(+), 661 deletions(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java index fab62cbab3..7eaf416782 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java @@ -40,9 +40,9 @@ public class AttachmentController { private final AttachmentViewInfo attachment; - AttachmentController(MessagingController controller, MessageViewFragment messageViewFragment, + AttachmentController(Context context, MessagingController controller, MessageViewFragment messageViewFragment, AttachmentViewInfo attachment) { - this.context = messageViewFragment.getApplicationContext(); + this.context = context; this.controller = controller; this.messageViewFragment = messageViewFragment; this.attachment = attachment; diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt index 81b1fd019e..b88fee3d4b 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt @@ -1,893 +1,808 @@ -package com.fsck.k9.ui.messageview; - - -import java.util.Collections; -import java.util.List; -import java.util.Locale; - -import android.app.Activity; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.content.IntentSender; -import android.content.IntentSender.SendIntentException; -import android.os.Bundle; -import android.os.Handler; -import android.os.Parcelable; -import android.os.SystemClock; - -import androidx.annotation.Nullable; -import androidx.fragment.app.DialogFragment; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.appcompat.widget.PopupMenu.OnMenuItemClickListener; -import android.text.TextUtils; -import android.view.ContextThemeWrapper; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.view.inputmethod.InputMethodManager; -import android.widget.Toast; - -import com.fsck.k9.Account; -import com.fsck.k9.DI; -import com.fsck.k9.K9; -import com.fsck.k9.Preferences; -import com.fsck.k9.activity.MessageCompose; -import com.fsck.k9.helper.MailtoUnsubscribeUri; -import com.fsck.k9.helper.UnsubscribeUri; -import com.fsck.k9.ui.choosefolder.ChooseFolderActivity; -import com.fsck.k9.activity.MessageLoaderHelper; -import com.fsck.k9.activity.MessageLoaderHelper.MessageLoaderCallbacks; -import com.fsck.k9.activity.MessageLoaderHelperFactory; -import com.fsck.k9.controller.MessageReference; -import com.fsck.k9.controller.MessagingController; -import com.fsck.k9.fragment.AttachmentDownloadDialogFragment; -import com.fsck.k9.fragment.ConfirmationDialogFragment; -import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener; -import com.fsck.k9.mail.Flag; -import com.fsck.k9.mailstore.AttachmentViewInfo; -import com.fsck.k9.mailstore.LocalMessage; -import com.fsck.k9.mailstore.MessageViewInfo; -import com.fsck.k9.ui.R; -import com.fsck.k9.ui.base.ThemeManager; -import com.fsck.k9.ui.messageview.CryptoInfoDialog.OnClickShowCryptoKeyListener; -import com.fsck.k9.ui.messageview.MessageCryptoPresenter.MessageCryptoMvpView; -import com.fsck.k9.ui.settings.account.AccountSettingsActivity; -import com.fsck.k9.ui.share.ShareIntentBuilder; -import com.fsck.k9.view.MessageCryptoDisplayStatus; -import timber.log.Timber; - - -public class MessageViewFragment extends Fragment implements ConfirmationDialogFragmentListener, - AttachmentViewCallback, OnClickShowCryptoKeyListener { - - private static final String ARG_REFERENCE = "reference"; - - private static final int ACTIVITY_CHOOSE_FOLDER_MOVE = 1; - private static final int ACTIVITY_CHOOSE_FOLDER_COPY = 2; - private static final int REQUEST_CODE_CREATE_DOCUMENT = 3; - - public static final int REQUEST_MASK_LOADER_HELPER = (1 << 8); - public static final int REQUEST_MASK_CRYPTO_PRESENTER = (1 << 9); - - public static final int PROGRESS_THRESHOLD_MILLIS = 500 * 1000; - - public static MessageViewFragment newInstance(MessageReference reference) { - MessageViewFragment fragment = new MessageViewFragment(); - - Bundle args = new Bundle(); - args.putString(ARG_REFERENCE, reference.toIdentityString()); - fragment.setArguments(args); - - return fragment; - } - - private final ThemeManager themeManager = DI.get(ThemeManager.class); - private final MessageLoaderHelperFactory messageLoaderHelperFactory = DI.get(MessageLoaderHelperFactory.class); - - private MessageTopView mMessageView; - - private Account mAccount; - private MessageReference mMessageReference; - private LocalMessage mMessage; - private MessagingController mController; - private Handler handler = new Handler(); - private MessageLoaderHelper messageLoaderHelper; - private MessageCryptoPresenter messageCryptoPresenter; - private Long showProgressThreshold; - private UnsubscribeUri preferredUnsubscribeUri; +package com.fsck.k9.ui.messageview + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.content.IntentSender +import android.content.IntentSender.SendIntentException +import android.os.Bundle +import android.os.Parcelable +import android.os.SystemClock +import android.view.ContextThemeWrapper +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager +import android.widget.Toast +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.Fragment +import com.fsck.k9.Account +import com.fsck.k9.K9 +import com.fsck.k9.activity.MessageCompose +import com.fsck.k9.activity.MessageLoaderHelper +import com.fsck.k9.activity.MessageLoaderHelper.MessageLoaderCallbacks +import com.fsck.k9.activity.MessageLoaderHelperFactory +import com.fsck.k9.controller.MessageReference +import com.fsck.k9.controller.MessagingController +import com.fsck.k9.fragment.AttachmentDownloadDialogFragment +import com.fsck.k9.fragment.ConfirmationDialogFragment +import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener +import com.fsck.k9.helper.HttpsUnsubscribeUri +import com.fsck.k9.helper.MailtoUnsubscribeUri +import com.fsck.k9.helper.UnsubscribeUri +import com.fsck.k9.mail.Flag +import com.fsck.k9.mailstore.AttachmentViewInfo +import com.fsck.k9.mailstore.LocalMessage +import com.fsck.k9.mailstore.MessageViewInfo +import com.fsck.k9.preferences.AccountManager +import com.fsck.k9.ui.R +import com.fsck.k9.ui.base.ThemeManager +import com.fsck.k9.ui.choosefolder.ChooseFolderActivity +import com.fsck.k9.ui.messageview.CryptoInfoDialog.OnClickShowCryptoKeyListener +import com.fsck.k9.ui.messageview.MessageCryptoPresenter.MessageCryptoMvpView +import com.fsck.k9.ui.settings.account.AccountSettingsActivity +import com.fsck.k9.ui.share.ShareIntentBuilder +import com.fsck.k9.ui.withArguments +import com.fsck.k9.view.MessageCryptoDisplayStatus +import java.util.Locale +import org.koin.android.ext.android.inject +import timber.log.Timber + +class MessageViewFragment : + Fragment(), + ConfirmationDialogFragmentListener, + AttachmentViewCallback, + OnClickShowCryptoKeyListener { + + private val themeManager: ThemeManager by inject() + private val messageLoaderHelperFactory: MessageLoaderHelperFactory by inject() + private val accountManager: AccountManager by inject() + private val messagingController: MessagingController by inject() + private val shareIntentBuilder: ShareIntentBuilder by inject() + + private lateinit var messageTopView: MessageTopView + + private var message: LocalMessage? = null + private lateinit var messageLoaderHelper: MessageLoaderHelper + private lateinit var messageCryptoPresenter: MessageCryptoPresenter + private var showProgressThreshold: Long? = null + private var preferredUnsubscribeUri: UnsubscribeUri? = null /** * Used to temporarily store the destination folder for refile operations if a confirmation * dialog is shown. */ - private Long destinationFolderId; + private var destinationFolderId: Long? = null + private lateinit var fragmentListener: MessageViewFragmentListener - private MessageViewFragmentListener mFragmentListener; + private lateinit var account: Account + lateinit var messageReference: MessageReference /** - * {@code true} after {@link #onCreate(Bundle)} has been executed. This is used by - * {@code MessageList.configureMenu()} to make sure the fragment has been initialized before - * it is used. + * `true` after [.onCreate] has been executed. This is used by `MessageList.configureMenu()` to make sure the + * fragment has been initialized before it is used. */ - private boolean mInitialized = false; + var isInitialized = false + private set - private Context mContext; + private var currentAttachmentViewInfo: AttachmentViewInfo? = null - private AttachmentViewInfo currentAttachmentViewInfo; + override fun onAttach(context: Context) { + super.onAttach(context) - @Override - public void onAttach(Context context) { - super.onAttach(context); - - mContext = context.getApplicationContext(); - - try { - mFragmentListener = (MessageViewFragmentListener) getActivity(); - } catch (ClassCastException e) { - throw new ClassCastException("This fragment must be attached to a MessageViewFragmentListener"); + fragmentListener = try { + activity as MessageViewFragmentListener + } catch (e: ClassCastException) { + throw ClassCastException("This fragment must be attached to a MessageViewFragmentListener") } } - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) - // This fragments adds options to the action bar - setHasOptionsMenu(true); + setHasOptionsMenu(true) - Context context = getActivity().getApplicationContext(); - mController = MessagingController.getInstance(context); - messageCryptoPresenter = new MessageCryptoPresenter(messageCryptoMvpView); + messageCryptoPresenter = MessageCryptoPresenter(messageCryptoMvpView) messageLoaderHelper = messageLoaderHelperFactory.createForMessageView( - context, getLoaderManager(), getParentFragmentManager(), messageLoaderCallbacks); - mInitialized = true; - } + context = requireContext().applicationContext, + loaderManager = loaderManager, + fragmentManager = parentFragmentManager, + callback = messageLoaderCallbacks + ) - @Override - public void onResume() { - super.onResume(); - - messageCryptoPresenter.onResume(); + isInitialized = true } - @Override - public void onDestroy() { - super.onDestroy(); + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val messageViewThemeResourceId = themeManager.messageViewThemeResourceId + val themedContext = ContextThemeWrapper(inflater.context, messageViewThemeResourceId) + val layoutInflater = LayoutInflater.from(themedContext) - Activity activity = getActivity(); - boolean isChangingConfigurations = activity != null && activity.isChangingConfigurations(); - if (isChangingConfigurations) { - messageLoaderHelper.onDestroyChangingConfigurations(); - return; - } + val view = layoutInflater.inflate(R.layout.message, container, false) + messageTopView = view.findViewById(R.id.message_view) - messageLoaderHelper.onDestroy(); - } + initializeMessageTopView(messageTopView) - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - int messageViewThemeResourceId = themeManager.getMessageViewThemeResourceId(); - Context context = new ContextThemeWrapper(inflater.getContext(), messageViewThemeResourceId); - LayoutInflater layoutInflater = LayoutInflater.from(context); - View view = layoutInflater.inflate(R.layout.message, container, false); + return view + } - mMessageView = view.findViewById(R.id.message_view); - mMessageView.setAttachmentCallback(this); - mMessageView.setMessageCryptoPresenter(messageCryptoPresenter); + private fun initializeMessageTopView(messageTopView: MessageTopView) { + messageTopView.setAttachmentCallback(this) + messageTopView.setMessageCryptoPresenter(messageCryptoPresenter) - mMessageView.setOnToggleFlagClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - onToggleFlagged(); - } - }); - - mMessageView.setOnMenuItemClickListener(new OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - int id = item.getItemId(); - if (id == R.id.reply) { - onReply(); - return true; - } else if (id == R.id.reply_all) { - onReplyAll(); - return true; - } else if (id == R.id.forward) { - onForward(); - return true; - } else if (id == R.id.forward_as_attachment) { - onForwardAsAttachment(); - return true; - } else if (id == R.id.share) { - onSendAlternate(); - return true; - } - return false; - } - }); + messageTopView.setOnToggleFlagClickListener { + onToggleFlagged() + } - mMessageView.setOnDownloadButtonClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - mMessageView.disableDownloadButton(); - messageLoaderHelper.downloadCompleteMessage(); - } - }); + messageTopView.setOnMenuItemClickListener { item -> + onReplyMenuItemClicked(item.itemId) + } - return view; + messageTopView.setOnDownloadButtonClickListener { + onDownloadButtonClicked() + } } - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) - Bundle arguments = getArguments(); - String messageReferenceString = arguments.getString(ARG_REFERENCE); - MessageReference messageReference = MessageReference.parse(messageReferenceString); + val messageReference = MessageReference.parse(arguments?.getString(ARG_REFERENCE)) + ?: error("Invalid argument '$ARG_REFERENCE'") - displayMessage(messageReference); + displayMessage(messageReference) } - private void displayMessage(MessageReference messageReference) { - mMessageReference = messageReference; - Timber.d("MessageView displaying message %s", mMessageReference); + private fun displayMessage(messageReference: MessageReference) { + Timber.d("MessageViewFragment displaying message %s", messageReference) + + this.messageReference = messageReference + account = accountManager.getAccount(messageReference.accountUuid) + ?: error("Account ${messageReference.accountUuid} not found") - mAccount = Preferences.getPreferences(getApplicationContext()).getAccount(mMessageReference.getAccountUuid()); - messageLoaderHelper.asyncStartOrResumeLoadingMessage(messageReference, null); + messageLoaderHelper.asyncStartOrResumeLoadingMessage(messageReference, null) - invalidateMenu(); + invalidateMenu() } - private void hideKeyboard() { - Activity activity = getActivity(); - if (activity == null) { - return; - } - InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); - View decorView = activity.getWindow().getDecorView(); - if (decorView != null) { - imm.hideSoftInputFromWindow(decorView.getApplicationWindowToken(), 0); + override fun onResume() { + super.onResume() + messageCryptoPresenter.onResume() + } + + override fun onDestroy() { + super.onDestroy() + + if (requireActivity().isChangingConfigurations) { + messageLoaderHelper.onDestroyChangingConfigurations() + } else { + messageLoaderHelper.onDestroy() } } - private void showMessage(MessageViewInfo messageViewInfo) { - hideKeyboard(); + private fun showMessage(messageViewInfo: MessageViewInfo) { + hideKeyboard() + + val handledByCryptoPresenter = messageCryptoPresenter.maybeHandleShowMessage( + messageTopView, account, messageViewInfo + ) - boolean handledByCryptoPresenter = messageCryptoPresenter.maybeHandleShowMessage( - mMessageView, mAccount, messageViewInfo); if (!handledByCryptoPresenter) { - mMessageView.showMessage(mAccount, messageViewInfo); - if (mAccount.isOpenPgpProviderConfigured()) { - mMessageView.getMessageHeaderView().setCryptoStatusDisabled(); + messageTopView.showMessage(account, messageViewInfo) + + if (account.isOpenPgpProviderConfigured) { + messageTopView.messageHeaderView.setCryptoStatusDisabled() } else { - mMessageView.getMessageHeaderView().hideCryptoStatus(); + messageTopView.messageHeaderView.hideCryptoStatus() } } if (messageViewInfo.subject != null) { - displaySubject(messageViewInfo.subject); + displaySubject(messageViewInfo.subject) } } - private void displayHeaderForLoadingMessage(LocalMessage message) { - boolean showStar = !isOutbox(); - mMessageView.setHeaders(message, mAccount, showStar); - if (mAccount.isOpenPgpProviderConfigured()) { - mMessageView.getMessageHeaderView().setCryptoStatusLoading(); + private fun hideKeyboard() { + val activity = activity ?: return + + val inputMethodManager = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + val decorView = activity.window.decorView + inputMethodManager.hideSoftInputFromWindow(decorView.applicationWindowToken, 0) + } + + private fun displayHeaderForLoadingMessage(message: LocalMessage) { + val showStar = !isOutbox + messageTopView.setHeaders(message, account, showStar) + + if (account.isOpenPgpProviderConfigured) { + messageTopView.messageHeaderView.setCryptoStatusLoading() } - displaySubject(message.getSubject()); - invalidateMenu(); + + displaySubject(message.subject) + invalidateMenu() } - private void displaySubject(String subject) { - if (TextUtils.isEmpty(subject)) { - subject = mContext.getString(R.string.general_no_subject); + private fun displaySubject(subject: String) { + val displaySubject = subject.ifEmpty { getString(R.string.general_no_subject) } + messageTopView.setSubject(displaySubject) + } + + private fun onReplyMenuItemClicked(itemId: Int): Boolean { + when (itemId) { + R.id.reply -> onReply() + R.id.reply_all -> onReplyAll() + R.id.forward -> onForward() + R.id.forward_as_attachment -> onForwardAsAttachment() + R.id.share -> onSendAlternate() + else -> error("Missing handler for reply menu item $itemId") } - mMessageView.setSubject(subject); + return true + } + + private fun onDownloadButtonClicked() { + messageTopView.disableDownloadButton() + messageLoaderHelper.downloadCompleteMessage() } /** * Called from UI thread when user select Delete */ - public void onDelete() { - if (K9.isConfirmDelete() || (K9.isConfirmDeleteStarred() && mMessage.isSet(Flag.FLAGGED))) { - showDialog(R.id.dialog_confirm_delete); + fun onDelete() { + val message = checkNotNull(message) + + if (K9.isConfirmDelete || K9.isConfirmDeleteStarred && message.isSet(Flag.FLAGGED)) { + showDialog(R.id.dialog_confirm_delete) } else { - delete(); + delete() } } - private void delete() { - if (mMessage != null) { - // Disable the delete button after it's tapped (to try to prevent - // accidental clicks) - mFragmentListener.disableDeleteAction(); - LocalMessage messageToDelete = mMessage; - mFragmentListener.showNextMessageOrReturn(); - mController.deleteMessage(mMessageReference); - } + private fun delete() { + // Disable the delete button after it has been tapped (to try to prevent accidental clicks) + fragmentListener.disableDeleteAction() + + fragmentListener.showNextMessageOrReturn() + + messagingController.deleteMessage(messageReference) } - public void onRefile(Long dstFolderId) { - if (dstFolderId == null || !mController.isMoveCapable(mAccount)) { - return; + private fun onRefile(destinationFolderId: Long?) { + if (destinationFolderId == null || !messagingController.isMoveCapable(account)) { + return } - if (!mController.isMoveCapable(mMessageReference)) { - Toast toast = Toast.makeText(getActivity(), R.string.move_copy_cannot_copy_unsynced_message, Toast.LENGTH_LONG); - toast.show(); - return; + + if (!messagingController.isMoveCapable(messageReference)) { + Toast.makeText(activity, R.string.move_copy_cannot_copy_unsynced_message, Toast.LENGTH_LONG).show() + return } - if (dstFolderId.equals(mAccount.getSpamFolderId()) && K9.isConfirmSpam()) { - destinationFolderId = dstFolderId; - showDialog(R.id.dialog_confirm_spam); + if (destinationFolderId == account.spamFolderId && K9.isConfirmSpam) { + this.destinationFolderId = destinationFolderId + showDialog(R.id.dialog_confirm_spam) } else { - refileMessage(dstFolderId); + refileMessage(destinationFolderId) } } - private void refileMessage(long dstFolderId) { - long srcFolderId = mMessageReference.getFolderId(); - MessageReference messageToMove = mMessageReference; - mFragmentListener.showNextMessageOrReturn(); - mController.moveMessage(mAccount, srcFolderId, messageToMove, dstFolderId); - } + private fun refileMessage(destinationFolderId: Long) { + fragmentListener.showNextMessageOrReturn() - public void onReply() { - if (mMessage != null) { - mFragmentListener.onReply(mMessage.makeMessageReference(), messageCryptoPresenter.getDecryptionResultForReply()); - } + val sourceFolderId = messageReference.folderId + messagingController.moveMessage(account, sourceFolderId, messageReference, destinationFolderId) } - public void onReplyAll() { - if (mMessage != null) { - mFragmentListener.onReplyAll(mMessage.makeMessageReference(), messageCryptoPresenter.getDecryptionResultForReply()); - } + fun onReply() { + val message = this.message ?: return + + fragmentListener.onReply( + messageReference = message.makeMessageReference(), + decryptionResultForReply = messageCryptoPresenter.decryptionResultForReply + ) } - public void onForward() { - if (mMessage != null) { - mFragmentListener.onForward(mMessage.makeMessageReference(), messageCryptoPresenter.getDecryptionResultForReply()); - } + fun onReplyAll() { + val message = checkNotNull(this.message) + + fragmentListener.onReplyAll( + messageReference = message.makeMessageReference(), + decryptionResultForReply = messageCryptoPresenter.decryptionResultForReply + ) } - public void onForwardAsAttachment() { - if (mMessage != null) { - mFragmentListener.onForwardAsAttachment(mMessage.makeMessageReference(), messageCryptoPresenter.getDecryptionResultForReply()); - } + fun onForward() { + val message = checkNotNull(this.message) + + fragmentListener.onForward( + messageReference = message.makeMessageReference(), + decryptionResultForReply = messageCryptoPresenter.decryptionResultForReply + ) } - public void onEditAsNewMessage() { - if (mMessage != null) { - mFragmentListener.onEditAsNewMessage(mMessage.makeMessageReference()); - } + fun onForwardAsAttachment() { + val message = checkNotNull(this.message) + + fragmentListener.onForwardAsAttachment( + messageReference = message.makeMessageReference(), + decryptionResultForReply = messageCryptoPresenter.decryptionResultForReply + ) } - public void onToggleFlagged() { - if (mMessage != null && !isOutbox()) { - boolean newState = !mMessage.isSet(Flag.FLAGGED); - mController.setFlag(mAccount, mMessage.getFolder().getDatabaseId(), - Collections.singletonList(mMessage), Flag.FLAGGED, newState); - mMessageView.setHeaders(mMessage, mAccount, true); - } + fun onEditAsNewMessage() { + val message = checkNotNull(this.message) + + fragmentListener.onEditAsNewMessage(message.makeMessageReference()) } - public void onMove() { - if ((!mController.isMoveCapable(mAccount)) - || (mMessage == null)) { - return; - } - if (!mController.isMoveCapable(mMessageReference)) { - Toast toast = Toast.makeText(getActivity(), R.string.move_copy_cannot_copy_unsynced_message, Toast.LENGTH_LONG); - toast.show(); - return; - } + fun onMove() { + check(messagingController.isMoveCapable(account)) + checkNotNull(message) - startRefileActivity(FolderOperation.MOVE, ACTIVITY_CHOOSE_FOLDER_MOVE); + if (!messagingController.isMoveCapable(messageReference)) { + Toast.makeText(activity, R.string.move_copy_cannot_copy_unsynced_message, Toast.LENGTH_LONG).show() + return + } + startRefileActivity(FolderOperation.MOVE, ACTIVITY_CHOOSE_FOLDER_MOVE) } - public void onCopy() { - if ((!mController.isCopyCapable(mAccount)) - || (mMessage == null)) { - return; - } - if (!mController.isCopyCapable(mMessageReference)) { - Toast toast = Toast.makeText(getActivity(), R.string.move_copy_cannot_copy_unsynced_message, Toast.LENGTH_LONG); - toast.show(); - return; + fun onCopy() { + check(messagingController.isCopyCapable(account)) + checkNotNull(message) + + if (!messagingController.isCopyCapable(messageReference)) { + Toast.makeText(activity, R.string.move_copy_cannot_copy_unsynced_message, Toast.LENGTH_LONG).show() + return } - startRefileActivity(FolderOperation.COPY, ACTIVITY_CHOOSE_FOLDER_COPY); + startRefileActivity(FolderOperation.COPY, ACTIVITY_CHOOSE_FOLDER_COPY) } - public void onMoveToDrafts() { - Account account = mAccount; - long folderId = mMessageReference.getFolderId(); - List messages = Collections.singletonList(mMessageReference); + fun onMoveToDrafts() { + fragmentListener.showNextMessageOrReturn() - mFragmentListener.showNextMessageOrReturn(); - - mController.moveToDraftsFolder(account, folderId, messages); + val account = account + val folderId = messageReference.folderId + val messages = listOf(messageReference) + messagingController.moveToDraftsFolder(account, folderId, messages) } - public void onArchive() { - onRefile(mAccount.getArchiveFolderId()); + fun onArchive() { + onRefile(account.archiveFolderId) } - public void onSpam() { - onRefile(mAccount.getSpamFolderId()); + fun onSpam() { + onRefile(account.spamFolderId) } - private void startRefileActivity(FolderOperation operation, int requestCode) { - String accountUuid = mAccount.getUuid(); - long currentFolderId = mMessageReference.getFolderId(); - Long scrollToFolderId = mAccount.getLastSelectedFolderId(); - final ChooseFolderActivity.Action action; - if (operation == FolderOperation.MOVE) { - action = ChooseFolderActivity.Action.MOVE; + private fun startRefileActivity(operation: FolderOperation, requestCode: Int) { + val action = if (operation == FolderOperation.MOVE) { + ChooseFolderActivity.Action.MOVE } else { - action = ChooseFolderActivity.Action.COPY; + ChooseFolderActivity.Action.COPY } - Intent intent = ChooseFolderActivity.buildLaunchIntent(requireActivity(), action, accountUuid, currentFolderId, - scrollToFolderId, false, mMessageReference); + val intent = ChooseFolderActivity.buildLaunchIntent( + context = requireActivity(), + action = action, + accountUuid = account.uuid, + currentFolderId = messageReference.folderId, + scrollToFolderId = account.lastSelectedFolderId, + showDisplayableOnly = false, + messageReference = messageReference + ) - startActivityForResult(intent, requestCode); + startActivityForResult(intent, requestCode) } - public void onPendingIntentResult(int requestCode, int resultCode, Intent data) { - if ((requestCode & REQUEST_MASK_LOADER_HELPER) == REQUEST_MASK_LOADER_HELPER) { - requestCode ^= REQUEST_MASK_LOADER_HELPER; - messageLoaderHelper.onActivityResult(requestCode, resultCode, data); - return; + fun onPendingIntentResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode and REQUEST_MASK_LOADER_HELPER == REQUEST_MASK_LOADER_HELPER) { + val maskedRequestCode = requestCode xor REQUEST_MASK_LOADER_HELPER + messageLoaderHelper.onActivityResult(maskedRequestCode, resultCode, data) + } else if (requestCode and REQUEST_MASK_CRYPTO_PRESENTER == REQUEST_MASK_CRYPTO_PRESENTER) { + val maskedRequestCode = requestCode xor REQUEST_MASK_CRYPTO_PRESENTER + messageCryptoPresenter.onActivityResult(maskedRequestCode, resultCode, data) } + } - if ((requestCode & REQUEST_MASK_CRYPTO_PRESENTER) == REQUEST_MASK_CRYPTO_PRESENTER) { - requestCode ^= REQUEST_MASK_CRYPTO_PRESENTER; - messageCryptoPresenter.onActivityResult(requestCode, resultCode, data); + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (resultCode != Activity.RESULT_OK) return + + when (requestCode) { + REQUEST_CODE_CREATE_DOCUMENT -> onCreateDocumentResult(data) + ACTIVITY_CHOOSE_FOLDER_MOVE -> onChooseFolderMoveResult(data) + ACTIVITY_CHOOSE_FOLDER_COPY -> onChooseFolderCopyResult(data) } } - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (resultCode != Activity.RESULT_OK) { - return; + private fun onCreateDocumentResult(data: Intent?) { + if (data != null && data.data != null) { + createAttachmentController(currentAttachmentViewInfo).saveAttachmentTo(data.data) } + } - // Note: because fragments do not have a startIntentSenderForResult method, pending intent activities are - // launched through the MessageList activity, and delivered back via onPendingIntentResult() + private fun onChooseFolderMoveResult(data: Intent?) { + if (data == null) return - switch (requestCode) { - case REQUEST_CODE_CREATE_DOCUMENT: { - if (data != null && data.getData() != null) { - getAttachmentController(currentAttachmentViewInfo).saveAttachmentTo(data.getData()); - } - break; - } - case ACTIVITY_CHOOSE_FOLDER_MOVE: - case ACTIVITY_CHOOSE_FOLDER_COPY: { - if (data == null) { - return; - } + val destinationFolderId = data.getLongExtra(ChooseFolderActivity.RESULT_SELECTED_FOLDER_ID, -1L) + val messageReferenceString = data.getStringExtra(ChooseFolderActivity.RESULT_MESSAGE_REFERENCE) + val messageReference = MessageReference.parse(messageReferenceString) + if (this.messageReference != messageReference) return - long destFolderId = data.getLongExtra(ChooseFolderActivity.RESULT_SELECTED_FOLDER_ID, -1L); - String messageReferenceString = data.getStringExtra(ChooseFolderActivity.RESULT_MESSAGE_REFERENCE); - MessageReference ref = MessageReference.parse(messageReferenceString); - if (mMessageReference.equals(ref)) { - mAccount.setLastSelectedFolderId(destFolderId); - switch (requestCode) { - case ACTIVITY_CHOOSE_FOLDER_MOVE: { - mFragmentListener.showNextMessageOrReturn(); - moveMessage(ref, destFolderId); - break; - } - case ACTIVITY_CHOOSE_FOLDER_COPY: { - copyMessage(ref, destFolderId); - break; - } - } - } - break; - } - } - } - - public void onSendAlternate() { - if (mMessage != null) { - ShareIntentBuilder shareIntentBuilder = DI.get(ShareIntentBuilder.class); - Intent shareIntent = shareIntentBuilder.createShareIntent(mMessage); + account.setLastSelectedFolderId(destinationFolderId) - String shareTitle = getString(R.string.send_alternate_chooser_title); - Intent chooserIntent = Intent.createChooser(shareIntent, shareTitle); + fragmentListener.showNextMessageOrReturn() - startActivity(chooserIntent); - } + moveMessage(messageReference, destinationFolderId) } - public void onToggleRead() { - if (mMessage != null && !isOutbox()) { - mController.setFlag(mAccount, mMessage.getFolder().getDatabaseId(), - Collections.singletonList(mMessage), Flag.SEEN, !mMessage.isSet(Flag.SEEN)); + private fun onChooseFolderCopyResult(data: Intent?) { + if (data == null) return - mMessageView.setHeaders(mMessage, mAccount, true); - invalidateMenu(); - } - } + val destinationFolderId = data.getLongExtra(ChooseFolderActivity.RESULT_SELECTED_FOLDER_ID, -1L) + val messageReferenceString = data.getStringExtra(ChooseFolderActivity.RESULT_MESSAGE_REFERENCE) + val messageReference = MessageReference.parse(messageReferenceString) + if (this.messageReference != messageReference) return - private void setProgress(boolean enable) { - if (mFragmentListener != null) { - mFragmentListener.setProgress(enable); - } - } + account.setLastSelectedFolderId(destinationFolderId) - public void moveMessage(MessageReference reference, long folderId) { - mController.moveMessage(mAccount, mMessageReference.getFolderId(), reference, folderId); + copyMessage(messageReference, destinationFolderId) } - public void copyMessage(MessageReference reference, long folderId) { - mController.copyMessage(mAccount, mMessageReference.getFolderId(), reference, folderId); - } + fun onSendAlternate() { + val message = checkNotNull(message) - private void showDialog(int dialogId) { - DialogFragment fragment; - if (dialogId == R.id.dialog_confirm_delete) { - String title = getString(R.string.dialog_confirm_delete_title); - String message = getString(R.string.dialog_confirm_delete_message); - String confirmText = getString(R.string.dialog_confirm_delete_confirm_button); - String cancelText = getString(R.string.dialog_confirm_delete_cancel_button); + val shareIntent = shareIntentBuilder.createShareIntent(message) + val shareTitle = getString(R.string.send_alternate_chooser_title) + val chooserIntent = Intent.createChooser(shareIntent, shareTitle) - fragment = ConfirmationDialogFragment.newInstance(dialogId, title, message, - confirmText, cancelText); - } else if (dialogId == R.id.dialog_confirm_spam) { - String title = getString(R.string.dialog_confirm_spam_title); - String message = getResources().getQuantityString(R.plurals.dialog_confirm_spam_message, 1); - String confirmText = getString(R.string.dialog_confirm_spam_confirm_button); - String cancelText = getString(R.string.dialog_confirm_spam_cancel_button); - - fragment = ConfirmationDialogFragment.newInstance(dialogId, title, message, - confirmText, cancelText); - } else if (dialogId == R.id.dialog_attachment_progress) { - String message = getString(R.string.dialog_attachment_progress_title); - long size = currentAttachmentViewInfo.size; - fragment = AttachmentDownloadDialogFragment.newInstance(size, message); - } else { - throw new RuntimeException("Called showDialog(int) with unknown dialog id."); - } + startActivity(chooserIntent) + } - fragment.setTargetFragment(this, dialogId); - fragment.show(getParentFragmentManager(), getDialogTag(dialogId)); + fun onToggleRead() { + toggleFlag(Flag.SEEN) } - private void removeDialog(int dialogId) { - if (!isAdded()) { - return; - } + fun onToggleFlagged() { + toggleFlag(Flag.FLAGGED) + } - FragmentManager fm = getParentFragmentManager(); + private fun toggleFlag(flag: Flag) { + check(!isOutbox) + val message = checkNotNull(this.message) - // Make sure the "show dialog" transaction has been processed when we call - // findFragmentByTag() below. Otherwise the fragment won't be found and the dialog will - // never be dismissed. - fm.executePendingTransactions(); + val newState = !message.isSet(flag) + messagingController.setFlag(account, message.folder.databaseId, listOf(message), flag, newState) - DialogFragment fragment = (DialogFragment) fm.findFragmentByTag(getDialogTag(dialogId)); + messageTopView.setHeaders(message, account, true) - if (fragment != null) { - fragment.dismissAllowingStateLoss(); - } + invalidateMenu() } - private String getDialogTag(int dialogId) { - return String.format(Locale.US, "dialog-%d", dialogId); + private fun moveMessage(reference: MessageReference?, folderId: Long) { + messagingController.moveMessage(account, messageReference.folderId, reference, folderId) } - public void zoom(KeyEvent event) { - // mMessageView.zoom(event); + private fun copyMessage(reference: MessageReference?, folderId: Long) { + messagingController.copyMessage(account, messageReference.folderId, reference, folderId) } - @Override - public void doPositiveClick(int dialogId) { - if (dialogId == R.id.dialog_confirm_delete) { - delete(); - } else if (dialogId == R.id.dialog_confirm_spam) { - refileMessage(destinationFolderId); - destinationFolderId = null; + private fun showDialog(dialogId: Int) { + val fragment = when (dialogId) { + R.id.dialog_confirm_delete -> { + val title = getString(R.string.dialog_confirm_delete_title) + val message = getString(R.string.dialog_confirm_delete_message) + val confirmText = getString(R.string.dialog_confirm_delete_confirm_button) + val cancelText = getString(R.string.dialog_confirm_delete_cancel_button) + ConfirmationDialogFragment.newInstance( + dialogId, title, message, + confirmText, cancelText + ) + } + R.id.dialog_confirm_spam -> { + val title = getString(R.string.dialog_confirm_spam_title) + val message = resources.getQuantityString(R.plurals.dialog_confirm_spam_message, 1) + val confirmText = getString(R.string.dialog_confirm_spam_confirm_button) + val cancelText = getString(R.string.dialog_confirm_spam_cancel_button) + ConfirmationDialogFragment.newInstance( + dialogId, title, message, + confirmText, cancelText + ) + } + R.id.dialog_attachment_progress -> { + val currentAttachmentViewInfo = checkNotNull(this.currentAttachmentViewInfo) + + val message = getString(R.string.dialog_attachment_progress_title) + val size = currentAttachmentViewInfo.size + AttachmentDownloadDialogFragment.newInstance(size, message) + } + else -> { + throw RuntimeException("Called showDialog(int) with unknown dialog id.") + } } - } - @Override - public void doNegativeClick(int dialogId) { - /* do nothing */ + fragment.setTargetFragment(this, dialogId) + fragment.show(parentFragmentManager, getDialogTag(dialogId)) } - @Override - public void dialogCancelled(int dialogId) { - /* do nothing */ - } + private fun removeDialog(dialogId: Int) { + if (!isAdded) return - /** - * Get the {@link MessageReference} of the currently displayed message. - */ - public MessageReference getMessageReference() { - return mMessageReference; - } + val fragmentManager = parentFragmentManager - public boolean isOutbox() { - if (mMessage == null || mAccount == null) { - return false; - } + // Make sure the "show dialog" transaction has been processed when we call findFragmentByTag() below. + // Otherwise the fragment won't be found and the dialog will never be dismissed. + fragmentManager.executePendingTransactions() - long folderId = mMessage.getFolder().getDatabaseId(); - Long outboxFolderId = mAccount.getOutboxFolderId(); - return outboxFolderId != null && outboxFolderId == folderId; + val fragment = fragmentManager.findFragmentByTag(getDialogTag(dialogId)) as DialogFragment? + fragment?.dismissAllowingStateLoss() } - public boolean isMessageRead() { - return (mMessage != null) && mMessage.isSet(Flag.SEEN); + private fun getDialogTag(dialogId: Int): String { + return String.format(Locale.US, "dialog-%d", dialogId) } - public boolean isCopyCapable() { - return !isOutbox() && mController.isCopyCapable(mAccount); - } + override fun doPositiveClick(dialogId: Int) { + if (dialogId == R.id.dialog_confirm_delete) { + delete() + } else if (dialogId == R.id.dialog_confirm_spam) { + val destinationFolderId = checkNotNull(this.destinationFolderId) - public boolean isMoveCapable() { - return !isOutbox() && mController.isMoveCapable(mAccount); + refileMessage(destinationFolderId) + this.destinationFolderId = null + } } - public boolean canMessageBeArchived() { - Long archiveFolderId = mAccount.getArchiveFolderId(); - if (archiveFolderId == null) { - return false; - } + override fun doNegativeClick(dialogId: Int) = Unit - return mMessageReference.getFolderId() != archiveFolderId; - } + override fun dialogCancelled(dialogId: Int) = Unit - public boolean canMessageBeMovedToSpam() { - Long spamFolderId = mAccount.getSpamFolderId(); - if (spamFolderId == null) { - return false; - } + val isOutbox: Boolean + get() = messageReference.folderId == account.outboxFolderId - return mMessageReference.getFolderId() != spamFolderId; - } + val isMessageRead: Boolean + get() = message?.isSet(Flag.SEEN) == true - public boolean canMessageBeUnsubscribed() { - return preferredUnsubscribeUri != null; - } + val isCopyCapable: Boolean + get() = !isOutbox && messagingController.isCopyCapable(account) - public void onUnsubscribe() { - if (preferredUnsubscribeUri instanceof MailtoUnsubscribeUri) { - Intent intent = new Intent(mContext, MessageCompose.class); - intent.setAction(Intent.ACTION_VIEW); - intent.setData(preferredUnsubscribeUri.getUri()); - intent.putExtra(MessageCompose.EXTRA_ACCOUNT, mMessageReference.getAccountUuid()); - startActivity(intent); - } else { - Intent intent = new Intent(Intent.ACTION_VIEW, preferredUnsubscribeUri.getUri()); - startActivity(intent); - } + val isMoveCapable: Boolean + get() = !isOutbox && messagingController.isMoveCapable(account) + + fun canMessageBeArchived(): Boolean { + val archiveFolderId = account.archiveFolderId ?: return false + return messageReference.folderId != archiveFolderId } - public Context getApplicationContext() { - return mContext; + fun canMessageBeMovedToSpam(): Boolean { + val spamFolderId = account.spamFolderId ?: return false + return messageReference.folderId != spamFolderId } - public void disableAttachmentButtons(AttachmentViewInfo attachment) { - // mMessageView.disableAttachmentButtons(attachment); + fun canMessageBeUnsubscribed(): Boolean { + return preferredUnsubscribeUri != null } - public void enableAttachmentButtons(AttachmentViewInfo attachment) { - // mMessageView.enableAttachmentButtons(attachment); + fun onUnsubscribe() { + val intent = when (val unsubscribeUri = preferredUnsubscribeUri) { + is MailtoUnsubscribeUri -> { + Intent(requireContext(), MessageCompose::class.java).apply { + action = Intent.ACTION_VIEW + data = unsubscribeUri.uri + putExtra(MessageCompose.EXTRA_ACCOUNT, messageReference.accountUuid) + } + } + is HttpsUnsubscribeUri -> { + Intent(Intent.ACTION_VIEW, unsubscribeUri.uri) + } + else -> error("Unknown UnsubscribeUri - $unsubscribeUri") + } + + startActivity(intent) } - public void runOnMainThread(Runnable runnable) { - handler.post(runnable); + fun disableAttachmentButtons(attachment: AttachmentViewInfo?) = Unit + + fun enableAttachmentButtons(attachment: AttachmentViewInfo?) = Unit + + fun runOnMainThread(runnable: Runnable) { + requireActivity().runOnUiThread(runnable) } - public void showAttachmentLoadingDialog() { - // mMessageView.disableAttachmentButtons(); - showDialog(R.id.dialog_attachment_progress); + fun showAttachmentLoadingDialog() { + showDialog(R.id.dialog_attachment_progress) } - public void hideAttachmentLoadingDialogOnMainThread() { - handler.post(new Runnable() { - @Override - public void run() { - removeDialog(R.id.dialog_attachment_progress); - // mMessageView.enableAttachmentButtons(); - } - }); + fun hideAttachmentLoadingDialogOnMainThread() { + runOnMainThread { + removeDialog(R.id.dialog_attachment_progress) + } } - public void refreshAttachmentThumbnail(AttachmentViewInfo attachment) { - mMessageView.refreshAttachmentThumbnail(attachment); + fun refreshAttachmentThumbnail(attachment: AttachmentViewInfo?) { + messageTopView.refreshAttachmentThumbnail(attachment) } - private MessageCryptoMvpView messageCryptoMvpView = new MessageCryptoMvpView() { - @Override - public void redisplayMessage() { - messageLoaderHelper.asyncReloadMessage(); + private val messageCryptoMvpView: MessageCryptoMvpView = object : MessageCryptoMvpView { + override fun redisplayMessage() { + messageLoaderHelper.asyncReloadMessage() } - @Override - public void startPendingIntentForCryptoPresenter(IntentSender si, Integer requestCode, Intent fillIntent, - int flagsMask, int flagValues, int extraFlags) throws SendIntentException { + @Throws(SendIntentException::class) + override fun startPendingIntentForCryptoPresenter( + intentSender: IntentSender, + requestCode: Int?, + fillIntent: Intent?, + flagsMask: Int, + flagValues: Int, + extraFlags: Int + ) { if (requestCode == null) { - getActivity().startIntentSender(si, fillIntent, flagsMask, flagValues, extraFlags); - return; + requireActivity().startIntentSender(intentSender, fillIntent, flagsMask, flagValues, extraFlags) + return } - requestCode |= REQUEST_MASK_CRYPTO_PRESENTER; - getActivity().startIntentSenderForResult( - si, requestCode, fillIntent, flagsMask, flagValues, extraFlags); + val maskedRequestCode = requestCode or REQUEST_MASK_CRYPTO_PRESENTER + requireActivity().startIntentSenderForResult( + intentSender, maskedRequestCode, fillIntent, flagsMask, flagValues, extraFlags + ) } - @Override - public void showCryptoInfoDialog(MessageCryptoDisplayStatus displayStatus, boolean hasSecurityWarning) { - CryptoInfoDialog dialog = CryptoInfoDialog.newInstance(displayStatus, hasSecurityWarning); - dialog.setTargetFragment(MessageViewFragment.this, 0); - dialog.show(getParentFragmentManager(), "crypto_info_dialog"); + override fun showCryptoInfoDialog(displayStatus: MessageCryptoDisplayStatus, hasSecurityWarning: Boolean) { + val dialog = CryptoInfoDialog.newInstance(displayStatus, hasSecurityWarning) + dialog.setTargetFragment(this@MessageViewFragment, 0) + dialog.show(parentFragmentManager, "crypto_info_dialog") } - @Override - public void restartMessageCryptoProcessing() { - mMessageView.setToLoadingState(); - messageLoaderHelper.asyncRestartMessageCryptoProcessing(); + override fun restartMessageCryptoProcessing() { + messageTopView.setToLoadingState() + messageLoaderHelper.asyncRestartMessageCryptoProcessing() } - @Override - public void showCryptoConfigDialog() { - AccountSettingsActivity.startCryptoSettings(getActivity(), mAccount.getUuid()); + override fun showCryptoConfigDialog() { + AccountSettingsActivity.startCryptoSettings(requireActivity(), account.uuid) } - }; - - @Override - public void onClickShowSecurityWarning() { - messageCryptoPresenter.onClickShowCryptoWarningDetails(); } - @Override - public void onClickSearchKey() { - messageCryptoPresenter.onClickSearchKey(); + override fun onClickShowSecurityWarning() { + messageCryptoPresenter.onClickShowCryptoWarningDetails() } - @Override - public void onClickShowCryptoKey() { - messageCryptoPresenter.onClickShowCryptoKey(); + override fun onClickSearchKey() { + messageCryptoPresenter.onClickSearchKey() } - public interface MessageViewFragmentListener { - void onForward(MessageReference messageReference, @Nullable Parcelable decryptionResultForReply); - void onForwardAsAttachment(MessageReference messageReference, @Nullable Parcelable decryptionResultForReply); - void onEditAsNewMessage(MessageReference messageReference); - void disableDeleteAction(); - void onReplyAll(MessageReference messageReference, @Nullable Parcelable decryptionResultForReply); - void onReply(MessageReference messageReference, @Nullable Parcelable decryptionResultForReply); - void setProgress(boolean b); - void showNextMessageOrReturn(); + override fun onClickShowCryptoKey() { + messageCryptoPresenter.onClickShowCryptoKey() } - public boolean isInitialized() { - return mInitialized ; + interface MessageViewFragmentListener { + fun onForward(messageReference: MessageReference, decryptionResultForReply: Parcelable?) + fun onForwardAsAttachment(messageReference: MessageReference, decryptionResultForReply: Parcelable?) + fun onEditAsNewMessage(messageReference: MessageReference) + fun disableDeleteAction() + fun onReplyAll(messageReference: MessageReference, decryptionResultForReply: Parcelable?) + fun onReply(messageReference: MessageReference, decryptionResultForReply: Parcelable?) + fun setProgress(enable: Boolean) + fun showNextMessageOrReturn() } + private val messageLoaderCallbacks: MessageLoaderCallbacks = object : MessageLoaderCallbacks { + override fun onMessageDataLoadFinished(message: LocalMessage) { + this@MessageViewFragment.message = message - private MessageLoaderCallbacks messageLoaderCallbacks = new MessageLoaderCallbacks() { - @Override - public void onMessageDataLoadFinished(LocalMessage message) { - mMessage = message; - - displayHeaderForLoadingMessage(message); - mMessageView.setToLoadingState(); - showProgressThreshold = null; + displayHeaderForLoadingMessage(message) + messageTopView.setToLoadingState() + showProgressThreshold = null } - @Override - public void onMessageDataLoadFailed() { - Toast.makeText(getActivity(), R.string.status_loading_error, Toast.LENGTH_LONG).show(); - showProgressThreshold = null; + override fun onMessageDataLoadFailed() { + Toast.makeText(activity, R.string.status_loading_error, Toast.LENGTH_LONG).show() + showProgressThreshold = null } - @Override - public void onMessageViewInfoLoadFinished(MessageViewInfo messageViewInfo) { - showMessage(messageViewInfo); - preferredUnsubscribeUri = messageViewInfo.preferredUnsubscribeUri; - showProgressThreshold = null; + override fun onMessageViewInfoLoadFinished(messageViewInfo: MessageViewInfo) { + showMessage(messageViewInfo) + preferredUnsubscribeUri = messageViewInfo.preferredUnsubscribeUri + showProgressThreshold = null } - @Override - public void onMessageViewInfoLoadFailed(MessageViewInfo messageViewInfo) { - showMessage(messageViewInfo); - preferredUnsubscribeUri = null; - showProgressThreshold = null; + override fun onMessageViewInfoLoadFailed(messageViewInfo: MessageViewInfo) { + showMessage(messageViewInfo) + preferredUnsubscribeUri = null + showProgressThreshold = null } - @Override - public void setLoadingProgress(int current, int max) { - if (showProgressThreshold == null) { - showProgressThreshold = SystemClock.elapsedRealtime() + PROGRESS_THRESHOLD_MILLIS; - } else if (showProgressThreshold == 0L || SystemClock.elapsedRealtime() > showProgressThreshold) { - showProgressThreshold = 0L; - mMessageView.setLoadingProgress(current, max); + override fun setLoadingProgress(current: Int, max: Int) { + val oldShowProgressThreshold = showProgressThreshold + + if (oldShowProgressThreshold == null) { + showProgressThreshold = SystemClock.elapsedRealtime() + PROGRESS_THRESHOLD_MILLIS + } else if (oldShowProgressThreshold == 0L || SystemClock.elapsedRealtime() > oldShowProgressThreshold) { + showProgressThreshold = 0L + messageTopView.setLoadingProgress(current, max) } } - @Override - public void onDownloadErrorMessageNotFound() { - mMessageView.enableDownloadButton(); - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(getActivity(), R.string.status_invalid_id_error, Toast.LENGTH_LONG).show(); - } - }); + override fun onDownloadErrorMessageNotFound() { + messageTopView.enableDownloadButton() + Toast.makeText(requireContext(), R.string.status_invalid_id_error, Toast.LENGTH_LONG).show() } - @Override - public void onDownloadErrorNetworkError() { - mMessageView.enableDownloadButton(); - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(getActivity(), R.string.status_network_error, Toast.LENGTH_LONG).show(); - } - }); + override fun onDownloadErrorNetworkError() { + messageTopView.enableDownloadButton() + Toast.makeText(requireContext(), R.string.status_network_error, Toast.LENGTH_LONG).show() } - @Override - public void startIntentSenderForMessageLoaderHelper(IntentSender si, int requestCode, Intent fillIntent, - int flagsMask, int flagValues, int extraFlags) { - showProgressThreshold = null; + override fun startIntentSenderForMessageLoaderHelper( + intentSender: IntentSender, + requestCode: Int, + fillIntent: Intent?, + flagsMask: Int, + flagValues: Int, + extraFlags: Int + ) { + showProgressThreshold = null try { - requestCode |= REQUEST_MASK_LOADER_HELPER; - getActivity().startIntentSenderForResult( - si, requestCode, fillIntent, flagsMask, flagValues, extraFlags); - } catch (SendIntentException e) { - Timber.e(e, "Irrecoverable error calling PendingIntent!"); + val maskedRequestCode = requestCode or REQUEST_MASK_LOADER_HELPER + requireActivity().startIntentSenderForResult( + intentSender, maskedRequestCode, fillIntent, flagsMask, flagValues, extraFlags + ) + } catch (e: SendIntentException) { + Timber.e(e, "Irrecoverable error calling PendingIntent!") } } - }; + } + override fun onViewAttachment(attachment: AttachmentViewInfo) { + currentAttachmentViewInfo = attachment - @Override - public void onViewAttachment(AttachmentViewInfo attachment) { - currentAttachmentViewInfo = attachment; - getAttachmentController(attachment).viewAttachment(); + createAttachmentController(attachment).viewAttachment() } - @Override - public void onSaveAttachment(final AttachmentViewInfo attachment) { - currentAttachmentViewInfo = attachment; + override fun onSaveAttachment(attachment: AttachmentViewInfo) { + currentAttachmentViewInfo = attachment - Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); - intent.setType(attachment.mimeType); - intent.putExtra(Intent.EXTRA_TITLE, attachment.displayName); - intent.addCategory(Intent.CATEGORY_OPENABLE); + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + type = attachment.mimeType + putExtra(Intent.EXTRA_TITLE, attachment.displayName) + addCategory(Intent.CATEGORY_OPENABLE) + } try { - startActivityForResult(intent, REQUEST_CODE_CREATE_DOCUMENT); - } catch (ActivityNotFoundException e) { - Toast.makeText(requireContext(), R.string.error_activity_not_found, Toast.LENGTH_LONG).show(); + startActivityForResult(intent, REQUEST_CODE_CREATE_DOCUMENT) + } catch (e: ActivityNotFoundException) { + Toast.makeText(requireContext(), R.string.error_activity_not_found, Toast.LENGTH_LONG).show() } } - private AttachmentController getAttachmentController(AttachmentViewInfo attachment) { - return new AttachmentController(mController, this, attachment); + private fun createAttachmentController(attachment: AttachmentViewInfo?): AttachmentController { + return AttachmentController(requireContext(), messagingController, this, attachment) } - private void invalidateMenu() { - requireActivity().invalidateMenu(); + private fun invalidateMenu() { + requireActivity().invalidateMenu() } - private enum FolderOperation { + private enum class FolderOperation { COPY, MOVE } + + companion object { + const val REQUEST_MASK_LOADER_HELPER = 1 shl 8 + const val REQUEST_MASK_CRYPTO_PRESENTER = 1 shl 9 + const val PROGRESS_THRESHOLD_MILLIS = 500 * 1000 + + private const val ARG_REFERENCE = "reference" + + private const val ACTIVITY_CHOOSE_FOLDER_MOVE = 1 + private const val ACTIVITY_CHOOSE_FOLDER_COPY = 2 + private const val REQUEST_CODE_CREATE_DOCUMENT = 3 + + fun newInstance(reference: MessageReference): MessageViewFragment { + return MessageViewFragment().withArguments( + ARG_REFERENCE to reference.toIdentityString() + ) + } + } } -- GitLab From 03de64e84cb318a1716e125c66c114f2563c7572 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 20 Jul 2022 16:43:40 +0200 Subject: [PATCH 10/47] Move code to prepare the message view menu to `MessageViewFragment` --- .../java/com/fsck/k9/activity/MessageList.kt | 120 ------------------ .../k9/ui/messageview/MessageViewFragment.kt | 84 ++++++++++++ .../src/main/res/menu/message_list_option.xml | 13 ++ 3 files changed, 97 insertions(+), 120 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 b56b7ecc98..e86e9bd822 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 @@ -1011,126 +1011,6 @@ open class MessageList : return true } - override fun onPrepareOptionsMenu(menu: Menu): Boolean { - super.onPrepareOptionsMenu(menu) - configureMenu(menu) - return true - } - - /** - * Hide menu items not appropriate for the current context. - * - * **Note:** - * Please adjust the comments in `res/menu/message_list_option.xml` if you change the visibility of a menu item - * in this method. - */ - private fun configureMenu(menu: Menu?) { - if (menu == null) return - - // Set visibility of menu items related to the message view - if (displayMode == DisplayMode.MESSAGE_LIST || messageViewFragment == null || - !messageViewFragment!!.isInitialized - ) { - menu.findItem(R.id.next_message).isVisible = false - menu.findItem(R.id.previous_message).isVisible = false - menu.findItem(R.id.single_message_options).isVisible = false - menu.findItem(R.id.delete).isVisible = false - menu.findItem(R.id.compose).isVisible = false - menu.findItem(R.id.archive).isVisible = false - menu.findItem(R.id.move).isVisible = false - menu.findItem(R.id.copy).isVisible = false - menu.findItem(R.id.spam).isVisible = false - menu.findItem(R.id.refile).isVisible = false - menu.findItem(R.id.toggle_unread).isVisible = false - menu.findItem(R.id.toggle_message_view_theme).isVisible = false - menu.findItem(R.id.unsubscribe).isVisible = false - menu.findItem(R.id.show_headers).isVisible = false - } else { - // hide prev/next buttons in split mode - if (displayMode != DisplayMode.MESSAGE_VIEW) { - menu.findItem(R.id.next_message).isVisible = false - menu.findItem(R.id.previous_message).isVisible = false - } else { - val ref = messageViewFragment!!.messageReference - val initialized = messageListFragment != null && - messageListFragment!!.isLoadFinished - val canDoPrev = initialized && !messageListFragment!!.isFirst(ref) - val canDoNext = initialized && !messageListFragment!!.isLast(ref) - val prev = menu.findItem(R.id.previous_message) - prev.isEnabled = canDoPrev - prev.icon.alpha = if (canDoPrev) 255 else 127 - val next = menu.findItem(R.id.next_message) - next.isEnabled = canDoNext - next.icon.alpha = if (canDoNext) 255 else 127 - } - - val toggleTheme = menu.findItem(R.id.toggle_message_view_theme) - if (generalSettingsManager.getSettings().fixedMessageViewTheme) { - toggleTheme.isVisible = false - } else { - // Set title of menu item to switch to dark/light theme - if (themeManager.messageViewTheme === Theme.DARK) { - toggleTheme.setTitle(R.string.message_view_theme_action_light) - } else { - toggleTheme.setTitle(R.string.message_view_theme_action_dark) - } - toggleTheme.isVisible = true - } - - if (messageViewFragment!!.isOutbox) { - menu.findItem(R.id.toggle_unread).isVisible = false - } else { - // Set title of menu item to toggle the read state of the currently displayed message - val drawableAttr = if (messageViewFragment!!.isMessageRead) { - menu.findItem(R.id.toggle_unread).setTitle(R.string.mark_as_unread_action) - intArrayOf(R.attr.iconActionMarkAsUnread) - } else { - menu.findItem(R.id.toggle_unread).setTitle(R.string.mark_as_read_action) - intArrayOf(R.attr.iconActionMarkAsRead) - } - val typedArray = obtainStyledAttributes(drawableAttr) - menu.findItem(R.id.toggle_unread).icon = typedArray.getDrawable(0) - typedArray.recycle() - } - - menu.findItem(R.id.delete).isVisible = K9.isMessageViewDeleteActionVisible - - // Set visibility of copy, move, archive, spam in action bar and refile submenu - if (messageViewFragment!!.isCopyCapable) { - menu.findItem(R.id.copy).isVisible = K9.isMessageViewCopyActionVisible - menu.findItem(R.id.refile_copy).isVisible = true - } else { - menu.findItem(R.id.copy).isVisible = false - menu.findItem(R.id.refile_copy).isVisible = false - } - - if (messageViewFragment!!.isMoveCapable) { - val canMessageBeArchived = messageViewFragment!!.canMessageBeArchived() - val canMessageBeMovedToSpam = messageViewFragment!!.canMessageBeMovedToSpam() - - menu.findItem(R.id.move).isVisible = K9.isMessageViewMoveActionVisible - menu.findItem(R.id.archive).isVisible = canMessageBeArchived && K9.isMessageViewArchiveActionVisible - menu.findItem(R.id.spam).isVisible = canMessageBeMovedToSpam && K9.isMessageViewSpamActionVisible - - menu.findItem(R.id.refile_move).isVisible = true - menu.findItem(R.id.refile_archive).isVisible = canMessageBeArchived - menu.findItem(R.id.refile_spam).isVisible = canMessageBeMovedToSpam - } else { - menu.findItem(R.id.move).isVisible = false - menu.findItem(R.id.archive).isVisible = false - menu.findItem(R.id.spam).isVisible = false - - menu.findItem(R.id.refile).isVisible = false - } - - if (messageViewFragment!!.isOutbox) { - menu.findItem(R.id.move_to_drafts).isVisible = true - } - - menu.findItem(R.id.unsubscribe).isVisible = messageViewFragment!!.canMessageBeUnsubscribed() - } - } - fun setActionBarTitle(title: String, subtitle: String? = null) { actionBar.title = title actionBar.subtitle = subtitle diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt index b88fee3d4b..74c8c27572 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt @@ -11,15 +11,19 @@ import android.os.Parcelable import android.os.SystemClock import android.view.ContextThemeWrapper import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager import android.widget.Toast +import androidx.core.content.withStyledAttributes import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import com.fsck.k9.Account import com.fsck.k9.K9 import com.fsck.k9.activity.MessageCompose +import com.fsck.k9.activity.MessageList import com.fsck.k9.activity.MessageLoaderHelper import com.fsck.k9.activity.MessageLoaderHelper.MessageLoaderCallbacks import com.fsck.k9.activity.MessageLoaderHelperFactory @@ -36,7 +40,9 @@ import com.fsck.k9.mailstore.AttachmentViewInfo import com.fsck.k9.mailstore.LocalMessage import com.fsck.k9.mailstore.MessageViewInfo import com.fsck.k9.preferences.AccountManager +import com.fsck.k9.preferences.GeneralSettingsManager import com.fsck.k9.ui.R +import com.fsck.k9.ui.base.Theme import com.fsck.k9.ui.base.ThemeManager import com.fsck.k9.ui.choosefolder.ChooseFolderActivity import com.fsck.k9.ui.messageview.CryptoInfoDialog.OnClickShowCryptoKeyListener @@ -60,6 +66,7 @@ class MessageViewFragment : private val accountManager: AccountManager by inject() private val messagingController: MessagingController by inject() private val shareIntentBuilder: ShareIntentBuilder by inject() + private val generalSettingsManager: GeneralSettingsManager by inject() private lateinit var messageTopView: MessageTopView @@ -180,6 +187,83 @@ class MessageViewFragment : } } + override fun onPrepareOptionsMenu(menu: Menu) { + menu.findItem(R.id.delete).isVisible = K9.isMessageViewDeleteActionVisible + + val showToggleUnread = !isOutbox + menu.findItem(R.id.toggle_unread).isVisible = showToggleUnread + + if (showToggleUnread) { + // Set title of menu item to toggle the read state of the currently displayed message + if (isMessageRead) { + menu.findItem(R.id.toggle_unread).setTitle(R.string.mark_as_unread_action) + } else { + menu.findItem(R.id.toggle_unread).setTitle(R.string.mark_as_read_action) + } + + val drawableAttr = if (isMessageRead) { + intArrayOf(R.attr.iconActionMarkAsUnread) + } else { + intArrayOf(R.attr.iconActionMarkAsRead) + } + + requireContext().withStyledAttributes(attrs = drawableAttr) { + menu.findItem(R.id.toggle_unread).icon = getDrawable(0) + } + } + + // FIXME: Remove previous/next actions + menu.findItem(R.id.next_message).isVisible = false + menu.findItem(R.id.previous_message).isVisible = false + + if (isMoveCapable) { + val canMessageBeArchived = canMessageBeArchived() + val canMessageBeMovedToSpam = canMessageBeMovedToSpam() + + menu.findItem(R.id.move).isVisible = K9.isMessageViewMoveActionVisible + menu.findItem(R.id.archive).isVisible = canMessageBeArchived && K9.isMessageViewArchiveActionVisible + menu.findItem(R.id.spam).isVisible = canMessageBeMovedToSpam && K9.isMessageViewSpamActionVisible + + menu.findItem(R.id.refile_move).isVisible = true + menu.findItem(R.id.refile_archive).isVisible = canMessageBeArchived + menu.findItem(R.id.refile_spam).isVisible = canMessageBeMovedToSpam + + menu.findItem(R.id.refile).isVisible = true + } else { + menu.findItem(R.id.move).isVisible = false + menu.findItem(R.id.archive).isVisible = false + menu.findItem(R.id.spam).isVisible = false + + menu.findItem(R.id.refile).isVisible = false + } + + if (isCopyCapable) { + menu.findItem(R.id.copy).isVisible = K9.isMessageViewCopyActionVisible + menu.findItem(R.id.refile_copy).isVisible = true + } else { + menu.findItem(R.id.copy).isVisible = false + menu.findItem(R.id.refile_copy).isVisible = false + } + + menu.findItem(R.id.move_to_drafts).isVisible = isOutbox + menu.findItem(R.id.single_message_options).isVisible = true + menu.findItem(R.id.unsubscribe).isVisible = canMessageBeUnsubscribed() + menu.findItem(R.id.show_headers).isVisible = true + + val toggleTheme = menu.findItem(R.id.toggle_message_view_theme) + if (generalSettingsManager.getSettings().fixedMessageViewTheme) { + toggleTheme.isVisible = false + } else { + // Set title of menu item to switch to dark/light theme + if (themeManager.messageViewTheme === Theme.DARK) { + toggleTheme.setTitle(R.string.message_view_theme_action_light) + } else { + toggleTheme.setTitle(R.string.message_view_theme_action_dark) + } + toggleTheme.isVisible = true + } + } + private fun showMessage(messageViewInfo: MessageViewInfo) { hideKeyboard() diff --git a/app/ui/legacy/src/main/res/menu/message_list_option.xml b/app/ui/legacy/src/main/res/menu/message_list_option.xml index db167ddc63..da41a81ff1 100644 --- a/app/ui/legacy/src/main/res/menu/message_list_option.xml +++ b/app/ui/legacy/src/main/res/menu/message_list_option.xml @@ -30,6 +30,7 @@ @@ -37,6 +38,7 @@ @@ -44,6 +46,7 @@ @@ -51,6 +54,7 @@ @@ -58,6 +62,7 @@ @@ -73,6 +79,7 @@ @@ -80,6 +87,7 @@ @@ -94,6 +102,7 @@ @@ -122,6 +131,7 @@ @@ -148,10 +158,12 @@ @@ -227,6 +239,7 @@ -- GitLab From 4d7a40b52068f7e69284ec500bc269ce747dc545 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 20 Jul 2022 16:46:19 +0200 Subject: [PATCH 11/47] Reformat message_list_option.xml --- .../src/main/res/menu/message_list_option.xml | 165 +++++++++--------- 1 file changed, 81 insertions(+), 84 deletions(-) diff --git a/app/ui/legacy/src/main/res/menu/message_list_option.xml b/app/ui/legacy/src/main/res/menu/message_list_option.xml index da41a81ff1..db6cbfbc9f 100644 --- a/app/ui/legacy/src/main/res/menu/message_list_option.xml +++ b/app/ui/legacy/src/main/res/menu/message_list_option.xml @@ -1,207 +1,204 @@ - + + app:showAsAction="always" /> + android:visible="false" + app:showAsAction="always" /> + android:title="@string/archive_action" + android:visible="false" + app:showAsAction="always" /> + android:title="@string/delete_action" + android:visible="false" + app:showAsAction="always" /> + android:title="@string/mark_as_unread_action" + android:visible="false" + app:showAsAction="always" /> + android:title="@string/next_action" + android:visible="false" + app:showAsAction="always" /> + android:title="@string/spam_action" + android:visible="false" + app:showAsAction="ifRoom" /> + android:title="@string/move_action" + android:visible="false" + app:showAsAction="ifRoom" /> + android:title="@string/copy_action" + android:visible="false" + app:showAsAction="ifRoom" /> + android:visible="false" + app:showAsAction="never" /> + android:title="@string/single_message_options_action" + android:visible="false" + app:showAsAction="ifRoom"> + android:title="@string/reply_action" /> + android:title="@string/reply_all_action" /> + android:title="@string/forward_action" /> + android:title="@string/forward_as_attachment_action" /> + android:title="@string/edit_as_new_message_action" /> + android:title="@string/send_alternate_action" /> + android:title="@string/refile_action" + android:visible="false" + app:showAsAction="never"> + android:title="@string/archive_action" /> - + android:title="@string/spam_action" /> - + android:title="@string/move_action" /> - + android:title="@string/copy_action" /> - + + app:showAsAction="never" /> - + + app:showAsAction="never" /> + android:title="@string/compose_action" + app:showAsAction="ifRoom" /> + android:title="@string/sort_by" + app:showAsAction="ifRoom"> + android:title="@string/sort_by_date" /> + android:title="@string/sort_by_arrival" /> + android:title="@string/sort_by_subject" /> + android:title="@string/sort_by_sender" /> + android:title="@string/sort_by_flag" /> + android:title="@string/sort_by_unread" /> + android:title="@string/sort_by_attach" /> @@ -209,21 +206,21 @@ + android:title="@string/batch_select_all" + app:showAsAction="never" /> + android:title="@string/mark_all_as_read" + app:showAsAction="never" /> + android:title="@string/send_messages_action" + app:showAsAction="never" /> + android:title="@string/expunge_action" + app:showAsAction="never" /> + android:title="@string/message_view_theme_action_dark" + android:visible="false" + app:showAsAction="never" /> + android:title="@string/search_everywhere_action" + app:showAsAction="never" /> -- GitLab From 0bb4695d4b76a1bfd150676dbf6d1b86e037dc8e Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 20 Jul 2022 18:06:51 +0200 Subject: [PATCH 12/47] Move message view menu item handling code to `MessageViewFragment` --- .../java/com/fsck/k9/activity/MessageList.kt | 61 --------------- .../k9/ui/messageview/MessageViewFragment.kt | 74 +++++++++++++------ 2 files changed, 50 insertions(+), 85 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 e86e9bd822..7a358bd5a8 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 @@ -49,12 +49,10 @@ import com.fsck.k9.ui.BuildConfig import com.fsck.k9.ui.K9Drawer import com.fsck.k9.ui.R import com.fsck.k9.ui.base.K9Activity -import com.fsck.k9.ui.base.Theme import com.fsck.k9.ui.changelog.RecentChangesActivity import com.fsck.k9.ui.changelog.RecentChangesViewModel import com.fsck.k9.ui.managefolders.ManageFoldersActivity import com.fsck.k9.ui.messagelist.DefaultFolderProvider -import com.fsck.k9.ui.messagesource.MessageSourceActivity import com.fsck.k9.ui.messageview.MessageViewFragment import com.fsck.k9.ui.messageview.MessageViewFragment.MessageViewFragmentListener import com.fsck.k9.ui.messageview.PlaceholderFragment @@ -916,63 +914,9 @@ open class MessageList : goBack() } return true - } else if (id == R.id.toggle_message_view_theme) { - onToggleTheme() - return true } else if (id == R.id.search_everywhere) { searchEverywhere() return true - } else if (id == R.id.next_message) { // MessageView - showNextMessage() - return true - } else if (id == R.id.previous_message) { - showPreviousMessage() - return true - } else if (id == R.id.delete) { - messageViewFragment!!.onDelete() - return true - } else if (id == R.id.reply) { - messageViewFragment!!.onReply() - return true - } else if (id == R.id.reply_all) { - messageViewFragment!!.onReplyAll() - return true - } else if (id == R.id.forward) { - messageViewFragment!!.onForward() - return true - } else if (id == R.id.forward_as_attachment) { - messageViewFragment!!.onForwardAsAttachment() - return true - } else if (id == R.id.edit_as_new_message) { - messageViewFragment!!.onEditAsNewMessage() - return true - } else if (id == R.id.share) { - messageViewFragment!!.onSendAlternate() - return true - } else if (id == R.id.toggle_unread) { - messageViewFragment!!.onToggleRead() - return true - } else if (id == R.id.archive || id == R.id.refile_archive) { - messageViewFragment!!.onArchive() - return true - } else if (id == R.id.spam || id == R.id.refile_spam) { - messageViewFragment!!.onSpam() - return true - } else if (id == R.id.move || id == R.id.refile_move) { - messageViewFragment!!.onMove() - return true - } else if (id == R.id.copy || id == R.id.refile_copy) { - messageViewFragment!!.onCopy() - return true - } else if (id == R.id.move_to_drafts) { - messageViewFragment!!.onMoveToDrafts() - return true - } else if (id == R.id.unsubscribe) { - messageViewFragment!!.onUnsubscribe() - return true - } else if (id == R.id.show_headers) { - startActivity(MessageSourceActivity.createLaunchIntent(this, messageViewFragment!!.messageReference)) - return true } return super.onOptionsItemSelected(item) @@ -1293,11 +1237,6 @@ open class MessageList : menu!!.findItem(R.id.delete).isEnabled = false } - private fun onToggleTheme() { - themeManager.toggleMessageViewTheme() - recreateCompat() - } - private fun showDefaultTitleView() { if (messageListFragment != null) { messageListFragment!!.updateTitle() diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt index 74c8c27572..a192e7d5e5 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt @@ -12,18 +12,18 @@ import android.os.SystemClock import android.view.ContextThemeWrapper import android.view.LayoutInflater import android.view.Menu -import android.view.MenuInflater +import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager import android.widget.Toast +import androidx.core.app.ActivityCompat import androidx.core.content.withStyledAttributes import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import com.fsck.k9.Account import com.fsck.k9.K9 import com.fsck.k9.activity.MessageCompose -import com.fsck.k9.activity.MessageList import com.fsck.k9.activity.MessageLoaderHelper import com.fsck.k9.activity.MessageLoaderHelper.MessageLoaderCallbacks import com.fsck.k9.activity.MessageLoaderHelperFactory @@ -45,6 +45,7 @@ import com.fsck.k9.ui.R import com.fsck.k9.ui.base.Theme import com.fsck.k9.ui.base.ThemeManager import com.fsck.k9.ui.choosefolder.ChooseFolderActivity +import com.fsck.k9.ui.messagesource.MessageSourceActivity import com.fsck.k9.ui.messageview.CryptoInfoDialog.OnClickShowCryptoKeyListener import com.fsck.k9.ui.messageview.MessageCryptoPresenter.MessageCryptoMvpView import com.fsck.k9.ui.settings.account.AccountSettingsActivity @@ -86,13 +87,6 @@ class MessageViewFragment : private lateinit var account: Account lateinit var messageReference: MessageReference - /** - * `true` after [.onCreate] has been executed. This is used by `MessageList.configureMenu()` to make sure the - * fragment has been initialized before it is used. - */ - var isInitialized = false - private set - private var currentAttachmentViewInfo: AttachmentViewInfo? = null override fun onAttach(context: Context) { @@ -117,8 +111,6 @@ class MessageViewFragment : fragmentManager = parentFragmentManager, callback = messageLoaderCallbacks ) - - isInitialized = true } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -264,6 +256,40 @@ class MessageViewFragment : } } + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.toggle_message_view_theme -> onToggleTheme() + R.id.delete -> onDelete() + R.id.reply -> onReply() + R.id.reply_all -> onReplyAll() + R.id.forward -> onForward() + R.id.forward_as_attachment -> onForwardAsAttachment() + R.id.edit_as_new_message -> onEditAsNewMessage() + R.id.share -> onSendAlternate() + R.id.toggle_unread -> onToggleRead() + R.id.archive, R.id.refile_archive -> onArchive() + R.id.spam, R.id.refile_spam -> onSpam() + R.id.move, R.id.refile_move -> onMove() + R.id.copy, R.id.refile_copy -> onCopy() + R.id.move_to_drafts -> onMoveToDrafts() + R.id.unsubscribe -> onUnsubscribe() + R.id.show_headers -> onShowHeaders() + else -> return false + } + + return true + } + + private fun onShowHeaders() { + val launchIntent = MessageSourceActivity.createLaunchIntent(requireActivity(), messageReference) + startActivity(launchIntent) + } + + private fun onToggleTheme() { + themeManager.toggleMessageViewTheme() + ActivityCompat.recreate(requireActivity()) + } + private fun showMessage(messageViewInfo: MessageViewInfo) { hideKeyboard() @@ -403,7 +429,7 @@ class MessageViewFragment : ) } - fun onForwardAsAttachment() { + private fun onForwardAsAttachment() { val message = checkNotNull(this.message) fragmentListener.onForwardAsAttachment( @@ -412,7 +438,7 @@ class MessageViewFragment : ) } - fun onEditAsNewMessage() { + private fun onEditAsNewMessage() { val message = checkNotNull(this.message) fragmentListener.onEditAsNewMessage(message.makeMessageReference()) @@ -442,7 +468,7 @@ class MessageViewFragment : startRefileActivity(FolderOperation.COPY, ACTIVITY_CHOOSE_FOLDER_COPY) } - fun onMoveToDrafts() { + private fun onMoveToDrafts() { fragmentListener.showNextMessageOrReturn() val account = account @@ -455,7 +481,7 @@ class MessageViewFragment : onRefile(account.archiveFolderId) } - fun onSpam() { + private fun onSpam() { onRefile(account.spamFolderId) } @@ -533,7 +559,7 @@ class MessageViewFragment : copyMessage(messageReference, destinationFolderId) } - fun onSendAlternate() { + private fun onSendAlternate() { val message = checkNotNull(message) val shareIntent = shareIntentBuilder.createShareIntent(message) @@ -641,33 +667,33 @@ class MessageViewFragment : override fun dialogCancelled(dialogId: Int) = Unit - val isOutbox: Boolean + private val isOutbox: Boolean get() = messageReference.folderId == account.outboxFolderId - val isMessageRead: Boolean + private val isMessageRead: Boolean get() = message?.isSet(Flag.SEEN) == true - val isCopyCapable: Boolean + private val isCopyCapable: Boolean get() = !isOutbox && messagingController.isCopyCapable(account) - val isMoveCapable: Boolean + private val isMoveCapable: Boolean get() = !isOutbox && messagingController.isMoveCapable(account) - fun canMessageBeArchived(): Boolean { + private fun canMessageBeArchived(): Boolean { val archiveFolderId = account.archiveFolderId ?: return false return messageReference.folderId != archiveFolderId } - fun canMessageBeMovedToSpam(): Boolean { + private fun canMessageBeMovedToSpam(): Boolean { val spamFolderId = account.spamFolderId ?: return false return messageReference.folderId != spamFolderId } - fun canMessageBeUnsubscribed(): Boolean { + private fun canMessageBeUnsubscribed(): Boolean { return preferredUnsubscribeUri != null } - fun onUnsubscribe() { + private fun onUnsubscribe() { val intent = when (val unsubscribeUri = preferredUnsubscribeUri) { is MailtoUnsubscribeUri -> { Intent(requireContext(), MessageCompose::class.java).apply { -- GitLab From 59aac05a8c9ea849d2dcaa17a09b70b2c1b68daf Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 20 Jul 2022 18:12:51 +0200 Subject: [PATCH 13/47] Initialize `MessageViewFragment.messageReference` earlier --- .../com/fsck/k9/ui/messageview/MessageViewFragment.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt index a192e7d5e5..01a8f7f2e7 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt @@ -104,6 +104,9 @@ class MessageViewFragment : setHasOptionsMenu(true) + messageReference = MessageReference.parse(arguments?.getString(ARG_REFERENCE)) + ?: error("Invalid argument '$ARG_REFERENCE'") + messageCryptoPresenter = MessageCryptoPresenter(messageCryptoMvpView) messageLoaderHelper = messageLoaderHelperFactory.createForMessageView( context = requireContext().applicationContext, @@ -146,16 +149,12 @@ class MessageViewFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val messageReference = MessageReference.parse(arguments?.getString(ARG_REFERENCE)) - ?: error("Invalid argument '$ARG_REFERENCE'") - - displayMessage(messageReference) + loadMessage(messageReference) } - private fun displayMessage(messageReference: MessageReference) { + private fun loadMessage(messageReference: MessageReference) { Timber.d("MessageViewFragment displaying message %s", messageReference) - this.messageReference = messageReference account = accountManager.getAccount(messageReference.accountUuid) ?: error("Account ${messageReference.accountUuid} not found") -- GitLab From 9c8ac61ed61f376e5deddbb8e3a554ba93a6d438 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 20 Jul 2022 18:38:55 +0200 Subject: [PATCH 14/47] Set `MessageViewFragment` to "active" when its menu should be displayed --- .../java/com/fsck/k9/activity/MessageList.kt | 34 +++++++++++++------ .../k9/ui/messageview/MessageViewFragment.kt | 15 +++++++- 2 files changed, 37 insertions(+), 12 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 7a358bd5a8..fdacc9052c 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 @@ -332,16 +332,21 @@ open class MessageList : showMessageView() } DisplayMode.SPLIT_VIEW -> { + val messageListFragment = checkNotNull(this.messageListFragment) + messageListWasDisplayed = true - messageListFragment?.onListVisible() - if (messageViewFragment == null) { - showMessageViewPlaceHolder() - } else { - val activeMessage = messageViewFragment!!.messageReference - if (activeMessage != null) { - messageListFragment!!.setActiveMessage(activeMessage) + messageListFragment.onListVisible() + + messageViewFragment.let { messageViewFragment -> + if (messageViewFragment == null) { + showMessageViewPlaceHolder() + } else { + messageViewFragment.isActive = true + val activeMessage = messageViewFragment.messageReference + messageListFragment.setActiveMessage(activeMessage) } } + setDrawerLockState() onMessageListDisplayed() } @@ -987,12 +992,15 @@ open class MessageList : } val fragment = MessageViewFragment.newInstance(messageReference) - val fragmentTransaction = supportFragmentManager.beginTransaction() - fragmentTransaction.replace(R.id.message_view_container, fragment, FRAGMENT_TAG_MESSAGE_VIEW) - fragmentTransaction.commit() + supportFragmentManager.commit { + replace(R.id.message_view_container, fragment, FRAGMENT_TAG_MESSAGE_VIEW) + } + messageViewFragment = fragment - if (displayMode != DisplayMode.SPLIT_VIEW) { + if (displayMode == DisplayMode.SPLIT_VIEW) { + fragment.isActive = true + } else { showMessageView() } } @@ -1195,6 +1203,7 @@ open class MessageList : displayMode = DisplayMode.MESSAGE_LIST viewSwitcher!!.showFirstView() + messageViewFragment?.isActive = false messageListFragment!!.onListVisible() messageListFragment!!.setActiveMessage(null) @@ -1217,8 +1226,11 @@ open class MessageList : } private fun showMessageView() { + val messageViewFragment = checkNotNull(this.messageViewFragment) + displayMode = DisplayMode.MESSAGE_VIEW messageListFragment?.onListHidden() + messageViewFragment.isActive = true if (!messageListWasDisplayed) { viewSwitcher!!.animateFirstView = false diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt index 01a8f7f2e7..ce8c45f989 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt @@ -89,6 +89,17 @@ class MessageViewFragment : private var currentAttachmentViewInfo: AttachmentViewInfo? = null + /** + * Set this to `true` when the fragment should be considered active. When active, the fragment adds its actions to + * the toolbar. When inactive, the fragment won't add its actions to the toolbar, even it is still visible, e.g. as + * part of an animation. + */ + var isActive: Boolean = false + set(value) { + field = value + invalidateMenu() + } + override fun onAttach(context: Context) { super.onAttach(context) @@ -179,6 +190,8 @@ class MessageViewFragment : } override fun onPrepareOptionsMenu(menu: Menu) { + if (!isActive) return + menu.findItem(R.id.delete).isVisible = K9.isMessageViewDeleteActionVisible val showToggleUnread = !isOutbox @@ -890,7 +903,7 @@ class MessageViewFragment : } private fun invalidateMenu() { - requireActivity().invalidateMenu() + activity?.invalidateMenu() } private enum class FolderOperation { -- GitLab From d9803c5987fb40ea897c0fd60063522331fb9cd7 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 20 Jul 2022 18:58:41 +0200 Subject: [PATCH 15/47] Fix visibility of compose action during transitions --- .../src/main/java/com/fsck/k9/fragment/MessageListFragment.kt | 2 ++ .../main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt | 1 + 2 files changed, 3 insertions(+) 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 ed72925398..211e3e7c59 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 @@ -706,6 +706,7 @@ class MessageListFragment : } private fun prepareMenu(menu: Menu) { + menu.findItem(R.id.compose).isVisible = true menu.findItem(R.id.set_sort).isVisible = true menu.findItem(R.id.select_all).isVisible = true menu.findItem(R.id.compose).isVisible = true @@ -726,6 +727,7 @@ class MessageListFragment : } private fun hideMenu(menu: Menu) { + menu.findItem(R.id.compose).isVisible = false menu.findItem(R.id.search).isVisible = false menu.findItem(R.id.search_remote).isVisible = false menu.findItem(R.id.set_sort).isVisible = false diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt index ce8c45f989..7a1340d650 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt @@ -253,6 +253,7 @@ class MessageViewFragment : menu.findItem(R.id.single_message_options).isVisible = true menu.findItem(R.id.unsubscribe).isVisible = canMessageBeUnsubscribed() menu.findItem(R.id.show_headers).isVisible = true + menu.findItem(R.id.compose).isVisible = true val toggleTheme = menu.findItem(R.id.toggle_message_view_theme) if (generalSettingsManager.getSettings().fixedMessageViewTheme) { -- GitLab From 995703cc5f5ed61e5a0414ff9375eaa21346ce56 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 20 Jul 2022 19:53:55 +0200 Subject: [PATCH 16/47] Set the "message list visible" flag when the back stack has changed --- app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt | 2 ++ 1 file changed, 2 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 fdacc9052c..9fd5686a47 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 @@ -1032,6 +1032,8 @@ open class MessageList : override fun onBackStackChanged() { findFragments() + messageListFragment?.onListVisible() + if (isDrawerEnabled && !isAdditionalMessageListDisplayed) { unlockDrawer() } -- GitLab From 17c51a49008982080ead9aebc88f4659322cf1e4 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 20 Jul 2022 20:34:27 +0200 Subject: [PATCH 17/47] Remove next/previous entries from message view menu --- .../k9/ui/messageview/MessageViewFragment.kt | 4 ---- .../src/main/res/drawable/ic_chevron_left.xml | 10 ---------- .../src/main/res/menu/message_list_option.xml | 16 ---------------- app/ui/legacy/src/main/res/values/attrs.xml | 2 -- app/ui/legacy/src/main/res/values/themes.xml | 4 ---- 5 files changed, 36 deletions(-) delete mode 100644 app/ui/legacy/src/main/res/drawable/ic_chevron_left.xml diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt index 7a1340d650..148e4737f1 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt @@ -216,10 +216,6 @@ class MessageViewFragment : } } - // FIXME: Remove previous/next actions - menu.findItem(R.id.next_message).isVisible = false - menu.findItem(R.id.previous_message).isVisible = false - if (isMoveCapable) { val canMessageBeArchived = canMessageBeArchived() val canMessageBeMovedToSpam = canMessageBeMovedToSpam() diff --git a/app/ui/legacy/src/main/res/drawable/ic_chevron_left.xml b/app/ui/legacy/src/main/res/drawable/ic_chevron_left.xml deleted file mode 100644 index e3b3964da1..0000000000 --- a/app/ui/legacy/src/main/res/drawable/ic_chevron_left.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/ui/legacy/src/main/res/menu/message_list_option.xml b/app/ui/legacy/src/main/res/menu/message_list_option.xml index db6cbfbc9f..de9fdba7c1 100644 --- a/app/ui/legacy/src/main/res/menu/message_list_option.xml +++ b/app/ui/legacy/src/main/res/menu/message_list_option.xml @@ -48,22 +48,6 @@ android:visible="false" app:showAsAction="always" /> - - - - - - - - diff --git a/app/ui/legacy/src/main/res/values/themes.xml b/app/ui/legacy/src/main/res/values/themes.xml index dd5e001147..0500a0236d 100644 --- a/app/ui/legacy/src/main/res/values/themes.xml +++ b/app/ui/legacy/src/main/res/values/themes.xml @@ -33,8 +33,6 @@ @drawable/ic_folder @drawable/ic_content_copy @drawable/ic_chevron_right - @drawable/ic_chevron_right - @drawable/ic_chevron_left @drawable/ic_refresh @drawable/ic_magnify @drawable/ic_folder_magnify @@ -154,8 +152,6 @@ @drawable/ic_folder @drawable/ic_content_copy @drawable/ic_chevron_right - @drawable/ic_chevron_right - @drawable/ic_chevron_left @drawable/ic_refresh @drawable/ic_magnify @drawable/ic_folder_magnify -- GitLab From 213cda8881b41f7ee38de6091881226f2060a774 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 21 Jul 2022 11:50:26 +0200 Subject: [PATCH 18/47] Rename `MessageListFragment.onListVisible()` to `isActive` Use the same name for the same concept in `MessageListFragment` and `MessageViewFragment`. --- .../java/com/fsck/k9/activity/MessageList.kt | 12 ++++----- .../fsck/k9/fragment/MessageListFragment.kt | 25 +++++++++---------- 2 files changed, 18 insertions(+), 19 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 9fd5686a47..c2fb3578e9 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 @@ -335,7 +335,7 @@ open class MessageList : val messageListFragment = checkNotNull(this.messageListFragment) messageListWasDisplayed = true - messageListFragment.onListVisible() + messageListFragment.isActive = true messageViewFragment.let { messageViewFragment -> if (messageViewFragment == null) { @@ -627,7 +627,7 @@ open class MessageList : openFolderTransaction!!.commit() openFolderTransaction = null - messageListFragment!!.onListVisible() + messageListFragment!!.isActive = true onMessageListDisplayed() } @@ -1032,7 +1032,7 @@ open class MessageList : override fun onBackStackChanged() { findFragments() - messageListFragment?.onListVisible() + messageListFragment?.isActive = true if (isDrawerEnabled && !isAdditionalMessageListDisplayed) { unlockDrawer() @@ -1059,7 +1059,7 @@ open class MessageList : } messageListFragment = fragment - fragment.onListVisible() + fragment.isActive = true if (isDrawerEnabled) { lockDrawer() @@ -1206,7 +1206,7 @@ open class MessageList : viewSwitcher!!.showFirstView() messageViewFragment?.isActive = false - messageListFragment!!.onListVisible() + messageListFragment!!.isActive = true messageListFragment!!.setActiveMessage(null) setDrawerLockState() @@ -1231,7 +1231,7 @@ open class MessageList : val messageViewFragment = checkNotNull(this.messageViewFragment) displayMode = DisplayMode.MESSAGE_VIEW - messageListFragment?.onListHidden() + messageListFragment?.isActive = false messageViewFragment.isActive = true if (!messageListWasDisplayed) { 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 211e3e7c59..9b8ce01948 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 @@ -130,7 +130,16 @@ class MessageListFragment : */ private var isInitialized = false - private var isListVisible = false + /** + * Set this to `true` when the fragment should be considered active. When active, the fragment adds its actions to + * the toolbar. When inactive, the fragment won't add its actions to the toolbar, even it is still visible, e.g. as + * part of an animation. + */ + var isActive: Boolean = false + set(value) { + field = value + resetActionMode() + } override fun onAttach(context: Context) { super.onAttach(context) @@ -698,7 +707,7 @@ class MessageListFragment : } override fun onPrepareOptionsMenu(menu: Menu) { - if (isListVisible) { + if (isActive) { prepareMenu(menu) } else { hideMenu(menu) @@ -1524,7 +1533,7 @@ class MessageListFragment : private fun resetActionMode() { if (!isResumed) return - if (!isListVisible || selected.isEmpty()) { + if (!isActive || selected.isEmpty()) { actionMode?.finish() actionMode = null return @@ -1592,16 +1601,6 @@ class MessageListFragment : } } - fun onListVisible() { - isListVisible = true - resetActionMode() - } - - fun onListHidden() { - isListVisible = false - resetActionMode() - } - private fun invalidateMenu() { requireActivity().invalidateMenu() } -- GitLab From 1cb0d15398e1589bde14ae88f322c8ee3466739b Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 21 Jul 2022 12:03:53 +0200 Subject: [PATCH 19/47] Move "search everywhere" menu handling to `MessageListFragment` --- .../java/com/fsck/k9/activity/MessageList.kt | 11 ----------- .../com/fsck/k9/fragment/MessageListFragment.kt | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 11 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 c2fb3578e9..e3ff9154f2 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 @@ -919,22 +919,11 @@ open class MessageList : goBack() } return true - } else if (id == R.id.search_everywhere) { - searchEverywhere() - return true } return super.onOptionsItemSelected(item) } - private fun searchEverywhere() { - val searchIntent = Intent(this, Search::class.java).apply { - action = Intent.ACTION_SEARCH - putExtra(SearchManager.QUERY, intent.getStringExtra(SearchManager.QUERY)) - } - onNewIntent(searchIntent) - } - override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.message_list_option, menu) this.menu = menu 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 9b8ce01948..bcb3665a08 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 @@ -1,6 +1,7 @@ package com.fsck.k9.fragment import android.app.Activity +import android.app.SearchManager import android.content.Context import android.content.Intent import android.os.Bundle @@ -28,6 +29,7 @@ import com.fsck.k9.Clock import com.fsck.k9.K9 import com.fsck.k9.Preferences import com.fsck.k9.activity.FolderInfoHolder +import com.fsck.k9.activity.Search import com.fsck.k9.activity.misc.ContactPicture import com.fsck.k9.controller.MessageReference import com.fsck.k9.controller.MessagingController @@ -764,12 +766,27 @@ class MessageListFragment : R.id.send_messages -> onSendPendingMessages() R.id.empty_trash -> onEmptyTrash() R.id.expunge -> onExpunge() + R.id.search_everywhere -> onSearchEverywhere() else -> return super.onOptionsItemSelected(item) } return true } + private fun onSearchEverywhere() { + val searchQuery = requireActivity().intent.getStringExtra(SearchManager.QUERY) + + val searchIntent = Intent(requireContext(), Search::class.java).apply { + action = Intent.ACTION_SEARCH + putExtra(SearchManager.QUERY, searchQuery) + + addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + } + + startActivity(searchIntent) + } + private fun onSendPendingMessages() { messagingController.sendPendingMessages(account, null) } -- GitLab From 8214ef3fb7f81f504446dec941f81d50215232f0 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 21 Jul 2022 12:21:46 +0200 Subject: [PATCH 20/47] Move code to disable the delete menu item to `MessageViewFragment` --- .../main/java/com/fsck/k9/activity/MessageList.kt | 6 ------ .../fsck/k9/ui/messageview/MessageViewFragment.kt | 15 +++++++++++---- 2 files changed, 11 insertions(+), 10 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 e3ff9154f2..0a045c1713 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 @@ -98,7 +98,6 @@ open class MessageList : private lateinit var searchView: SearchView private var drawer: K9Drawer? = null private var openFolderTransaction: FragmentTransaction? = null - private var menu: Menu? = null private var progressBar: ProgressBar? = null private var messageViewPlaceHolder: PlaceholderFragment? = null private var messageListFragment: MessageListFragment? = null @@ -926,7 +925,6 @@ open class MessageList : override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.message_list_option, menu) - this.menu = menu // setup search view val searchItem = menu.findItem(R.id.search) @@ -1236,10 +1234,6 @@ open class MessageList : invalidateMenu() } - override fun disableDeleteAction() { - menu!!.findItem(R.id.delete).isEnabled = false - } - private fun showDefaultTitleView() { if (messageListFragment != null) { messageListFragment!!.updateTitle() diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt index 148e4737f1..d3045ab25b 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt @@ -88,6 +88,7 @@ class MessageViewFragment : lateinit var messageReference: MessageReference private var currentAttachmentViewInfo: AttachmentViewInfo? = null + private var isDeleteMenuItemDisabled: Boolean = false /** * Set this to `true` when the fragment should be considered active. When active, the fragment adds its actions to @@ -192,7 +193,10 @@ class MessageViewFragment : override fun onPrepareOptionsMenu(menu: Menu) { if (!isActive) return - menu.findItem(R.id.delete).isVisible = K9.isMessageViewDeleteActionVisible + menu.findItem(R.id.delete).apply { + isVisible = K9.isMessageViewDeleteActionVisible + isEnabled = !isDeleteMenuItemDisabled + } val showToggleUnread = !isOutbox menu.findItem(R.id.toggle_unread).isVisible = showToggleUnread @@ -378,14 +382,18 @@ class MessageViewFragment : } private fun delete() { - // Disable the delete button after it has been tapped (to try to prevent accidental clicks) - fragmentListener.disableDeleteAction() + disableDeleteMenuItem() fragmentListener.showNextMessageOrReturn() messagingController.deleteMessage(messageReference) } + private fun disableDeleteMenuItem() { + isDeleteMenuItemDisabled = true + invalidateMenu() + } + private fun onRefile(destinationFolderId: Long?) { if (destinationFolderId == null || !messagingController.isMoveCapable(account)) { return @@ -799,7 +807,6 @@ class MessageViewFragment : fun onForward(messageReference: MessageReference, decryptionResultForReply: Parcelable?) fun onForwardAsAttachment(messageReference: MessageReference, decryptionResultForReply: Parcelable?) fun onEditAsNewMessage(messageReference: MessageReference) - fun disableDeleteAction() fun onReplyAll(messageReference: MessageReference, decryptionResultForReply: Parcelable?) fun onReply(messageReference: MessageReference, decryptionResultForReply: Parcelable?) fun setProgress(enable: Boolean) -- GitLab From 3fce00902a5c7aec5a46621f430c6f0c77ca7541 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 21 Jul 2022 12:55:05 +0200 Subject: [PATCH 21/47] Don't invalidate the menu in `MessageList` --- .../src/main/java/com/fsck/k9/activity/MessageList.kt | 9 --------- .../java/com/fsck/k9/fragment/MessageListFragment.kt | 6 +++--- 2 files changed, 3 insertions(+), 12 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 0a045c1713..6985863dd3 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 @@ -1028,8 +1028,6 @@ open class MessageList : if (displayMode == DisplayMode.SPLIT_VIEW) { showMessageViewPlaceHolder() } - - invalidateMenu() } private fun addMessageListFragment(fragment: MessageListFragment) { @@ -1121,11 +1119,6 @@ open class MessageList : fragmentTransaction.commit() } - override fun remoteSearchStarted() { - // Remove action button for remote search - invalidateMenu() - } - override fun goBack() { val fragmentManager = supportFragmentManager when { @@ -1199,7 +1192,6 @@ open class MessageList : setDrawerLockState() showDefaultTitleView() - invalidateMenu() onMessageListDisplayed() } @@ -1231,7 +1223,6 @@ open class MessageList : } showMessageTitleView() - invalidateMenu() } private fun showDefaultTitleView() { 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 bcb3665a08..1130b490e0 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 @@ -141,6 +141,7 @@ class MessageListFragment : set(value) { field = value resetActionMode() + invalidateMenu() } override fun onAttach(context: Context) { @@ -537,7 +538,7 @@ class MessageListFragment : activityListener ) - fragmentListener.remoteSearchStarted() + invalidateMenu() } /** @@ -1619,7 +1620,7 @@ class MessageListFragment : } private fun invalidateMenu() { - requireActivity().invalidateMenu() + activity?.invalidateMenu() } private val isCheckMailSupported: Boolean @@ -1978,7 +1979,6 @@ class MessageListFragment : fun setMessageListTitle(title: String, subtitle: String?) fun onCompose(account: Account?) fun startSearch(query: String, account: Account?, folderId: Long?): Boolean - fun remoteSearchStarted() fun goBack() fun onFolderNotFoundError() -- GitLab From cc5fc8e2b040ee2d69f6b71b67044db3e51bbcf7 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 21 Jul 2022 12:56:57 +0200 Subject: [PATCH 22/47] Remove unused code --- .../src/main/java/com/fsck/k9/fragment/MessageListFragment.kt | 4 ---- 1 file changed, 4 deletions(-) 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 1130b490e0..0d2701333b 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 @@ -111,8 +111,6 @@ class MessageListFragment : private var isThreadDisplay = false private var activeMessage: MessageReference? = null - var isLoadFinished = false - private set lateinit var localSearch: LocalSearch private set var isSingleAccountMode = false @@ -1524,8 +1522,6 @@ class MessageListFragment : computeBatchDirection() computeSelectAllVisibility() - isLoadFinished = true - if (savedListState != null) { handler.restoreListPosition(savedListState) savedListState = null -- GitLab From 70f5a589fa98d41e0ca7d753f14db056ba139bf7 Mon Sep 17 00:00:00 2001 From: "r.zarchi" Date: Mon, 25 Jul 2022 10:27:40 +0430 Subject: [PATCH 23/47] searchEnabled set to false when expanding the search view after the rotate --- .../com/fsck/k9/ui/settings/general/GeneralSettingsActivity.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsActivity.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsActivity.kt index 35c4bfd22e..e249416541 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsActivity.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsActivity.kt @@ -100,6 +100,9 @@ class GeneralSettingsActivity : K9Activity(), OnPreferenceStartScreenCallback, S searchPreferenceMenuItem.expandActionView() searchPreferenceActionView.setQuery(searchQuery, false) } + // searchEnabled should be set to false because it caused to expand the search view again + // when we click on any item after each rotation + searchEnabled = false } return true } -- GitLab From d27a566a329fcb9278d89035d51fd4e01c3a5c75 Mon Sep 17 00:00:00 2001 From: "r.zarchi" Date: Mon, 25 Jul 2022 13:06:03 +0430 Subject: [PATCH 24/47] Cc and Bcc text fields checked when handling hide empty field --- .../java/com/fsck/k9/activity/compose/RecipientMvpView.kt | 6 ++++++ .../java/com/fsck/k9/activity/compose/RecipientPresenter.kt | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/compose/RecipientMvpView.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/compose/RecipientMvpView.kt index 60f5b84f62..e3d8ca8557 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/compose/RecipientMvpView.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/compose/RecipientMvpView.kt @@ -72,6 +72,12 @@ class RecipientMvpView(private val activity: MessageCompose) : View.OnFocusChang val bccRecipients: List get() = bccView.objects + val isCcTextEmpty: Boolean + get() = ccView.text.isEmpty() + + val isBccTextEmpty: Boolean + get() = bccView.text.isEmpty() + fun setPresenter(presenter: RecipientPresenter) { this.presenter = presenter toView.setTokenListener(object : RecipientSelectView.TokenListener { diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/compose/RecipientPresenter.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/compose/RecipientPresenter.kt index 9b0e6d2ed3..11eec7594a 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/compose/RecipientPresenter.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/compose/RecipientPresenter.kt @@ -347,14 +347,14 @@ class RecipientPresenter( } private fun hideEmptyExtendedRecipientFields() { - if (recipientMvpView.ccAddresses.isEmpty()) { + if (recipientMvpView.ccAddresses.isEmpty() && recipientMvpView.isCcTextEmpty) { recipientMvpView.setCcVisibility(false) if (lastFocusedType == RecipientType.CC) { lastFocusedType = RecipientType.TO } } - if (recipientMvpView.bccAddresses.isEmpty()) { + if (recipientMvpView.bccAddresses.isEmpty() && recipientMvpView.isBccTextEmpty) { recipientMvpView.setBccVisibility(false) if (lastFocusedType == RecipientType.BCC) { lastFocusedType = RecipientType.TO -- GitLab From c8db3b350ade19e874c8f2cf463a42d188433594 Mon Sep 17 00:00:00 2001 From: Croydon Date: Mon, 25 Jul 2022 20:07:23 +0200 Subject: [PATCH 25/47] Update GitHub URLs to new location --- .github/CONTRIBUTING.md | 6 +++--- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .../com/fsck/k9/activity/setup/AccountSetupCheckSettings.kt | 2 +- .../java/com/fsck/k9/ui/crypto/MessageCryptoHelper.java | 2 +- app/ui/legacy/src/main/res/values/constants.xml | 4 ++-- docs/google-play/full_description.txt | 4 ++-- fastlane/metadata/android/en-US/full_description.txt | 2 +- .../java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 9592db5454..143e5414ce 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -11,7 +11,7 @@ submitting a new issue. * The issue tracker is solely for bug reports and feature/enhancement requests. If you have a question of any kind, please use the [support forum](https://forum.k9mail.app/c/support) instead. -* Search the [existing issues](https://github.com/k9mail/k-9/issues?q=) first to make sure your issue hasn't been +* Search the [existing issues](https://github.com/thundernest/k-9/issues?q=) first to make sure your issue hasn't been reported before. @@ -22,8 +22,8 @@ We're using [Transifex](https://www.transifex.com/k-9/k9mail/) to manage transla ## Contributing code -We love [pull requests](https://github.com/k9mail/k-9/pulls) from everyone! +We love [pull requests](https://github.com/thundernest/k-9/pulls) from everyone! Any contributions, large or small, major features, bug fixes, unit/integration tests are welcomed and appreciated but will be thoroughly reviewed and discussed. -Please make sure you read the [Code Style Guidelines](https://github.com/k9mail/k-9/wiki/CodeStyle). +Please make sure you read the [Code Style Guidelines](https://github.com/thundernest/k-9/wiki/CodeStyle). diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index a746ed32f7..a556ffeb32 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -72,6 +72,6 @@ body: attributes: label: Logs description: | - Please take some time to [retrieve logs](https://github.com/k9mail/k-9/wiki/LoggingErrors) and attach them here. + Please take some time to [retrieve logs](https://github.com/thundernest/k-9/wiki/LoggingErrors) and attach them here. Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupCheckSettings.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupCheckSettings.kt index c6e88d1796..461865655a 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupCheckSettings.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupCheckSettings.kt @@ -388,7 +388,7 @@ class AccountSetupCheckSettings : K9Activity(), ConfirmationDialogFragmentListen /** * FIXME: Don't use an AsyncTask to perform network operations. - * See also discussion in https://github.com/k9mail/k-9/pull/560 + * See also discussion in https://github.com/thundernest/k-9/pull/560 */ private inner class CheckAccountTask(private val account: Account) : AsyncTask() { override fun doInBackground(vararg params: CheckDirection) { diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/crypto/MessageCryptoHelper.java b/app/ui/legacy/src/main/java/com/fsck/k9/ui/crypto/MessageCryptoHelper.java index 4398ead621..b0b9fb039d 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/crypto/MessageCryptoHelper.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/crypto/MessageCryptoHelper.java @@ -619,7 +619,7 @@ public class MessageCryptoHelper { private void onCryptoOperationCanceled() { // there are weird states that get us here when we're not actually processing any part. just skip in that case - // see https://github.com/k9mail/k-9/issues/1878 + // see https://github.com/thundernest/k-9/issues/1878 if (currentCryptoPart != null) { CryptoResultAnnotation errorPart = CryptoResultAnnotation.createOpenPgpCanceledAnnotation(); addCryptoResultAnnotationToMessage(errorPart); diff --git a/app/ui/legacy/src/main/res/values/constants.xml b/app/ui/legacy/src/main/res/values/constants.xml index 840330a5e9..14b6ff81c1 100644 --- a/app/ui/legacy/src/main/res/values/constants.xml +++ b/app/ui/legacy/src/main/res/values/constants.xml @@ -4,8 +4,8 @@ https://docs.k9mail.app/ https://forum.k9mail.app/ K-9 Mail for Android - https://github.com/k9mail/k-9/graphs/contributors - https://github.com/k9mail/k-9 + https://github.com/thundernest/k-9/graphs/contributors + https://github.com/thundernest/k-9 https://www.apache.org/licenses/LICENSE-2.0 \@k9mail@fosstodon.org https://fosstodon.org/@k9mail diff --git a/docs/google-play/full_description.txt b/docs/google-play/full_description.txt index 9571ebdd9d..c51bb04198 100644 --- a/docs/google-play/full_description.txt +++ b/docs/google-play/full_description.txt @@ -4,10 +4,10 @@ K-9 supports IMAP, POP3 and Exchange 2003/2007 (with WebDAV). Install the app "OpenKeychain: Easy PGP" to encrypt/decrypt your emails using OpenPGP. -K-9 is a community developed project. If you're interested in helping to make the most popular open source email client on Android even better, please join us! You can find our bug tracker, source code, mailing list and wiki at https://github.com/k9mail/k-9 +K-9 is a community developed project. If you're interested in helping to make the most popular open source email client on Android even better, please join us! You can find our bug tracker, source code, mailing list and wiki at https://github.com/thundernest/k-9 We're always happy to welcome new developers, designers, documenters, bug triagers and friends. -If you're having trouble with K-9, please report a bug at https://github.com/k9mail/k-9 rather than just leaving a one-star review. We don't mind you telling the world that you're frustrated, but if you use our bug tracker, we have a better chance of fixing whatever's giving you a hard time. +If you're having trouble with K-9, please report a bug at https://github.com/thundernest/k-9 rather than just leaving a one-star review. We don't mind you telling the world that you're frustrated, but if you use our bug tracker, we have a better chance of fixing whatever's giving you a hard time. You can find K-9's release notes at: https://bit.ly/new-k9 diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 6853457f90..82f4dc859d 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -20,5 +20,5 @@ If you're having trouble with K-9 Mail, ask for help in our https://github.com/k9mail/k-9. +You can find our bug tracker, source code, and wiki at https://github.com/thundernest/k-9. We're always happy to welcome new developers, designers, documenters, translators, bug triagers and friends. diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt index c4fadbdac0..43c78cc49b 100644 --- a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt +++ b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt @@ -277,7 +277,7 @@ class SmtpTransport( private fun buildHostnameToReport(): String { val localAddress = socket!!.localAddress - // We use local IP statically for privacy reasons, see https://github.com/k9mail/k-9/pull/3798 + // We use local IP statically for privacy reasons, see https://github.com/thundernest/k-9/pull/3798 return if (localAddress is Inet6Address) { "[IPv6:::1]" } else { -- GitLab From fd006bdb2fdf02074eb5acbf9d63573aa048d0d3 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 26 Jul 2022 12:34:54 +0200 Subject: [PATCH 26/47] Add Western Frisian translation --- .../values/arrays_general_settings_values.xml | 4 +- app/k9mail/build.gradle | 3 +- .../legacy/src/main/res/values-fy/strings.xml | 1043 +++++++++++++++++ .../arrays_general_settings_strings.xml | 3 +- 4 files changed, 1050 insertions(+), 3 deletions(-) create mode 100644 app/ui/legacy/src/main/res/values-fy/strings.xml diff --git a/app/core/src/main/res/values/arrays_general_settings_values.xml b/app/core/src/main/res/values/arrays_general_settings_values.xml index 560eb00e8e..6922273349 100644 --- a/app/core/src/main/res/values/arrays_general_settings_values.xml +++ b/app/core/src/main/res/values/arrays_general_settings_values.xml @@ -17,6 +17,7 @@ eo eu fr + fy gd gl hr @@ -75,6 +76,7 @@ ee fr fr_CA + fy ff ga gd @@ -99,7 +101,6 @@ pl pt_PT pt_BR - ru ro sq sk @@ -118,6 +119,7 @@ ky kk mk + ru sr uk hy diff --git a/app/k9mail/build.gradle b/app/k9mail/build.gradle index f893dc7a1b..58f27128ff 100644 --- a/app/k9mail/build.gradle +++ b/app/k9mail/build.gradle @@ -53,7 +53,8 @@ android { // 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", "hr", "is", "it", "lv", "lt", "hu", "nl", "nb", "pl", "pt_PT", "pt_BR", "ru", "ro", "sq", "sk", "sl", - "fi", "sv", "tr", "el", "be", "bg", "sr", "uk", "iw", "ar", "fa", "ml", "ko", "zh_CN", "zh_TW", "ja" + "fi", "sv", "tr", "el", "be", "bg", "sr", "uk", "iw", "ar", "fa", "ml", "ko", "zh_CN", "zh_TW", "ja", + "fy" minSdkVersion buildConfig.minSdk targetSdkVersion buildConfig.targetSdk diff --git a/app/ui/legacy/src/main/res/values-fy/strings.xml b/app/ui/legacy/src/main/res/values-fy/strings.xml new file mode 100644 index 0000000000..ad2c4723a2 --- /dev/null +++ b/app/ui/legacy/src/main/res/values-fy/strings.xml @@ -0,0 +1,1043 @@ + + + + + + K-9 Mail + K-9-accounts + K-9 Net lêzen + + It K-9-ûntwikkelteam + Boarnekoade + Apache-lisinsje, ferzje 2.0 + Iepenboarnekoade-projekt + Website + Brûkershantlieding + Help krije + Brûkersfoarum + Fediverse + Twitter + Biblioteken + Lisinsje + Wizigingsloch + It laden fan it wizigingsloch is mislearre. + Ferzje %s + Wat is der nij + Lit wizigingen sjen as de app resint bywurke is + Untdek wat der nij is yn dizze ferzje + + Wolkom by K-9 Mail + K-9 Mail is in fergees krêftige e-mailclient foar Android.

De ferbettere mooglikheden bestean ûnder oare út: +

+
    +
  • Pushmail middels IMAP IDLE
  • +
  • Bettere prestaasjes
  • +
  • Berjocht werklassifikaasje
  • +
  • E-mailhantekeningen
  • +
  • Bcc nei josels
  • +
  • Mapabonneminten
  • +
  • Syngronisaasje fan alle mappen
  • +
  • Antwurdadres ynstelle
  • +
  • Toetseboerd fluchkeppelingen
  • +
  • Bettere stipe IMAP
  • +
  • Bylage bewarje nei SD
  • +
  • Jiskefet leegje
  • +
  • Berjochten sortearje
  • +
  • en mear…
  • +
+

+Hâld der rekkening mei dat K-9 de measte fergeze Hotmail-accounts net stipet, en krekt as in protte e-mailclients, problemen hawwe kin om te ferbinen mei Microsoft Exchange. +

+Graach flaterrapporten stjoere, bydragen foar nije funksjes en fragen stelle op +https://github.com/k9mail/k-9/. +

+]]>
+ + -- \nFerstjoerd fan myn Android-apparaat ôf mei K-9 Mail. + + De account ‘%s’ wurdt út K-9 Mail fuortsmiten. + + Auteurs + Oer K-9 mail + Accounts + Avansearre + Nij berjocht + Beäntwurdzje + Alle beäntwurdzje + Trochstjoere + Trochstjoere as bylage + Kies in account + Kies in map + Ferpleatse nei… + Kopiearje nei… + %d selektearre + Folgjende + Foarige + + OK + Annulearje + Ferstjoere + Der is gjin ûnderwerp ynfierd. Tik nochris om dochs te ferstjoeren. + Antwurdzje + Allen beäntwurdzje + Fuortsmite + Argyf + Net-winske + Trochstjoere + Trochstjoere as bylage + As nij berjocht bewurkje + Ferpleatse + Nei konsepten ferpleatse + Ferstjoere… + Opnij bewarje… + Dien + Ferjitte + As konsept bewarje + Op e-mailberjochten kontrolearje + Berjochten ferstjoere + Maplist ferfarskje + Map sykje + Account tafoegje + Nij berjocht + Sykje + Oeral sykje + Sykresultaten + Nije berjochten + Ynstellingen + Mappen beheare + Accountynstellingen + Account fuortsmite + As lêzen markearje + Diele + Ofstjoerder kieze + Stjer tafoegje + Stjer fuortsmite + Kopiearje + Utskriuwe + Berjochtkoppen toane + + Adres nei klamboerd kopiearre + Adressen nei klamboerd kopiearre + + It ûnderwerp is nei it klamboerd kopiearre + Nei donker tema wikselje + Nei ljocht tema wikselje + As net-lêzen markearje + Untfangstbefêstiging + Untfangstbefêstiging freegje + Gjin ûntfangstbefêstiging freegje + Bylage tafoegje + Jiskefet leegje + Wiskje + Oer + Ynstellingen + + (Gjin ûnderwerp) + Gjin ôfstjoerder + Berjochten oan it laden\u2026 + Netwurkflater + Berjocht net fûn + Berjocht laden is mislearre + Folgjende %d berjochten + %.1f GB + %.1f MB + %.1f kB + %d B + Nij berjocht + + %d nije berjocht + %d nije berjochten + + %d Net lêzen (%s) + + %1$d mear by %2$s + Beäntwurdzje + As lêzen markearje + Alles as lêzen markearje + Fuortsmite + Alle fuortsmite + Argivearje + Alles argivearje + Net-winske + Sertifikaatflater + Sertifikaatflater foar %s + Kontrolearje de serverynstellingen + Autentikaasje mislearre + Autentikaasje foar %s mislearre. Wurkje de serverynstellingen by. + + Meldingsflater + + Der is in flater bard wylst it meitsje fan in systeemmelding foar in nij berjocht. De reden is wierskynlik in ûntbrekken fan in meldingslûd.\n\nTik om meldingsynstellingen te iepenjen. + Berjochten kontrolearje: %s:%s + Berjochten kontrolearje + Berjochten ferstjoere: %s + Berjochten ferstjoere + : + Syngronisearje (Push) + Wurdt yn ôfwachting fan nije berjochten werjûn + Berjochten + Meldingen relatearre oan berjochten + Diversken + Oare meldingen, lykas flaters en soksawat. + Postfek YN + Postfek ÚT + Konsepten + Jiskefet + Ferstjoerd + Flater by ferstjoeren fan berjochten + Ferzje + Debuglog tastean + Ekstra diagnostyske ynformaasje logge + Gefoelige ynformaasje logge + Kin wachtwurden yn logs toane. + Logs eksportearje + Eksportearjen is slagge. Der sit mooglik persoanlike ynformaasje yn de logs. Kies ferstannich nei wa’t jo jo logs stjoere. + Eksportearjen is mislearre + Mear berjochten lade + Oan:%s + Underwerp + Berjochttekst + Hantekening + -------- Orizjineel berjocht -------- + Underwerp: + Ferstjoerd: + Fan: + Oan: + Cc: + %s skriuw: + %2$s skreau op %1$s: + Foegje minimaal 1 ûntfanger ta. + De ûntfanger is net (folslein) ynfolle! + Gjin e-mailadres fûn. + Guon bylagen kinne net trochstjoerd wurde, omdat se net download binne. + Dit berjocht kin net trochstjoerd wurde, omdat bylagen net download binne. + Sitearre berjocht tafoegje + Sitaattekst fuortsmite + Sitaattekst bewurkje + Bylage fuortsmite + Fan: %s <%s> + Oan: + Cc: + Bcc: + Kin bylage net bewarje. + Ofbyldingen toane + Gjin viewer te finen foar %s. + Folslein berjocht downloade + fia %1$s + Mear fan dizze ôfstjoerder + Fan %s + Berjocht fuortsmiten + Berjocht as konsept bewarre + Stjerren toane + Stjerren jouwe markearre berjochten oan + Rigels yn it foar besjen + Namme by berjocht toane + Toan by foarkar namme fan ôfstjoerder/adressearre y.s.f. e-mailadres + Korrespondint boppe ûnderwerp + Nammen korrespondinten boppe de ûnderwerprigel toane, net derûnder + Namme út adreslist toane + Brûk namme út it adresboek + Kontakten kleur jaan + Nammen yn jo kontaktlist kleur jaan + Kleur foar nammen fan kontaktpersoanen + Fêste breedte lettertypen + Brûk in lettertype mei fêste breedte by it werjaan fan platte-tekstberjochten + Berjochten auto-passe + Berjochten passend meitsje op it skerm + Werom nei list nei fuortsmiten + Werom nei berjochtelist nei fuortsmiten berjocht + Folgjend berjocht toane nei fuortsmiten + Standert folgjend berjocht toane nei fuortsmiten + Aksjes befêstigje + Altyd in dialoochfinster toane wannear’t jo de selektearre aksjes útfiere + Fuortsmite + Berjochten mei in stjer fuortsmite (yn berjochtwerjefte) + Net-winske + Berjocht ôfbrekke + Alles as lêzen markearje + Fuortsmite (fan meldingen) + E-mailclient ferstopje + K-9-brûkersagent fan e-mailkopteksten fuortsmite + Tiidsône ferstopje + UTC brûke yn stee fan de lokale tiidsône yn de e-mailkopteksten en by it beäntwurdzjen fan e-mailberjochten + Knop ‘Fuortsmite’ toane + Nea + Melding foar los berjocht + Altyd + Startskermmelding + Gjin startskermmelding + Applikaasjenamme + Oantal nije berjochten + Berjochteteller (ek ferstjoerd) + Itselde. Ek nei skermûntskoatteling + Stilteperioade + Beltoan, trille en leds yn de nacht útskeakelje + Notifikaasjes útsette + Meldingen wylst stilteperioade folslein útskeakelje + Stilteperioade start + Stilteperioade ein + In nij account ynstelle + E-mailadres + Wachtwurd + Om mei K-9 dit e-mailaccount te brûken moatte jo jo oanmelde om K-9 tagong te jaan ta jo e-mailberjochten. + + Oanmelde + + Oanmelde mei Google + + Skeakelje op dit apparaat in skermbeskoatteling yn om hjir jo wachtwurd sjen te kinnen. + Jo identiteit ferifiearje + Untskoattelje om jo wachtwurd te sjen + Hânmjittich ynstelle + + Accountynformaasje ophelje\u2026 + Kontrôle fan ynstellingen ynkommende server\u2026 + Kontrôle fan ynstellingen útgeande server\u2026 + Autentikaasje\u2026 + Accountynstellingen ophelje\u2026 + Annulearje\u2026 + Hast klear! + Jou dit account in namme (opsjoneel): + Typ jo namme (sichtber by útgeande berjochten): + Accounttype + Hokker type account is dit? + POP3 + IMAP + Normaal wachtwurd + Wachtwurd, net feilich ferstjoerd + Fersifere wachtwurd + Clientsertifikaat + OAuth 2.0 + Ynkommende serverynstellingen + Brûkersnamme + Wachtwurd + Clientsertifikaat + POP3-server + IMAP-server + WebDAV (Exchange)-server + Poarte + Befeiligingstype + Autentikaasjetype + Gjin + SSL/TLS + STARTTLS + %1$s = %2$s’ is net jildich mei ‘%3$s = %4$s + Wannear ik in berjocht fuortsmyt + Net fan server fuortsmite + Fan server fuortsmite + Op server as lêzen markearje + Kompresje brûke + Op server fuortsmiten berjochten wiskje + Daliks nei it fuortsmiten of ferpleatsen + By elke berjochtkontrôle + Hânmjittich + IMAP-namespace automatysk detektearje + IMAP-paadfoarfoegsel + Konseptmap + Ferstjoerdmap + Jiskefetmap + Argyfmap + Net-winskemap + Allinnich abonnearre mappen toane + Map automatysk útklappe + WebDAV (Exchange) path + Autentikaasjepaad + Postbusalias + Utgeande serverynstellingen + SMTP-server + Poarte + Befeiligingstype + Oanmelden fereaske + Brûkersnamme + Wachtwurd + Autentikaasjetype + %1$s = %2$s’ is net jildich mei ‘%3$s = %4$s + Unjildige ynstellingen: %s + Accountopsjes + Frekwinsje mapkontrôle + Nea + Elke 15 minuten + Elke 30 minuten + Elk oere + Elke 2 oeren + Elke 3 oeren + Elke 6 oeren + Elke 12 oeren + Elke 24 oeren + Ynaktive ferbining fernije + Elke 2 minuten + Elke 3 minuten + Elke 6 minuten + Elke 12 minuten + Elke 24 minuten + Elke 36 minuten + Elke 48 minuten + Elke 60 minuten + Warskôgje my wannear nije e-mailberjochten ynkomme + Oantal berjochten om te toanen + 10 berjochten + 25 berjochten + 50 berjochten + 100 berjochten + 250 berjochten + 500 berjochten + 1000 berjochten + 2500 berjochten + 5000 berjochten + 10000 berjochten + alle berjochten + Kin berjocht net kopiearje of ferpleatse, omdat dizze net syngronisearre is mei de server + Ynstellen koe net foltôgje + Brûkersnamme of wachtwurd ûnjildich.\n(%s) + De server presintearre in ûnjildich SSL-sertifikaat. Dit kin barre troch in ferkeard konfigurearre server. Dit kin ek barre trochdat ien jo e-mailserver probearret oan te fallen. As jo net wis binne wat der oan de hân is, klik dan op Reject en nim kontakt op mei de behearder fan de e-mailserver.\n\n(%s) + Kin gjin ferbining mei server meitsje.\n(%s) + Autorisaasje is annulearre + Autorisaasje is mislearre mei de folgjende flater: %s + OAuth 2.0 wurdt op dit stuit net stipe troch dizze tsjinstferliener. + Dizze app kin gjin browser fine. In browser is nedich om tagong te jaan ta jo account. + Details oanpasse + Trochgean + Avansearre + Accountynstellingen + Nije-e-mailmelding + Meldingsmappen + Alles + Allinnich 1e-klassemappen + 1e- en 2e-klassemappen + Alle útsein 2e-klassemappen + Gjin + Syngronisaasjemeldingen + Jo e-mailadres + Melding yn steatbalke by in nij e-mailberjocht + Yn steatbalke melding werjaan wannear op nije e-mail kontrolearre wurdt + By eigen berjochten + Melding ek foar e-mail ferstjoerd fan in identiteit ôf + Allinnich kontakten + Meldingen allinnich werjaan foar bekende kontakten + Petearberjochten negearje + Gjin meldingen toane foar berjochten dy’t ûnderdiel binne fan in e-mailpetear + As lêzen markearje as iepene + As lêzen markearje as besjoen + As lêzen markearje by wiskjen + Markearje in berjocht as lêzen wannear’t dy wiske wurdt + Meldingskategoryen + Meldingen foar nije berjochten ynstelle + Flater- en steatmeldingen ynstelle + Ofbyldingen automatysk toane + Nea + Allinnich fan kontakten + Altyd + Berjochten ferstjoere + Berjocht by beäntwurdzjen sitearje + Orizjinele berjocht yn it antwurd meinimme. + Beäntwurdzje nei sitaat + Wannear’t jo antwurdzje op in berjocht, sil it orizjinele berjocht boppe jo antwurd stean. + Hantekening by reaksje fuortsmite + Hantekeningen wurde by sitearre berjochten fuortsmiten + Berjochtopmaak + Platte tekst (ôfbyldingen en opmaak wurde fuortsmiten) + HTML (ôfbyldingen en opmaak bliuwe behâlden) + Automatysk + Cc/Bcc altyd toane + Untfangstbefêstiging + Altyd in lêsbefêstiging freegje + Sitaatstyl by beäntwurdzjen + Prefix (lykas Gmail, Pine) + Kop (lykas Outlook) + Ferstjoerde berjochten oplade + Berjochten nei ferstjoeren nei ‘Ferstjoerd’-map oplade + Algemiene ynstellingen + Berjochten lêze + Berjochten ophelje + Mappen + Sitaatfoarfoegsel + Ein-ta-einfersifering + OpenPGP-stipe ynskeakelje + OpenPGP-app selektearje + Ein-ta-einkaai ynstelle + Gjin OpenPGP-app ynsteld + Ferbûn mei %s + Konfiguraasje dwaande… + Alle konsepten fersifere bewarje. + Alle konsepten sille fersifere bewarre wurde + Konsepten allinnich fersiferje as fersifering ynskeakele is + Frekwinsje mapkontrôle + Accountkleur + De accountkleur brûkt by mappen en accountlist + Grutte fan lokale map + Automatysk berjochten downloade oant + 1 KiB + 2 KiB + 4 KiB + 8 KiB + 16 KiB + 32 KiB + 64 KiB + 128 KiB + 256 KiB + 512 KiB + 1 MiB + 2 MiB + 5 MiB + 10 MiB + elke grutte (gjin limyt) + Berjochten syngronisearje fan + alles (gjin limyt) + hjoed + lêste 2 dagen + lêtste 3 dagen + ôfrûne wike + ôfrûne 2 wiken + ôfrûne 3 wiken + ôfrûne moanne + ôfrûne 2 moannen + ôfrûne 3 moannen + ôfrûne 6 moannen + ôfrûne jier + Mappen om te toanen + Alles + Allinnich 1e-klassemappen + 1e- en 2e-klassemappen + Alle útsein 2e-klassemappen + Peiling mappen + Alles + Allinnich 1e-klassemappen + 1e- en 2e-klassemappen + Alle útsein 2e-klassemappen + Gjin + Push-mappen + Alles + Allinnich 1e-klassemappen + 1e- en 2e-klassemappen + Alle útsein 2e-klassemappen + Gjin + Ferpleats/kopiearje doelmappen + Alles + Allinnich 1e-klassemappen + 1e- en 2e-klassemappen + Alle útsein 2e-klassemappen + Ferwideringen op server syngronisearje + Berjochten fuortsmite as fuortsmiten fan server + OpenPGP-app net oanwêzich – is de app fuortsmiten? + Mapynstellingen + Yn topgroep toane + By top fan de maplist toane + Mapwerjefteklasse + Gjin klasse + 1e klasse + 2e klasse + Mappeilingklasse + Gjin + 1e klasse + 2e klasse + Selde as werjefteklasse + Pushklassemap + Gjin klasse + 1e klasse + 2e klasse + Selde as peilingklasse + Mapmeldingsklasse + Gjin klasse + 1e klasse + 2e klasse + Selde as pushklasse + Lokale berjochten wiskje + Ynkommende server + Ynstellen fan de ynkommende mailserver + Utgeande server + Ynstellen fan de útgeande (SMTP) server + Accountnamme + Jo namme + Meldingen + Trille + Trille + Trilpatroan + Standert + Patroan 1 + Patroan 2 + Patroan 3 + Patroan 4 + Patroan 5 + Oantal trillingen + Utskeakele + Beltoan nij e-mailberjocht + Meldingsljocht + Utskeakele + Accountkleur + Systeemstandertkleur + Wyt + Read + Grien + Blau + Giel + Syaan + Maginta + Opsjes berjochtgearstalling + Standert gearstalling + Stel standert yn foar: Fan, Bcc en hântekening + Identiteiten beheare + Ynstellen alternative ‘Fan’-adressen en hantekeningen + Identiteiten beheare + Identiteit beheare + Identiteit oanpasse + Bewarje + Nije identiteit + Alle berjochten Bcc nei + Bewurkje + Omheech ferpleatse + Omleech ferpleatse + Nei boppe ferpleatse / standert meitsje + Fuortsmite + Identiteitsbeskriuwing + (Opsjoneel) + Jo namme + (Opsjoneel) + E-mailadres + (Fereaske) + Antwurdadres + (Opsjoneel) + Hantekening + (Opsjoneel) + Hantekening brûke + Hantekening + Inisjele identiteit + Identiteit kieze + Ferstjoere as + Jo kinne jo eigen identiteit net fuosrtsmite + Jo kinne in identiteit net brûke sûnder e-mailadres + Aldste berjochten earst + Nijste berjochten earst + Underwerp alfabetysk + Underwerp omkeard alfabetysk + Ofstjoerder alfabetysk + Ofstjoerder omkeard alfabetysk + Berjochten mei stjer earst + Berjochten sûnder stjer earst + Net-lêzen berjochten earst + Lêzen berjochten earst + Berjochten mei bylagen earst + Berjochten sûnder bylagen earst + Sortearje op… + Datum + Oankomst + Underwerp + Ofstjoerder + Stjer + Lêzen/net-lêzen + Bylagen + Account fuortsmite + Unbekend sertifikaat + Kaai akseptearje + Kaai wegerje + Del (of D) - Fuortsmite\nR - Beäntwurdzje\nA - Elkenien beäntwurdzje\nC - Opstelle\nF - Trochstjoere\nM - Ferpleatse\nV - Argivearje\nY - Kopiearje\nZ - (net)lêzen markearje\nG - Stjer\nO - Sorteartype\nI - Sortearfolchoarder\nQ - Tebek nei Mappen\nS - Selektearje/deselektearje\nJ of P - Foarich berjocht\nK of N - Folgjende berjocht + Del (of D) - Fuortsmite\nC - Opstelle\nM - Ferpleatse\nV - Argivearje\nY - Kopiearje\nZ - (net)lêzen markearje\nG - Stjer\nO - Sorteartype\nI - Sortearfolchoarder\nQ - Tebek nei Mappen\nS - Selektearje/deselektearje + Mapnamme befettet + Mappen + Alle mappen + 1e-klassemappen + 1e- & 2e-klassemappen + 2e-klassemappen ferstopje + Posysje hantekening + Foar sitearre berjocht + Nei sitearre berjocht + App-tema brûke + Donker + Ljocht + Systeemstandert brûke + Algemiene ynstellingen + Algemien + Debugge + Privacy + Netwurk + Ynteraksje + Accountlist + Berjochtlisten + Berjochten + Tema + Tema om berjochten te sjen + Tema om berjochten te skriuwen + Taal + Gjin ynstellingen fûn + Fêst berjochtetema + Kies it tema wylst it besjen fan it berjocht + Fêst tema brûke om it berjocht te besjen + Systeemstandert + Eftergrûnsyngronisaasje + Nea + Altyd + As ‘Auto-sync’ selektearre is + Alle selektearje + Maks. mappen om te kontrolearjen mei push + 5 mappen + 10 mappen + 25 mappen + 50 mappen + 100 mappen + 250 mappen + 500 mappen + 1000 mappen + Animaasje + Opsichtige fisuele effekten brûke + Folumetoetsnavigaasje + Berjochtbyld + Fariabele listwerjefte + Kombinearre Postfek Yn werjaan + It oantal berjochten mei in stjer werjaan + Kombinearre Postfek Yn + Alle berjochten yn kombinearre mappen + Kombinearje + Alle berjochten wurde yn it kombinearre Postfek Yn werjûn + Sykmappen + Alles + Sichtbere + Gjin + Gjin + Automatysk (%s) + Lettergrutte + Lettergrutte ynstelle + Accountlist + Accountnamme + Accountbeskriuwing + Maplisten + Mapnamme + Mapsteat + Berjochtlisten + Underwerp + Ofstjoerder + Datum + Foarbyld + Berjochten + Ofstjoerder + Oan + Cc + Bcc + Ekstra koppen + Underwerp + Tiid en datum + Berichtynhâld + Berjocht opstelle + Tekst ynfierfjilden + Standert + Meast lytse + Hiel lyts + Lytser + Lyts + Gemiddeld + Grut + Grutter + Gjin geskikte applikaasje fûn foar dizze aksje. + Ferstjoeren mislearre: %s + Konsept bewarje? + Dit berjocht bewarje of ferwerpe? + Wizigingen bewarje of ferwerpe? + Berjocht ôfbrekke? + Binne jo wis dat jo dit berjocht fuortsmite wolle + Selektearje tekst om te kopiearjen. + Lokale berjochten wiskje? + Dit sil alle lokale berjochten út dizze map wiskje. Der wurde gjin berjochten fan de server wiske. + Berjochten wiskje + Fuortsmiten befêstigje + Wolle jo dit berjocht fuortsmite? + + Wolle jo dit berjocht echt fuortsmite? + Wolle jo echt %1$d berjochten fuortsmite? + + Ja + Nea + Alles as lêzen markearje + Wolle jo alle berjochten as lêzen markearje? + Jiskefet leegje befêstigje + Wolle jo it jiskefet leech smite? + Ja + Nea + Ferpleatsing nei net-winskemap befêstigje + + Wolle jo dit berjocht echt ferpleatse nei de map Net-winske? + Wolle jo echt %1$dferpleatse nei de map Net-winske? + + Ja + Nea + Bylage wurdt ophelle + » + + Reservekopy + Diversken + Eksportynstellingen + Eksportearje + Diele + Eksportynstellingen… + Ynstellingen mei sukses eksportearre + Ynstellingen eksportearjen mislearre + Ymportynstellingen + Bestân selektearje + Ymportearje + Ynstellingen mei sukses ymportearre + + Fier de wachtwurden yn + + Oanmelde + + Meld jo oan en fier it wachtwurd yn + Ynstellingen ymportearjen mislearre + Lêzen fan ynstellingenbestân mislearre + Ymportearjen fan guon ynstellingen mislearre + Mei sukses ymportearre + Wachtwurd fereaske + + Oanmelden fereaske + Net ymportearre + Ymportearjen mislearre + Letter + Ymportynstellingen + Ymportynstellingen… + + Om de account ‘%s’ te brûken moatte jo it wachtwurd fan de server opjaan. + Om de account ‘%s’ te brûken moatte jo de wachtwurden fan de server opjaan. + + Wachtwurd ynkommende server + Wachtwurd útgeande server + Brûk itselde wachtwurd foar de útgeande server + Servernamme: %s + Oantal net-lêzen werjaan foar… + Account + De account wêrfan it oantal net-lêzen berjochten toand wurdt + Kombinearre Postfek Yn + Mapoantal + Toan it oantal net-lêzen berjochten fan in inkelde map + Map + De map wêrby oantal net-lêzen berjochten toand wurdt + Dien + %1$s - %2$s + Gjin account keazen + Gjin map keazen + Gjin tekst + Keppeling iepenje + Keppeling diele + Keppeling nei klamboerd kopiearje + Keppeling + Keppelingtekst nei klamboerd kopiearje + Keppelingstekst + Ofbylding + Ofbylding toane + Ofbylding bewarje + Ofbylding downloade + Ofbylding-URL nei klamboerd kopiearje + Ofbylding-URL + Nûmer belje + Yn kontakten bewarje + Nûmer nei klamboerd kopiearje + Telefoannûmer + E-mailberjocht stjoere + Yn kontakten bewarje + Adres nei klamboerd kopiearje + E-mailadres + Alles + 10 + 25 + 50 + 100 + 250 + 500 + 1000 + Server-sykklimyt + Sykopdracht nei server stjoere + + %d resultaat ophelje + %d resultaten ophelje + + + %1$d fan %2$d berjochten ophelje + %1$d fan %2$d berjochten ophelje + + Sykopdracht mislearre + Sykje + Berjochten op server sykje + In netwurkferbining is foar sykjen op server nedich. + Kleur wizigje nei lêzen + In oare eftergrûn toane neidat it e-mailberjocht lêzen is + Petearoersjoch + Berjochten per petear groepearje + Databases bywurkje + Databases bywurkje dwaande… + Databases fan account ‘%s’ bywurkje + Splitst skerm toane + Altyd + Nea + Yn lizzende oriïntaasje + Selektearje in berjocht oan de linker kant + Kontaktôfbyldingen toane + Kontactôfbyldingen yn de berjochtelist toane + Alles as lêzen markearje + Kontaktôfbyldingen kleurje + Ofwêzige kontaktôfbyldingen in kleur jaan + Sichtbere berjochtaksjes + Selektearre aksjes yn it Berjochte-menu + Bylage oan it laden… + Berjocht wurdt ferstjoerd + Konsept wurdt bewarre + Bylage oan it opheljen… + Kin net autentisearje. De server stipet gjin SASL EXTERNAL. Dit kin komme troch in probleem mei it clientsertifikaat (ferrûn of ûnbekende CA) of in oar konfiguraasjeprobleem. + + Clientsertifikaat brûke + Gjin clientsertifikaat + Clientsertifikaatseleksje fuortsmite + Clientsertifikaat net ûntfongen foar alias \‘%s\’ + Avansearre opsjes + Clientsertifikaat \‘%1$s\’ is ferrûn of net jildich (%2$s) + + *Fersifere* + Fan kontakten út tafoegje + Bericht ontvanger (CC) + Bericht ontvanger (BCC) + Oan + Fan + Antwurdzje nei + <Unbekende ûntfanger> + <Unbekende ôfstjoerder> + Privee + Wurk + Oars + Mobyl + Gjin konseptemap ynsteld foar dit account! + Gjin kaai ynsteld foar dit account! Kontrolearje jo ynstellingen. + Cryptoprovider brûkt ynkompatibele ferzje. Kontrolearje jo ynstellingen! + Kin gjin ferbining meitsje mei de cryptoprovider. Kontrolearje de ynstellingen of klik op it cryptopiktogram om nochris te probearjen. + Inisjalisaasje fan ein-ta-einfersifering is mislearre, kontrolearje de ynstellingen + PGP/MIME-ynstelling stipet gjin bylagen! + PGP/INLINE tastean + PGP/INLINE útskeakelje + PGP-hantekening tastean + PGP-hantekening útskeakelje + PGP/INLINE-ynstellingen + It e-mailberjocht is ferstjoerd yn PGP/INLINE-formaat.\nDit wurdt allinnich brûkt foar kompatibiliteit: + Guon clients stypje allinnich dit formaat + Hantekeningen kinne ûnderweis brekke + Bylagen wurde net stipe + Begrepen! + Utskeakelje + Ynskeakele hâlde + Begrepen! + Utskeakelje + Ynskeakele hâlde + allinnich PGP-hantekeningmodus + Yn dizze modus wurdt dyn PGP-kaai brûkt foar in kryptografyske hantekening of in net-kodearre e-mailberjocht. + Dit fersiferet net it e-mailberjocht, mar kontrolearret dat jo eigen kaai brûkt is. + Hantekeningen kinne by ferstjoeren nei mailinglist brekke + Hantekeningen kinne by guon programma\'s as ‘signature.asc’-bylage werjûn wurde + Fersifere berjochten befetsje altyd in hantekening. + Platte tekst + ein-ta-ein-hantekening befettet in flater + moat berjocht folslein downloade om hantekening te ferwurkjen + befettet net-stipe ein-ta-ein-hantekening + Berjocht is fersifere, mar yn in net-stipe formaat. + Berjocht is fersifere, mar ûntsiferjen is annulearre. + Ein-ta-ein tekene platte tekst + fan ferifiearre ûndertekener + Platte tekst tekene + mar ein-ta-ein-kaai komt net oerien mei ôfstjoerder + mar ein-ta-ein-kaai is ferrûn + mar ein-ta-ein-kaai is ynlutsen + mar ein-ta-ein-kaai is net feilich + fan in ûnbekende ein-ta-ein-kaai + Fersifere + mar der is in ûntsiferflater bard + moat berjocht folslein downloade foar ûntsifering + mar der is gjin crypto-app konfigurearre + Fersifere + mar net ein-ta-ein + Ein-ta-ein fersifere + fan ferifiearre ôfstjoerder + Fersifere + fan in ûnbekende ein-ta-ein-kaai + mar ein-ta-ein-kaai komt net oerien mei ôfstjoerder + mar ein-ta-ein-kaai is ferrûn + mar ein-ta-ein-kaai is ynlutsen + mar ein-ta-ein-kaai is net feilich + mar ein-ta-ein-gegevens befetsje flaters + mar fersifering is net feilich + OK + Kaai sykje + Undertekener besjen + Ofstjoerder besjen + Details + Deblokkearje + Dit ûnderdiel is net fersifere en is miskien net feilich. + Unbefeilige bylage + Lade… + Untsifering is stoppe + Opnij + Fersifere berjocht moat download wêze foar ûntsifering. + Flater wylst ûntsiferjen e-mailberjocht + Spesjale lêstekens wurde noch net stipe! + Flater by ferwurkjen fan adres! + Net-fersifere hantekeningen ferstopje + Allinnich fersifere hantekeningen wurde werjûn + Alle hantekeningen wurde werjûn + Fersifering net mooglik yn sign-only-modus! + net ûndertekene tekst + Dit e-mailberjocht is fersifere + Dit e-mailberjocht is OpenPGP-fersifere.\nYnstallearje en stel in OpenPGP-app yn om it e-mailberjocht te lêzen. + Gean nei Ynstellingen + K-9-berjochtelist + Berjochten oan it laden… + Fersifering net mooglik + Guon fan de ûntfangers stypje dizze funksje net! + Fersifering ynskeakelje + Fersifering útskeakelje + Fersifering makket dat berjochten allinnich troch de ûntfanger lêzen wurde kin en nearne oars. + Fersifering is allinnich beskikber as alle ûntfangers dit stypje en dyjinge moatte jo al earder in e-mailberjocht stjoerd hawwe. + Skeakelje fersifering yn of út mei dit piktogram. + Ik begryp it + Tebek + Fersifering útskeakelje + OpenPGP-fersifering + Autocrypt fan wjerskantenmodus + Autocrypt fan wjerskantenmodus + Berjochten wurde op fersyk fersifere, of by beäntwurdzjen fan in fersifere berjocht. + As ôfstjoerder en ûntfanger de fan-wjerskantenmodus ynskeakelje, dan wurdt standert fersifering ynsteld. + Klik hjir foar mear ynformaasje. + Algemiene ynstellingen + Gjin OpenPGP-app ynstallearre + Ynstallearje + K-9 Mail fereasket OpenKeychain foar ein-ta-ein-fersifering. + Fersifere berjocht + Underwerp fan berjocht fersiferje + Mooglik net stipe troch alle ûntfangers + Ynterne flater: Unjildich account! + Flater by it ferbinen mei %s! + Autocrypt ynstelberjocht ferstjoere + Feilich dielen fan ein-ta-ein-ynstellingen mei oare apparaten + Autocrypt ynstelberjocht + In Autocrypt ynstelberjocht dielt jo ein-ta-ein-ynstellingen op in befeilige manier mei oare apparaten. + Ynstelberjocht ferstjoere + It berjocht wurdt ferstjoerd nei jo adres: + Ynstelberjocht oan it generearjen… + Berjocht stjoere nei: + Om te foltôgjen, iepenje it berjocht op jo oare apparaat en fier de ynstelkoade yn. + Ynstelkoade werjaan + Autocrypt ynstelberjocht + Dit berjocht befettet alle ynformaasje om jo Autocrypt-ynstellingen mei geheime kaai befeilige oer te bringen fan jo oarspronklike apparaat ôf. + +Folgje de ynstruksjes op jo nije apparaat om dêrop Autocrypt yn te stellen. + +Jo kinne dit berjocht bewarje as reservekopy foar jo geheime kaai. As jo dit dwaan wolle, skriuw dan it wachtwurd op en bewarje it op in feilich plak. + + Der is in flater bard wylst it ferstjoeren fan it berjocht. Kontrolearje de netwurkferbining en útgeande serverkonfiguraasje. + Oan + Ut + + Tagong ta kontakten tastean + Om kontaktsuggestjes biede te kinnen en om nammen en/of foto\'s fan kontakten wer te jaan, hat de app tagong nedich ta jo kontaktelist. + Der is in flater bard by it laden fan de gegevens + Oan it inisjalisearjen… + Yn ôfwachting fan nije e-mailberjochten + Docht neat oant eftergrûnsyngronisaasje tastien wurdt + Docht neat oant der in netwurk beskikber is + Tik hjir om der mear oer te lêzen. + Push-ynformaasje + As jo push brûke, dan hâld K-9 Mail in ferbining iepen mei de e-mailserver. Android fereasket dat in app dy op de eftergrûn aktyf bliuwt in melding pleatst. %s + Android makket it echter ek mooglik om meldingen te ferstopjen. + Mear ynfo + Melding ynstelle + As jo it net nedich fine om daliks meldingen te ûntfangen fan nije berjochten, dan kinne jo better push útskeakelje en yn stee dêrfan kieze foar peilen. Mei peilen wurdt der om de sa folle tiid kontrolearre oft der nije e-mailberjochten ynkommen binne. Dêrfoar is it werjaan fan in melding net kontinu nedich. + Push útskeakelje +
diff --git a/app/ui/legacy/src/main/res/values/arrays_general_settings_strings.xml b/app/ui/legacy/src/main/res/values/arrays_general_settings_strings.xml index d2e38b39c2..328f4cce3b 100644 --- a/app/ui/legacy/src/main/res/values/arrays_general_settings_strings.xml +++ b/app/ui/legacy/src/main/res/values/arrays_general_settings_strings.xml @@ -23,6 +23,7 @@ Ɛʋɛ Français Français (Canada) + Frysk Fulfulde, Pulaar, Pular Gaeilge Gàidhlig @@ -47,7 +48,6 @@ Polski Português Português (Brasil) - Pyccĸий Română Shqip Slovenčina @@ -66,6 +66,7 @@ Кыргыз Қазақ Македонски + Русский Српски Українська Հայերեն -- GitLab From 7dbae49c8e73a63eabf8458483a095434c64b098 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 26 Jul 2022 18:03:01 +0200 Subject: [PATCH 27/47] Fix removing multiple notifications at once --- .../k9/notification/NotificationDataStore.kt | 49 ++++++++++++------- .../notification/NotificationDataStoreTest.kt | 32 ++++++++++++ 2 files changed, 62 insertions(+), 19 deletions(-) diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationDataStore.kt b/app/core/src/main/java/com/fsck/k9/notification/NotificationDataStore.kt index db99f40340..de9f8c94be 100644 --- a/app/core/src/main/java/com/fsck/k9/notification/NotificationDataStore.kt +++ b/app/core/src/main/java/com/fsck/k9/notification/NotificationDataStore.kt @@ -108,32 +108,43 @@ internal class NotificationDataStore { account: Account, selector: (List) -> List ): RemoveNotificationsResult? { - val notificationData = getNotificationData(account) + var notificationData = getNotificationData(account) if (notificationData.isEmpty()) return null val removeMessageReferences = selector.invoke(notificationData.messageReferences) + if (removeMessageReferences.isEmpty()) return null val operations = mutableListOf() val newNotificationHolders = mutableListOf() val cancelNotificationIds = mutableListOf() - for (messageReference in removeMessageReferences) { - val notificationHolder = notificationData.activeNotifications.firstOrNull { - it.content.messageReference == messageReference + val activeMessageReferences = notificationData.activeNotifications.map { it.content.messageReference }.toSet() + val (removeActiveMessageReferences, removeInactiveMessageReferences) = removeMessageReferences + .partition { it in activeMessageReferences } + + if (removeInactiveMessageReferences.isNotEmpty()) { + val inactiveMessageReferences = notificationData.inactiveNotifications + .map { it.content.messageReference }.toSet() + + for (messageReference in removeInactiveMessageReferences) { + if (messageReference in inactiveMessageReferences) { + operations.add(NotificationStoreOperation.Remove(messageReference)) + } } - if (notificationHolder == null) { - val inactiveNotificationHolder = notificationData.inactiveNotifications.firstOrNull { - it.content.messageReference == messageReference - } ?: continue + val removeMessageReferenceSet = removeInactiveMessageReferences.toSet() + notificationData = notificationData.copy( + inactiveNotifications = notificationData.inactiveNotifications + .filter { it.content.messageReference !in removeMessageReferenceSet } + ) + } - operations.add(NotificationStoreOperation.Remove(messageReference)) + for (messageReference in removeActiveMessageReferences) { + val notificationHolder = notificationData.activeNotifications.first { + it.content.messageReference == messageReference + } - val newNotificationData = notificationData.copy( - inactiveNotifications = notificationData.inactiveNotifications - inactiveNotificationHolder - ) - notificationDataMap[account.uuid] = newNotificationData - } else if (notificationData.inactiveNotifications.isNotEmpty()) { + if (notificationData.inactiveNotifications.isNotEmpty()) { val newNotificationHolder = notificationData.inactiveNotifications.first() .toNotificationHolder(notificationHolder.notificationId) @@ -148,29 +159,29 @@ internal class NotificationDataStore { ) ) - val newNotificationData = notificationData.copy( + notificationData = notificationData.copy( activeNotifications = notificationData.activeNotifications - notificationHolder + newNotificationHolder, inactiveNotifications = notificationData.inactiveNotifications.drop(1) ) - notificationDataMap[account.uuid] = newNotificationData } else { cancelNotificationIds.add(notificationHolder.notificationId) operations.add(NotificationStoreOperation.Remove(messageReference)) - val newNotificationData = notificationData.copy( + notificationData = notificationData.copy( activeNotifications = notificationData.activeNotifications - notificationHolder ) - notificationDataMap[account.uuid] = newNotificationData } } + notificationDataMap[account.uuid] = notificationData + return if (operations.isEmpty()) { null } else { RemoveNotificationsResult( - notificationData = getNotificationData(account), + notificationData = notificationData, notificationStoreOperations = operations, notificationHolders = newNotificationHolders, cancelNotificationIds = cancelNotificationIds diff --git a/app/core/src/test/java/com/fsck/k9/notification/NotificationDataStoreTest.kt b/app/core/src/test/java/com/fsck/k9/notification/NotificationDataStoreTest.kt index 57ffc8aba4..c3000f7711 100644 --- a/app/core/src/test/java/com/fsck/k9/notification/NotificationDataStoreTest.kt +++ b/app/core/src/test/java/com/fsck/k9/notification/NotificationDataStoreTest.kt @@ -92,6 +92,38 @@ class NotificationDataStoreTest : RobolectricTest() { } } + @Test + fun `remove multiple notifications`() { + repeat(MAX_NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS + 1) { index -> + notificationDataStore.addNotification(account, createNotificationContent(index.toString()), TIMESTAMP) + } + + val result = notificationDataStore.removeNotifications(account) { it.dropLast(1) } + + assertNotNull(result) { removeResult -> + assertThat(removeResult.notificationData.newMessagesCount).isEqualTo(1) + assertThat(removeResult.cancelNotificationIds).hasSize(MAX_NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS) + } + } + + @Test + fun `remove all notifications`() { + repeat(MAX_NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS + 1) { index -> + notificationDataStore.addNotification(account, createNotificationContent(index.toString()), TIMESTAMP) + } + + val result = notificationDataStore.removeNotifications(account) { it } + + assertNotNull(result) { removeResult -> + assertThat(removeResult.notificationData.newMessagesCount).isEqualTo(0) + assertThat(removeResult.notificationHolders).hasSize(0) + assertThat(removeResult.notificationStoreOperations).hasSize(MAX_NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS + 1) + for (notificationStoreOperation in removeResult.notificationStoreOperations) { + assertThat(notificationStoreOperation).isInstanceOf(NotificationStoreOperation.Remove::class.java) + } + } + } + @Test fun testRemoveDoesNotLeakNotificationIds() { for (i in 1..MAX_NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS + 1) { -- GitLab From 147945f7578593ade7611497e5eddc90c74c7b54 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 26 Jul 2022 22:01:39 +0200 Subject: [PATCH 28/47] Make sure `Intents` used by `K9NotificationActionCreator` are unique --- .../K9NotificationActionCreator.kt | 117 ++++++++++++------ 1 file changed, 77 insertions(+), 40 deletions(-) diff --git a/app/k9mail/src/main/java/com/fsck/k9/notification/K9NotificationActionCreator.kt b/app/k9mail/src/main/java/com/fsck/k9/notification/K9NotificationActionCreator.kt index 77a997ced2..0da55f29a6 100644 --- a/app/k9mail/src/main/java/com/fsck/k9/notification/K9NotificationActionCreator.kt +++ b/app/k9mail/src/main/java/com/fsck/k9/notification/K9NotificationActionCreator.kt @@ -5,6 +5,7 @@ import android.app.PendingIntent.FLAG_CANCEL_CURRENT import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.content.Context import android.content.Intent +import android.net.Uri import com.fsck.k9.Account import com.fsck.k9.K9 import com.fsck.k9.activity.MessageList @@ -25,8 +26,10 @@ import com.fsck.k9.ui.notification.DeleteConfirmationActivity * We need to take special care to ensure the `PendingIntent`s are unique as defined in the documentation of * [PendingIntent]. Otherwise selecting a notification action might perform the action on the wrong message. * - * We use the notification ID as `requestCode` argument to ensure each notification/action pair gets a unique - * `PendingIntent`. + * We add unique values to `Intent.data` so we end up with unique `PendingIntent`s. + * + * In the past we've used the notification ID as `requestCode` argument when creating a `PendingIntent`. But since we're + * reusing notification IDs, it's safer to make sure the `Intent` itself is unique. */ internal class K9NotificationActionCreator( private val context: Context, @@ -41,12 +44,12 @@ internal class K9NotificationActionCreator( val openInUnifiedInbox = K9.isShowUnifiedInbox && isIncludedInUnifiedInbox(messageReference) val intent = createMessageViewIntent(messageReference, openInUnifiedInbox) - return PendingIntent.getActivity(context, notificationId, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) + return PendingIntent.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) } override fun createViewFolderPendingIntent(account: Account, folderId: Long, notificationId: Int): PendingIntent { val intent = createMessageListIntent(account, folderId) - return PendingIntent.getActivity(context, notificationId, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) + return PendingIntent.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) } override fun createViewMessagesPendingIntent( @@ -64,38 +67,46 @@ internal class K9NotificationActionCreator( createNewMessagesIntent(account) } - return PendingIntent.getActivity(context, notificationId, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) + return PendingIntent.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) } override fun createViewFolderListPendingIntent(account: Account, notificationId: Int): PendingIntent { val intent = createMessageListIntent(account) - return PendingIntent.getActivity(context, notificationId, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) + return PendingIntent.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) } override fun createDismissAllMessagesPendingIntent(account: Account, notificationId: Int): PendingIntent { - val intent = NotificationActionService.createDismissAllMessagesIntent(context, account) - return PendingIntent.getService(context, notificationId, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) + val intent = NotificationActionService.createDismissAllMessagesIntent(context, account).apply { + data = Uri.parse("data:,dismissAll/${account.uuid}/${System.currentTimeMillis()}") + } + return PendingIntent.getService(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) } override fun createDismissMessagePendingIntent( messageReference: MessageReference, notificationId: Int ): PendingIntent { - val intent = NotificationActionService.createDismissMessageIntent(context, messageReference) - return PendingIntent.getService(context, notificationId, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) + val intent = NotificationActionService.createDismissMessageIntent(context, messageReference).apply { + data = Uri.parse("data:,dismiss/${messageReference.toIdentityString()}") + } + return PendingIntent.getService(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) } override fun createReplyPendingIntent(messageReference: MessageReference, notificationId: Int): PendingIntent { - val intent = MessageActions.getActionReplyIntent(context, messageReference) - return PendingIntent.getActivity(context, notificationId, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) + val intent = MessageActions.getActionReplyIntent(context, messageReference).apply { + data = Uri.parse("data:,reply/${messageReference.toIdentityString()}") + } + return PendingIntent.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) } override fun createMarkMessageAsReadPendingIntent( messageReference: MessageReference, notificationId: Int ): PendingIntent { - val intent = NotificationActionService.createMarkMessageAsReadIntent(context, messageReference) - return PendingIntent.getService(context, notificationId, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) + val intent = NotificationActionService.createMarkMessageAsReadIntent(context, messageReference).apply { + data = Uri.parse("data:,markAsRead/${messageReference.toIdentityString()}") + } + return PendingIntent.getService(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) } override fun createMarkAllAsReadPendingIntent( @@ -104,8 +115,10 @@ internal class K9NotificationActionCreator( notificationId: Int ): PendingIntent { val accountUuid = account.uuid - val intent = NotificationActionService.createMarkAllAsReadIntent(context, accountUuid, messageReferences) - return PendingIntent.getService(context, notificationId, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) + val intent = NotificationActionService.createMarkAllAsReadIntent(context, accountUuid, messageReferences).apply { + data = Uri.parse("data:,markAllAsRead/$accountUuid/${System.currentTimeMillis()}") + } + return PendingIntent.getService(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) } override fun getEditIncomingServerSettingsIntent(account: Account): PendingIntent { @@ -123,9 +136,9 @@ internal class K9NotificationActionCreator( notificationId: Int ): PendingIntent { return if (K9.isConfirmDeleteFromNotification) { - createDeleteConfirmationPendingIntent(messageReference, notificationId) + createDeleteConfirmationPendingIntent(messageReference, 0) } else { - createDeleteServicePendingIntent(messageReference, notificationId) + createDeleteServicePendingIntent(messageReference, 0) } } @@ -133,16 +146,20 @@ internal class K9NotificationActionCreator( messageReference: MessageReference, notificationId: Int ): PendingIntent { - val intent = NotificationActionService.createDeleteMessageIntent(context, messageReference) - return PendingIntent.getService(context, notificationId, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) + val intent = NotificationActionService.createDeleteMessageIntent(context, messageReference).apply { + data = Uri.parse("data:,delete/${messageReference.toIdentityString()}") + } + return PendingIntent.getService(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) } private fun createDeleteConfirmationPendingIntent( messageReference: MessageReference, notificationId: Int ): PendingIntent { - val intent = DeleteConfirmationActivity.getIntent(context, messageReference) - return PendingIntent.getActivity(context, notificationId, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) + val intent = DeleteConfirmationActivity.getIntent(context, messageReference).apply { + data = Uri.parse("data:,deleteConfirmation/${messageReference.toIdentityString()}") + } + return PendingIntent.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) } override fun createDeleteAllPendingIntent( @@ -151,9 +168,9 @@ internal class K9NotificationActionCreator( notificationId: Int ): PendingIntent { return if (K9.isConfirmDeleteFromNotification) { - getDeleteAllConfirmationPendingIntent(messageReferences, notificationId) + getDeleteAllConfirmationPendingIntent(messageReferences, 0) } else { - getDeleteAllServicePendingIntent(account, messageReferences, notificationId) + getDeleteAllServicePendingIntent(account, messageReferences, 0) } } @@ -161,8 +178,10 @@ internal class K9NotificationActionCreator( messageReferences: List, notificationId: Int ): PendingIntent { - val intent = DeleteConfirmationActivity.getIntent(context, messageReferences) - return PendingIntent.getActivity(context, notificationId, intent, FLAG_CANCEL_CURRENT or FLAG_IMMUTABLE) + val intent = DeleteConfirmationActivity.getIntent(context, messageReferences).apply { + data = Uri.parse("data:,deleteAllConfirmation/${System.currentTimeMillis()}") + } + return PendingIntent.getActivity(context, 0, intent, FLAG_CANCEL_CURRENT or FLAG_IMMUTABLE) } private fun getDeleteAllServicePendingIntent( @@ -171,16 +190,20 @@ internal class K9NotificationActionCreator( notificationId: Int ): PendingIntent { val accountUuid = account.uuid - val intent = NotificationActionService.createDeleteAllMessagesIntent(context, accountUuid, messageReferences) - return PendingIntent.getService(context, notificationId, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) + val intent = NotificationActionService.createDeleteAllMessagesIntent(context, accountUuid, messageReferences).apply { + data = Uri.parse("data:,deleteAll/$accountUuid/${System.currentTimeMillis()}") + } + return PendingIntent.getService(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) } override fun createArchiveMessagePendingIntent( messageReference: MessageReference, notificationId: Int ): PendingIntent { - val intent = NotificationActionService.createArchiveMessageIntent(context, messageReference) - return PendingIntent.getService(context, notificationId, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) + val intent = NotificationActionService.createArchiveMessageIntent(context, messageReference).apply { + data = Uri.parse("data:,archive/${messageReference.toIdentityString()}") + } + return PendingIntent.getService(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) } override fun createArchiveAllPendingIntent( @@ -188,16 +211,20 @@ internal class K9NotificationActionCreator( messageReferences: List, notificationId: Int ): PendingIntent { - val intent = NotificationActionService.createArchiveAllIntent(context, account, messageReferences) - return PendingIntent.getService(context, notificationId, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) + val intent = NotificationActionService.createArchiveAllIntent(context, account, messageReferences).apply { + data = Uri.parse("data:,archiveAll/${account.uuid}/${System.currentTimeMillis()}") + } + return PendingIntent.getService(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) } override fun createMarkMessageAsSpamPendingIntent( messageReference: MessageReference, notificationId: Int ): PendingIntent { - val intent = NotificationActionService.createMarkMessageAsSpamIntent(context, messageReference) - return PendingIntent.getService(context, notificationId, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) + val intent = NotificationActionService.createMarkMessageAsSpamIntent(context, messageReference).apply { + data = Uri.parse("data:,spam/${messageReference.toIdentityString()}") + } + return PendingIntent.getService(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) } private fun createMessageListIntent(account: Account): Intent { @@ -213,7 +240,9 @@ internal class K9NotificationActionCreator( noThreading = false, newTask = true, clearTop = true - ) + ).apply { + data = Uri.parse("data:,messageList/${account.uuid}/$folderId") + } } private fun createMessageListIntent(account: Account, folderId: Long): Intent { @@ -228,19 +257,27 @@ internal class K9NotificationActionCreator( noThreading = false, newTask = true, clearTop = true - ) + ).apply { + data = Uri.parse("data:,messageList/${account.uuid}/$folderId") + } } - private fun createMessageViewIntent(message: MessageReference, openInUnifiedInbox: Boolean): Intent { - return MessageList.actionDisplayMessageIntent(context, message, openInUnifiedInbox) + private fun createMessageViewIntent(messageReference: MessageReference, openInUnifiedInbox: Boolean): Intent { + return MessageList.actionDisplayMessageIntent(context, messageReference, openInUnifiedInbox).apply { + data = Uri.parse("data:,messageView/${messageReference.toIdentityString()}") + } } private fun createUnifiedInboxIntent(account: Account): Intent { - return MessageList.createUnifiedInboxIntent(context, account) + return MessageList.createUnifiedInboxIntent(context, account).apply { + data = Uri.parse("data:,unifiedInbox/${account.uuid}") + } } private fun createNewMessagesIntent(account: Account): Intent { - return MessageList.createNewMessagesIntent(context, account) + return MessageList.createNewMessagesIntent(context, account).apply { + data = Uri.parse("data:,newMessages/${account.uuid}") + } } private fun extractFolderIds(messageReferences: List): Set { -- GitLab From bc7758b9cfbfc54b3c56cd34a74b5848a3150290 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 26 Jul 2022 23:19:27 +0200 Subject: [PATCH 29/47] Remove `notificationId` parameters from methods in `NotificationActionCreator` --- .../notification/NotificationActionCreator.kt | 47 ++++-------- .../SendFailedNotificationController.kt | 8 +- .../SingleMessageNotificationCreator.kt | 37 +++------ .../SummaryNotificationCreator.kt | 29 ++----- .../SyncNotificationController.kt | 8 +- .../SendFailedNotificationControllerTest.kt | 5 +- .../SyncNotificationControllerTest.kt | 3 +- .../K9NotificationActionCreator.kt | 76 ++++++------------- 8 files changed, 60 insertions(+), 153 deletions(-) diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationActionCreator.kt b/app/core/src/main/java/com/fsck/k9/notification/NotificationActionCreator.kt index d7e403b8c3..84297e06b4 100644 --- a/app/core/src/main/java/com/fsck/k9/notification/NotificationActionCreator.kt +++ b/app/core/src/main/java/com/fsck/k9/notification/NotificationActionCreator.kt @@ -5,54 +5,35 @@ import com.fsck.k9.Account import com.fsck.k9.controller.MessageReference interface NotificationActionCreator { - fun createViewMessagePendingIntent(messageReference: MessageReference, notificationId: Int): PendingIntent + fun createViewMessagePendingIntent(messageReference: MessageReference): PendingIntent - fun createViewFolderPendingIntent(account: Account, folderId: Long, notificationId: Int): PendingIntent + fun createViewFolderPendingIntent(account: Account, folderId: Long): PendingIntent - fun createViewMessagesPendingIntent( - account: Account, - messageReferences: List, - notificationId: Int - ): PendingIntent + fun createViewMessagesPendingIntent(account: Account, messageReferences: List): PendingIntent - fun createViewFolderListPendingIntent(account: Account, notificationId: Int): PendingIntent + fun createViewFolderListPendingIntent(account: Account): PendingIntent - fun createDismissAllMessagesPendingIntent(account: Account, notificationId: Int): PendingIntent + fun createDismissAllMessagesPendingIntent(account: Account): PendingIntent - fun createDismissMessagePendingIntent( - messageReference: MessageReference, - notificationId: Int - ): PendingIntent + fun createDismissMessagePendingIntent(messageReference: MessageReference): PendingIntent - fun createReplyPendingIntent(messageReference: MessageReference, notificationId: Int): PendingIntent + fun createReplyPendingIntent(messageReference: MessageReference): PendingIntent - fun createMarkMessageAsReadPendingIntent(messageReference: MessageReference, notificationId: Int): PendingIntent + fun createMarkMessageAsReadPendingIntent(messageReference: MessageReference): PendingIntent - fun createMarkAllAsReadPendingIntent( - account: Account, - messageReferences: List, - notificationId: Int - ): PendingIntent + fun createMarkAllAsReadPendingIntent(account: Account, messageReferences: List): PendingIntent fun getEditIncomingServerSettingsIntent(account: Account): PendingIntent fun getEditOutgoingServerSettingsIntent(account: Account): PendingIntent - fun createDeleteMessagePendingIntent(messageReference: MessageReference, notificationId: Int): PendingIntent + fun createDeleteMessagePendingIntent(messageReference: MessageReference): PendingIntent - fun createDeleteAllPendingIntent( - account: Account, - messageReferences: List, - notificationId: Int - ): PendingIntent + fun createDeleteAllPendingIntent(account: Account, messageReferences: List): PendingIntent - fun createArchiveMessagePendingIntent(messageReference: MessageReference, notificationId: Int): PendingIntent + fun createArchiveMessagePendingIntent(messageReference: MessageReference): PendingIntent - fun createArchiveAllPendingIntent( - account: Account, - messageReferences: List, - notificationId: Int - ): PendingIntent + fun createArchiveAllPendingIntent(account: Account, messageReferences: List): PendingIntent - fun createMarkMessageAsSpamPendingIntent(messageReference: MessageReference, notificationId: Int): PendingIntent + fun createMarkMessageAsSpamPendingIntent(messageReference: MessageReference): PendingIntent } diff --git a/app/core/src/main/java/com/fsck/k9/notification/SendFailedNotificationController.kt b/app/core/src/main/java/com/fsck/k9/notification/SendFailedNotificationController.kt index 3dd80b75ed..2f82eabe11 100644 --- a/app/core/src/main/java/com/fsck/k9/notification/SendFailedNotificationController.kt +++ b/app/core/src/main/java/com/fsck/k9/notification/SendFailedNotificationController.kt @@ -19,13 +19,9 @@ internal class SendFailedNotificationController( val pendingIntent = account.outboxFolderId.let { outboxFolderId -> if (outboxFolderId != null) { - actionBuilder.createViewFolderPendingIntent( - account, outboxFolderId, notificationId - ) + actionBuilder.createViewFolderPendingIntent(account, outboxFolderId) } else { - actionBuilder.createViewFolderListPendingIntent( - account, notificationId - ) + actionBuilder.createViewFolderListPendingIntent(account) } } diff --git a/app/core/src/main/java/com/fsck/k9/notification/SingleMessageNotificationCreator.kt b/app/core/src/main/java/com/fsck/k9/notification/SingleMessageNotificationCreator.kt index 21553997b7..93e9456b67 100644 --- a/app/core/src/main/java/com/fsck/k9/notification/SingleMessageNotificationCreator.kt +++ b/app/core/src/main/java/com/fsck/k9/notification/SingleMessageNotificationCreator.kt @@ -1,6 +1,5 @@ package com.fsck.k9.notification -import android.app.PendingIntent import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat.WearableExtender import com.fsck.k9.notification.NotificationChannelManager.ChannelType @@ -35,8 +34,8 @@ internal class SingleMessageNotificationCreator( .setContentText(content.subject) .setSubText(baseNotificationData.accountName) .setBigText(content.preview) - .setContentIntent(createViewIntent(content, notificationId)) - .setDeleteIntent(createDismissIntent(content, notificationId)) + .setContentIntent(actionCreator.createViewMessagePendingIntent(content.messageReference)) + .setDeleteIntent(actionCreator.createDismissMessagePendingIntent(content.messageReference)) .setDeviceActions(singleNotificationData) .setWearActions(singleNotificationData) .setAppearance(singleNotificationData.isSilent, baseNotificationData.appearance) @@ -57,14 +56,6 @@ internal class SingleMessageNotificationCreator( setStyle(NotificationCompat.BigTextStyle().bigText(text)) } - private fun createViewIntent(content: NotificationContent, notificationId: Int): PendingIntent { - return actionCreator.createViewMessagePendingIntent(content.messageReference, notificationId) - } - - private fun createDismissIntent(content: NotificationContent, notificationId: Int): PendingIntent { - return actionCreator.createDismissMessagePendingIntent(content.messageReference, notificationId) - } - private fun NotificationBuilder.setDeviceActions(notificationData: SingleNotificationData) = apply { val actions = notificationData.actions for (action in actions) { @@ -81,8 +72,7 @@ internal class SingleMessageNotificationCreator( val title = resourceProvider.actionReply() val content = notificationData.content val messageReference = content.messageReference - val replyToMessagePendingIntent = - actionCreator.createReplyPendingIntent(messageReference, notificationData.notificationId) + val replyToMessagePendingIntent = actionCreator.createReplyPendingIntent(messageReference) addAction(icon, title, replyToMessagePendingIntent) } @@ -91,9 +81,8 @@ internal class SingleMessageNotificationCreator( val icon = resourceProvider.iconMarkAsRead val title = resourceProvider.actionMarkAsRead() val content = notificationData.content - val notificationId = notificationData.notificationId val messageReference = content.messageReference - val action = actionCreator.createMarkMessageAsReadPendingIntent(messageReference, notificationId) + val action = actionCreator.createMarkMessageAsReadPendingIntent(messageReference) addAction(icon, title, action) } @@ -102,9 +91,8 @@ internal class SingleMessageNotificationCreator( val icon = resourceProvider.iconDelete val title = resourceProvider.actionDelete() val content = notificationData.content - val notificationId = notificationData.notificationId val messageReference = content.messageReference - val action = actionCreator.createDeleteMessagePendingIntent(messageReference, notificationId) + val action = actionCreator.createDeleteMessagePendingIntent(messageReference) addAction(icon, title, action) } @@ -129,8 +117,7 @@ internal class SingleMessageNotificationCreator( val icon = resourceProvider.wearIconReplyAll val title = resourceProvider.actionReply() val messageReference = notificationData.content.messageReference - val notificationId = notificationData.notificationId - val action = actionCreator.createReplyPendingIntent(messageReference, notificationId) + val action = actionCreator.createReplyPendingIntent(messageReference) val replyAction = NotificationCompat.Action.Builder(icon, title, action).build() addAction(replyAction) @@ -140,8 +127,7 @@ internal class SingleMessageNotificationCreator( val icon = resourceProvider.wearIconMarkAsRead val title = resourceProvider.actionMarkAsRead() val messageReference = notificationData.content.messageReference - val notificationId = notificationData.notificationId - val action = actionCreator.createMarkMessageAsReadPendingIntent(messageReference, notificationId) + val action = actionCreator.createMarkMessageAsReadPendingIntent(messageReference) val markAsReadAction = NotificationCompat.Action.Builder(icon, title, action).build() addAction(markAsReadAction) @@ -151,8 +137,7 @@ internal class SingleMessageNotificationCreator( val icon = resourceProvider.wearIconDelete val title = resourceProvider.actionDelete() val messageReference = notificationData.content.messageReference - val notificationId = notificationData.notificationId - val action = actionCreator.createDeleteMessagePendingIntent(messageReference, notificationId) + val action = actionCreator.createDeleteMessagePendingIntent(messageReference) val deleteAction = NotificationCompat.Action.Builder(icon, title, action).build() addAction(deleteAction) @@ -162,8 +147,7 @@ internal class SingleMessageNotificationCreator( val icon = resourceProvider.wearIconArchive val title = resourceProvider.actionArchive() val messageReference = notificationData.content.messageReference - val notificationId = notificationData.notificationId - val action = actionCreator.createArchiveMessagePendingIntent(messageReference, notificationId) + val action = actionCreator.createArchiveMessagePendingIntent(messageReference) val archiveAction = NotificationCompat.Action.Builder(icon, title, action).build() addAction(archiveAction) @@ -173,8 +157,7 @@ internal class SingleMessageNotificationCreator( val icon = resourceProvider.wearIconMarkAsSpam val title = resourceProvider.actionMarkAsSpam() val messageReference = notificationData.content.messageReference - val notificationId = notificationData.notificationId - val action = actionCreator.createMarkMessageAsSpamPendingIntent(messageReference, notificationId) + val action = actionCreator.createMarkMessageAsSpamPendingIntent(messageReference) val spamAction = NotificationCompat.Action.Builder(icon, title, action).build() addAction(spamAction) diff --git a/app/core/src/main/java/com/fsck/k9/notification/SummaryNotificationCreator.kt b/app/core/src/main/java/com/fsck/k9/notification/SummaryNotificationCreator.kt index eb55bc9eda..a1f06df7bc 100644 --- a/app/core/src/main/java/com/fsck/k9/notification/SummaryNotificationCreator.kt +++ b/app/core/src/main/java/com/fsck/k9/notification/SummaryNotificationCreator.kt @@ -5,7 +5,6 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat.WearableExtender import com.fsck.k9.Account import com.fsck.k9.notification.NotificationChannelManager.ChannelType -import com.fsck.k9.notification.NotificationIds.getNewMailSummaryNotificationId import timber.log.Timber import androidx.core.app.NotificationCompat.Builder as NotificationBuilder @@ -65,7 +64,7 @@ internal class SummaryNotificationCreator( .setSubText(accountName) .setInboxStyle(title, summary, notificationData.content) .setContentIntent(createViewIntent(account, notificationData)) - .setDeleteIntent(createDismissIntent(account, notificationData.notificationId)) + .setDeleteIntent(actionCreator.createDismissAllMessagesPendingIntent(account)) .setDeviceActions(account, notificationData) .setWearActions(account, notificationData) .setAppearance(notificationData.isSilent, baseNotificationData.appearance) @@ -101,15 +100,7 @@ internal class SummaryNotificationCreator( } private fun createViewIntent(account: Account, notificationData: SummaryInboxNotificationData): PendingIntent { - return actionCreator.createViewMessagesPendingIntent( - account = account, - messageReferences = notificationData.messageReferences, - notificationId = notificationData.notificationId - ) - } - - private fun createDismissIntent(account: Account, notificationId: Int): PendingIntent { - return actionCreator.createDismissAllMessagesPendingIntent(account, notificationId) + return actionCreator.createViewMessagesPendingIntent(account, notificationData.messageReferences) } private fun NotificationBuilder.setDeviceActions( @@ -131,9 +122,7 @@ internal class SummaryNotificationCreator( val icon = resourceProvider.iconMarkAsRead val title = resourceProvider.actionMarkAsRead() val messageReferences = notificationData.messageReferences - val notificationId = notificationData.notificationId - val markAllAsReadPendingIntent = - actionCreator.createMarkAllAsReadPendingIntent(account, messageReferences, notificationId) + val markAllAsReadPendingIntent = actionCreator.createMarkAllAsReadPendingIntent(account, messageReferences) addAction(icon, title, markAllAsReadPendingIntent) } @@ -144,9 +133,8 @@ internal class SummaryNotificationCreator( ) { val icon = resourceProvider.iconDelete val title = resourceProvider.actionDelete() - val notificationId = getNewMailSummaryNotificationId(account) val messageReferences = notificationData.messageReferences - val action = actionCreator.createDeleteAllPendingIntent(account, messageReferences, notificationId) + val action = actionCreator.createDeleteAllPendingIntent(account, messageReferences) addAction(icon, title, action) } @@ -175,8 +163,7 @@ internal class SummaryNotificationCreator( val icon = resourceProvider.wearIconMarkAsRead val title = resourceProvider.actionMarkAllAsRead() val messageReferences = notificationData.messageReferences - val notificationId = getNewMailSummaryNotificationId(account) - val action = actionCreator.createMarkAllAsReadPendingIntent(account, messageReferences, notificationId) + val action = actionCreator.createMarkAllAsReadPendingIntent(account, messageReferences) val markAsReadAction = NotificationCompat.Action.Builder(icon, title, action).build() addAction(markAsReadAction) @@ -186,8 +173,7 @@ internal class SummaryNotificationCreator( val icon = resourceProvider.wearIconDelete val title = resourceProvider.actionDeleteAll() val messageReferences = notificationData.messageReferences - val notificationId = getNewMailSummaryNotificationId(account) - val action = actionCreator.createDeleteAllPendingIntent(account, messageReferences, notificationId) + val action = actionCreator.createDeleteAllPendingIntent(account, messageReferences) val deleteAction = NotificationCompat.Action.Builder(icon, title, action).build() addAction(deleteAction) @@ -197,8 +183,7 @@ internal class SummaryNotificationCreator( val icon = resourceProvider.wearIconArchive val title = resourceProvider.actionArchiveAll() val messageReferences = notificationData.messageReferences - val notificationId = getNewMailSummaryNotificationId(account) - val action = actionCreator.createArchiveAllPendingIntent(account, messageReferences, notificationId) + val action = actionCreator.createArchiveAllPendingIntent(account, messageReferences) val archiveAction = NotificationCompat.Action.Builder(icon, title, action).build() addAction(archiveAction) diff --git a/app/core/src/main/java/com/fsck/k9/notification/SyncNotificationController.kt b/app/core/src/main/java/com/fsck/k9/notification/SyncNotificationController.kt index 3140fb813a..441d569833 100644 --- a/app/core/src/main/java/com/fsck/k9/notification/SyncNotificationController.kt +++ b/app/core/src/main/java/com/fsck/k9/notification/SyncNotificationController.kt @@ -18,9 +18,7 @@ internal class SyncNotificationController( val notificationId = NotificationIds.getFetchingMailNotificationId(account) val outboxFolderId = account.outboxFolderId ?: error("Outbox folder not configured") - val showMessageListPendingIntent = actionBuilder.createViewFolderPendingIntent( - account, outboxFolderId, notificationId - ) + val showMessageListPendingIntent = actionBuilder.createViewFolderPendingIntent(account, outboxFolderId) val notificationBuilder = notificationHelper .createNotificationBuilder(account, NotificationChannelManager.ChannelType.MISCELLANEOUS) @@ -53,9 +51,7 @@ internal class SyncNotificationController( val text = accountName + resourceProvider.checkingMailSeparator() + folderName val notificationId = NotificationIds.getFetchingMailNotificationId(account) - val showMessageListPendingIntent = actionBuilder.createViewFolderPendingIntent( - account, folderId, notificationId - ) + val showMessageListPendingIntent = actionBuilder.createViewFolderPendingIntent(account, folderId) val notificationBuilder = notificationHelper .createNotificationBuilder(account, NotificationChannelManager.ChannelType.MISCELLANEOUS) diff --git a/app/core/src/test/java/com/fsck/k9/notification/SendFailedNotificationControllerTest.kt b/app/core/src/test/java/com/fsck/k9/notification/SendFailedNotificationControllerTest.kt index 591fe1a176..e92089fc68 100644 --- a/app/core/src/test/java/com/fsck/k9/notification/SendFailedNotificationControllerTest.kt +++ b/app/core/src/test/java/com/fsck/k9/notification/SendFailedNotificationControllerTest.kt @@ -9,7 +9,6 @@ import com.fsck.k9.Account import com.fsck.k9.RobolectricTest import com.fsck.k9.testing.MockHelper.mockBuilder import org.junit.Test -import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyLong import org.mockito.Mockito.verify import org.mockito.kotlin.any @@ -88,8 +87,8 @@ class SendFailedNotificationControllerTest : RobolectricTest() { private fun createActionBuilder(contentIntent: PendingIntent): NotificationActionCreator { return mock { - on { createViewFolderListPendingIntent(any(), anyInt()) } doReturn contentIntent - on { createViewFolderPendingIntent(any(), anyLong(), anyInt()) } doReturn contentIntent + on { createViewFolderListPendingIntent(any()) } doReturn contentIntent + on { createViewFolderPendingIntent(any(), anyLong()) } doReturn contentIntent } } } diff --git a/app/core/src/test/java/com/fsck/k9/notification/SyncNotificationControllerTest.kt b/app/core/src/test/java/com/fsck/k9/notification/SyncNotificationControllerTest.kt index cde4972259..18b5aa11cd 100644 --- a/app/core/src/test/java/com/fsck/k9/notification/SyncNotificationControllerTest.kt +++ b/app/core/src/test/java/com/fsck/k9/notification/SyncNotificationControllerTest.kt @@ -11,7 +11,6 @@ import com.fsck.k9.mailstore.LocalFolder import com.fsck.k9.notification.NotificationIds.getFetchingMailNotificationId import com.fsck.k9.testing.MockHelper.mockBuilder import org.junit.Test -import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyLong import org.mockito.Mockito.verify import org.mockito.kotlin.any @@ -140,7 +139,7 @@ class SyncNotificationControllerTest : RobolectricTest() { private fun createActionBuilder(contentIntent: PendingIntent): NotificationActionCreator { return mock { - on { createViewFolderPendingIntent(eq(account), anyLong(), anyInt()) } doReturn contentIntent + on { createViewFolderPendingIntent(eq(account), anyLong()) } doReturn contentIntent } } diff --git a/app/k9mail/src/main/java/com/fsck/k9/notification/K9NotificationActionCreator.kt b/app/k9mail/src/main/java/com/fsck/k9/notification/K9NotificationActionCreator.kt index 0da55f29a6..b08374a516 100644 --- a/app/k9mail/src/main/java/com/fsck/k9/notification/K9NotificationActionCreator.kt +++ b/app/k9mail/src/main/java/com/fsck/k9/notification/K9NotificationActionCreator.kt @@ -37,25 +37,21 @@ internal class K9NotificationActionCreator( private val messageStoreManager: MessageStoreManager ) : NotificationActionCreator { - override fun createViewMessagePendingIntent( - messageReference: MessageReference, - notificationId: Int - ): PendingIntent { + override fun createViewMessagePendingIntent(messageReference: MessageReference): PendingIntent { val openInUnifiedInbox = K9.isShowUnifiedInbox && isIncludedInUnifiedInbox(messageReference) val intent = createMessageViewIntent(messageReference, openInUnifiedInbox) return PendingIntent.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) } - override fun createViewFolderPendingIntent(account: Account, folderId: Long, notificationId: Int): PendingIntent { + override fun createViewFolderPendingIntent(account: Account, folderId: Long): PendingIntent { val intent = createMessageListIntent(account, folderId) return PendingIntent.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) } override fun createViewMessagesPendingIntent( account: Account, - messageReferences: List, - notificationId: Int + messageReferences: List ): PendingIntent { val folderIds = extractFolderIds(messageReferences) @@ -70,39 +66,33 @@ internal class K9NotificationActionCreator( return PendingIntent.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) } - override fun createViewFolderListPendingIntent(account: Account, notificationId: Int): PendingIntent { + override fun createViewFolderListPendingIntent(account: Account): PendingIntent { val intent = createMessageListIntent(account) return PendingIntent.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) } - override fun createDismissAllMessagesPendingIntent(account: Account, notificationId: Int): PendingIntent { + override fun createDismissAllMessagesPendingIntent(account: Account): PendingIntent { val intent = NotificationActionService.createDismissAllMessagesIntent(context, account).apply { data = Uri.parse("data:,dismissAll/${account.uuid}/${System.currentTimeMillis()}") } return PendingIntent.getService(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) } - override fun createDismissMessagePendingIntent( - messageReference: MessageReference, - notificationId: Int - ): PendingIntent { + override fun createDismissMessagePendingIntent(messageReference: MessageReference): PendingIntent { val intent = NotificationActionService.createDismissMessageIntent(context, messageReference).apply { data = Uri.parse("data:,dismiss/${messageReference.toIdentityString()}") } return PendingIntent.getService(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) } - override fun createReplyPendingIntent(messageReference: MessageReference, notificationId: Int): PendingIntent { + override fun createReplyPendingIntent(messageReference: MessageReference): PendingIntent { val intent = MessageActions.getActionReplyIntent(context, messageReference).apply { data = Uri.parse("data:,reply/${messageReference.toIdentityString()}") } return PendingIntent.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) } - override fun createMarkMessageAsReadPendingIntent( - messageReference: MessageReference, - notificationId: Int - ): PendingIntent { + override fun createMarkMessageAsReadPendingIntent(messageReference: MessageReference): PendingIntent { val intent = NotificationActionService.createMarkMessageAsReadIntent(context, messageReference).apply { data = Uri.parse("data:,markAsRead/${messageReference.toIdentityString()}") } @@ -111,8 +101,7 @@ internal class K9NotificationActionCreator( override fun createMarkAllAsReadPendingIntent( account: Account, - messageReferences: List, - notificationId: Int + messageReferences: List ): PendingIntent { val accountUuid = account.uuid val intent = NotificationActionService.createMarkAllAsReadIntent(context, accountUuid, messageReferences).apply { @@ -131,31 +120,22 @@ internal class K9NotificationActionCreator( return PendingIntent.getActivity(context, account.accountNumber, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) } - override fun createDeleteMessagePendingIntent( - messageReference: MessageReference, - notificationId: Int - ): PendingIntent { + override fun createDeleteMessagePendingIntent(messageReference: MessageReference): PendingIntent { return if (K9.isConfirmDeleteFromNotification) { - createDeleteConfirmationPendingIntent(messageReference, 0) + createDeleteConfirmationPendingIntent(messageReference) } else { - createDeleteServicePendingIntent(messageReference, 0) + createDeleteServicePendingIntent(messageReference) } } - private fun createDeleteServicePendingIntent( - messageReference: MessageReference, - notificationId: Int - ): PendingIntent { + private fun createDeleteServicePendingIntent(messageReference: MessageReference): PendingIntent { val intent = NotificationActionService.createDeleteMessageIntent(context, messageReference).apply { data = Uri.parse("data:,delete/${messageReference.toIdentityString()}") } return PendingIntent.getService(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) } - private fun createDeleteConfirmationPendingIntent( - messageReference: MessageReference, - notificationId: Int - ): PendingIntent { + private fun createDeleteConfirmationPendingIntent(messageReference: MessageReference): PendingIntent { val intent = DeleteConfirmationActivity.getIntent(context, messageReference).apply { data = Uri.parse("data:,deleteConfirmation/${messageReference.toIdentityString()}") } @@ -164,20 +144,16 @@ internal class K9NotificationActionCreator( override fun createDeleteAllPendingIntent( account: Account, - messageReferences: List, - notificationId: Int + messageReferences: List ): PendingIntent { return if (K9.isConfirmDeleteFromNotification) { - getDeleteAllConfirmationPendingIntent(messageReferences, 0) + getDeleteAllConfirmationPendingIntent(messageReferences) } else { - getDeleteAllServicePendingIntent(account, messageReferences, 0) + getDeleteAllServicePendingIntent(account, messageReferences) } } - private fun getDeleteAllConfirmationPendingIntent( - messageReferences: List, - notificationId: Int - ): PendingIntent { + private fun getDeleteAllConfirmationPendingIntent(messageReferences: List): PendingIntent { val intent = DeleteConfirmationActivity.getIntent(context, messageReferences).apply { data = Uri.parse("data:,deleteAllConfirmation/${System.currentTimeMillis()}") } @@ -186,8 +162,7 @@ internal class K9NotificationActionCreator( private fun getDeleteAllServicePendingIntent( account: Account, - messageReferences: List, - notificationId: Int + messageReferences: List ): PendingIntent { val accountUuid = account.uuid val intent = NotificationActionService.createDeleteAllMessagesIntent(context, accountUuid, messageReferences).apply { @@ -196,10 +171,7 @@ internal class K9NotificationActionCreator( return PendingIntent.getService(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) } - override fun createArchiveMessagePendingIntent( - messageReference: MessageReference, - notificationId: Int - ): PendingIntent { + override fun createArchiveMessagePendingIntent(messageReference: MessageReference): PendingIntent { val intent = NotificationActionService.createArchiveMessageIntent(context, messageReference).apply { data = Uri.parse("data:,archive/${messageReference.toIdentityString()}") } @@ -208,8 +180,7 @@ internal class K9NotificationActionCreator( override fun createArchiveAllPendingIntent( account: Account, - messageReferences: List, - notificationId: Int + messageReferences: List ): PendingIntent { val intent = NotificationActionService.createArchiveAllIntent(context, account, messageReferences).apply { data = Uri.parse("data:,archiveAll/${account.uuid}/${System.currentTimeMillis()}") @@ -217,10 +188,7 @@ internal class K9NotificationActionCreator( return PendingIntent.getService(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) } - override fun createMarkMessageAsSpamPendingIntent( - messageReference: MessageReference, - notificationId: Int - ): PendingIntent { + override fun createMarkMessageAsSpamPendingIntent(messageReference: MessageReference): PendingIntent { val intent = NotificationActionService.createMarkMessageAsSpamIntent(context, messageReference).apply { data = Uri.parse("data:,spam/${messageReference.toIdentityString()}") } -- GitLab From f60f573f0779aa576092f1db21e3281a406d36e7 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 27 Jul 2022 00:21:07 +0200 Subject: [PATCH 30/47] Don't auto-cancel new message notifications On one of my test devices (Android 12), tapping a single message notification opens the message view, which leads to the notification being removed. If there's an inactive notification it will be promoted to an active notification and use the notification ID of the notification that was just removed. Due to auto-cancel being used, the delete intent of the first notification is then triggered. However, the system seems to use the notification ID to retrieve the delete intent. Because it will fetch the delete intent from the new notification, not the old one. (I made sure to check that it's not a PendingIntent reuse issue) Since we remove the notification ourselves, we can simply stop using the (apparently buggy) auto-cancel mechanism. --- .../com/fsck/k9/notification/SingleMessageNotificationCreator.kt | 1 - .../java/com/fsck/k9/notification/SummaryNotificationCreator.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/app/core/src/main/java/com/fsck/k9/notification/SingleMessageNotificationCreator.kt b/app/core/src/main/java/com/fsck/k9/notification/SingleMessageNotificationCreator.kt index 93e9456b67..d6d5e3626a 100644 --- a/app/core/src/main/java/com/fsck/k9/notification/SingleMessageNotificationCreator.kt +++ b/app/core/src/main/java/com/fsck/k9/notification/SingleMessageNotificationCreator.kt @@ -23,7 +23,6 @@ internal class SingleMessageNotificationCreator( val notification = notificationHelper.createNotificationBuilder(account, ChannelType.MESSAGES) .setCategory(NotificationCompat.CATEGORY_EMAIL) - .setAutoCancel(true) .setGroup(baseNotificationData.groupKey) .setGroupSummary(isGroupSummary) .setSmallIcon(resourceProvider.iconNewMail) diff --git a/app/core/src/main/java/com/fsck/k9/notification/SummaryNotificationCreator.kt b/app/core/src/main/java/com/fsck/k9/notification/SummaryNotificationCreator.kt index a1f06df7bc..c780415f11 100644 --- a/app/core/src/main/java/com/fsck/k9/notification/SummaryNotificationCreator.kt +++ b/app/core/src/main/java/com/fsck/k9/notification/SummaryNotificationCreator.kt @@ -52,7 +52,6 @@ internal class SummaryNotificationCreator( val notification = notificationHelper.createNotificationBuilder(account, ChannelType.MESSAGES) .setCategory(NotificationCompat.CATEGORY_EMAIL) - .setAutoCancel(true) .setGroup(baseNotificationData.groupKey) .setGroupSummary(true) .setSmallIcon(resourceProvider.iconNewMail) -- GitLab From f16522cdc43e1dab7822ef53e455abcde996a847 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 28 Jul 2022 15:46:39 +0200 Subject: [PATCH 31/47] Remove unused code --- .../ui/messageview/MessageContainerView.java | 13 ------------- .../java/com/fsck/k9/view/MessageWebView.java | 19 ------------------- app/ui/legacy/src/main/res/values/strings.xml | 2 -- 3 files changed, 34 deletions(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.java b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.java index 3efcff567c..1b38cfe181 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.java @@ -16,7 +16,6 @@ import android.text.TextUtils; import android.util.AttributeSet; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; -import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; @@ -500,18 +499,6 @@ public class MessageContainerView extends LinearLayout implements OnCreateContex } } - public void zoom(KeyEvent event) { - if (event.isShiftPressed()) { - mMessageContentView.zoomIn(); - } else { - mMessageContentView.zoomOut(); - } - } - - public void beginSelectingText() { - mMessageContentView.emulateShiftHeld(); - } - public void resetView() { setLoadPictures(false); mAttachments.removeAllViews(); diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/view/MessageWebView.java b/app/ui/legacy/src/main/java/com/fsck/k9/view/MessageWebView.java index 206785237f..023bfb1236 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/view/MessageWebView.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/view/MessageWebView.java @@ -7,14 +7,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import android.util.AttributeSet; import timber.log.Timber; -import android.view.KeyEvent; import android.webkit.WebSettings; import android.webkit.WebSettings.LayoutAlgorithm; import android.webkit.WebSettings.RenderPriority; import android.webkit.WebView; -import android.widget.Toast; -import com.fsck.k9.ui.R; import com.fsck.k9.mailstore.AttachmentResolver; @@ -134,22 +131,6 @@ public class MessageWebView extends WebView { resumeTimers(); } - /* - * Emulate the shift key being pressed to trigger the text selection mode - * of a WebView. - */ - public void emulateShiftHeld() { - try { - - KeyEvent shiftPressEvent = new KeyEvent(0, 0, KeyEvent.ACTION_DOWN, - KeyEvent.KEYCODE_SHIFT_LEFT, 0, 0); - shiftPressEvent.dispatch(this, null, null); - Toast.makeText(getContext() , R.string.select_text_now, Toast.LENGTH_SHORT).show(); - } catch (Exception e) { - Timber.e(e, "Exception in emulateShiftHeld()"); - } - } - public interface OnPageFinishedListener { void onPageFinished(); } diff --git a/app/ui/legacy/src/main/res/values/strings.xml b/app/ui/legacy/src/main/res/values/strings.xml index 8e2285d265..d6fb93cff4 100644 --- a/app/ui/legacy/src/main/res/values/strings.xml +++ b/app/ui/legacy/src/main/res/values/strings.xml @@ -887,8 +887,6 @@ Please submit bug reports, contribute new features and ask questions at Discard message? Are you sure you want to discard this message? - Select text to copy. - Clear local messages? This will remove all local messages from the folder. No messages will be deleted from the server. Clear messages -- GitLab From b35e6ac269755c3d056786fa23fab2317da551dc Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 28 Jul 2022 16:05:32 +0200 Subject: [PATCH 32/47] Rename .java to .kt --- .../com/fsck/k9/view/{MessageWebView.java => MessageWebView.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/ui/legacy/src/main/java/com/fsck/k9/view/{MessageWebView.java => MessageWebView.kt} (100%) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/view/MessageWebView.java b/app/ui/legacy/src/main/java/com/fsck/k9/view/MessageWebView.kt similarity index 100% rename from app/ui/legacy/src/main/java/com/fsck/k9/view/MessageWebView.java rename to app/ui/legacy/src/main/java/com/fsck/k9/view/MessageWebView.kt -- GitLab From b05309f316a5fa6a5dd212147e0e1e5dcec277f7 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 28 Jul 2022 16:05:32 +0200 Subject: [PATCH 33/47] Convert `MessageWebView` to Kotlin --- .../java/com/fsck/k9/view/MessageWebView.kt | 178 +++++++----------- 1 file changed, 70 insertions(+), 108 deletions(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/view/MessageWebView.kt b/app/ui/legacy/src/main/java/com/fsck/k9/view/MessageWebView.kt index 023bfb1236..946cd704cf 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/view/MessageWebView.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/view/MessageWebView.kt @@ -1,137 +1,99 @@ -package com.fsck.k9.view; - - -import android.content.Context; -import android.content.pm.PackageManager; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import android.util.AttributeSet; -import timber.log.Timber; -import android.webkit.WebSettings; -import android.webkit.WebSettings.LayoutAlgorithm; -import android.webkit.WebSettings.RenderPriority; -import android.webkit.WebView; - -import com.fsck.k9.mailstore.AttachmentResolver; - - -public class MessageWebView extends WebView { - - public MessageWebView(Context context) { - super(context); - } - - public MessageWebView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public MessageWebView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - /** - * Configure a web view to load or not load network data. A true setting here means that - * network data will be blocked. - * @param shouldBlockNetworkData True if network data should be blocked, false to allow network data. - */ - public void blockNetworkData(final boolean shouldBlockNetworkData) { - /* - * Block network loads. - * - * Images with content: URIs will not be blocked, nor - * will network images that are already in the WebView cache. - * - */ +package com.fsck.k9.view + +import android.content.Context +import android.content.pm.PackageManager +import android.util.AttributeSet +import android.webkit.WebSettings.LayoutAlgorithm +import android.webkit.WebSettings.RenderPriority +import android.webkit.WebView +import com.fsck.k9.mailstore.AttachmentResolver +import timber.log.Timber + +class MessageWebView : WebView { + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) + + fun blockNetworkData(shouldBlockNetworkData: Boolean) { + // Images with content: URIs will not be blocked, nor will network images that are already in the WebView cache. try { - getSettings().setBlockNetworkLoads(shouldBlockNetworkData); - } catch (SecurityException e) { - Timber.e(e, "Failed to unblock network loads. Missing INTERNET permission?"); + settings.blockNetworkLoads = shouldBlockNetworkData + } catch (e: SecurityException) { + Timber.e(e, "Failed to unblock network loads. Missing INTERNET permission?") } } + fun configure(config: WebViewConfig) { + isVerticalScrollBarEnabled = true + setVerticalScrollbarOverlay(true) + scrollBarStyle = SCROLLBARS_INSIDE_OVERLAY + isLongClickable = true - /** - * Configure a {@link WebView} to display a Message. This method takes into account a user's - * preferences when configuring the view. This message is used to view a message and to display a message being - * replied to. - */ - public void configure(WebViewConfig config) { - this.setVerticalScrollBarEnabled(true); - this.setVerticalScrollbarOverlay(true); - this.setScrollBarStyle(SCROLLBARS_INSIDE_OVERLAY); - this.setLongClickable(true); - - if (config.getUseDarkMode()) { - // Black theme should get a black webview background - // we'll set the background of the messages on load - this.setBackgroundColor(0xff000000); + if (config.useDarkMode) { + setBackgroundColor(0xff000000L.toInt()) } - final WebSettings webSettings = this.getSettings(); + with(settings) { + setSupportZoom(true) + builtInZoomControls = true + useWideViewPort = true - /* TODO this might improve rendering smoothness when webview is animated into view - if (VERSION.SDK_INT >= VERSION_CODES.M) { - webSettings.setOffscreenPreRaster(true); - } - */ - - webSettings.setSupportZoom(true); - webSettings.setBuiltInZoomControls(true); - webSettings.setUseWideViewPort(true); - if (config.getAutoFitWidth()) { - webSettings.setLoadWithOverviewMode(true); - } + if (config.autoFitWidth) { + loadWithOverviewMode = true + } - disableDisplayZoomControls(); + disableDisplayZoomControls() - webSettings.setJavaScriptEnabled(false); - webSettings.setLoadsImagesAutomatically(true); - webSettings.setRenderPriority(RenderPriority.HIGH); + javaScriptEnabled = false + loadsImagesAutomatically = true + setRenderPriority(RenderPriority.HIGH) - // TODO: Review alternatives. NARROW_COLUMNS is deprecated on KITKAT - webSettings.setLayoutAlgorithm(LayoutAlgorithm.NARROW_COLUMNS); + // TODO: Review alternatives. NARROW_COLUMNS is deprecated on KITKAT + layoutAlgorithm = LayoutAlgorithm.NARROW_COLUMNS - setOverScrollMode(OVER_SCROLL_NEVER); + overScrollMode = OVER_SCROLL_NEVER - webSettings.setTextZoom(config.getTextZoom()); + textZoom = config.textZoom + } - // Disable network images by default. This is overridden by preferences. - blockNetworkData(true); + // Disable network images by default. This is overridden by preferences. + blockNetworkData(true) } - /** - * Disable on-screen zoom controls on devices that support zooming via pinch-to-zoom. - */ - private void disableDisplayZoomControls() { - PackageManager pm = getContext().getPackageManager(); - boolean supportsMultiTouch = - pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH) || - pm.hasSystemFeature(PackageManager.FEATURE_FAKETOUCH_MULTITOUCH_DISTINCT); + private fun disableDisplayZoomControls() { + val packageManager = context.packageManager + val supportsMultiTouch = packageManager.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH) || + packageManager.hasSystemFeature(PackageManager.FEATURE_FAKETOUCH_MULTITOUCH_DISTINCT) - getSettings().setDisplayZoomControls(!supportsMultiTouch); + settings.displayZoomControls = !supportsMultiTouch } - public void displayHtmlContentWithInlineAttachments(@NonNull String htmlText, - @Nullable AttachmentResolver attachmentResolver, @Nullable OnPageFinishedListener onPageFinishedListener) { - setWebViewClient(attachmentResolver, onPageFinishedListener); - setHtmlContent(htmlText); + fun displayHtmlContentWithInlineAttachments( + htmlText: String, + attachmentResolver: AttachmentResolver?, + onPageFinishedListener: OnPageFinishedListener? + ) { + setWebViewClient(attachmentResolver, onPageFinishedListener) + setHtmlContent(htmlText) } - private void setWebViewClient(@Nullable AttachmentResolver attachmentResolver, - @Nullable OnPageFinishedListener onPageFinishedListener) { - K9WebViewClient webViewClient = K9WebViewClient.newInstance(attachmentResolver); + private fun setWebViewClient( + attachmentResolver: AttachmentResolver?, + onPageFinishedListener: OnPageFinishedListener? + ) { + val webViewClient = K9WebViewClient.newInstance(attachmentResolver) if (onPageFinishedListener != null) { - webViewClient.setOnPageFinishedListener(onPageFinishedListener); + webViewClient.setOnPageFinishedListener(onPageFinishedListener) } - setWebViewClient(webViewClient); + setWebViewClient(webViewClient) } - private void setHtmlContent(@NonNull String htmlText) { - loadDataWithBaseURL("about:blank", htmlText, "text/html", "utf-8", null); - resumeTimers(); + private fun setHtmlContent(htmlText: String) { + loadDataWithBaseURL("about:blank", htmlText, "text/html", "utf-8", null) + resumeTimers() } - public interface OnPageFinishedListener { - void onPageFinished(); + interface OnPageFinishedListener { + fun onPageFinished() } } -- GitLab From 66ebddbd1a44548bceefef063820d40074c1a60f Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 28 Jul 2022 17:41:16 +0200 Subject: [PATCH 34/47] Remove unused code --- .../ui/messageview/MessageContainerView.java | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.java b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.java index 1b38cfe181..83d4129c4d 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.java @@ -74,7 +74,6 @@ public class MessageContainerView extends LinearLayout implements OnCreateContex private View unsignedTextContainer; private View unsignedTextDivider; private TextView unsignedText; - private View mAttachmentsContainer; private boolean showingPictures; private LayoutInflater mInflater; @@ -98,7 +97,6 @@ public class MessageContainerView extends LinearLayout implements OnCreateContex mMessageContentView.setOnCreateContextMenuListener(this); mMessageContentView.setVisibility(View.VISIBLE); - mAttachmentsContainer = findViewById(R.id.attachments_container); mAttachments = findViewById(R.id.attachments); unsignedTextContainer = findViewById(R.id.message_unsigned_container); @@ -387,18 +385,6 @@ public class MessageContainerView extends LinearLayout implements OnCreateContex refreshDisplayedContent(); } - public void enableAttachmentButtons() { - for (AttachmentView attachmentView : attachmentViewMap.values()) { - attachmentView.enableButtons(); - } - } - - public void disableAttachmentButtons() { - for (AttachmentView attachmentView : attachmentViewMap.values()) { - attachmentView.disableButtons(); - } - } - public void displayMessageViewContainer(MessageViewInfo messageViewInfo, final OnRenderingFinishedListener onRenderingFinishedListener, boolean loadPictures, boolean hideUnsignedTextDivider, AttachmentViewCallback attachmentCallback) { @@ -516,14 +502,6 @@ public class MessageContainerView extends LinearLayout implements OnCreateContex clearDisplayedContent(); } - public void enableAttachmentButtons(AttachmentViewInfo attachment) { - getAttachmentView(attachment).enableButtons(); - } - - public void disableAttachmentButtons(AttachmentViewInfo attachment) { - getAttachmentView(attachment).disableButtons(); - } - public void refreshAttachmentThumbnail(AttachmentViewInfo attachment) { getAttachmentView(attachment).refreshThumbnail(); } -- GitLab From ec80646b6c386605437b54a7abc1fcd42b3567c0 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 28 Jul 2022 18:02:22 +0200 Subject: [PATCH 35/47] Simplify `message_container` layout --- .../k9/ui/messageview/MessageContainerView.java | 15 ++++++++------- .../src/main/res/layout/message_container.xml | 12 ++---------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.java b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.java index 83d4129c4d..222fc57e61 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.java @@ -22,6 +22,7 @@ import android.view.MenuItem; import android.view.MenuItem.OnMenuItemClickListener; import android.view.View; import android.view.View.OnCreateContextMenuListener; +import android.view.ViewGroup; import android.webkit.WebView; import android.webkit.WebView.HitTestResult; import android.widget.LinearLayout; @@ -70,7 +71,7 @@ public class MessageContainerView extends LinearLayout implements OnCreateContex private final ClipboardManager clipboardManager = DI.get(ClipboardManager.class); private MessageWebView mMessageContentView; - private LinearLayout mAttachments; + private ViewGroup attachmentsContainer; private View unsignedTextContainer; private View unsignedTextDivider; private TextView unsignedText; @@ -97,7 +98,7 @@ public class MessageContainerView extends LinearLayout implements OnCreateContex mMessageContentView.setOnCreateContextMenuListener(this); mMessageContentView.setVisibility(View.VISIBLE); - mAttachments = findViewById(R.id.attachments); + attachmentsContainer = findViewById(R.id.attachments_container); unsignedTextContainer = findViewById(R.id.message_unsigned_container); unsignedTextDivider = findViewById(R.id.message_unsigned_divider); @@ -458,12 +459,12 @@ public class MessageContainerView extends LinearLayout implements OnCreateContex } AttachmentView view = - (AttachmentView) mInflater.inflate(R.layout.message_view_attachment, mAttachments, false); + (AttachmentView) mInflater.inflate(R.layout.message_view_attachment, attachmentsContainer, false); view.setCallback(attachmentCallback); view.setAttachment(attachment); attachmentViewMap.put(attachment, view); - mAttachments.addView(view); + attachmentsContainer.addView(view); } } @@ -475,19 +476,19 @@ public class MessageContainerView extends LinearLayout implements OnCreateContex } LockedAttachmentView view = (LockedAttachmentView) mInflater - .inflate(R.layout.message_view_attachment_locked, mAttachments, false); + .inflate(R.layout.message_view_attachment_locked, attachmentsContainer, false); view.setCallback(attachmentCallback); view.setAttachment(attachment); // attachments.put(attachment, view); - mAttachments.addView(view); + attachmentsContainer.addView(view); } } } public void resetView() { setLoadPictures(false); - mAttachments.removeAllViews(); + attachmentsContainer.removeAllViews(); currentHtmlText = null; currentAttachmentResolver = null; diff --git a/app/ui/legacy/src/main/res/layout/message_container.xml b/app/ui/legacy/src/main/res/layout/message_container.xml index fb5c072b1c..3898c4a3d3 100644 --- a/app/ui/legacy/src/main/res/layout/message_container.xml +++ b/app/ui/legacy/src/main/res/layout/message_container.xml @@ -68,16 +68,8 @@ - - - + android:layout_height="wrap_content" + android:orientation="vertical" /> -- GitLab From 87dbcfecc56306d21a9271156c2fc8211176c3de Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 28 Jul 2022 18:44:34 +0200 Subject: [PATCH 36/47] Rename .java to .kt --- .../{MessageContainerView.java => MessageContainerView.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/{MessageContainerView.java => MessageContainerView.kt} (100%) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.java b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.kt similarity index 100% rename from app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.java rename to app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.kt -- GitLab From bfdc0eb04c8e9296e502c7b23ee7913da5a1cf6f Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 28 Jul 2022 18:44:34 +0200 Subject: [PATCH 37/47] Convert `MessageContainerView` to Kotlin --- .../messageview/AttachmentViewCallback.java | 2 +- .../k9/ui/messageview/MessageContainerView.kt | 853 +++++++++--------- .../java/com/fsck/k9/view/MessageWebView.kt | 2 +- 3 files changed, 439 insertions(+), 418 deletions(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentViewCallback.java b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentViewCallback.java index 43c2e40e6d..e78fe3a09d 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentViewCallback.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentViewCallback.java @@ -4,7 +4,7 @@ package com.fsck.k9.ui.messageview; import com.fsck.k9.mailstore.AttachmentViewInfo; -interface AttachmentViewCallback { +public interface AttachmentViewCallback { void onViewAttachment(AttachmentViewInfo attachment); void onSaveAttachment(AttachmentViewInfo attachment); } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.kt index 222fc57e61..887c7b559b 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.kt @@ -1,497 +1,502 @@ -package com.fsck.k9.ui.messageview; - - -import java.util.HashMap; -import java.util.Map; - -import android.app.DownloadManager; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Build; -import android.os.Environment; -import android.os.Message; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.MenuItem.OnMenuItemClickListener; -import android.view.View; -import android.view.View.OnCreateContextMenuListener; -import android.view.ViewGroup; -import android.webkit.WebView; -import android.webkit.WebView.HitTestResult; -import android.widget.LinearLayout; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.core.app.ShareCompat; -import com.fsck.k9.DI; -import com.fsck.k9.message.html.DisplayHtml; -import com.fsck.k9.ui.R; -import com.fsck.k9.helper.ClipboardManager; -import com.fsck.k9.helper.Contacts; -import com.fsck.k9.helper.Utility; -import com.fsck.k9.mail.Address; -import com.fsck.k9.mailstore.AttachmentResolver; -import com.fsck.k9.mailstore.AttachmentViewInfo; -import com.fsck.k9.mailstore.MessageViewInfo; -import com.fsck.k9.ui.helper.DisplayHtmlUiFactory; -import com.fsck.k9.view.MessageWebView; -import com.fsck.k9.view.MessageWebView.OnPageFinishedListener; -import com.fsck.k9.view.WebViewConfigProvider; - -import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED; - - -public class MessageContainerView extends LinearLayout implements OnCreateContextMenuListener { - private static final int MENU_ITEM_LINK_VIEW = Menu.FIRST; - private static final int MENU_ITEM_LINK_SHARE = Menu.FIRST + 1; - private static final int MENU_ITEM_LINK_COPY = Menu.FIRST + 2; - private static final int MENU_ITEM_LINK_TEXT_COPY = Menu.FIRST + 3; - - private static final int MENU_ITEM_IMAGE_VIEW = Menu.FIRST; - private static final int MENU_ITEM_IMAGE_SAVE = Menu.FIRST + 1; - private static final int MENU_ITEM_IMAGE_COPY = Menu.FIRST + 2; - - private static final int MENU_ITEM_PHONE_CALL = Menu.FIRST; - private static final int MENU_ITEM_PHONE_SAVE = Menu.FIRST + 1; - private static final int MENU_ITEM_PHONE_COPY = Menu.FIRST + 2; - - private static final int MENU_ITEM_EMAIL_SEND = Menu.FIRST; - private static final int MENU_ITEM_EMAIL_SAVE = Menu.FIRST + 1; - private static final int MENU_ITEM_EMAIL_COPY = Menu.FIRST + 2; - - private final DisplayHtml displayHtml = DI.get(DisplayHtmlUiFactory.class).createForMessageView(); - private final WebViewConfigProvider webViewConfigProvider = DI.get(WebViewConfigProvider.class); - private final ClipboardManager clipboardManager = DI.get(ClipboardManager.class); - - private MessageWebView mMessageContentView; - private ViewGroup attachmentsContainer; - private View unsignedTextContainer; - private View unsignedTextDivider; - private TextView unsignedText; - - private boolean showingPictures; - private LayoutInflater mInflater; - private AttachmentViewCallback attachmentCallback; - private Map attachmentViewMap = new HashMap<>(); - private Map attachments = new HashMap<>(); - private boolean hasHiddenExternalImages; - - private String currentHtmlText; - private AttachmentResolver currentAttachmentResolver; - - - @Override - public void onFinishInflate() { - super.onFinishInflate(); - - mMessageContentView = findViewById(R.id.message_content); - if (!isInEditMode()) { - mMessageContentView.configure(webViewConfigProvider.createForMessageView()); - } - mMessageContentView.setOnCreateContextMenuListener(this); - mMessageContentView.setVisibility(View.VISIBLE); - - attachmentsContainer = findViewById(R.id.attachments_container); - - unsignedTextContainer = findViewById(R.id.message_unsigned_container); - unsignedTextDivider = findViewById(R.id.message_unsigned_divider); - unsignedText = findViewById(R.id.message_unsigned_text); - - showingPictures = false; - - Context context = getContext(); - mInflater = LayoutInflater.from(context); +package com.fsck.k9.ui.messageview + +import android.app.DownloadManager +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.util.AttributeSet +import android.view.ContextMenu +import android.view.ContextMenu.ContextMenuInfo +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.View.OnCreateContextMenuListener +import android.view.ViewGroup +import android.webkit.WebView +import android.webkit.WebView.HitTestResult +import android.widget.LinearLayout +import android.widget.TextView +import android.widget.Toast +import androidx.core.app.ShareCompat.IntentBuilder +import androidx.core.view.isGone +import androidx.core.view.isVisible +import com.fsck.k9.helper.ClipboardManager +import com.fsck.k9.helper.Contacts +import com.fsck.k9.helper.Utility +import com.fsck.k9.mail.Address +import com.fsck.k9.mailstore.AttachmentResolver +import com.fsck.k9.mailstore.AttachmentViewInfo +import com.fsck.k9.mailstore.MessageViewInfo +import com.fsck.k9.ui.R +import com.fsck.k9.ui.helper.DisplayHtmlUiFactory +import com.fsck.k9.view.MessageWebView +import com.fsck.k9.view.MessageWebView.OnPageFinishedListener +import com.fsck.k9.view.WebViewConfigProvider +import org.koin.core.component.KoinComponent +import org.koin.core.component.get +import org.koin.core.component.inject + +class MessageContainerView(context: Context, attrs: AttributeSet?) : + LinearLayout(context, attrs), + OnCreateContextMenuListener, + KoinComponent { + + private val displayHtml by lazy(mode = LazyThreadSafetyMode.NONE) { + get().createForMessageView() } + private val webViewConfigProvider: WebViewConfigProvider by inject() + private val clipboardManager: ClipboardManager by inject() + private val linkTextHandler: LinkTextHandler by inject() - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { - super.onCreateContextMenu(menu); + private lateinit var layoutInflater: LayoutInflater - WebView webview = (WebView) v; - WebView.HitTestResult result = webview.getHitTestResult(); + private lateinit var messageContentView: MessageWebView + private lateinit var attachmentsContainer: ViewGroup + private lateinit var unsignedTextContainer: View + private lateinit var unsignedTextDivider: View + private lateinit var unsignedText: TextView - if (result == null) { - return; - } - - int type = result.getType(); - Context context = getContext(); - - switch (type) { - case HitTestResult.SRC_ANCHOR_TYPE: { - final String url = result.getExtra(); - OnMenuItemClickListener listener = new OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - switch (item.getItemId()) { - case MENU_ITEM_LINK_VIEW: { - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - startActivityIfAvailable(getContext(), intent); - break; - } - case MENU_ITEM_LINK_SHARE: { - new ShareCompat.IntentBuilder(getContext()) - .setType("text/plain") - .setText(url) - .startChooser(); - break; - } - case MENU_ITEM_LINK_COPY: { - String label = getContext().getString( - R.string.webview_contextmenu_link_clipboard_label); - clipboardManager.setText(label, url); - break; - } - case MENU_ITEM_LINK_TEXT_COPY: { - LinkTextHandler linkTextHandler = DI.get(LinkTextHandler.class); - Message message = linkTextHandler.obtainMessage(); - webview.requestFocusNodeHref(message); - break; - } - } - return true; - } - }; + private var isShowingPictures = false + private var currentHtmlText: String? = null + private val attachmentViewMap = mutableMapOf() + private val attachments = mutableMapOf() + private var attachmentCallback: AttachmentViewCallback? = null + private var currentAttachmentResolver: AttachmentResolver? = null - menu.setHeaderTitle(url); + @get:JvmName("hasHiddenExternalImages") + var hasHiddenExternalImages = false + private set - menu.add(Menu.NONE, MENU_ITEM_LINK_VIEW, 0, - context.getString(R.string.webview_contextmenu_link_view_action)) - .setOnMenuItemClickListener(listener); + public override fun onFinishInflate() { + super.onFinishInflate() - menu.add(Menu.NONE, MENU_ITEM_LINK_SHARE, 1, - context.getString(R.string.webview_contextmenu_link_share_action)) - .setOnMenuItemClickListener(listener); + layoutInflater = LayoutInflater.from(context) - menu.add(Menu.NONE, MENU_ITEM_LINK_COPY, 2, - context.getString(R.string.webview_contextmenu_link_copy_action)) - .setOnMenuItemClickListener(listener); + messageContentView = findViewById(R.id.message_content).apply { + if (!isInEditMode) { + configure(webViewConfigProvider.createForMessageView()) + } - menu.add(Menu.NONE, MENU_ITEM_LINK_TEXT_COPY, 3, - context.getString(R.string.webview_contextmenu_link_text_copy_action)) - .setOnMenuItemClickListener(listener); + setOnCreateContextMenuListener(this@MessageContainerView) + visibility = VISIBLE + } - break; - } - case HitTestResult.IMAGE_TYPE: - case HitTestResult.SRC_IMAGE_ANCHOR_TYPE: { - final Uri uri = Uri.parse(result.getExtra()); - if (uri == null) { - return; - } - - final AttachmentViewInfo attachmentViewInfo = getAttachmentViewInfoIfCidUri(uri); - final boolean inlineImage = attachmentViewInfo != null; - - OnMenuItemClickListener listener = new OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - switch (item.getItemId()) { - case MENU_ITEM_IMAGE_VIEW: { - if (inlineImage) { - attachmentCallback.onViewAttachment(attachmentViewInfo); - } else { - Intent intent = new Intent(Intent.ACTION_VIEW, uri); - startActivityIfAvailable(getContext(), intent); - } - break; - } - case MENU_ITEM_IMAGE_SAVE: { - if (inlineImage) { - attachmentCallback.onSaveAttachment(attachmentViewInfo); - } else { - downloadImage(uri); - } - break; - } - case MENU_ITEM_IMAGE_COPY: { - String label = getContext().getString( - R.string.webview_contextmenu_image_clipboard_label); - clipboardManager.setText(label, uri.toString()); - break; - } - } - return true; - } - }; + attachmentsContainer = findViewById(R.id.attachments_container) + unsignedTextContainer = findViewById(R.id.message_unsigned_container) + unsignedTextDivider = findViewById(R.id.message_unsigned_divider) + unsignedText = findViewById(R.id.message_unsigned_text) + } - menu.setHeaderTitle(inlineImage ? - context.getString(R.string.webview_contextmenu_image_title) : uri.toString()); + override fun onCreateContextMenu(menu: ContextMenu, view: View, menuInfo: ContextMenuInfo) { + super.onCreateContextMenu(menu) - menu.add(Menu.NONE, MENU_ITEM_IMAGE_VIEW, 0, - context.getString(R.string.webview_contextmenu_image_view_action)) - .setOnMenuItemClickListener(listener); + val webView = view as WebView + val hitTestResult = webView.hitTestResult - menu.add(Menu.NONE, MENU_ITEM_IMAGE_SAVE, 1, - inlineImage ? - context.getString(R.string.webview_contextmenu_image_save_action) : - context.getString(R.string.webview_contextmenu_image_download_action)) - .setOnMenuItemClickListener(listener); + when (hitTestResult.type) { + HitTestResult.SRC_ANCHOR_TYPE -> { + createLinkMenu(menu, webView, linkUrl = hitTestResult.extra) + } + HitTestResult.IMAGE_TYPE, HitTestResult.SRC_IMAGE_ANCHOR_TYPE -> { + createImageMenu(menu, imageUrl = hitTestResult.extra) + } + HitTestResult.PHONE_TYPE -> { + createPhoneNumberMenu(menu, phoneNumber = hitTestResult.extra) + } + HitTestResult.EMAIL_TYPE -> { + createEmailMenu(menu, email = hitTestResult.extra) + } + } + } - if (!inlineImage) { - menu.add(Menu.NONE, MENU_ITEM_IMAGE_COPY, 2, - context.getString(R.string.webview_contextmenu_image_copy_action)) - .setOnMenuItemClickListener(listener); + private fun createLinkMenu( + menu: ContextMenu, + webView: WebView, + linkUrl: String? + ) { + if (linkUrl == null) return + + val listener = MenuItem.OnMenuItemClickListener { item -> + when (item.itemId) { + MENU_ITEM_LINK_VIEW -> { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(linkUrl)) + startActivityIfAvailable(context, intent) + } + MENU_ITEM_LINK_SHARE -> { + IntentBuilder(context) + .setType("text/plain") + .setText(linkUrl) + .startChooser() + } + MENU_ITEM_LINK_COPY -> { + val label = context.getString(R.string.webview_contextmenu_link_clipboard_label) + clipboardManager.setText(label, linkUrl) + } + MENU_ITEM_LINK_TEXT_COPY -> { + val message = linkTextHandler.obtainMessage() + webView.requestFocusNodeHref(message) } - - break; } - case HitTestResult.PHONE_TYPE: { - final String phoneNumber = result.getExtra(); - OnMenuItemClickListener listener = new OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - switch (item.getItemId()) { - case MENU_ITEM_PHONE_CALL: { - Uri uri = Uri.parse(WebView.SCHEME_TEL + phoneNumber); - Intent intent = new Intent(Intent.ACTION_VIEW, uri); - startActivityIfAvailable(getContext(), intent); - break; - } - case MENU_ITEM_PHONE_SAVE: { - Contacts contacts = Contacts.getInstance(getContext()); - contacts.addPhoneContact(phoneNumber); - break; - } - case MENU_ITEM_PHONE_COPY: { - String label = getContext().getString( - R.string.webview_contextmenu_phone_clipboard_label); - clipboardManager.setText(label, phoneNumber); - break; - } - } - - return true; - } - }; + true + } - menu.setHeaderTitle(phoneNumber); + menu.setHeaderTitle(linkUrl) + + menu.add( + Menu.NONE, + MENU_ITEM_LINK_VIEW, + 0, + context.getString(R.string.webview_contextmenu_link_view_action) + ).setOnMenuItemClickListener(listener) + + menu.add( + Menu.NONE, + MENU_ITEM_LINK_SHARE, + 1, + context.getString(R.string.webview_contextmenu_link_share_action) + ).setOnMenuItemClickListener(listener) + + menu.add( + Menu.NONE, + MENU_ITEM_LINK_COPY, + 2, + context.getString(R.string.webview_contextmenu_link_copy_action) + ).setOnMenuItemClickListener(listener) + + menu.add( + Menu.NONE, + MENU_ITEM_LINK_TEXT_COPY, + 3, + context.getString(R.string.webview_contextmenu_link_text_copy_action) + ).setOnMenuItemClickListener(listener) + } - menu.add(Menu.NONE, MENU_ITEM_PHONE_CALL, 0, - context.getString(R.string.webview_contextmenu_phone_call_action)) - .setOnMenuItemClickListener(listener); + private fun createImageMenu(menu: ContextMenu, imageUrl: String?) { + if (imageUrl == null) return - menu.add(Menu.NONE, MENU_ITEM_PHONE_SAVE, 1, - context.getString(R.string.webview_contextmenu_phone_save_action)) - .setOnMenuItemClickListener(listener); + val imageUri = Uri.parse(imageUrl) + val attachmentViewInfo = getAttachmentViewInfoIfCidUri(imageUri) + val inlineImage = attachmentViewInfo != null - menu.add(Menu.NONE, MENU_ITEM_PHONE_COPY, 2, - context.getString(R.string.webview_contextmenu_phone_copy_action)) - .setOnMenuItemClickListener(listener); + val listener = MenuItem.OnMenuItemClickListener { item -> + val attachmentCallback = checkNotNull(attachmentCallback) - break; - } - case WebView.HitTestResult.EMAIL_TYPE: { - final String email = result.getExtra(); - OnMenuItemClickListener listener = new OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - switch (item.getItemId()) { - case MENU_ITEM_EMAIL_SEND: { - Uri uri = Uri.parse(WebView.SCHEME_MAILTO + email); - Intent intent = new Intent(Intent.ACTION_VIEW, uri); - startActivityIfAvailable(getContext(), intent); - break; - } - case MENU_ITEM_EMAIL_SAVE: { - Contacts contacts = Contacts.getInstance(getContext()); - contacts.createContact(new Address(email)); - break; - } - case MENU_ITEM_EMAIL_COPY: { - String label = getContext().getString( - R.string.webview_contextmenu_email_clipboard_label); - clipboardManager.setText(label, email); - break; - } - } - - return true; + when (item.itemId) { + MENU_ITEM_IMAGE_VIEW -> { + if (inlineImage) { + attachmentCallback.onViewAttachment(attachmentViewInfo) + } else { + val intent = Intent(Intent.ACTION_VIEW, imageUri) + startActivityIfAvailable(context, intent) } - }; - - menu.setHeaderTitle(email); - - menu.add(Menu.NONE, MENU_ITEM_EMAIL_SEND, 0, - context.getString(R.string.webview_contextmenu_email_send_action)) - .setOnMenuItemClickListener(listener); - - menu.add(Menu.NONE, MENU_ITEM_EMAIL_SAVE, 1, - context.getString(R.string.webview_contextmenu_email_save_action)) - .setOnMenuItemClickListener(listener); + } + MENU_ITEM_IMAGE_SAVE -> { + if (inlineImage) { + attachmentCallback.onSaveAttachment(attachmentViewInfo) + } else { + downloadImage(imageUri) + } + } + MENU_ITEM_IMAGE_COPY -> { + val label = context.getString(R.string.webview_contextmenu_image_clipboard_label) + clipboardManager.setText(label, imageUri.toString()) + } + } + true + } - menu.add(Menu.NONE, MENU_ITEM_EMAIL_COPY, 2, - context.getString(R.string.webview_contextmenu_email_copy_action)) - .setOnMenuItemClickListener(listener); + if (inlineImage) { + menu.setHeaderTitle(R.string.webview_contextmenu_image_title) + } else { + menu.setHeaderTitle(imageUrl) + } - break; + menu.add( + Menu.NONE, + MENU_ITEM_IMAGE_VIEW, + 0, + context.getString(R.string.webview_contextmenu_image_view_action) + ).setOnMenuItemClickListener(listener) + + menu.add( + Menu.NONE, + MENU_ITEM_IMAGE_SAVE, + 1, + if (inlineImage) { + context.getString(R.string.webview_contextmenu_image_save_action) + } else { + context.getString(R.string.webview_contextmenu_image_download_action) } + ).setOnMenuItemClickListener(listener) + + if (!inlineImage) { + menu.add( + Menu.NONE, + MENU_ITEM_IMAGE_COPY, + 2, + context.getString(R.string.webview_contextmenu_image_copy_action) + ).setOnMenuItemClickListener(listener) } } - private void downloadImage(Uri uri) { - DownloadManager.Request request = new DownloadManager.Request(uri); - if (Build.VERSION.SDK_INT >= 29) { - String filename = uri.getLastPathSegment(); - request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename); + private fun createPhoneNumberMenu(menu: ContextMenu, phoneNumber: String?) { + if (phoneNumber == null) return + + val listener = MenuItem.OnMenuItemClickListener { item -> + when (item.itemId) { + MENU_ITEM_PHONE_CALL -> { + val uri = Uri.parse(WebView.SCHEME_TEL + phoneNumber) + val intent = Intent(Intent.ACTION_VIEW, uri) + startActivityIfAvailable(context, intent) + } + MENU_ITEM_PHONE_SAVE -> { + val contacts = Contacts.getInstance(context) + contacts.addPhoneContact(phoneNumber) + } + MENU_ITEM_PHONE_COPY -> { + val label = context.getString(R.string.webview_contextmenu_phone_clipboard_label) + clipboardManager.setText(label, phoneNumber) + } + } + true } - request.setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED); - DownloadManager downloadManager = (DownloadManager) getContext().getSystemService(Context.DOWNLOAD_SERVICE); - downloadManager.enqueue(request); + menu.setHeaderTitle(phoneNumber) + + menu.add( + Menu.NONE, + MENU_ITEM_PHONE_CALL, + 0, + context.getString(R.string.webview_contextmenu_phone_call_action) + ).setOnMenuItemClickListener(listener) + + menu.add( + Menu.NONE, + MENU_ITEM_PHONE_SAVE, + 1, + context.getString(R.string.webview_contextmenu_phone_save_action) + ).setOnMenuItemClickListener(listener) + + menu.add( + Menu.NONE, + MENU_ITEM_PHONE_COPY, + 2, + context.getString(R.string.webview_contextmenu_phone_copy_action) + ).setOnMenuItemClickListener(listener) } - private AttachmentViewInfo getAttachmentViewInfoIfCidUri(Uri uri) { - if (!"cid".equals(uri.getScheme())) { - return null; - } + private fun createEmailMenu(menu: ContextMenu, email: String?) { + if (email == null) return - String cid = uri.getSchemeSpecificPart(); - Uri internalUri = currentAttachmentResolver.getAttachmentUriForContentId(cid); + val listener = MenuItem.OnMenuItemClickListener { item -> + when (item.itemId) { + MENU_ITEM_EMAIL_SEND -> { + val uri = Uri.parse(WebView.SCHEME_MAILTO + email) + val intent = Intent(Intent.ACTION_VIEW, uri) + startActivityIfAvailable(context, intent) + } + MENU_ITEM_EMAIL_SAVE -> { + val contacts = Contacts.getInstance(context) + contacts.createContact(Address(email)) + } + MENU_ITEM_EMAIL_COPY -> { + val label = context.getString(R.string.webview_contextmenu_email_clipboard_label) + clipboardManager.setText(label, email) + } + } + true + } - return attachments.get(internalUri); + menu.setHeaderTitle(email) + + menu.add( + Menu.NONE, + MENU_ITEM_EMAIL_SEND, + 0, + context.getString(R.string.webview_contextmenu_email_send_action) + ).setOnMenuItemClickListener(listener) + + menu.add( + Menu.NONE, + MENU_ITEM_EMAIL_SAVE, + 1, + context.getString(R.string.webview_contextmenu_email_save_action) + ).setOnMenuItemClickListener(listener) + + menu.add( + Menu.NONE, + MENU_ITEM_EMAIL_COPY, + 2, + context.getString(R.string.webview_contextmenu_email_copy_action) + ).setOnMenuItemClickListener(listener) } - private void startActivityIfAvailable(Context context, Intent intent) { - try { - context.startActivity(intent); - } catch (ActivityNotFoundException e) { - Toast.makeText(context, R.string.error_activity_not_found, Toast.LENGTH_LONG).show(); + private fun downloadImage(uri: Uri) { + val request = DownloadManager.Request(uri).apply { + if (Build.VERSION.SDK_INT >= 29) { + val filename = uri.lastPathSegment + setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename) + } + + setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) } - } - public MessageContainerView(Context context, AttributeSet attrs) { - super(context, attrs); + val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager + downloadManager.enqueue(request) } + private fun getAttachmentViewInfoIfCidUri(uri: Uri): AttachmentViewInfo? { + if (uri.scheme != "cid") return null - private boolean isShowingPictures() { - return showingPictures; - } + val attachmentResolver = checkNotNull(currentAttachmentResolver) - private void setLoadPictures(boolean enable) { - mMessageContentView.blockNetworkData(!enable); - showingPictures = enable; - } + val cid = uri.schemeSpecificPart + val internalUri = attachmentResolver.getAttachmentUriForContentId(cid) - public void showPictures() { - setLoadPictures(true); - refreshDisplayedContent(); + return attachments[internalUri] } - public void displayMessageViewContainer(MessageViewInfo messageViewInfo, - final OnRenderingFinishedListener onRenderingFinishedListener, boolean loadPictures, - boolean hideUnsignedTextDivider, AttachmentViewCallback attachmentCallback) { - - this.attachmentCallback = attachmentCallback; + private fun startActivityIfAvailable(context: Context, intent: Intent) { + try { + context.startActivity(intent) + } catch (e: ActivityNotFoundException) { + Toast.makeText(context, R.string.error_activity_not_found, Toast.LENGTH_LONG).show() + } + } - resetView(); + private fun setLoadPictures(enable: Boolean) { + messageContentView.blockNetworkData(!enable) + isShowingPictures = enable + } - renderAttachments(messageViewInfo); + fun showPictures() { + setLoadPictures(true) + refreshDisplayedContent() + } - String textToDisplay = messageViewInfo.text; - if (textToDisplay != null && !isShowingPictures()) { - if (Utility.hasExternalImages(textToDisplay)) { + fun displayMessageViewContainer( + messageViewInfo: MessageViewInfo, + onRenderingFinishedListener: OnRenderingFinishedListener, + loadPictures: Boolean, + hideUnsignedTextDivider: Boolean, + attachmentCallback: AttachmentViewCallback? + ) { + this.attachmentCallback = attachmentCallback + + resetView() + renderAttachments(messageViewInfo) + + val messageText = messageViewInfo.text + if (messageText != null && !isShowingPictures) { + if (Utility.hasExternalImages(messageText)) { if (loadPictures) { - setLoadPictures(true); + setLoadPictures(true) } else { - hasHiddenExternalImages = true; + hasHiddenExternalImages = true } } } - if (textToDisplay == null) { - String noTextMessage = getContext().getString(R.string.webview_empty_message); - textToDisplay = displayHtml.wrapStatusMessage(noTextMessage); - } - - OnPageFinishedListener onPageFinishedListener = new OnPageFinishedListener() { - @Override - public void onPageFinished() { - onRenderingFinishedListener.onLoadFinished(); - } - }; + val textToDisplay = messageText + ?: displayHtml.wrapStatusMessage(context.getString(R.string.webview_empty_message)) displayHtmlContentWithInlineAttachments( - textToDisplay, messageViewInfo.attachmentResolver, onPageFinishedListener); - - if (!TextUtils.isEmpty(messageViewInfo.extraText)) { - unsignedTextContainer.setVisibility(View.VISIBLE); - unsignedTextDivider.setVisibility(hideUnsignedTextDivider ? View.GONE : View.VISIBLE); - unsignedText.setText(messageViewInfo.extraText); + htmlText = textToDisplay, + attachmentResolver = messageViewInfo.attachmentResolver, + onPageFinishedListener = onRenderingFinishedListener::onLoadFinished + ) + + if (!messageViewInfo.extraText.isNullOrEmpty()) { + unsignedTextContainer.isVisible = true + unsignedTextDivider.isGone = hideUnsignedTextDivider + unsignedText.text = messageViewInfo.extraText } } - public boolean hasHiddenExternalImages() { - return hasHiddenExternalImages; + private fun displayHtmlContentWithInlineAttachments( + htmlText: String, + attachmentResolver: AttachmentResolver, + onPageFinishedListener: OnPageFinishedListener + ) { + currentHtmlText = htmlText + currentAttachmentResolver = attachmentResolver + messageContentView.displayHtmlContentWithInlineAttachments(htmlText, attachmentResolver, onPageFinishedListener) } - private void displayHtmlContentWithInlineAttachments(String htmlText, AttachmentResolver attachmentResolver, - OnPageFinishedListener onPageFinishedListener) { - currentHtmlText = htmlText; - currentAttachmentResolver = attachmentResolver; - mMessageContentView.displayHtmlContentWithInlineAttachments(htmlText, attachmentResolver, onPageFinishedListener); - } + private fun refreshDisplayedContent() { + val htmlText = checkNotNull(currentHtmlText) - private void refreshDisplayedContent() { - mMessageContentView.displayHtmlContentWithInlineAttachments(currentHtmlText, currentAttachmentResolver, null); + messageContentView.displayHtmlContentWithInlineAttachments( + htmlText = htmlText, + attachmentResolver = currentAttachmentResolver, + onPageFinishedListener = null + ) } - private void clearDisplayedContent() { - mMessageContentView.displayHtmlContentWithInlineAttachments("", null, null); - unsignedTextContainer.setVisibility(View.GONE); - unsignedText.setText(""); + private fun clearDisplayedContent() { + messageContentView.displayHtmlContentWithInlineAttachments( + htmlText = "", + attachmentResolver = null, + onPageFinishedListener = null + ) + + unsignedTextContainer.isVisible = false + unsignedText.text = "" } - public void renderAttachments(MessageViewInfo messageViewInfo) { + private fun renderAttachments(messageViewInfo: MessageViewInfo) { if (messageViewInfo.attachments != null) { - for (AttachmentViewInfo attachment : messageViewInfo.attachments) { - attachments.put(attachment.internalUri, attachment); + for (attachment in messageViewInfo.attachments) { + attachments[attachment.internalUri] = attachment if (attachment.inlineAttachment) { - continue; + continue } - AttachmentView view = - (AttachmentView) mInflater.inflate(R.layout.message_view_attachment, attachmentsContainer, false); - view.setCallback(attachmentCallback); - view.setAttachment(attachment); + val attachmentView = layoutInflater.inflate( + R.layout.message_view_attachment, + attachmentsContainer, + false + ) as AttachmentView - attachmentViewMap.put(attachment, view); - attachmentsContainer.addView(view); + attachmentView.setCallback(attachmentCallback) + attachmentView.setAttachment(attachment) + + attachmentViewMap[attachment] = attachmentView + attachmentsContainer.addView(attachmentView) } } if (messageViewInfo.extraAttachments != null) { - for (AttachmentViewInfo attachment : messageViewInfo.extraAttachments) { - attachments.put(attachment.internalUri, attachment); + for (attachment in messageViewInfo.extraAttachments) { + attachments[attachment.internalUri] = attachment if (attachment.inlineAttachment) { - continue; + continue } - LockedAttachmentView view = (LockedAttachmentView) mInflater - .inflate(R.layout.message_view_attachment_locked, attachmentsContainer, false); - view.setCallback(attachmentCallback); - view.setAttachment(attachment); + val lockedAttachmentView = layoutInflater.inflate( + R.layout.message_view_attachment_locked, + attachmentsContainer, + false + ) as LockedAttachmentView + + lockedAttachmentView.setCallback(attachmentCallback) + lockedAttachmentView.setAttachment(attachment) - // attachments.put(attachment, view); - attachmentsContainer.addView(view); + attachmentsContainer.addView(lockedAttachmentView) } } } - public void resetView() { - setLoadPictures(false); - attachmentsContainer.removeAllViews(); + private fun resetView() { + setLoadPictures(false) + attachmentsContainer.removeAllViews() - currentHtmlText = null; - currentAttachmentResolver = null; + currentHtmlText = null + currentAttachmentResolver = null /* * Clear the WebView content @@ -500,18 +505,34 @@ public class MessageContainerView extends LinearLayout implements OnCreateContex * its size because the button to download the complete message was previously shown and * is now hidden. */ - clearDisplayedContent(); + clearDisplayedContent() } - public void refreshAttachmentThumbnail(AttachmentViewInfo attachment) { - getAttachmentView(attachment).refreshThumbnail(); + fun refreshAttachmentThumbnail(attachment: AttachmentViewInfo) { + getAttachmentView(attachment)?.refreshThumbnail() } - private AttachmentView getAttachmentView(AttachmentViewInfo attachment) { - return attachmentViewMap.get(attachment); + private fun getAttachmentView(attachment: AttachmentViewInfo): AttachmentView? { + return attachmentViewMap[attachment] } interface OnRenderingFinishedListener { - void onLoadFinished(); + fun onLoadFinished() + } + + companion object { + private const val MENU_ITEM_LINK_VIEW = Menu.FIRST + private const val MENU_ITEM_LINK_SHARE = Menu.FIRST + 1 + private const val MENU_ITEM_LINK_COPY = Menu.FIRST + 2 + private const val MENU_ITEM_LINK_TEXT_COPY = Menu.FIRST + 3 + private const val MENU_ITEM_IMAGE_VIEW = Menu.FIRST + private const val MENU_ITEM_IMAGE_SAVE = Menu.FIRST + 1 + private const val MENU_ITEM_IMAGE_COPY = Menu.FIRST + 2 + private const val MENU_ITEM_PHONE_CALL = Menu.FIRST + private const val MENU_ITEM_PHONE_SAVE = Menu.FIRST + 1 + private const val MENU_ITEM_PHONE_COPY = Menu.FIRST + 2 + private const val MENU_ITEM_EMAIL_SEND = Menu.FIRST + private const val MENU_ITEM_EMAIL_SAVE = Menu.FIRST + 1 + private const val MENU_ITEM_EMAIL_COPY = Menu.FIRST + 2 } } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/view/MessageWebView.kt b/app/ui/legacy/src/main/java/com/fsck/k9/view/MessageWebView.kt index 946cd704cf..fde796689f 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/view/MessageWebView.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/view/MessageWebView.kt @@ -93,7 +93,7 @@ class MessageWebView : WebView { resumeTimers() } - interface OnPageFinishedListener { + fun interface OnPageFinishedListener { fun onPageFinished() } } -- GitLab From d014c2f7f3840971e019beed2effe0d0aa8b7c5d Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 29 Jul 2022 15:29:32 +0200 Subject: [PATCH 38/47] Fix nullability of parameter --- .../java/com/fsck/k9/ui/messageview/MessageContainerView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.kt index 887c7b559b..0d3534f269 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.kt @@ -91,7 +91,7 @@ class MessageContainerView(context: Context, attrs: AttributeSet?) : unsignedText = findViewById(R.id.message_unsigned_text) } - override fun onCreateContextMenu(menu: ContextMenu, view: View, menuInfo: ContextMenuInfo) { + override fun onCreateContextMenu(menu: ContextMenu, view: View, menuInfo: ContextMenuInfo?) { super.onCreateContextMenu(menu) val webView = view as WebView -- GitLab From 7bed9b05b618dea679efb4f7b3dcf926da31c4b6 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 2 Aug 2022 16:42:58 +0200 Subject: [PATCH 39/47] Rename .java to .kt --- .../message/html/{HtmlConverterTest.java => HtmlConverterTest.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/core/src/test/java/com/fsck/k9/message/html/{HtmlConverterTest.java => HtmlConverterTest.kt} (100%) diff --git a/app/core/src/test/java/com/fsck/k9/message/html/HtmlConverterTest.java b/app/core/src/test/java/com/fsck/k9/message/html/HtmlConverterTest.kt similarity index 100% rename from app/core/src/test/java/com/fsck/k9/message/html/HtmlConverterTest.java rename to app/core/src/test/java/com/fsck/k9/message/html/HtmlConverterTest.kt -- GitLab From 114af53c8491f54396d7982235897bd65c1b837d Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 2 Aug 2022 16:42:58 +0200 Subject: [PATCH 40/47] Convert `HtmlConverterTest` to Kotlin --- .../fsck/k9/message/html/HtmlConverterTest.kt | 664 +++++++++++------- .../java/com/fsck/k9/mail/StringHelper.kt | 2 + 2 files changed, 422 insertions(+), 244 deletions(-) diff --git a/app/core/src/test/java/com/fsck/k9/message/html/HtmlConverterTest.kt b/app/core/src/test/java/com/fsck/k9/message/html/HtmlConverterTest.kt index d7c34f9fb7..178110da15 100644 --- a/app/core/src/test/java/com/fsck/k9/message/html/HtmlConverterTest.kt +++ b/app/core/src/test/java/com/fsck/k9/message/html/HtmlConverterTest.kt @@ -1,329 +1,505 @@ -package com.fsck.k9.message.html; +package com.fsck.k9.message.html +import com.fsck.k9.mail.crlf +import com.fsck.k9.mail.removeLineBreaks +import com.google.common.truth.Truth.assertThat +import org.junit.Test -import org.junit.Test; - -import static junit.framework.Assert.assertEquals; - - -public class HtmlConverterTest { +class HtmlConverterTest { @Test - public void testTextQuoteToHtmlBlockquote() { - String message = "Panama!\r\n" + - "\r\n" + - "Bob Barker wrote:\r\n" + - "> a canal\r\n" + - ">\r\n" + - "> Dorothy Jo Gideon espoused:\r\n" + - "> >A man, a plan...\r\n" + - "> Too easy!\r\n" + - "\r\n" + - "Nice job :)\r\n" + - ">> Guess!"; - - String result = HtmlConverter.textToHtml(message); - - assertEquals("
"
-                + "Panama!
" - + "
" - + "Bob Barker <bob@aol.com> wrote:
" - + - "
" - + " a canal
" - + "
" - + " Dorothy Jo Gideon <dorothy@aol.com> espoused:
" - + - "
" - + "A man, a plan...
" - + "
" - + "Too easy!
" - + "
" - + "
" - + "Nice job :)
" - + - "
" - + - "
" - + "Guess!" - + "
" - + "
" - + "
", result); + fun `textToHtml() should convert quoted text using blockquote tags`() { + val message = + """ + Panama! + + Bob Barker wrote: + > a canal + > + > Dorothy Jo Gideon espoused: + > >A man, a plan... + > Too easy! + + Nice job :) + >> Guess! + """.trimIndent().crlf() + + val result = HtmlConverter.textToHtml(message) + + assertThat(result).isEqualTo( + """ + |
+            |Panama!
+ |
+ |Bob Barker <bob@aol.com> wrote:
+ |
+ | a canal
+ |
+ | Dorothy Jo Gideon <dorothy@aol.com> espoused:
+ |
+ |A man, a plan...
+ |
+ |Too easy!
+ |
+ |
+ |Nice job :)
+ |
+ |
+ |Guess! + |
+ |
+ |
+ """.trimMargin().removeLineBreaks() + ) } @Test - public void testTextQuoteToHtmlBlockquoteIndented() { - String message = "*facepalm*\r\n" + - "\r\n" + - "Bob Barker wrote:\r\n" + - "> A wise man once said...\r\n" + - ">\r\n" + - "> LOL F1RST!!!!!\r\n" + - ">\r\n" + - "> :)"; - - String result = HtmlConverter.textToHtml(message); - - assertEquals("
"
-                + "*facepalm*
" - + "
" - + "Bob Barker <bob@aol.com> wrote:
" - + "
" - + " A wise man once said...
" - + "
" - + " LOL F1RST!!!!!
" - + "
" - + " :)" - + "
", result); - + fun `textToHtml() should retain indentation inside quoted text`() { + val message = + """ + *facepalm* + + Bob Barker wrote: + > A wise man once said... + > + > LOL F1RST!!!!! + > + > :) + """.trimIndent().crlf() + + val result = HtmlConverter.textToHtml(message) + + assertThat(result).isEqualTo( + """ + |
+            |*facepalm*
+ |
+ |Bob Barker <bob@aol.com> wrote:
+ |
+ | A wise man once said...
+ |
+ | LOL F1RST!!!!!
+ |
+ | :) + |
+ |
+ """.trimMargin().removeLineBreaks() + ) } @Test - public void testQuoteDepthColor() { - String message = "zero\r\n" + - "> one\r\n" + - ">> two\r\n" + - ">>> three\r\n" + - ">>>> four\r\n" + - ">>>>> five\r\n" + - ">>>>>> six"; - - String result = HtmlConverter.textToHtml(message); - - assertEquals("
"
-                + "zero
" - + "
" - + "one
" - + "
" - + "two
" - + "
" - + "three
" - + "
" - + "four
" - + "
" - + "five
" - + "
" - + "six" - + "
" - + "
" - + "
" - + "
" - + "
" - + "
" - + "
", result); + fun `textToHtml() with various quotation depths`() { + val message = + """ + zero + > one + >> two + >>> three + >>>> four + >>>>> five + >>>>>> six + """.trimIndent().crlf() + + val result = HtmlConverter.textToHtml(message) + + assertThat(result).isEqualTo( + """ + |
+            |zero
+ |
+ |one
+ |
+ |two
+ |
+ |three
+ |
+ |four
+ |
+ |five
+ |
+ |six + |
+ |
+ |
+ |
+ |
+ |
+ |
+ """.trimMargin().removeLineBreaks() + ) } @Test - public void testPreserveSpacesAtFirst() { - String message = "foo\r\n" - + " bar\r\n" - + " baz\r\n"; - - String result = HtmlConverter.textToHtml(message); - - assertEquals("
"
-                + "foo
" - + " bar
" - + " baz
" - + "
", result); + fun `textToHtml() should preserve spaces at the start of a line`() { + val message = + """ + |foo + | bar + | baz + | + """.trimMargin().crlf() + + val result = HtmlConverter.textToHtml(message) + + assertThat(result).isEqualTo( + """ + |
+            |foo
+ | bar
+ | baz
+ |
+ """.trimMargin().removeLineBreaks() + ) } @Test - public void testPreserveSpacesAtFirstForSpecialCharacters() { - String message = - " \r\n" - + " &\r\n" - + " \n" - + " <\r\n" - + " > \r\n"; - - String result = HtmlConverter.textToHtml(message); - - assertEquals("
"
-                + " 
" - + " &
" - + "
" - + " <
" - + "
" - + "
" - + "
" - + "
", result); + fun `textToHtml() should preserve spaces at the start of a line followed by special characters`() { + val message = + """ + | + | & + | ${" "} + | < + | >${" "} + | + """.trimMargin().crlf() + + val result = HtmlConverter.textToHtml(message) + + assertThat(result).isEqualTo( + """ + |
+            | 
+ | &
+ |
+ | <
+ |
+ |
+ |
+ |
+ """.trimMargin().removeLineBreaks() + ) } @Test - public void issue2259Spec() { - String text = "text\n" + - "---------------------------\n" + - "some other text\n" + - "===========================\n" + - "more text\n" + - "-=-=-=-=-=-=-=-=-=-=-=-=-=-\n" + - "scissors below\n" + - "-- >8 --\n" + - "other direction\n" + - "-- 8< --\n" + - "end"; - String result = HtmlConverter.textToHtml(text); - assertEquals("
text
" + - "some other text
" + - "more text
" + - "scissors below
" + - "other direction
" + - "end
", - result); + fun `textToHtml() should replace common horizontal divider ASCII patterns with HR tags`() { + val text = + """ + text + --------------------------- + some other text + =========================== + more text + -=-=-=-=-=-=-=-=-=-=-=-=-=- + scissors below + -- >8 -- + other direction + -- 8< -- + end + """.trimIndent() + + val result = HtmlConverter.textToHtml(text) + + assertThat(result).isEqualTo( + """ +
+            text
+            
+ some other text +
+ more text +
+ scissors below +
+ other direction +
+ end +
+ """.trimIndent().removeLineBreaks() + ) } @Test - public void dashesContainingSpacesIgnoredAsHR() { - String text = "hello\n--- --- --- --- ---\nfoo bar"; - String result = HtmlConverter.textToHtml(text); - assertEquals("
hello
--- --- --- --- ---
foo bar
", - result); + fun `textToHtml() should not convert dashes mixed with spaces to an HR tag`() { + val text = + """ + hello + --- --- --- --- --- + foo bar + """.trimIndent() + + val result = HtmlConverter.textToHtml(text) + + assertThat(result).isEqualTo( + """ +
+            hello
+ --- --- --- --- ---
+ foo bar +
+ """.trimIndent().removeLineBreaks() + ) } @Test - public void mergeConsecutiveBreaksIntoOne() { - String text = "hello\n------------\n---------------\nfoo bar"; - String result = HtmlConverter.textToHtml(text); - assertEquals("
hello
foo bar
", result); + fun `textToHtml() should merge consecutive horizontal dividers into a single HR tag`() { + val text = + """ + hello + ------------ + --------------- + foo bar + """.trimIndent() + + val result = HtmlConverter.textToHtml(text) + + assertThat(result).isEqualTo( + """ +
+            hello
+            
+ foo bar +
+ """.trimIndent().removeLineBreaks() + ) } @Test - public void dashedHorizontalRulePrefixedWithTextIgnoredAsHR() { - String text = "hello----\n\n"; - String result = HtmlConverter.textToHtml(text); - assertEquals("
hello----

", result); + fun `textToHtml() should not replace dashed horizontal divider prefixed with text`() { + val text = "hello----\n\n" + + val result = HtmlConverter.textToHtml(text) + + assertThat(result).isEqualTo( + """ +
+            hello----
+
+
+ """.trimIndent().removeLineBreaks() + ) } @Test - public void doubleMinusIgnoredAsHR() { - String text = "--\n"; - String result = HtmlConverter.textToHtml(text); - assertEquals("
--
", result); - } + fun `textToHtml() should not replace double dash with an HR tag`() { + val text = "--\n" - @Test - public void doubleEqualsIgnoredAsHR() { - String text = "==\n"; - String result = HtmlConverter.textToHtml(text); - assertEquals("
==
", result); + val result = HtmlConverter.textToHtml(text) + + assertThat(result).isEqualTo("""
--
""") } @Test - public void doubleUnderscoreIgnoredAsHR() { - String text = "__\n"; - String result = HtmlConverter.textToHtml(text); - assertEquals("
__
", result); + fun `textToHtml() should not replace double equal sign with an HR tag`() { + val text = "==\n" + + val result = HtmlConverter.textToHtml(text) + + assertThat(result).isEqualTo("""
==
""") } @Test - public void anyTripletIsHRuledOut() { - String text = "--=\n-=-\n===\n___\n\n"; - String result = HtmlConverter.textToHtml(text); - assertEquals("

", result); + fun `textToHtml() should not replace double underscore with an HR tag`() { + val text = "__\n" + + val result = HtmlConverter.textToHtml(text) + + assertThat(result).isEqualTo("""
__
""") } @Test - public void replaceSpaceSeparatedDashesWithHR() { - String text = "hello\n---------------------------\nfoo bar"; - String result = HtmlConverter.textToHtml(text); - assertEquals("
hello
foo bar
", result); + fun `textToHtml() should replace any combination of three consecutive divider characters with an HR tag`() { + val text = + """ + --= + -=- + === + ___ + + """.trimIndent() + + val result = HtmlConverter.textToHtml(text) + + assertThat(result).isEqualTo("""

""") } @Test - public void replacementWithHRAtBeginning() { - String text = "---------------------------\nfoo bar"; - String result = HtmlConverter.textToHtml(text); - assertEquals("

foo bar
", result); + fun `textToHtml() should replace dashes at the start of the input`() { + val text = "---------------------------\nfoo bar" + + val result = HtmlConverter.textToHtml(text) + + assertThat(result).isEqualTo( + """ +
+            
+ foo bar +
+ """.trimIndent().removeLineBreaks() + ) } @Test - public void replacementWithHRAtEnd() { - String text = "hello\n__________________________________"; - String result = HtmlConverter.textToHtml(text); - assertEquals("
hello
", result); + fun `textToHtml() should replace dashes at the end of the input`() { + val text = "hello\n__________________________________" + + val result = HtmlConverter.textToHtml(text) + + assertThat(result).isEqualTo( + """ +
+            hello
+            
+
+ """.trimIndent().removeLineBreaks() + ) } @Test - public void replacementOfScissorsByHR() { - String text = "hello\n-- %< -------------- >8 --\nworld\n"; - String result = HtmlConverter.textToHtml(text); - assertEquals("
hello
world
", result); + fun `textToHtml() should replace horizontal divider using ASCII scissors with an HR tag`() { + val text = + """ + hello + -- %< -------------- >8 -- + world + """.trimIndent() + + val result = HtmlConverter.textToHtml(text) + + assertThat(result).isEqualTo( + """ +
+            hello
+            
+ world +
+ """.trimIndent().removeLineBreaks() + ) } @Test - public void signatureEndingWithUrl() { - String text = "text\n-- \nsignature with url: https://domain.example/"; - String result = HtmlConverter.textToHtml(text); - assertEquals("
" +
-                "text
" + - "
" + - "--
" + - "signature with url: https://domain.example/" + - "
", result); + fun `textToHtml() should wrap email signature in a DIV`() { + val text = + """ + text + --${" "} + signature with url: https://domain.example/ + """.trimIndent() + + val result = HtmlConverter.textToHtml(text) + + assertThat(result).isEqualTo( + """ +
+            text
+
+ --
+ signature with url: https://domain.example/ +
+
+ """.trimIndent().removeLineBreaks() + ) } @Test - public void htmlToText_withLineBreaks() { - String input = "One
Two

Three"; - - String result = HtmlConverter.htmlToText(input); - - assertEquals("One\nTwo\n\nThree", result); + fun `htmlToText() should convert BR tags to line breaks`() { + val input = + """ + One
+ Two
+
+ Three + """.trimIndent().removeLineBreaks() + + val result = HtmlConverter.htmlToText(input) + + assertThat(result).isEqualTo( + """ + One + Two + + Three + """.trimIndent() + ) } @Test - public void htmlToText_withBlockElements() { - String input = "

One

Two
Three

Four
"; - - String result = HtmlConverter.htmlToText(input); - - assertEquals("One\n\nTwo\nThree\n\nFour", result); + fun `htmlToText() should insert line breaks after block elements`() { + val input = + """ +

One

+

+ Two
+ Three +

+
Four
+ """.trimIndent().removeLineBreaks() + + val result = HtmlConverter.htmlToText(input) + + assertThat(result).isEqualTo( + """ + One + + Two + Three + + Four + """.trimIndent() + ) } @Test - public void htmlToText_withLink() { - String input = "Link text"; + fun `htmlToText() should include link URIs`() { + val input = "Link text" - String result = HtmlConverter.htmlToText(input); + val result = HtmlConverter.htmlToText(input) - assertEquals("Link text ", result); + assertThat(result).isEqualTo("Link text ") } @Test - public void htmlToText_withLinkifiedUrl() { - String input = "Text https://domain.example/path/ more text"; + fun `htmlToText() should not duplicate URI when link URI and text are the same`() { + val input = "Text https://domain.example/path/ more text" - String result = HtmlConverter.htmlToText(input); + val result = HtmlConverter.htmlToText(input) - assertEquals("Text https://domain.example/path/ more text", result); + assertThat(result).isEqualTo("Text https://domain.example/path/ more text") } @Test - public void htmlToText_withLinkifiedUrlContainingFormatting() { - String input = "https://domain.example/path/"; + fun `htmlToText() should not duplicate URI when the link text is just the link URI with some formatting`() { + val input = "https://domain.example/path/" - String result = HtmlConverter.htmlToText(input); + val result = HtmlConverter.htmlToText(input) - assertEquals("https://domain.example/path/", result); + assertThat(result).isEqualTo("https://domain.example/path/") } @Test - public void htmlToText_withLineBreaksInHtml() { - String input = "One\nTwo\r\nThree"; + fun `htmlToText() should strip line breaks`() { + val input = + """ + One + Two + Three + """.trimIndent() - String result = HtmlConverter.htmlToText(input); + val result = HtmlConverter.htmlToText(input) - assertEquals("One Two Three", result); + assertThat(result).isEqualTo("One Two Three") } @Test - public void htmlToText_withLongTextLine_shouldNotAddLineBreaksToOutput() { - String input = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam sit amet finibus felis, " + - "viverra ullamcorper justo. Suspendisse potenti. Etiam erat sem, interdum a condimentum quis, " + - "fringilla quis orci."; + fun `htmlToText() with long text line should not add line breaks to output`() { + val input = + """ + |Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam sit amet finibus felis, + | viverra ullamcorper justo. Suspendisse potenti. Etiam erat sem, interdum a condimentum quis, + | fringilla quis orci. + """.trimMargin().removeLineBreaks() - String result = HtmlConverter.htmlToText(input); + val result = HtmlConverter.htmlToText(input) - assertEquals(input, result); + assertThat(result).isEqualTo(input) } } diff --git a/mail/testing/src/main/java/com/fsck/k9/mail/StringHelper.kt b/mail/testing/src/main/java/com/fsck/k9/mail/StringHelper.kt index 73766b23ee..cada3ad040 100644 --- a/mail/testing/src/main/java/com/fsck/k9/mail/StringHelper.kt +++ b/mail/testing/src/main/java/com/fsck/k9/mail/StringHelper.kt @@ -1,3 +1,5 @@ package com.fsck.k9.mail fun String.crlf() = replace("\n", "\r\n") + +fun String.removeLineBreaks() = replace(Regex("""\r|\n"""), "") -- GitLab From d82b4625654e9bfb3588ba38d921c86b192f7b20 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 2 Aug 2022 19:36:30 +0200 Subject: [PATCH 41/47] IMAP: Fix creating header values from a BODYSTRUCTURE item --- .../k9/mail/internet/MimeParameterEncoder.kt | 4 +- .../fsck/k9/mail/store/imap/RealImapFolder.kt | 8 +- .../k9/mail/store/imap/RealImapFolderTest.kt | 94 ++++++++++++++++++- 3 files changed, 101 insertions(+), 5 deletions(-) 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 b4023de0f4..51e0505ac7 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 @@ -138,14 +138,14 @@ object MimeParameterEncoder { return length } - private fun String.isToken() = when { + fun String.isToken() = when { isEmpty() -> false else -> all { it.isTokenChar() } } private fun String.isQuotable() = all { it.isQuotable() } - private fun String.quoted(): String { + fun String.quoted(): String { // quoted-string = [CFWS] DQUOTE *([FWS] qcontent) [FWS] DQUOTE [CFWS] // qcontent = qtext / quoted-pair // quoted-pair = ("\" (VCHAR / WSP)) 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 9508716b77..c67605f645 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 @@ -15,6 +15,8 @@ import com.fsck.k9.mail.internet.MimeBodyPart 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.MimeUtility import java.io.IOException import java.io.InputStream @@ -889,7 +891,8 @@ internal class RealImapFolder( for (i in bodyParams.indices step 2) { val paramName = bodyParams.getString(i) val paramValue = bodyParams.getString(i + 1) - contentType.append(String.format(";\r\n %s=\"%s\"", paramName, paramValue)) + val encodedValue = if (paramValue.isToken()) paramValue else paramValue.quoted() + contentType.append(String.format(";\r\n %s=%s", paramName, encodedValue)) } } @@ -915,7 +918,8 @@ internal class RealImapFolder( for (i in bodyDispositionParams.indices step 2) { val paramName = bodyDispositionParams.getString(i).lowercase() val paramValue = bodyDispositionParams.getString(i + 1) - contentDisposition.append(String.format(";\r\n %s=\"%s\"", paramName, paramValue)) + val encodedValue = if (paramValue.isToken()) paramValue else paramValue.quoted() + 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 1fea9ae2ca..5b2251fc57 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 @@ -731,7 +731,83 @@ class RealImapFolderTest { folder.fetch(messages, fetchProfile, null, MAX_DOWNLOAD_SIZE) - verify(messages[0]).setHeader(MimeHeader.HEADER_CONTENT_TYPE, "text/plain;\r\n CHARSET=\"US-ASCII\"") + verify(messages[0]).setHeader(MimeHeader.HEADER_CONTENT_TYPE, "text/plain;\r\n CHARSET=US-ASCII") + } + + @Test + fun `fetch() with simple content type parameter`() { + testHeaderFromBodyStructure( + bodyStructure = """("text" "plain" ("name" "token") NIL NIL "7bit" 42 23)""", + headerName = MimeHeader.HEADER_CONTENT_TYPE, + expectedHeaderValue = "text/plain;\r\n name=token" + ) + } + + @Test + fun `fetch() with content type parameter that needs to be a quoted string`() { + testHeaderFromBodyStructure( + bodyStructure = """("text" "plain" ("name" "one two three") NIL NIL "7bit" 42 23)""", + headerName = MimeHeader.HEADER_CONTENT_TYPE, + expectedHeaderValue = "text/plain;\r\n name=\"one two three\"" + ) + } + + @Test + fun `fetch() with content type parameter that needs to be a quoted string with escaped characters`() { + testHeaderFromBodyStructure( + bodyStructure = """("text" "plain" ("name" "one \"two\" three") NIL NIL "7bit" 42 23)""", + headerName = MimeHeader.HEADER_CONTENT_TYPE, + expectedHeaderValue = "text/plain;\r\n name=\"one \\\"two\\\" three\"" + ) + } + + @Test + fun `fetch() with RFC 2231 encoded content type parameter`() { + testHeaderFromBodyStructure( + bodyStructure = """("text" "plain" ("name*" "utf-8''filen%C3%A4me.ext") NIL NIL "7bit" 42 23)""", + headerName = MimeHeader.HEADER_CONTENT_TYPE, + expectedHeaderValue = "text/plain;\r\n name*=utf-8''filen%C3%A4me.ext" + ) + } + + @Test + fun `fetch() with simple content disposition parameter`() { + testHeaderFromBodyStructure( + bodyStructure = """("application" "octet-stream" NIL NIL NIL "8bit" 23 NIL """ + + """("attachment" ("filename" "token")) NIL NIL)""", + headerName = MimeHeader.HEADER_CONTENT_DISPOSITION, + expectedHeaderValue = "attachment;\r\n filename=token;\r\n size=23" + ) + } + + @Test + fun `fetch() with content disposition parameter that needs to be a quoted string`() { + testHeaderFromBodyStructure( + bodyStructure = """("application" "octet-stream" NIL NIL NIL "8bit" 23 NIL """ + + """("attachment" ("filename" "one two three")) NIL NIL)""", + headerName = MimeHeader.HEADER_CONTENT_DISPOSITION, + expectedHeaderValue = "attachment;\r\n filename=\"one two three\";\r\n size=23" + ) + } + + @Test + fun `fetch() with content disposition parameter that needs to be a quoted string with escaped characters`() { + testHeaderFromBodyStructure( + bodyStructure = """("application" "octet-stream" NIL NIL NIL "8bit" 23 NIL """ + + """("attachment" ("filename" "one \"two\" three")) NIL NIL)""", + headerName = MimeHeader.HEADER_CONTENT_DISPOSITION, + expectedHeaderValue = "attachment;\r\n filename=\"one \\\"two\\\" three\";\r\n size=23" + ) + } + + @Test + fun `fetch() with RFC 2231 encoded content disposition parameter`() { + testHeaderFromBodyStructure( + bodyStructure = """("application" "octet-stream" NIL NIL NIL "8bit" 23 NIL """ + + """("attachment" ("filename*" "utf-8''filen%C3%A4me.ext")) NIL NIL)""", + headerName = MimeHeader.HEADER_CONTENT_DISPOSITION, + expectedHeaderValue = "attachment;\r\n filename*=utf-8''filen%C3%A4me.ext;\r\n size=23" + ) } @Test @@ -1118,6 +1194,22 @@ class RealImapFolderTest { .thenReturn(imapResponses) } + private fun testHeaderFromBodyStructure(bodyStructure: String, headerName: String, expectedHeaderValue: String) { + val folder = createFolder("Folder") + prepareImapFolderForOpen(OpenMode.READ_ONLY) + folder.open(OpenMode.READ_ONLY) + whenever(imapConnection.readResponse(anyOrNull())) + .thenReturn(createImapResponse("* 1 FETCH (BODYSTRUCTURE $bodyStructure UID 1)")) + .thenReturn(createImapResponse("x OK")) + val imapMessage = ImapMessage("1") + val messages = listOf(imapMessage) + val fetchProfile = createFetchProfile(FetchProfile.Item.STRUCTURE) + + folder.fetch(messages, fetchProfile, null, MAX_DOWNLOAD_SIZE) + + assertThat(imapMessage.getHeader(headerName)).asList().containsExactly(expectedHeaderValue) + } + companion object { private const val MAX_DOWNLOAD_SIZE = -1 } -- GitLab From 16ae99ac09f3ede34f9834ee292093e2f151a8bb Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 3 Aug 2022 17:30:07 +0200 Subject: [PATCH 42/47] Fix initialization problems with MessageListFragment and MessageViewFragment --- .../legacy/src/main/java/com/fsck/k9/activity/MessageList.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 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 6985863dd3..fbbe2ffe22 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 @@ -24,6 +24,7 @@ import androidx.drawerlayout.widget.DrawerLayout.DrawerListener import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.commit +import androidx.fragment.app.commitNow import androidx.lifecycle.Observer import com.fsck.k9.Account import com.fsck.k9.K9 @@ -270,7 +271,7 @@ open class MessageList : search!!, false, K9.isThreadedViewEnabled && !noThreading ) fragmentTransaction.add(R.id.message_list_container, messageListFragment) - fragmentTransaction.commit() + fragmentTransaction.commitNow() this.messageListFragment = messageListFragment } @@ -979,7 +980,7 @@ open class MessageList : } val fragment = MessageViewFragment.newInstance(messageReference) - supportFragmentManager.commit { + supportFragmentManager.commitNow { replace(R.id.message_view_container, fragment, FRAGMENT_TAG_MESSAGE_VIEW) } -- GitLab From de6d4197f244a75064f4182504f7fa5c4528d1bd Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 22 Jul 2022 14:56:25 +0200 Subject: [PATCH 43/47] Add basic support for swiping between messages --- .../java/com/fsck/k9/activity/MessageList.kt | 179 +++++----- .../fsck/k9/fragment/MessageListFragment.kt | 34 +- .../com/fsck/k9/ui/messageview/Direction.kt | 6 + .../MessageViewContainerFragment.kt | 311 ++++++++++++++++++ .../k9/ui/messageview/MessageViewFragment.kt | 13 - .../res/layout/message_view_container.xml | 5 + 6 files changed, 415 insertions(+), 133 deletions(-) create mode 100644 app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/Direction.kt create mode 100644 app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewContainerFragment.kt create mode 100644 app/ui/legacy/src/main/res/layout/message_view_container.xml 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 fbbe2ffe22..b9a66cd2cf 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 @@ -54,7 +54,9 @@ import com.fsck.k9.ui.changelog.RecentChangesActivity import com.fsck.k9.ui.changelog.RecentChangesViewModel import com.fsck.k9.ui.managefolders.ManageFoldersActivity import com.fsck.k9.ui.messagelist.DefaultFolderProvider -import com.fsck.k9.ui.messageview.MessageViewFragment +import com.fsck.k9.ui.messageview.Direction +import com.fsck.k9.ui.messageview.MessageViewContainerFragment +import com.fsck.k9.ui.messageview.MessageViewContainerFragment.MessageViewContainerListener import com.fsck.k9.ui.messageview.MessageViewFragment.MessageViewFragmentListener import com.fsck.k9.ui.messageview.PlaceholderFragment import com.fsck.k9.ui.onboarding.OnboardingActivity @@ -80,6 +82,7 @@ open class MessageList : K9Activity(), MessageListFragmentListener, MessageViewFragmentListener, + MessageViewContainerListener, FragmentManager.OnBackStackChangedListener, OnSwitchCompleteListener, PermissionUiHelper { @@ -102,11 +105,16 @@ open class MessageList : private var progressBar: ProgressBar? = null private var messageViewPlaceHolder: PlaceholderFragment? = null private var messageListFragment: MessageListFragment? = null - private var messageViewFragment: MessageViewFragment? = null + private var messageViewContainerFragment: MessageViewContainerFragment? = null private var account: Account? = null private var search: LocalSearch? = null private var singleFolderMode = false - private var lastDirection = if (K9.isMessageViewShowNext) NEXT else PREVIOUS + + private val lastDirection: Direction + get() { + return messageViewContainerFragment?.lastDirection + ?: if (K9.isMessageViewShowNext) Direction.NEXT else Direction.PREVIOUS + } private var messageListActivityAppearance: MessageListActivityAppearance? = null @@ -225,7 +233,7 @@ open class MessageList : supportFragmentManager.popBackStackImmediate(FIRST_FRAGMENT_TRANSACTION, FragmentManager.POP_BACK_STACK_INCLUSIVE) removeMessageListFragment() - removeMessageViewFragment() + removeMessageViewContainerFragment() messageReference = null search = null @@ -253,9 +261,11 @@ open class MessageList : private fun findFragments() { val fragmentManager = supportFragmentManager messageListFragment = fragmentManager.findFragmentById(R.id.message_list_container) as MessageListFragment? - messageViewFragment = fragmentManager.findFragmentByTag(FRAGMENT_TAG_MESSAGE_VIEW) as MessageViewFragment? + messageViewContainerFragment = + fragmentManager.findFragmentByTag(FRAGMENT_TAG_MESSAGE_VIEW_CONTAINER) as MessageViewContainerFragment? messageListFragment?.let { messageListFragment -> + messageViewContainerFragment?.setViewModel(messageListFragment.viewModel) initializeFromLocalSearch(messageListFragment.localSearch) } } @@ -278,7 +288,7 @@ open class MessageList : // Check if the fragment wasn't restarted and has a MessageReference in the arguments. // If so, open the referenced message. - if (!hasMessageListFragment && messageViewFragment == null && messageReference != null) { + if (!hasMessageListFragment && messageViewContainerFragment == null && messageReference != null) { openMessage(messageReference!!) } } @@ -304,7 +314,7 @@ open class MessageList : } } - displayMode = if (messageViewFragment != null || messageReference != null) { + displayMode = if (messageViewContainerFragment != null || messageReference != null) { DisplayMode.MESSAGE_VIEW } else { DisplayMode.MESSAGE_LIST @@ -337,12 +347,12 @@ open class MessageList : messageListWasDisplayed = true messageListFragment.isActive = true - messageViewFragment.let { messageViewFragment -> - if (messageViewFragment == null) { + messageViewContainerFragment.let { messageViewContainerFragment -> + if (messageViewContainerFragment == null) { showMessageViewPlaceHolder() } else { - messageViewFragment.isActive = true - val activeMessage = messageViewFragment.messageReference + messageViewContainerFragment.isActive = true + val activeMessage = messageViewContainerFragment.messageReference messageListFragment.setActiveMessage(activeMessage) } } @@ -607,7 +617,7 @@ open class MessageList : fun openFolder(folderId: Long) { if (displayMode == DisplayMode.SPLIT_VIEW) { - removeMessageViewFragment() + removeMessageViewContainerFragment() showMessageViewPlaceHolder() } @@ -736,7 +746,7 @@ open class MessageList : when (event.keyCode) { KeyEvent.KEYCODE_VOLUME_UP -> { - if (messageViewFragment != null && displayMode != DisplayMode.MESSAGE_LIST && + if (messageViewContainerFragment != null && displayMode != DisplayMode.MESSAGE_LIST && K9.isUseVolumeKeysForNavigation ) { showPreviousMessage() @@ -747,7 +757,7 @@ open class MessageList : } } KeyEvent.KEYCODE_VOLUME_DOWN -> { - if (messageViewFragment != null && displayMode != DisplayMode.MESSAGE_LIST && + if (messageViewContainerFragment != null && displayMode != DisplayMode.MESSAGE_LIST && K9.isUseVolumeKeysForNavigation ) { showNextMessage() @@ -762,14 +772,14 @@ open class MessageList : return true } KeyEvent.KEYCODE_DPAD_LEFT -> { - return if (messageViewFragment != null && displayMode == DisplayMode.MESSAGE_VIEW) { + return if (messageViewContainerFragment != null && displayMode == DisplayMode.MESSAGE_VIEW) { showPreviousMessage() } else { false } } KeyEvent.KEYCODE_DPAD_RIGHT -> { - return if (messageViewFragment != null && displayMode == DisplayMode.MESSAGE_VIEW) { + return if (messageViewContainerFragment != null && displayMode == DisplayMode.MESSAGE_VIEW) { showNextMessage() } else { false @@ -801,69 +811,69 @@ open class MessageList : 'g' -> { if (displayMode == DisplayMode.MESSAGE_LIST) { messageListFragment!!.onToggleFlagged() - } else if (messageViewFragment != null) { - messageViewFragment!!.onToggleFlagged() + } else if (messageViewContainerFragment != null) { + messageViewContainerFragment!!.onToggleFlagged() } return true } 'm' -> { if (displayMode == DisplayMode.MESSAGE_LIST) { messageListFragment!!.onMove() - } else if (messageViewFragment != null) { - messageViewFragment!!.onMove() + } else if (messageViewContainerFragment != null) { + messageViewContainerFragment!!.onMove() } return true } 'v' -> { if (displayMode == DisplayMode.MESSAGE_LIST) { messageListFragment!!.onArchive() - } else if (messageViewFragment != null) { - messageViewFragment!!.onArchive() + } else if (messageViewContainerFragment != null) { + messageViewContainerFragment!!.onArchive() } return true } 'y' -> { if (displayMode == DisplayMode.MESSAGE_LIST) { messageListFragment!!.onCopy() - } else if (messageViewFragment != null) { - messageViewFragment!!.onCopy() + } else if (messageViewContainerFragment != null) { + messageViewContainerFragment!!.onCopy() } return true } 'z' -> { if (displayMode == DisplayMode.MESSAGE_LIST) { messageListFragment!!.onToggleRead() - } else if (messageViewFragment != null) { - messageViewFragment!!.onToggleRead() + } else if (messageViewContainerFragment != null) { + messageViewContainerFragment!!.onToggleRead() } return true } 'f' -> { - if (messageViewFragment != null) { - messageViewFragment!!.onForward() + if (messageViewContainerFragment != null) { + messageViewContainerFragment!!.onForward() } return true } 'a' -> { - if (messageViewFragment != null) { - messageViewFragment!!.onReplyAll() + if (messageViewContainerFragment != null) { + messageViewContainerFragment!!.onReplyAll() } return true } 'r' -> { - if (messageViewFragment != null) { - messageViewFragment!!.onReply() + if (messageViewContainerFragment != null) { + messageViewContainerFragment!!.onReply() } return true } 'j', 'p' -> { - if (messageViewFragment != null) { + if (messageViewContainerFragment != null) { showPreviousMessage() } return true } 'n', 'k' -> { - if (messageViewFragment != null) { + if (messageViewContainerFragment != null) { showNextMessage() } return true @@ -885,8 +895,8 @@ open class MessageList : private fun onDeleteHotKey() { if (displayMode == DisplayMode.MESSAGE_LIST) { messageListFragment!!.onDelete() - } else if (messageViewFragment != null) { - messageViewFragment!!.onDelete() + } else if (messageViewContainerFragment != null) { + messageViewContainerFragment!!.onDelete() } } @@ -979,12 +989,16 @@ open class MessageList : messageListFragment!!.setActiveMessage(messageReference) } - val fragment = MessageViewFragment.newInstance(messageReference) + val fragment = MessageViewContainerFragment.newInstance(messageReference) supportFragmentManager.commitNow { - replace(R.id.message_view_container, fragment, FRAGMENT_TAG_MESSAGE_VIEW) + replace(R.id.message_view_container, fragment, FRAGMENT_TAG_MESSAGE_VIEW_CONTAINER) } - messageViewFragment = fragment + messageViewContainerFragment = fragment + + messageListFragment?.let { messageListFragment -> + fragment.setViewModel(messageListFragment.viewModel) + } if (displayMode == DisplayMode.SPLIT_VIEW) { fragment.isActive = true @@ -1089,7 +1103,7 @@ open class MessageList : } private fun showMessageViewPlaceHolder() { - removeMessageViewFragment() + removeMessageViewContainerFragment() // Add placeholder fragment if necessary val fragmentManager = supportFragmentManager @@ -1102,11 +1116,11 @@ open class MessageList : messageListFragment!!.setActiveMessage(null) } - private fun removeMessageViewFragment() { - if (messageViewFragment != null) { + private fun removeMessageViewContainerFragment() { + if (messageViewContainerFragment != null) { val fragmentTransaction = supportFragmentManager.beginTransaction() - fragmentTransaction.remove(messageViewFragment!!) - messageViewFragment = null + fragmentTransaction.remove(messageViewContainerFragment!!) + messageViewContainerFragment = null fragmentTransaction.commit() showDefaultTitleView() @@ -1129,29 +1143,35 @@ open class MessageList : } } + override fun closeMessageView() { + returnToMessageList() + } + + override fun setActiveMessage(messageReference: MessageReference) { + val messageListFragment = checkNotNull(messageListFragment) + + messageListFragment.setActiveMessage(messageReference) + } + override fun showNextMessageOrReturn() { if (K9.isMessageViewReturnToList || !showLogicalNextMessage()) { - if (displayMode == DisplayMode.SPLIT_VIEW) { - showMessageViewPlaceHolder() - } else { - showMessageList() - } + returnToMessageList() } } - private fun showLogicalNextMessage(): Boolean { - var result = false - if (lastDirection == NEXT) { - result = showNextMessage() - } else if (lastDirection == PREVIOUS) { - result = showPreviousMessage() + private fun returnToMessageList() { + if (displayMode == DisplayMode.SPLIT_VIEW) { + showMessageViewPlaceHolder() + } else { + showMessageList() } + } - if (!result) { - result = showNextMessage() || showPreviousMessage() + private fun showLogicalNextMessage(): Boolean { + return when (lastDirection) { + Direction.NEXT -> showNextMessage() + Direction.PREVIOUS -> showPreviousMessage() } - - return result } override fun setProgress(enable: Boolean) { @@ -1159,25 +1179,15 @@ open class MessageList : } private fun showNextMessage(): Boolean { - val ref = messageViewFragment!!.messageReference - if (ref != null) { - if (messageListFragment!!.openNext(ref)) { - lastDirection = NEXT - return true - } - } - return false + val messageViewContainerFragment = checkNotNull(messageViewContainerFragment) + + return messageViewContainerFragment.showNextMessage() } private fun showPreviousMessage(): Boolean { - val ref = messageViewFragment!!.messageReference - if (ref != null) { - if (messageListFragment!!.openPrevious(ref)) { - lastDirection = PREVIOUS - return true - } - } - return false + val messageViewContainerFragment = checkNotNull(messageViewContainerFragment) + + return messageViewContainerFragment.showPreviousMessage() } private fun showMessageList() { @@ -1186,7 +1196,7 @@ open class MessageList : displayMode = DisplayMode.MESSAGE_LIST viewSwitcher!!.showFirstView() - messageViewFragment?.isActive = false + messageViewContainerFragment?.isActive = false messageListFragment!!.isActive = true messageListFragment!!.setActiveMessage(null) @@ -1208,11 +1218,11 @@ open class MessageList : } private fun showMessageView() { - val messageViewFragment = checkNotNull(this.messageViewFragment) + val messageViewContainerFragment = checkNotNull(this.messageViewContainerFragment) displayMode = DisplayMode.MESSAGE_VIEW messageListFragment?.isActive = false - messageViewFragment.isActive = true + messageViewContainerFragment.isActive = true if (!messageListWasDisplayed) { viewSwitcher!!.animateFirstView = false @@ -1238,7 +1248,7 @@ open class MessageList : override fun onSwitchComplete(displayedChild: Int) { if (displayedChild == 0) { - removeMessageViewFragment() + removeMessageViewContainerFragment() } } @@ -1276,8 +1286,8 @@ open class MessageList : if (requestCode and REQUEST_FLAG_PENDING_INTENT != 0) { val originalRequestCode = requestCode xor REQUEST_FLAG_PENDING_INTENT - if (messageViewFragment != null) { - messageViewFragment!!.onPendingIntentResult(originalRequestCode, resultCode, data) + if (messageViewContainerFragment != null) { + messageViewContainerFragment!!.onPendingIntentResult(originalRequestCode, resultCode, data) } } } @@ -1387,16 +1397,11 @@ 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_FIRST_BACK_STACK_ID = "firstBackstackId" private const val FIRST_FRAGMENT_TRANSACTION = "first" - private const val FRAGMENT_TAG_MESSAGE_VIEW = "MessageViewFragment" + private const val FRAGMENT_TAG_MESSAGE_VIEW_CONTAINER = "MessageViewContainerFragment" private const val FRAGMENT_TAG_PLACEHOLDER = "MessageViewPlaceholder" - // Used for navigating to next/previous message - private const val PREVIOUS = 1 - private const val NEXT = 2 - private const val REQUEST_CODE_MASK = 0xFFFF0000.toInt() private const val REQUEST_FLAG_PENDING_INTENT = 1 shl 15 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 0d2701333b..5e61517c89 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 @@ -66,7 +66,7 @@ class MessageListFragment : ConfirmationDialogFragmentListener, MessageListItemActionListener { - private val viewModel: MessageListViewModel by viewModel() + val viewModel: MessageListViewModel by viewModel() private val sortTypeToastProvider: SortTypeToastProvider by inject() private val folderNameFormatterFactory: FolderNameFormatterFactory by inject() private val folderNameFormatter: FolderNameFormatter by lazy { folderNameFormatterFactory.create(requireContext()) } @@ -1299,30 +1299,6 @@ class MessageListFragment : } } - fun openPrevious(messageReference: MessageReference): Boolean { - val position = getPosition(messageReference) - if (position <= 0) return false - - openMessageAtPosition(position - 1) - return true - } - - fun openNext(messageReference: MessageReference): Boolean { - val position = getPosition(messageReference) - if (position < 0 || position == adapter.count - 1) return false - - openMessageAtPosition(position + 1) - return true - } - - fun isFirst(messageReference: MessageReference): Boolean { - return adapter.isEmpty || messageReference == getReferenceForPosition(0) - } - - fun isLast(messageReference: MessageReference): Boolean { - return adapter.isEmpty || messageReference == getReferenceForPosition(adapter.count - 1) - } - private fun getReferenceForPosition(position: Int): MessageReference { val item = adapter.getItem(position) return MessageReference(item.account.uuid, item.folderId, item.messageUid) @@ -1349,14 +1325,6 @@ class MessageListFragment : fragmentListener.openMessage(messageReference) } - private fun getPosition(messageReference: MessageReference): Int { - return adapter.messages.indexOfFirst { messageListItem -> - messageListItem.account.uuid == messageReference.accountUuid && - messageListItem.folderId == messageReference.folderId && - messageListItem.messageUid == messageReference.uid - } - } - fun onReverseSort() { changeSort(sortType) } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/Direction.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/Direction.kt new file mode 100644 index 0000000000..62431d3871 --- /dev/null +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/Direction.kt @@ -0,0 +1,6 @@ +package com.fsck.k9.ui.messageview + +enum class Direction { + PREVIOUS, + NEXT +} 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 new file mode 100644 index 0000000000..6c4e79a388 --- /dev/null +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewContainerFragment.kt @@ -0,0 +1,311 @@ +package com.fsck.k9.ui.messageview + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +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.ViewPager2 +import com.fsck.k9.controller.MessageReference +import com.fsck.k9.ui.R +import com.fsck.k9.ui.messagelist.MessageListItem +import com.fsck.k9.ui.messagelist.MessageListViewModel +import com.fsck.k9.ui.withArguments + +/** + * A fragment that uses [ViewPager2] to allow the user to swipe between messages. + * + * Individual messages are displayed using a [MessageViewFragment]. + */ +class MessageViewContainerFragment : Fragment() { + var isActive: Boolean = false + set(value) { + field = value + setMenuVisibility(value) + } + + lateinit var messageReference: MessageReference + private set + + var lastDirection: Direction? = null + private set + + private lateinit var fragmentListener: MessageViewContainerListener + private lateinit var viewPager: ViewPager2 + private lateinit var adapter: MessageViewContainerAdapter + + private var currentPosition: Int? = null + + private val messageViewFragment: MessageViewFragment + get() { + check(isResumed) + val itemId = adapter.getItemId(messageReference) + + // ViewPager2/FragmentStateAdapter don't provide an easy way to get hold of the Fragment for the active + // page. So we're using an implementation detail (the fragment tag) to find the fragment. + return childFragmentManager.findFragmentByTag("f$itemId") as MessageViewFragment + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setHasOptionsMenu(true) + + if (savedInstanceState == null) { + messageReference = MessageReference.parse(arguments?.getString(ARG_REFERENCE)) + ?: error("Missing argument $ARG_REFERENCE") + } else { + messageReference = MessageReference.parse(savedInstanceState.getString(STATE_MESSAGE_REFERENCE)) + ?: error("Missing state $STATE_MESSAGE_REFERENCE") + + lastDirection = savedInstanceState.getSerializable(STATE_LAST_DIRECTION) as Direction? + } + + adapter = MessageViewContainerAdapter(this) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + + fragmentListener = try { + context as MessageViewContainerListener + } catch (e: ClassCastException) { + throw ClassCastException("This fragment must be attached to a MessageViewContainerListener") + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.message_view_container, container, false) + + viewPager = view.findViewById(R.id.message_viewpager) + viewPager.isUserInputEnabled = true + viewPager.offscreenPageLimit = ViewPager2.OFFSCREEN_PAGE_LIMIT_DEFAULT + viewPager.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.HORIZONTAL)) + 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. + override fun onPageScrollStateChanged(state: Int) { + if (state == ViewPager2.SCROLL_STATE_IDLE) { + setActiveMessage(viewPager.currentItem) + } + } + + override fun onPageSelected(position: Int) { + if (viewPager.scrollState == ViewPager2.SCROLL_STATE_IDLE) { + setActiveMessage(position) + } + } + }) + + return view + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putString(STATE_MESSAGE_REFERENCE, messageReference.toIdentityString()) + outState.putSerializable(STATE_LAST_DIRECTION, lastDirection) + } + + fun setViewModel(viewModel: MessageListViewModel) { + viewModel.getMessageListLiveData().observe(this) { messageListInfo -> + updateMessageList(messageListInfo.messageListItems) + } + } + + private fun updateMessageList(messageListItems: List) { + if (messageListItems.isEmpty() || messageListItems.none { it.messageReference == messageReference }) { + fragmentListener.closeMessageView() + return + } + + adapter.messageList = messageListItems + + // We only set the adapter on ViewPager2 after the message list has been loaded. This way ViewPager2 can + // restore its saved state after a configuration change. + if (viewPager.adapter == null) { + viewPager.adapter = adapter + } + + val position = adapter.getPosition(messageReference) + viewPager.setCurrentItem(position, false) + } + + private fun setActiveMessage(position: Int) { + rememberNavigationDirection(position, messageReference) + + messageReference = adapter.getMessageReference(position) + + fragmentListener.setActiveMessage(messageReference) + } + + private fun rememberNavigationDirection(newPosition: Int, currentMessageReference: MessageReference) { + // When messages are added or removed from the list, the current position will change even though we're still + // displaying the same message. In those cases we don't want to update `lastDirection`. + val newMessageReference = adapter.getMessageReference(newPosition) + if (newMessageReference == currentMessageReference) { + currentPosition = newPosition + return + } + + currentPosition?.let { currentPosition -> + lastDirection = if (newPosition < currentPosition) Direction.PREVIOUS else Direction.NEXT + } + currentPosition = newPosition + } + + fun showPreviousMessage(): Boolean { + val newPosition = viewPager.currentItem - 1 + return if (newPosition >= 0) { + setActiveMessage(newPosition) + + val smoothScroll = true + viewPager.setCurrentItem(newPosition, smoothScroll) + true + } else { + false + } + } + + fun showNextMessage(): Boolean { + val newPosition = viewPager.currentItem + 1 + return if (newPosition < adapter.itemCount) { + setActiveMessage(newPosition) + + val smoothScroll = true + viewPager.setCurrentItem(newPosition, smoothScroll) + true + } else { + false + } + } + + fun onToggleFlagged() { + messageViewFragment.onToggleFlagged() + } + + fun onMove() { + messageViewFragment.onMove() + } + + fun onArchive() { + messageViewFragment.onArchive() + } + + fun onCopy() { + messageViewFragment.onCopy() + } + + fun onToggleRead() { + messageViewFragment.onToggleRead() + } + + fun onForward() { + messageViewFragment.onForward() + } + + fun onReplyAll() { + messageViewFragment.onReplyAll() + } + + fun onReply() { + messageViewFragment.onReply() + } + + fun onDelete() { + messageViewFragment.onDelete() + } + + fun onPendingIntentResult(requestCode: Int, resultCode: Int, data: Intent?) { + messageViewFragment.onPendingIntentResult(requestCode, resultCode, data) + } + + private class MessageViewContainerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) { + var messageList: List = emptyList() + set(value) { + val diffResult = DiffUtil.calculateDiff( + MessageListDiffCallback(oldMessageList = messageList, newMessageList = value) + ) + + field = value + + diffResult.dispatchUpdatesTo(this) + } + + override fun getItemCount(): Int { + return messageList.size + } + + override fun getItemId(position: Int): Long { + return messageList[position].uniqueId + } + + override fun containsItem(itemId: Long): Boolean { + return messageList.any { it.uniqueId == itemId } + } + + override fun createFragment(position: Int): Fragment { + check(position in messageList.indices) + + val messageReference = messageList[position].messageReference + return MessageViewFragment.newInstance(messageReference) + } + + fun getMessageReference(position: Int): MessageReference { + check(position in messageList.indices) + + return messageList[position].messageReference + } + + fun getPosition(messageReference: MessageReference): Int { + return messageList.indexOfFirst { it.messageReference == messageReference } + } + + fun getItemId(messageReference: MessageReference): Long { + return messageList.first { it.messageReference == messageReference }.uniqueId + } + } + + private class MessageListDiffCallback( + private val oldMessageList: List, + private val newMessageList: List + ) : DiffUtil.Callback() { + override fun getOldListSize(): Int = oldMessageList.size + + override fun getNewListSize(): Int = newMessageList.size + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return oldMessageList[oldItemPosition].uniqueId == newMessageList[newItemPosition].uniqueId + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + // Let MessageViewFragment deal with content changes + return areItemsTheSame(oldItemPosition, newItemPosition) + } + } + + interface MessageViewContainerListener { + fun closeMessageView() + fun setActiveMessage(messageReference: MessageReference) + } + + companion object { + private const val ARG_REFERENCE = "reference" + + private const val STATE_MESSAGE_REFERENCE = "messageReference" + private const val STATE_LAST_DIRECTION = "lastDirection" + + fun newInstance(reference: MessageReference): MessageViewContainerFragment { + return MessageViewContainerFragment().withArguments( + ARG_REFERENCE to reference.toIdentityString() + ) + } + } +} + +private val MessageListItem.messageReference: MessageReference + get() = MessageReference(account.uuid, folderId, messageUid) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt index d3045ab25b..5fd7f706b6 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt @@ -90,17 +90,6 @@ class MessageViewFragment : private var currentAttachmentViewInfo: AttachmentViewInfo? = null private var isDeleteMenuItemDisabled: Boolean = false - /** - * Set this to `true` when the fragment should be considered active. When active, the fragment adds its actions to - * the toolbar. When inactive, the fragment won't add its actions to the toolbar, even it is still visible, e.g. as - * part of an animation. - */ - var isActive: Boolean = false - set(value) { - field = value - invalidateMenu() - } - override fun onAttach(context: Context) { super.onAttach(context) @@ -191,8 +180,6 @@ class MessageViewFragment : } override fun onPrepareOptionsMenu(menu: Menu) { - if (!isActive) return - menu.findItem(R.id.delete).apply { isVisible = K9.isMessageViewDeleteActionVisible isEnabled = !isDeleteMenuItemDisabled diff --git a/app/ui/legacy/src/main/res/layout/message_view_container.xml b/app/ui/legacy/src/main/res/layout/message_view_container.xml new file mode 100644 index 0000000000..e8b24d6cb9 --- /dev/null +++ b/app/ui/legacy/src/main/res/layout/message_view_container.xml @@ -0,0 +1,5 @@ + + -- GitLab From 3e90e92b2e1a2362f7a7a667e5ac2bf1b94da6d5 Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 25 Jul 2022 18:40:18 +0200 Subject: [PATCH 44/47] Only mark a message as read when it is the active message Previously we marked a message as read when loading it for viewing. But with swiping between messages we could now load a message before it is considered the active message. --- .../k9/controller/MessagingController.java | 25 +++++++++---- .../k9/ui/messageview/MessageViewFragment.kt | 37 +++++++++++++++++++ 2 files changed, 54 insertions(+), 8 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 5165f2b52c..409dedc6dd 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 @@ -1309,9 +1309,6 @@ public class MessagingController { fp.add(FetchProfile.Item.BODY); localFolder.fetch(Collections.singletonList(message), fp, null); - notificationController.removeNewMailNotification(account, message.makeMessageReference()); - markMessageAsOpened(account, message); - return message; } @@ -1333,7 +1330,15 @@ public class MessagingController { return message; } - private void markMessageAsOpened(Account account, LocalMessage message) throws MessagingException { + public void markMessageAsOpened(Account account, LocalMessage message) { + put("markMessageAsOpened", null, () -> { + markMessageAsOpenedBlocking(account, message); + }); + } + + private void markMessageAsOpenedBlocking(Account account, LocalMessage message) { + notificationController.removeNewMailNotification(account,message.makeMessageReference()); + if (!message.isSet(Flag.SEEN)) { if (account.isMarkMessageAsReadOnView()) { markMessageAsReadOnView(account, message); @@ -1345,11 +1350,15 @@ public class MessagingController { } } - private void markMessageAsReadOnView(Account account, LocalMessage message) throws MessagingException { - List messageIds = Collections.singletonList(message.getDatabaseId()); - setFlag(account, messageIds, Flag.SEEN, true); + private void markMessageAsReadOnView(Account account, LocalMessage message) { + try { + List messageIds = Collections.singletonList(message.getDatabaseId()); + setFlag(account, messageIds, Flag.SEEN, true); - message.setFlagInternal(Flag.SEEN, true); + message.setFlagInternal(Flag.SEEN, true); + } catch (MessagingException e) { + Timber.e(e, "Error while marking message as read"); + } } private void markMessageAsNotNew(Account account, LocalMessage message) { diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt index 5fd7f706b6..9eec05e6f5 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt @@ -89,6 +89,7 @@ class MessageViewFragment : private var currentAttachmentViewInfo: AttachmentViewInfo? = null private var isDeleteMenuItemDisabled: Boolean = false + private var wasMessageMarkedAsOpened: Boolean = false override fun onAttach(context: Context) { super.onAttach(context) @@ -108,6 +109,10 @@ class MessageViewFragment : messageReference = MessageReference.parse(arguments?.getString(ARG_REFERENCE)) ?: error("Invalid argument '$ARG_REFERENCE'") + if (savedInstanceState != null) { + wasMessageMarkedAsOpened = savedInstanceState.getBoolean(STATE_WAS_MESSAGE_MARKED_AS_OPENED) + } + messageCryptoPresenter = MessageCryptoPresenter(messageCryptoMvpView) messageLoaderHelper = messageLoaderHelperFactory.createForMessageView( context = requireContext().applicationContext, @@ -164,8 +169,24 @@ class MessageViewFragment : invalidateMenu() } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putBoolean(STATE_WAS_MESSAGE_MARKED_AS_OPENED, wasMessageMarkedAsOpened) + } + + override fun setMenuVisibility(menuVisible: Boolean) { + super.setMenuVisibility(menuVisible) + + // When the menu is hidden, the message associated with this fragment is no longer active. If the user returns + // to it, we want to mark the message as opened again. + if (!menuVisible) { + wasMessageMarkedAsOpened = false + } + } + override fun onResume() { super.onResume() + markMessageAsOpened() messageCryptoPresenter.onResume() } @@ -737,6 +758,15 @@ class MessageViewFragment : messageTopView.refreshAttachmentThumbnail(attachment) } + private fun markMessageAsOpened() { + val message = message ?: return + + if (!wasMessageMarkedAsOpened) { + messagingController.markMessageAsOpened(account, message) + wasMessageMarkedAsOpened = true + } + } + private val messageCryptoMvpView: MessageCryptoMvpView = object : MessageCryptoMvpView { override fun redisplayMessage() { messageLoaderHelper.asyncReloadMessage() @@ -807,6 +837,11 @@ class MessageViewFragment : displayHeaderForLoadingMessage(message) messageTopView.setToLoadingState() showProgressThreshold = null + + // Only mark the message as opened when the fragment is resumed, i.e. when this is the active message. + if (isResumed) { + markMessageAsOpened() + } } override fun onMessageDataLoadFailed() { @@ -908,6 +943,8 @@ class MessageViewFragment : private const val ARG_REFERENCE = "reference" + private const val STATE_WAS_MESSAGE_MARKED_AS_OPENED = "wasMessageMarkedAsOpened" + private const val ACTIVITY_CHOOSE_FOLDER_MOVE = 1 private const val ACTIVITY_CHOOSE_FOLDER_COPY = 2 private const val REQUEST_CODE_CREATE_DOCUMENT = 3 -- GitLab From 6992cc732f1550c1fa5573964f02c231a80e4b5d Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 1 Aug 2022 17:22:11 +0200 Subject: [PATCH 45/47] Handle view pager swipe vs. WebView horizontal scrolling --- .../k9/ui/messageview/TouchInterceptView.kt | 69 +++++++++++++++++++ app/ui/legacy/src/main/res/layout/message.xml | 1 + .../res/layout/message_view_container.xml | 12 +++- 3 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/TouchInterceptView.kt diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/TouchInterceptView.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/TouchInterceptView.kt new file mode 100644 index 0000000000..df6fe2c015 --- /dev/null +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/TouchInterceptView.kt @@ -0,0 +1,69 @@ +package com.fsck.k9.ui.messageview + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.ViewConfiguration +import android.webkit.WebView +import android.widget.FrameLayout +import android.widget.ScrollView +import com.fsck.k9.ui.R +import kotlin.math.absoluteValue + +/** + * A view that listens to touch events to make sure [R.id.message_viewpager] doesn't get to see events that should be + * handled by [R.id.message_scrollview] or [R.id.message_content]. + * + * We allow the view pager to listen to touch events until we know it's a gesture that will be handled by the scroll + * view or the web view. + * If we instead hid events from the view pager until we knew the scroll view or the web view don't want to handle the + * gesture, we'd risk minimal fling gestures ([MotionEvent.ACTION_DOWN], [MotionEvent.ACTION_MOVE], + * [MotionEvent.ACTION_UP]) not working because the single [MotionEvent.ACTION_MOVE] event would be invisible to the + * view pager. + */ +class TouchInterceptView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) { + private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop + + private var initialX: Float = 0f + private var initialY: Float = 0f + + override fun onInterceptTouchEvent(event: MotionEvent): Boolean { + handleOnInterceptTouchEvent(event) + return super.onInterceptTouchEvent(event) + } + + private fun handleOnInterceptTouchEvent(event: MotionEvent) { + val webView = findViewById(R.id.message_content) ?: return + val scrollView = findViewById(R.id.message_scrollview) ?: return + val scrollViewParent = scrollView.parent ?: return + + when (event.actionMasked) { + MotionEvent.ACTION_DOWN -> { + initialX = event.x + initialY = event.y + scrollViewParent.requestDisallowInterceptTouchEvent(false) + } + MotionEvent.ACTION_POINTER_DOWN -> { + // If a second finger/pointer is involved, never allow parents of the ScrollView to intercept + scrollViewParent.requestDisallowInterceptTouchEvent(true) + } + MotionEvent.ACTION_MOVE -> { + val deltaX = initialX - event.x + val deltaY = initialY - event.y + + val absoluteDeltaX = deltaX.absoluteValue + val absoluteDeltaY = deltaY.absoluteValue + + if (absoluteDeltaY > touchSlop && absoluteDeltaY > absoluteDeltaX && + (scrollView.canScrollVertically(deltaY.toInt()) || webView.canScrollVertically(deltaY.toInt())) + ) { + scrollViewParent.requestDisallowInterceptTouchEvent(true) + } else if (absoluteDeltaX > touchSlop && absoluteDeltaX > absoluteDeltaY && + webView.canScrollHorizontally(deltaX.toInt()) + ) { + scrollViewParent.requestDisallowInterceptTouchEvent(true) + } + } + } + } +} diff --git a/app/ui/legacy/src/main/res/layout/message.xml b/app/ui/legacy/src/main/res/layout/message.xml index 2494b31a1b..4d8f1f43d9 100644 --- a/app/ui/legacy/src/main/res/layout/message.xml +++ b/app/ui/legacy/src/main/res/layout/message.xml @@ -12,6 +12,7 @@ tools:context=".messageview.MessageViewFragment"> diff --git a/app/ui/legacy/src/main/res/layout/message_view_container.xml b/app/ui/legacy/src/main/res/layout/message_view_container.xml index e8b24d6cb9..99ce488fa2 100644 --- a/app/ui/legacy/src/main/res/layout/message_view_container.xml +++ b/app/ui/legacy/src/main/res/layout/message_view_container.xml @@ -1,5 +1,11 @@ - + android:layout_height="match_parent"> + + + + \ No newline at end of file -- GitLab From 8b546cadd6e23d64d88814f3fdf37b4c1fd6331a Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 3 Aug 2022 21:44:35 +0200 Subject: [PATCH 46/47] Scroll message list to active message --- .../fsck/k9/fragment/MessageListFragment.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) 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 5e61517c89..b998ae2059 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 @@ -1563,6 +1563,28 @@ class MessageListFragment : if (::adapter.isInitialized) { adapter.activeMessage = activeMessage adapter.notifyDataSetChanged() + + if (messageReference != null) { + scrollToMessage(messageReference) + } + } + } + + 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) + } + } + + private fun getPosition(messageReference: MessageReference): Int { + return adapter.messages.indexOfFirst { messageListItem -> + messageListItem.account.uuid == messageReference.accountUuid && + messageListItem.folderId == messageReference.folderId && + messageListItem.messageUid == messageReference.uid } } -- GitLab From ee7dcd07c64bd550f32276503d44386d04586be6 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 4 Aug 2022 17:27:43 +0200 Subject: [PATCH 47/47] Version 6.300 --- app/k9mail/build.gradle | 2 +- app/ui/legacy/src/main/res/raw/changelog_master.xml | 6 ++++++ fastlane/metadata/android/en-US/changelogs/33000.txt | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/33000.txt diff --git a/app/k9mail/build.gradle b/app/k9mail/build.gradle index 58f27128ff..ca0ee46377 100644 --- a/app/k9mail/build.gradle +++ b/app/k9mail/build.gradle @@ -48,7 +48,7 @@ android { testApplicationId "com.fsck.k9.tests" versionCode 33000 - versionName '6.300-SNAPSHOT' + versionName '6.300' // 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 a3e5eed962..4b7fdf4b09 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,12 @@ Locale-specific versions are kept in res/raw-/changelog.xml. --> + + Added support for swiping between messages + Fixed multiple bugs when there are notifications for more than 8 messages + Fixed bug that could lead to broken attachment names when large messages were only partially downloaded (IMAP) + Added Western Frisian translation + Increased timeout when sending messages because some users have reported problems with sending large attachments Allow all URI schemes in HTML links diff --git a/fastlane/metadata/android/en-US/changelogs/33000.txt b/fastlane/metadata/android/en-US/changelogs/33000.txt new file mode 100644 index 0000000000..8b5c3a67c0 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/33000.txt @@ -0,0 +1,4 @@ +- Added support for swiping between messages +- Fixed multiple bugs when there are notifications for more than 8 messages +- Fixed bug that could lead to broken attachment names when large messages were only partially downloaded (IMAP) +- Added Western Frisian translation -- GitLab