diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 9592db5454b05bb4b7435443364635f65b5210a7..143e5414ce9a4de38c40df3299680f854a4efa40 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 5b59297a1824b6d8652c9a580b5af005926a37cc..c44bf97bfc3571fb03d2190761624ce0122eb4db 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/core/src/main/java/com/fsck/k9/controller/MessagingController.java b/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java
index e654e6d06628534aa51bb56f69f617e54e7fc8d0..194aa9c112be906e7a3057d15ca6cff8188af51e 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
@@ -1311,9 +1311,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;
}
@@ -1335,7 +1332,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);
@@ -1347,11 +1352,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/core/src/main/java/com/fsck/k9/notification/NotificationActionCreator.kt b/app/core/src/main/java/com/fsck/k9/notification/NotificationActionCreator.kt
index d7e403b8c315bbd1d65326ab97bddfd161bbc6d0..84297e06b4ff357d61738995292c989774a6c9b7 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/NotificationDataStore.kt b/app/core/src/main/java/com/fsck/k9/notification/NotificationDataStore.kt
index db99f40340ec52f42ee475fa4ad315094611ecda..de9f8c94bea4cbe209a290961d3dcd28acb9ea23 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/main/java/com/fsck/k9/notification/SendFailedNotificationController.kt b/app/core/src/main/java/com/fsck/k9/notification/SendFailedNotificationController.kt
index 3dd80b75ed82e145e405154609c0c259d1bbd792..2f82eabe1163c1d9b89439f9a8ccdfd03effbc9e 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 21553997b709d73b9f68b23cc93b8859b723ab4f..d6d5e3626a2e925fe07640fff2c4bef07d6a07f1 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
@@ -24,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)
@@ -35,8 +33,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 +55,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 +71,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 +80,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 +90,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 +116,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 +126,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 +136,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 +146,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 +156,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 eb55bc9eda9bcc839bbdeebe98b4cdc34160faf7..c780415f115ee2069c9a4fd86a19895c733cc70b 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
@@ -53,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)
@@ -65,7 +63,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 +99,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 +121,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 +132,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 +162,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 +172,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 +182,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 3140fb813a829843f9bfe312336c952358b0b6f3..441d5698337633abfc980a5184bd24845b9c0624 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/main/res/values/arrays_general_settings_values.xml b/app/core/src/main/res/values/arrays_general_settings_values.xml
index 560eb00e8edfac15ef728c39ebdaddb1844b1ec9..692227334947aea1968ae80438c8408b7b89479b 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/core/src/test/java/com/fsck/k9/message/html/HtmlConverterTest.java b/app/core/src/test/java/com/fsck/k9/message/html/HtmlConverterTest.java
deleted file mode 100644
index d7c34f9fb7308985a15fe6559d430baaaebda237..0000000000000000000000000000000000000000
--- a/app/core/src/test/java/com/fsck/k9/message/html/HtmlConverterTest.java
+++ /dev/null
@@ -1,329 +0,0 @@
-package com.fsck.k9.message.html;
-
-
-import org.junit.Test;
-
-import static junit.framework.Assert.assertEquals;
-
-
-public 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);
- }
-
- @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);
-
- }
-
- @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);
- }
-
- @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);
- }
-
- @Test
- public void testPreserveSpacesAtFirstForSpecialCharacters() {
- String message =
- " \r\n"
- + " &\r\n"
- + " \n"
- + " <\r\n"
- + " > \r\n";
-
- String result = HtmlConverter.textToHtml(message);
-
- assertEquals(""
- + "
"
- + " &
"
- + "
"
- + " <
"
- + ""
- + "
"
- + "
"
- + "
", result);
- }
-
- @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);
- }
-
- @Test
- public void dashesContainingSpacesIgnoredAsHR() {
- String text = "hello\n--- --- --- --- ---\nfoo bar";
- String result = HtmlConverter.textToHtml(text);
- assertEquals("hello
--- --- --- --- ---
foo bar
",
- result);
- }
-
- @Test
- public void mergeConsecutiveBreaksIntoOne() {
- String text = "hello\n------------\n---------------\nfoo bar";
- String result = HtmlConverter.textToHtml(text);
- assertEquals("hello
foo bar
", result);
- }
-
- @Test
- public void dashedHorizontalRulePrefixedWithTextIgnoredAsHR() {
- String text = "hello----\n\n";
- String result = HtmlConverter.textToHtml(text);
- assertEquals("hello----
", result);
- }
-
- @Test
- public void doubleMinusIgnoredAsHR() {
- String text = "--\n";
- String result = HtmlConverter.textToHtml(text);
- assertEquals("--
", result);
- }
-
- @Test
- public void doubleEqualsIgnoredAsHR() {
- String text = "==\n";
- String result = HtmlConverter.textToHtml(text);
- assertEquals("==
", result);
- }
-
- @Test
- public void doubleUnderscoreIgnoredAsHR() {
- String text = "__\n";
- String result = HtmlConverter.textToHtml(text);
- assertEquals("__
", result);
- }
-
- @Test
- public void anyTripletIsHRuledOut() {
- String text = "--=\n-=-\n===\n___\n\n";
- String result = HtmlConverter.textToHtml(text);
- assertEquals("
", result);
- }
-
- @Test
- public void replaceSpaceSeparatedDashesWithHR() {
- String text = "hello\n---------------------------\nfoo bar";
- String result = HtmlConverter.textToHtml(text);
- assertEquals("hello
foo bar
", result);
- }
-
- @Test
- public void replacementWithHRAtBeginning() {
- String text = "---------------------------\nfoo bar";
- String result = HtmlConverter.textToHtml(text);
- assertEquals("
foo bar
", result);
- }
-
- @Test
- public void replacementWithHRAtEnd() {
- String text = "hello\n__________________________________";
- String result = HtmlConverter.textToHtml(text);
- assertEquals("hello
", result);
- }
-
- @Test
- public void replacementOfScissorsByHR() {
- String text = "hello\n-- %< -------------- >8 --\nworld\n";
- String result = HtmlConverter.textToHtml(text);
- assertEquals("hello
world
", result);
- }
-
- @Test
- public void signatureEndingWithUrl() {
- String text = "text\n-- \nsignature with url: https://domain.example/";
- String result = HtmlConverter.textToHtml(text);
- assertEquals("" +
- "text
" +
- "
", result);
- }
-
- @Test
- public void htmlToText_withLineBreaks() {
- String input = "One
Two
Three";
-
- String result = HtmlConverter.htmlToText(input);
-
- assertEquals("One\nTwo\n\nThree", result);
- }
-
- @Test
- public void htmlToText_withBlockElements() {
- String input = "One
Two
Three
Four
";
-
- String result = HtmlConverter.htmlToText(input);
-
- assertEquals("One\n\nTwo\nThree\n\nFour", result);
- }
-
- @Test
- public void htmlToText_withLink() {
- String input = "Link text";
-
- String result = HtmlConverter.htmlToText(input);
-
- assertEquals("Link text ", result);
- }
-
- @Test
- public void htmlToText_withLinkifiedUrl() {
- String input = "Text https://domain.example/path/ more text";
-
- String result = HtmlConverter.htmlToText(input);
-
- assertEquals("Text https://domain.example/path/ more text", result);
- }
-
- @Test
- public void htmlToText_withLinkifiedUrlContainingFormatting() {
- String input = "https://domain.example/path/";
-
- String result = HtmlConverter.htmlToText(input);
-
- assertEquals("https://domain.example/path/", result);
- }
-
- @Test
- public void htmlToText_withLineBreaksInHtml() {
- String input = "One\nTwo\r\nThree";
-
- String result = HtmlConverter.htmlToText(input);
-
- assertEquals("One Two Three", result);
- }
-
- @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.";
-
- String result = HtmlConverter.htmlToText(input);
-
- assertEquals(input, result);
- }
-}
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
new file mode 100644
index 0000000000000000000000000000000000000000..178110da15971c3963a93e3ddd282e15c0b0e20c
--- /dev/null
+++ b/app/core/src/test/java/com/fsck/k9/message/html/HtmlConverterTest.kt
@@ -0,0 +1,505 @@
+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
+
+class HtmlConverterTest {
+ @Test
+ 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
+ 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
+ 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
+ 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
+ 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
+ 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
+ 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
+ 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
+ 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
+ fun `textToHtml() should not replace double dash with an HR tag`() {
+ val text = "--\n"
+
+ val result = HtmlConverter.textToHtml(text)
+
+ assertThat(result).isEqualTo("""--
""")
+ }
+
+ @Test
+ fun `textToHtml() should not replace double equal sign with an HR tag`() {
+ val text = "==\n"
+
+ val result = HtmlConverter.textToHtml(text)
+
+ assertThat(result).isEqualTo("""==
""")
+ }
+
+ @Test
+ fun `textToHtml() should not replace double underscore with an HR tag`() {
+ val text = "__\n"
+
+ val result = HtmlConverter.textToHtml(text)
+
+ assertThat(result).isEqualTo("""__
""")
+ }
+
+ @Test
+ 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
+ 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
+ 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
+ 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
+ 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
+
+
+ """.trimIndent().removeLineBreaks()
+ )
+ }
+
+ @Test
+ 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
+ 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
+ fun `htmlToText() should include link URIs`() {
+ val input = "Link text"
+
+ val result = HtmlConverter.htmlToText(input)
+
+ assertThat(result).isEqualTo("Link text ")
+ }
+
+ @Test
+ fun `htmlToText() should not duplicate URI when link URI and text are the same`() {
+ val input = "Text https://domain.example/path/ more text"
+
+ val result = HtmlConverter.htmlToText(input)
+
+ assertThat(result).isEqualTo("Text https://domain.example/path/ more text")
+ }
+
+ @Test
+ fun `htmlToText() should not duplicate URI when the link text is just the link URI with some formatting`() {
+ val input = "https://domain.example/path/"
+
+ val result = HtmlConverter.htmlToText(input)
+
+ assertThat(result).isEqualTo("https://domain.example/path/")
+ }
+
+ @Test
+ fun `htmlToText() should strip line breaks`() {
+ val input =
+ """
+ One
+ Two
+ Three
+ """.trimIndent()
+
+ val result = HtmlConverter.htmlToText(input)
+
+ assertThat(result).isEqualTo("One Two Three")
+ }
+
+ @Test
+ 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()
+
+ val result = HtmlConverter.htmlToText(input)
+
+ assertThat(result).isEqualTo(input)
+ }
+}
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 57ffc8aba4fa3cff55554af60b40bd6c73e343df..c3000f771114b9c93037b267559420b9f5195819 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) {
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 591fe1a17609fb539594b5575209dac2267d0655..e92089fc6813e999720a70f84be275ba18e61df9 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 cde497225965a636e0bf2b691e01e4bc8d56acf2..18b5aa11cd1a382fdf0f79d1014d2b5fafda49cb 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-jmap/src/main/res/values/themes.xml b/app/k9mail-jmap/src/main/res/values/themes.xml
index 3b7ee3fdec095ed1ffbbde23e335848dc8d766a8..d76fd70e84aeee148121f989bb3faa8594677425 100644
--- a/app/k9mail-jmap/src/main/res/values/themes.xml
+++ b/app/k9mail-jmap/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
diff --git a/app/k9mail/build.gradle b/app/k9mail/build.gradle
index 979055c6dc5473580c9fb159b8a323ca1e16c51d..d594824ac1158dd0bd186eb4f9ab438684d3dbfd 100644
--- a/app/k9mail/build.gradle
+++ b/app/k9mail/build.gradle
@@ -48,13 +48,14 @@ android {
applicationId "foundation.e.mail"
testApplicationId "foundation.e.mail.tests"
- versionCode 32002
- versionName '6.202'
+ versionCode 33000
+ 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",
"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/k9mail/src/main/java/com/fsck/k9/notification/K9NotificationActionCreator.kt b/app/k9mail/src/main/java/com/fsck/k9/notification/K9NotificationActionCreator.kt
index 77a997ced20457241ed238c1c96a57b7cfd85b28..b08374a516d9cc8e4b6a1410972f54c207db5525 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,
@@ -34,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, 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 {
+ override fun createViewFolderPendingIntent(account: Account, folderId: Long): 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(
account: Account,
- messageReferences: List,
- notificationId: Int
+ messageReferences: List
): PendingIntent {
val folderIds = extractFolderIds(messageReferences)
@@ -64,48 +63,51 @@ 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 {
+ override fun createViewFolderListPendingIntent(account: Account): 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)
+ 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 {
- val intent = NotificationActionService.createDismissMessageIntent(context, messageReference)
- return PendingIntent.getService(context, notificationId, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
+ 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 {
- val intent = MessageActions.getActionReplyIntent(context, messageReference)
- return PendingIntent.getActivity(context, notificationId, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
+ 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 {
- val intent = NotificationActionService.createMarkMessageAsReadIntent(context, messageReference)
- return PendingIntent.getService(context, notificationId, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
+ override fun createMarkMessageAsReadPendingIntent(messageReference: MessageReference): PendingIntent {
+ 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(
account: Account,
- messageReferences: List,
- notificationId: Int
+ messageReferences: List
): 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 {
@@ -118,86 +120,79 @@ 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, notificationId)
+ createDeleteConfirmationPendingIntent(messageReference)
} else {
- createDeleteServicePendingIntent(messageReference, notificationId)
+ createDeleteServicePendingIntent(messageReference)
}
}
- private fun createDeleteServicePendingIntent(
- messageReference: MessageReference,
- notificationId: Int
- ): PendingIntent {
- val intent = NotificationActionService.createDeleteMessageIntent(context, messageReference)
- return PendingIntent.getService(context, notificationId, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
+ 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 {
- val intent = DeleteConfirmationActivity.getIntent(context, messageReference)
- return PendingIntent.getActivity(context, notificationId, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
+ private fun createDeleteConfirmationPendingIntent(messageReference: MessageReference): PendingIntent {
+ 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(
account: Account,
- messageReferences: List,
- notificationId: Int
+ messageReferences: List
): PendingIntent {
return if (K9.isConfirmDeleteFromNotification) {
- getDeleteAllConfirmationPendingIntent(messageReferences, notificationId)
+ getDeleteAllConfirmationPendingIntent(messageReferences)
} else {
- getDeleteAllServicePendingIntent(account, messageReferences, notificationId)
+ getDeleteAllServicePendingIntent(account, messageReferences)
}
}
- private fun getDeleteAllConfirmationPendingIntent(
- messageReferences: List,
- notificationId: Int
- ): PendingIntent {
- val intent = DeleteConfirmationActivity.getIntent(context, messageReferences)
- return PendingIntent.getActivity(context, notificationId, intent, FLAG_CANCEL_CURRENT or FLAG_IMMUTABLE)
+ private fun getDeleteAllConfirmationPendingIntent(messageReferences: List): PendingIntent {
+ 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(
account: Account,
- messageReferences: List,
- notificationId: Int
+ messageReferences: List
): 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)
+ override fun createArchiveMessagePendingIntent(messageReference: MessageReference): PendingIntent {
+ 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(
account: Account,
- messageReferences: List,
- notificationId: Int
+ messageReferences: List
): 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)
+ override fun createMarkMessageAsSpamPendingIntent(messageReference: MessageReference): PendingIntent {
+ 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 +208,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 +225,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 {
diff --git a/app/k9mail/src/main/res/values/themes.xml b/app/k9mail/src/main/res/values/themes.xml
index 543cf607561951a13a16a83cc6fe03b9afbd67d4..9982756f2c75259e2f0ff5f0d0de7edda0cdf5d3 100644
--- a/app/k9mail/src/main/res/values/themes.xml
+++ b/app/k9mail/src/main/res/values/themes.xml
@@ -59,8 +59,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
@@ -185,8 +183,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
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 d82501ae9f4c5e0e106710789a24ed86418d900a..da42725b5f57a9f49b845b9ef84b2d04d2f74a97 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
@@ -43,10 +43,10 @@ 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 androidx.lifecycle.lifecycleScope
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
@@ -72,13 +72,13 @@ 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.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.permissions.K9PermissionUiHelper
@@ -105,6 +105,7 @@ open class MessageList :
K9Activity(),
MessageListFragmentListener,
MessageViewFragmentListener,
+ MessageViewContainerListener,
FragmentManager.OnBackStackChangedListener,
OnSwitchCompleteListener,
PermissionUiHelper {
@@ -124,15 +125,19 @@ 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
- 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
@@ -245,7 +250,7 @@ open class MessageList :
supportFragmentManager.popBackStackImmediate(FIRST_FRAGMENT_TRANSACTION, FragmentManager.POP_BACK_STACK_INCLUSIVE)
removeMessageListFragment()
- removeMessageViewFragment()
+ removeMessageViewContainerFragment()
messageReference = null
search = null
@@ -267,9 +272,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)
}
}
@@ -285,14 +292,14 @@ open class MessageList :
search!!, false, K9.isThreadedViewEnabled && !noThreading
)
fragmentTransaction.add(R.id.message_list_container, messageListFragment)
- fragmentTransaction.commit()
+ fragmentTransaction.commitNow()
this.messageListFragment = messageListFragment
}
// 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!!)
}
}
@@ -318,7 +325,7 @@ open class MessageList :
}
}
- displayMode = if (messageViewFragment != null || messageReference != null) {
+ displayMode = if (messageViewContainerFragment != null || messageReference != null) {
DisplayMode.MESSAGE_VIEW
} else {
DisplayMode.MESSAGE_LIST
@@ -347,16 +354,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.isActive = true
+
+ messageViewContainerFragment.let { messageViewContainerFragment ->
+ if (messageViewContainerFragment == null) {
+ showMessageViewPlaceHolder()
+ } else {
+ messageViewContainerFragment.isActive = true
+ val activeMessage = messageViewContainerFragment.messageReference
+ messageListFragment.setActiveMessage(activeMessage)
}
}
+
setDrawerLockState()
onMessageListDisplayed()
}
@@ -618,7 +630,7 @@ open class MessageList :
fun openFolder(folderId: Long) {
if (displayMode == DisplayMode.SPLIT_VIEW) {
- removeMessageViewFragment()
+ removeMessageViewContainerFragment()
showMessageViewPlaceHolder()
}
@@ -638,7 +650,7 @@ open class MessageList :
openFolderTransaction!!.commit()
openFolderTransaction = null
- messageListFragment!!.onListVisible()
+ messageListFragment!!.isActive = true
onMessageListDisplayed()
}
@@ -747,7 +759,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()
@@ -758,7 +770,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()
@@ -773,14 +785,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
@@ -812,69 +824,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
@@ -896,8 +908,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()
}
}
@@ -930,134 +942,13 @@ 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
- } 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
}
- 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)
- }
- }
- }
-
- private fun searchEverywhere() {
- val searchIntent = Intent(this, Search::class.java).apply {
- action = Intent.ACTION_SEARCH
- putExtra(SearchManager.QUERY, intent.getStringExtra(SearchManager.QUERY))
- }
- onNewIntent(searchIntent)
+ return super.onOptionsItemSelected(item)
}
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)
@@ -1084,171 +975,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()
- }
-
- // 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) {
actionBar?.title = title
actionBar?.subtitle = subtitle
@@ -1278,13 +1004,20 @@ open class MessageList :
} else {
messageListFragment?.setActiveMessage(messageReference)
- val fragment = MessageViewFragment.newInstance(messageReference)
- val fragmentTransaction = supportFragmentManager.beginTransaction()
- fragmentTransaction.replace(R.id.message_view_container, fragment, FRAGMENT_TAG_MESSAGE_VIEW)
- fragmentTransaction.commit()
- messageViewFragment = fragment
+ val fragment = MessageViewContainerFragment.newInstance(messageReference)
+ supportFragmentManager.commitNow {
+ replace(R.id.message_view_container, fragment, FRAGMENT_TAG_MESSAGE_VIEW_CONTAINER)
+ }
+
+ messageViewContainerFragment = fragment
+
+ messageListFragment?.let { messageListFragment ->
+ fragment.setViewModel(messageListFragment.viewModel)
+ }
- if (displayMode != DisplayMode.SPLIT_VIEW) {
+ if (displayMode == DisplayMode.SPLIT_VIEW) {
+ fragment.isActive = true
+ } else {
showMessageView()
}
}
@@ -1316,6 +1049,8 @@ open class MessageList :
override fun onBackStackChanged() {
findFragments()
+ messageListFragment?.isActive = true
+
if (isDrawerEnabled && !isAdditionalMessageListDisplayed) {
unlockDrawer()
}
@@ -1323,8 +1058,6 @@ open class MessageList :
if (displayMode == DisplayMode.SPLIT_VIEW) {
showMessageViewPlaceHolder()
}
-
- configureMenu(menu)
}
private fun addMessageListFragment(fragment: MessageListFragment) {
@@ -1341,6 +1074,7 @@ open class MessageList :
}
messageListFragment = fragment
+ fragment.isActive = true
if (isDrawerEnabled) {
lockDrawer()
@@ -1384,7 +1118,7 @@ open class MessageList :
}
private fun showMessageViewPlaceHolder() {
- removeMessageViewFragment()
+ removeMessageViewContainerFragment()
// Add placeholder fragment if necessary
val fragmentManager = supportFragmentManager
@@ -1397,11 +1131,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()
@@ -1415,11 +1149,6 @@ open class MessageList :
fragmentTransaction.commit()
}
- override fun remoteSearchStarted() {
- // Remove action button for remote search
- configureMenu(menu)
- }
-
override fun goBack() {
val fragmentManager = supportFragmentManager
when {
@@ -1429,29 +1158,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) {
@@ -1459,25 +1194,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() {
@@ -1486,13 +1211,13 @@ open class MessageList :
displayMode = DisplayMode.MESSAGE_LIST
viewSwitcher!!.showFirstView()
- messageListFragment!!.onListVisible()
+ messageViewContainerFragment?.isActive = false
+ messageListFragment!!.isActive = true
messageListFragment!!.setActiveMessage(null)
setDrawerLockState()
showDefaultTitleView()
- configureMenu(menu)
onMessageListDisplayed()
}
@@ -1508,8 +1233,11 @@ open class MessageList :
}
private fun showMessageView() {
+ val messageViewContainerFragment = checkNotNull(this.messageViewContainerFragment)
+
displayMode = DisplayMode.MESSAGE_VIEW
- messageListFragment?.onListHidden()
+ messageListFragment?.isActive = false
+ messageViewContainerFragment.isActive = true
if (!messageListWasDisplayed) {
viewSwitcher!!.animateFirstView = false
@@ -1521,20 +1249,6 @@ open class MessageList :
}
showMessageTitleView()
- configureMenu(menu)
- }
-
- override fun updateMenu() {
- invalidateOptionsMenu()
- }
-
- override fun disableDeleteAction() {
- menu!!.findItem(R.id.delete).isEnabled = false
- }
-
- private fun onToggleTheme() {
- themeManager.toggleMessageViewTheme()
- recreateCompat()
}
private fun showDefaultTitleView() {
@@ -1549,7 +1263,7 @@ open class MessageList :
override fun onSwitchComplete(displayedChild: Int) {
if (displayedChild == 0) {
- removeMessageViewFragment()
+ removeMessageViewContainerFragment()
}
}
@@ -1587,8 +1301,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)
}
}
}
@@ -1718,16 +1432,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/activity/setup/AccountSetupCheckSettings.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupCheckSettings.kt
index c6e88d1796213bf885df60f683ba01dae5d35a60..461865655afda274ee5b8cad54c9347effc4b0ab 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/fragment/MessageListFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt
index 92e15487fb07c7e98cef064065b13a02e76aa65c..c7e3d66a81efdbff6a642e0b1cced76cef1b863d 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
@@ -66,7 +68,7 @@ class MessageListFragment :
MessageListItemActionListener {
private val foldersViewModel: FoldersViewModel by sharedViewModel()
- 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()) }
@@ -113,16 +115,12 @@ 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
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
@@ -134,10 +132,19 @@ 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
+ /**
+ * 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()
+ invalidateMenu()
+ }
override fun onAttach(context: Context) {
super.onAttach(context)
@@ -151,6 +158,7 @@ class MessageListFragment :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ setHasOptionsMenu(true)
restoreInstanceState(savedInstanceState)
decodeArguments() ?: return
@@ -533,7 +541,7 @@ class MessageListFragment :
}
}
- fun changeSort(sortType: SortType) {
+ private fun changeSort(sortType: SortType) {
val sortAscending = if (this.sortType == sortType) !sortAscending else null
changeSort(sortType, sortAscending)
}
@@ -555,7 +563,7 @@ class MessageListFragment :
activityListener
)
- fragmentListener.remoteSearchStarted()
+ invalidateMenu()
}
/**
@@ -655,23 +663,19 @@ 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)
}
}
- val isShowingTrashFolder: Boolean
+ private val isShowingTrashFolder: Boolean
get() {
if (!isSingleFolderMode) return false
return currentFolder!!.databaseId == account!!.trashFolderId
@@ -730,54 +734,86 @@ class MessageListFragment :
return "dialog-$dialogId"
}
- 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
+ override fun onPrepareOptionsMenu(menu: Menu) {
+ if (isActive) {
+ prepareMenu(menu)
+ } else {
+ hideMenu(menu)
}
+ }
- 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
- }
+ 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
+ menu.findItem(R.id.mark_all_as_read).isVisible = isMarkAllAsReadSupported
+ menu.findItem(R.id.empty_trash).isVisible = isShowingTrashFolder
- if (id == R.id.send_messages) {
- onSendPendingMessages()
- return true
- } else if (id == R.id.expunge) {
- currentFolder?.let { folderInfoHolder ->
- onExpunge(account, folderInfoHolder.databaseId)
- }
- return true
+ if (isSingleAccountMode) {
+ menu.findItem(R.id.send_messages).isVisible = isOutbox
+ menu.findItem(R.id.expunge).isVisible = isRemoteFolder && shouldShowExpungeAction()
} else {
- return super.onOptionsItemSelected(item)
+ 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.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
+ 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 {
+ 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)
+ 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.mark_all_as_read -> confirmMarkAllAsRead()
+ 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)
}
- fun onSendPendingMessages() {
+ private fun onSendPendingMessages() {
messagingController.sendPendingMessages(account, null)
}
@@ -1290,30 +1326,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)
@@ -1340,14 +1352,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)
}
@@ -1443,7 +1447,7 @@ class MessageListFragment :
return currentFolder.databaseId == folderId
}
- val isRemoteFolder: Boolean
+ private val isRemoteFolder: Boolean
get() {
if (localSearch.isManualSearch || isOutbox) return false
@@ -1455,15 +1459,15 @@ 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)
}
- fun onRemoteSearch() {
+ private fun onRemoteSearch() {
// Remote search is useless without the network.
if (hasConnectivity == true) {
onRemoteSearchRequested()
@@ -1472,7 +1476,7 @@ class MessageListFragment :
}
}
- val isRemoteSearchAllowed: Boolean
+ private val isRemoteSearchAllowed: Boolean
get() = isManualSearch && !isRemoteSearch && isSingleFolderMode && messagingController.isPushCapable(account)
fun onSearchRequested(query: String): Boolean {
@@ -1480,7 +1484,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()
@@ -1513,14 +1517,12 @@ class MessageListFragment :
computeBatchDirection()
computeSelectAllVisibility()
- isLoadFinished = true
-
if (savedListState != null) {
handler.restoreListPosition(savedListState)
savedListState = null
}
- fragmentListener.updateMenu()
+ invalidateMenu()
currentFolder?.let { currentFolder ->
currentFolder.moreMessages = messageListInfo.hasMoreMessages
@@ -1541,7 +1543,7 @@ class MessageListFragment :
private fun resetActionMode() {
if (!isResumed) return
- if (!isListVisible || selected.isEmpty()) {
+ if (!isActive || selected.isEmpty()) {
actionMode?.finish()
actionMode = null
return
@@ -1589,13 +1591,35 @@ class MessageListFragment :
if (::adapter.isInitialized) {
adapter.activeMessage = activeMessage
adapter.notifyDataSetChanged()
+
+ if (messageReference != null) {
+ scrollToMessage(messageReference)
+ }
}
}
- val isMarkAllAsReadSupported: Boolean
+ 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
+ }
+ }
+
+ 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 {
@@ -1609,17 +1633,11 @@ class MessageListFragment :
}
}
- fun onListVisible() {
- isListVisible = true
- resetActionMode()
- }
-
- fun onListHidden() {
- isListVisible = false
- resetActionMode()
+ private fun invalidateMenu() {
+ activity?.invalidateMenu()
}
- val isCheckMailSupported: Boolean
+ private val isCheckMailSupported: Boolean
get() = allAccounts || !isSingleAccountMode || !isSingleFolderMode || isRemoteFolder
private val isCheckMailAllowed: Boolean
@@ -1975,9 +1993,7 @@ 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 updateMenu()
fun onFolderNotFoundError()
companion object {
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 4398ead62174c88cf160ba029235c78de043338e..b0b9fb039d4fbea76bdedaafda5dadcca9a34289 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/java/com/fsck/k9/ui/messageview/AttachmentController.java b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java
index fab62cbab326b485e5fdd684f444ace7b60e7eb6..7eaf4167826ea336d1283b4d03ab48a927f46452 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/AttachmentViewCallback.java b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentViewCallback.java
index 43c2e40e6daacb1c36063f3d8c5c2811196a6a04..e78fe3a09dc4ff4a9191b898dcf203a3a02e7a71 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/Direction.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/Direction.kt
new file mode 100644
index 0000000000000000000000000000000000000000..62431d3871643db0b849e5a78881d9d57d72a4d3
--- /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/MessageContainerView.java b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.java
deleted file mode 100644
index 3efcff567c454e2f6bdf6117e654f66a24d00d3a..0000000000000000000000000000000000000000
--- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.java
+++ /dev/null
@@ -1,551 +0,0 @@
-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.KeyEvent;
-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.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 LinearLayout mAttachments;
- private View unsignedTextContainer;
- private View unsignedTextDivider;
- private TextView unsignedText;
- private View mAttachmentsContainer;
-
- 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);
-
- mAttachmentsContainer = findViewById(R.id.attachments_container);
- mAttachments = findViewById(R.id.attachments);
-
- 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);
- }
-
- @Override
- public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
- super.onCreateContextMenu(menu);
-
- WebView webview = (WebView) v;
- WebView.HitTestResult result = webview.getHitTestResult();
-
- 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;
- }
- };
-
- menu.setHeaderTitle(url);
-
- 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);
-
- 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;
- }
- };
-
- menu.setHeaderTitle(inlineImage ?
- context.getString(R.string.webview_contextmenu_image_title) : uri.toString());
-
- 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,
- inlineImage ?
- context.getString(R.string.webview_contextmenu_image_save_action) :
- 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);
- }
-
- 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;
- }
- };
-
- 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);
-
- 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;
- }
- };
-
- 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);
-
- break;
- }
- }
- }
-
- 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);
- }
- request.setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
-
- DownloadManager downloadManager = (DownloadManager) getContext().getSystemService(Context.DOWNLOAD_SERVICE);
- downloadManager.enqueue(request);
- }
-
- private AttachmentViewInfo getAttachmentViewInfoIfCidUri(Uri uri) {
- if (!"cid".equals(uri.getScheme())) {
- return null;
- }
-
- String cid = uri.getSchemeSpecificPart();
- Uri internalUri = currentAttachmentResolver.getAttachmentUriForContentId(cid);
-
- return attachments.get(internalUri);
- }
-
- 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();
- }
- }
-
- public MessageContainerView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
-
- private boolean isShowingPictures() {
- return showingPictures;
- }
-
- private void setLoadPictures(boolean enable) {
- mMessageContentView.blockNetworkData(!enable);
- showingPictures = enable;
- }
-
- public void showPictures() {
- setLoadPictures(true);
- 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) {
-
- this.attachmentCallback = attachmentCallback;
-
- resetView();
-
- renderAttachments(messageViewInfo);
-
- String textToDisplay = messageViewInfo.text;
- if (textToDisplay != null && !isShowingPictures()) {
- if (Utility.hasExternalImages(textToDisplay)) {
- if (loadPictures) {
- setLoadPictures(true);
- } else {
- 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();
- }
- };
-
- 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);
- }
- }
-
- public boolean hasHiddenExternalImages() {
- return hasHiddenExternalImages;
- }
-
- private void displayHtmlContentWithInlineAttachments(String htmlText, AttachmentResolver attachmentResolver,
- OnPageFinishedListener onPageFinishedListener) {
- currentHtmlText = htmlText;
- currentAttachmentResolver = attachmentResolver;
- mMessageContentView.displayHtmlContentWithInlineAttachments(htmlText, attachmentResolver, onPageFinishedListener);
- }
-
- private void refreshDisplayedContent() {
- mMessageContentView.displayHtmlContentWithInlineAttachments(currentHtmlText, currentAttachmentResolver, null);
- }
-
- private void clearDisplayedContent() {
- mMessageContentView.displayHtmlContentWithInlineAttachments("", null, null);
- unsignedTextContainer.setVisibility(View.GONE);
- unsignedText.setText("");
- }
-
- public void renderAttachments(MessageViewInfo messageViewInfo) {
- if (messageViewInfo.attachments != null) {
- for (AttachmentViewInfo attachment : messageViewInfo.attachments) {
- attachments.put(attachment.internalUri, attachment);
- if (attachment.inlineAttachment) {
- continue;
- }
-
- AttachmentView view =
- (AttachmentView) mInflater.inflate(R.layout.message_view_attachment, mAttachments, false);
- view.setCallback(attachmentCallback);
- view.setAttachment(attachment);
-
- attachmentViewMap.put(attachment, view);
- mAttachments.addView(view);
- }
- }
-
- if (messageViewInfo.extraAttachments != null) {
- for (AttachmentViewInfo attachment : messageViewInfo.extraAttachments) {
- attachments.put(attachment.internalUri, attachment);
- if (attachment.inlineAttachment) {
- continue;
- }
-
- LockedAttachmentView view = (LockedAttachmentView) mInflater
- .inflate(R.layout.message_view_attachment_locked, mAttachments, false);
- view.setCallback(attachmentCallback);
- view.setAttachment(attachment);
-
- // attachments.put(attachment, view);
- mAttachments.addView(view);
- }
- }
- }
-
- 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();
-
- currentHtmlText = null;
- currentAttachmentResolver = null;
-
- /*
- * Clear the WebView content
- *
- * For some reason WebView.clearView() doesn't clear the contents when the WebView changes
- * its size because the button to download the complete message was previously shown and
- * is now hidden.
- */
- 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();
- }
-
- private AttachmentView getAttachmentView(AttachmentViewInfo attachment) {
- return attachmentViewMap.get(attachment);
- }
-
- interface OnRenderingFinishedListener {
- void onLoadFinished();
- }
-}
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
new file mode 100644
index 0000000000000000000000000000000000000000..0d3534f26927b2808619a468414ff8cb2fc35493
--- /dev/null
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageContainerView.kt
@@ -0,0 +1,538 @@
+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()
+
+ private lateinit var layoutInflater: LayoutInflater
+
+ 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
+
+ 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
+
+ @get:JvmName("hasHiddenExternalImages")
+ var hasHiddenExternalImages = false
+ private set
+
+ public override fun onFinishInflate() {
+ super.onFinishInflate()
+
+ layoutInflater = LayoutInflater.from(context)
+
+ messageContentView = findViewById(R.id.message_content).apply {
+ if (!isInEditMode) {
+ configure(webViewConfigProvider.createForMessageView())
+ }
+
+ setOnCreateContextMenuListener(this@MessageContainerView)
+ visibility = 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)
+ }
+
+ override fun onCreateContextMenu(menu: ContextMenu, view: View, menuInfo: ContextMenuInfo?) {
+ super.onCreateContextMenu(menu)
+
+ val webView = view as WebView
+ val hitTestResult = webView.hitTestResult
+
+ 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)
+ }
+ }
+ }
+
+ 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)
+ }
+ }
+ true
+ }
+
+ 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)
+ }
+
+ private fun createImageMenu(menu: ContextMenu, imageUrl: String?) {
+ if (imageUrl == null) return
+
+ val imageUri = Uri.parse(imageUrl)
+ val attachmentViewInfo = getAttachmentViewInfoIfCidUri(imageUri)
+ val inlineImage = attachmentViewInfo != null
+
+ val listener = MenuItem.OnMenuItemClickListener { item ->
+ val attachmentCallback = checkNotNull(attachmentCallback)
+
+ when (item.itemId) {
+ MENU_ITEM_IMAGE_VIEW -> {
+ if (inlineImage) {
+ attachmentCallback.onViewAttachment(attachmentViewInfo)
+ } else {
+ val intent = Intent(Intent.ACTION_VIEW, imageUri)
+ startActivityIfAvailable(context, intent)
+ }
+ }
+ 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
+ }
+
+ if (inlineImage) {
+ menu.setHeaderTitle(R.string.webview_contextmenu_image_title)
+ } else {
+ menu.setHeaderTitle(imageUrl)
+ }
+
+ 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 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
+ }
+
+ 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 fun createEmailMenu(menu: ContextMenu, email: String?) {
+ if (email == null) return
+
+ 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
+ }
+
+ 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 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)
+ }
+
+ val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
+ downloadManager.enqueue(request)
+ }
+
+ private fun getAttachmentViewInfoIfCidUri(uri: Uri): AttachmentViewInfo? {
+ if (uri.scheme != "cid") return null
+
+ val attachmentResolver = checkNotNull(currentAttachmentResolver)
+
+ val cid = uri.schemeSpecificPart
+ val internalUri = attachmentResolver.getAttachmentUriForContentId(cid)
+
+ return attachments[internalUri]
+ }
+
+ 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()
+ }
+ }
+
+ private fun setLoadPictures(enable: Boolean) {
+ messageContentView.blockNetworkData(!enable)
+ isShowingPictures = enable
+ }
+
+ fun showPictures() {
+ setLoadPictures(true)
+ refreshDisplayedContent()
+ }
+
+ 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)
+ } else {
+ hasHiddenExternalImages = true
+ }
+ }
+ }
+
+ val textToDisplay = messageText
+ ?: displayHtml.wrapStatusMessage(context.getString(R.string.webview_empty_message))
+
+ displayHtmlContentWithInlineAttachments(
+ htmlText = textToDisplay,
+ attachmentResolver = messageViewInfo.attachmentResolver,
+ onPageFinishedListener = onRenderingFinishedListener::onLoadFinished
+ )
+
+ if (!messageViewInfo.extraText.isNullOrEmpty()) {
+ unsignedTextContainer.isVisible = true
+ unsignedTextDivider.isGone = hideUnsignedTextDivider
+ unsignedText.text = messageViewInfo.extraText
+ }
+ }
+
+ private fun displayHtmlContentWithInlineAttachments(
+ htmlText: String,
+ attachmentResolver: AttachmentResolver,
+ onPageFinishedListener: OnPageFinishedListener
+ ) {
+ currentHtmlText = htmlText
+ currentAttachmentResolver = attachmentResolver
+ messageContentView.displayHtmlContentWithInlineAttachments(htmlText, attachmentResolver, onPageFinishedListener)
+ }
+
+ private fun refreshDisplayedContent() {
+ val htmlText = checkNotNull(currentHtmlText)
+
+ messageContentView.displayHtmlContentWithInlineAttachments(
+ htmlText = htmlText,
+ attachmentResolver = currentAttachmentResolver,
+ onPageFinishedListener = null
+ )
+ }
+
+ private fun clearDisplayedContent() {
+ messageContentView.displayHtmlContentWithInlineAttachments(
+ htmlText = "",
+ attachmentResolver = null,
+ onPageFinishedListener = null
+ )
+
+ unsignedTextContainer.isVisible = false
+ unsignedText.text = ""
+ }
+
+ private fun renderAttachments(messageViewInfo: MessageViewInfo) {
+ if (messageViewInfo.attachments != null) {
+ for (attachment in messageViewInfo.attachments) {
+ attachments[attachment.internalUri] = attachment
+ if (attachment.inlineAttachment) {
+ continue
+ }
+
+ val attachmentView = layoutInflater.inflate(
+ R.layout.message_view_attachment,
+ attachmentsContainer,
+ false
+ ) as AttachmentView
+
+ attachmentView.setCallback(attachmentCallback)
+ attachmentView.setAttachment(attachment)
+
+ attachmentViewMap[attachment] = attachmentView
+ attachmentsContainer.addView(attachmentView)
+ }
+ }
+
+ if (messageViewInfo.extraAttachments != null) {
+ for (attachment in messageViewInfo.extraAttachments) {
+ attachments[attachment.internalUri] = attachment
+ if (attachment.inlineAttachment) {
+ continue
+ }
+
+ val lockedAttachmentView = layoutInflater.inflate(
+ R.layout.message_view_attachment_locked,
+ attachmentsContainer,
+ false
+ ) as LockedAttachmentView
+
+ lockedAttachmentView.setCallback(attachmentCallback)
+ lockedAttachmentView.setAttachment(attachment)
+
+ attachmentsContainer.addView(lockedAttachmentView)
+ }
+ }
+ }
+
+ private fun resetView() {
+ setLoadPictures(false)
+ attachmentsContainer.removeAllViews()
+
+ currentHtmlText = null
+ currentAttachmentResolver = null
+
+ /*
+ * Clear the WebView content
+ *
+ * For some reason WebView.clearView() doesn't clear the contents when the WebView changes
+ * its size because the button to download the complete message was previously shown and
+ * is now hidden.
+ */
+ clearDisplayedContent()
+ }
+
+ fun refreshAttachmentThumbnail(attachment: AttachmentViewInfo) {
+ getAttachmentView(attachment)?.refreshThumbnail()
+ }
+
+ private fun getAttachmentView(attachment: AttachmentViewInfo): AttachmentView? {
+ return attachmentViewMap[attachment]
+ }
+
+ interface OnRenderingFinishedListener {
+ 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/ui/messageview/MessageViewContainerFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewContainerFragment.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6c4e79a388273204c01c0a111ed2e7d23e560941
--- /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.java b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java
deleted file mode 100644
index e1ce84d49d4e084b78e028ad9cf0c34e7c655534..0000000000000000000000000000000000000000
--- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java
+++ /dev/null
@@ -1,890 +0,0 @@
-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;
-
- /**
- * Used to temporarily store the destination folder for refile operations if a confirmation
- * dialog is shown.
- */
- private Long destinationFolderId;
-
- private MessageViewFragmentListener mFragmentListener;
-
- /**
- * {@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.
- */
- private boolean mInitialized = false;
-
- private Context mContext;
-
- private AttachmentViewInfo currentAttachmentViewInfo;
-
- @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");
- }
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- // This fragments adds options to the action bar
- setHasOptionsMenu(true);
-
- Context context = getActivity().getApplicationContext();
- mController = MessagingController.getInstance(context);
- messageCryptoPresenter = new MessageCryptoPresenter(messageCryptoMvpView);
- messageLoaderHelper = messageLoaderHelperFactory.createForMessageView(
- context, getLoaderManager(), getParentFragmentManager(), messageLoaderCallbacks);
- mInitialized = true;
- }
-
- @Override
- public void onResume() {
- super.onResume();
-
- messageCryptoPresenter.onResume();
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
-
- Activity activity = getActivity();
- boolean isChangingConfigurations = activity != null && activity.isChangingConfigurations();
- if (isChangingConfigurations) {
- messageLoaderHelper.onDestroyChangingConfigurations();
- return;
- }
-
- messageLoaderHelper.onDestroy();
- }
-
- @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);
-
- mMessageView = view.findViewById(R.id.message_view);
- mMessageView.setAttachmentCallback(this);
- mMessageView.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;
- }
- });
-
- mMessageView.setOnDownloadButtonClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- mMessageView.disableDownloadButton();
- messageLoaderHelper.downloadCompleteMessage();
- }
- });
-
- return view;
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
-
- Bundle arguments = getArguments();
- String messageReferenceString = arguments.getString(ARG_REFERENCE);
- MessageReference messageReference = MessageReference.parse(messageReferenceString);
-
- displayMessage(messageReference);
- }
-
- private void displayMessage(MessageReference messageReference) {
- mMessageReference = messageReference;
- Timber.d("MessageView displaying message %s", mMessageReference);
-
- mAccount = Preferences.getPreferences(getApplicationContext()).getAccount(mMessageReference.getAccountUuid());
- messageLoaderHelper.asyncStartOrResumeLoadingMessage(messageReference, null);
-
- mFragmentListener.updateMenu();
- }
-
- 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);
- }
- }
-
- private void showMessage(MessageViewInfo messageViewInfo) {
- hideKeyboard();
-
- boolean handledByCryptoPresenter = messageCryptoPresenter.maybeHandleShowMessage(
- mMessageView, mAccount, messageViewInfo);
- if (!handledByCryptoPresenter) {
- mMessageView.showMessage(mAccount, messageViewInfo);
- if (mAccount.isOpenPgpProviderConfigured()) {
- mMessageView.getMessageHeaderView().setCryptoStatusDisabled();
- } else {
- mMessageView.getMessageHeaderView().hideCryptoStatus();
- }
- }
-
- if (messageViewInfo.subject != null) {
- displaySubject(messageViewInfo.subject);
- }
- }
-
- private void displayHeaderForLoadingMessage(LocalMessage message) {
- boolean showStar = !isOutbox();
- mMessageView.setHeaders(message, mAccount, showStar);
- if (mAccount.isOpenPgpProviderConfigured()) {
- mMessageView.getMessageHeaderView().setCryptoStatusLoading();
- }
- displaySubject(message.getSubject());
- mFragmentListener.updateMenu();
- }
-
- private void displaySubject(String subject) {
- if (TextUtils.isEmpty(subject)) {
- subject = mContext.getString(R.string.general_no_subject);
- }
-
- mMessageView.setSubject(subject);
- }
-
- /**
- * 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);
- } else {
- 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);
- }
- }
-
- public void onRefile(Long dstFolderId) {
- if (dstFolderId == null || !mController.isMoveCapable(mAccount)) {
- 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 (dstFolderId.equals(mAccount.getSpamFolderId()) && K9.isConfirmSpam()) {
- destinationFolderId = dstFolderId;
- showDialog(R.id.dialog_confirm_spam);
- } else {
- refileMessage(dstFolderId);
- }
- }
-
- private void refileMessage(long dstFolderId) {
- long srcFolderId = mMessageReference.getFolderId();
- MessageReference messageToMove = mMessageReference;
- mFragmentListener.showNextMessageOrReturn();
- mController.moveMessage(mAccount, srcFolderId, messageToMove, dstFolderId);
- }
-
- public void onReply() {
- if (mMessage != null) {
- mFragmentListener.onReply(mMessage.makeMessageReference(), messageCryptoPresenter.getDecryptionResultForReply());
- }
- }
-
- public void onReplyAll() {
- if (mMessage != null) {
- mFragmentListener.onReplyAll(mMessage.makeMessageReference(), messageCryptoPresenter.getDecryptionResultForReply());
- }
- }
-
- public void onForward() {
- if (mMessage != null) {
- mFragmentListener.onForward(mMessage.makeMessageReference(), messageCryptoPresenter.getDecryptionResultForReply());
- }
- }
-
- public void onForwardAsAttachment() {
- if (mMessage != null) {
- mFragmentListener.onForwardAsAttachment(mMessage.makeMessageReference(), messageCryptoPresenter.getDecryptionResultForReply());
- }
- }
-
- public void onEditAsNewMessage() {
- if (mMessage != null) {
- mFragmentListener.onEditAsNewMessage(mMessage.makeMessageReference());
- }
- }
-
- 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);
- }
- }
-
- 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;
- }
-
- 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;
- }
-
- startRefileActivity(FolderOperation.COPY, ACTIVITY_CHOOSE_FOLDER_COPY);
- }
-
- public void onMoveToDrafts() {
- Account account = mAccount;
- long folderId = mMessageReference.getFolderId();
- List messages = Collections.singletonList(mMessageReference);
-
- mFragmentListener.showNextMessageOrReturn();
-
- mController.moveToDraftsFolder(account, folderId, messages);
- }
-
- public void onArchive() {
- onRefile(mAccount.getArchiveFolderId());
- }
-
- public void onSpam() {
- onRefile(mAccount.getSpamFolderId());
- }
-
- 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;
- } else {
- action = ChooseFolderActivity.Action.COPY;
- }
-
- Intent intent = ChooseFolderActivity.buildLaunchIntent(requireActivity(), action, accountUuid, currentFolderId,
- scrollToFolderId, false, mMessageReference);
-
- 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;
- }
-
- if ((requestCode & REQUEST_MASK_CRYPTO_PRESENTER) == REQUEST_MASK_CRYPTO_PRESENTER) {
- requestCode ^= REQUEST_MASK_CRYPTO_PRESENTER;
- messageCryptoPresenter.onActivityResult(requestCode, resultCode, data);
- }
- }
-
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
- if (resultCode != Activity.RESULT_OK) {
- return;
- }
-
- // Note: because fragments do not have a startIntentSenderForResult method, pending intent activities are
- // launched through the MessageList activity, and delivered back via onPendingIntentResult()
-
- 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;
- }
-
- 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);
-
- String shareTitle = getString(R.string.send_alternate_chooser_title);
- Intent chooserIntent = Intent.createChooser(shareIntent, shareTitle);
-
- startActivity(chooserIntent);
- }
- }
-
- public void onToggleRead() {
- if (mMessage != null && !isOutbox()) {
- mController.setFlag(mAccount, mMessage.getFolder().getDatabaseId(),
- Collections.singletonList(mMessage), Flag.SEEN, !mMessage.isSet(Flag.SEEN));
-
- mMessageView.setHeaders(mMessage, mAccount, true);
- mFragmentListener.updateMenu();
- }
- }
-
- private void setProgress(boolean enable) {
- if (mFragmentListener != null) {
- mFragmentListener.setProgress(enable);
- }
- }
-
- public void moveMessage(MessageReference reference, long folderId) {
- mController.moveMessage(mAccount, mMessageReference.getFolderId(), reference, folderId);
- }
-
- public void copyMessage(MessageReference reference, long folderId) {
- mController.copyMessage(mAccount, mMessageReference.getFolderId(), reference, folderId);
- }
-
- 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);
-
- 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.");
- }
-
- fragment.setTargetFragment(this, dialogId);
- fragment.show(getParentFragmentManager(), getDialogTag(dialogId));
- }
-
- private void removeDialog(int dialogId) {
- if (!isAdded()) {
- return;
- }
-
- FragmentManager fm = getParentFragmentManager();
-
- // 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();
-
- DialogFragment fragment = (DialogFragment) fm.findFragmentByTag(getDialogTag(dialogId));
-
- if (fragment != null) {
- fragment.dismissAllowingStateLoss();
- }
- }
-
- private String getDialogTag(int dialogId) {
- return String.format(Locale.US, "dialog-%d", dialogId);
- }
-
- public void zoom(KeyEvent event) {
- // mMessageView.zoom(event);
- }
-
- @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;
- }
- }
-
- @Override
- public void doNegativeClick(int dialogId) {
- /* do nothing */
- }
-
- @Override
- public void dialogCancelled(int dialogId) {
- /* do nothing */
- }
-
- /**
- * Get the {@link MessageReference} of the currently displayed message.
- */
- public MessageReference getMessageReference() {
- return mMessageReference;
- }
-
- public boolean isOutbox() {
- if (mMessage == null || mAccount == null) {
- return false;
- }
-
- long folderId = mMessage.getFolder().getDatabaseId();
- Long outboxFolderId = mAccount.getOutboxFolderId();
- return outboxFolderId != null && outboxFolderId == folderId;
- }
-
- public boolean isMessageRead() {
- return (mMessage != null) && mMessage.isSet(Flag.SEEN);
- }
-
- public boolean isCopyCapable() {
- return !isOutbox() && mController.isCopyCapable(mAccount);
- }
-
- public boolean isMoveCapable() {
- return !isOutbox() && mController.isMoveCapable(mAccount);
- }
-
- public boolean canMessageBeArchived() {
- Long archiveFolderId = mAccount.getArchiveFolderId();
- if (archiveFolderId == null) {
- return false;
- }
-
- return mMessageReference.getFolderId() != archiveFolderId;
- }
-
- public boolean canMessageBeMovedToSpam() {
- Long spamFolderId = mAccount.getSpamFolderId();
- if (spamFolderId == null) {
- return false;
- }
-
- return mMessageReference.getFolderId() != spamFolderId;
- }
-
- public boolean canMessageBeUnsubscribed() {
- return preferredUnsubscribeUri != null;
- }
-
- 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);
- }
- }
-
- public Context getApplicationContext() {
- return mContext;
- }
-
- public void disableAttachmentButtons(AttachmentViewInfo attachment) {
- // mMessageView.disableAttachmentButtons(attachment);
- }
-
- public void enableAttachmentButtons(AttachmentViewInfo attachment) {
- // mMessageView.enableAttachmentButtons(attachment);
- }
-
- public void runOnMainThread(Runnable runnable) {
- handler.post(runnable);
- }
-
- public void showAttachmentLoadingDialog() {
- // mMessageView.disableAttachmentButtons();
- 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();
- }
- });
- }
-
- public void refreshAttachmentThumbnail(AttachmentViewInfo attachment) {
- mMessageView.refreshAttachmentThumbnail(attachment);
- }
-
- private MessageCryptoMvpView messageCryptoMvpView = new MessageCryptoMvpView() {
- @Override
- public void redisplayMessage() {
- messageLoaderHelper.asyncReloadMessage();
- }
-
- @Override
- public void startPendingIntentForCryptoPresenter(IntentSender si, Integer requestCode, Intent fillIntent,
- int flagsMask, int flagValues, int extraFlags) throws SendIntentException {
- if (requestCode == null) {
- getActivity().startIntentSender(si, fillIntent, flagsMask, flagValues, extraFlags);
- return;
- }
-
- requestCode |= REQUEST_MASK_CRYPTO_PRESENTER;
- getActivity().startIntentSenderForResult(
- si, requestCode, 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
- public void restartMessageCryptoProcessing() {
- mMessageView.setToLoadingState();
- messageLoaderHelper.asyncRestartMessageCryptoProcessing();
- }
-
- @Override
- public void showCryptoConfigDialog() {
- AccountSettingsActivity.startCryptoSettings(getActivity(), mAccount.getUuid());
- }
- };
-
- @Override
- public void onClickShowSecurityWarning() {
- messageCryptoPresenter.onClickShowCryptoWarningDetails();
- }
-
- @Override
- public void onClickSearchKey() {
- messageCryptoPresenter.onClickSearchKey();
- }
-
- @Override
- public void onClickShowCryptoKey() {
- messageCryptoPresenter.onClickShowCryptoKey();
- }
-
- 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();
- void updateMenu();
- }
-
- public boolean isInitialized() {
- return mInitialized ;
- }
-
-
- private MessageLoaderCallbacks messageLoaderCallbacks = new MessageLoaderCallbacks() {
- @Override
- public void onMessageDataLoadFinished(LocalMessage message) {
- mMessage = message;
-
- displayHeaderForLoadingMessage(message);
- mMessageView.setToLoadingState();
- showProgressThreshold = null;
- }
-
- @Override
- public void onMessageDataLoadFailed() {
- Toast.makeText(getActivity(), 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
- public void 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
- 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
- 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
- public void startIntentSenderForMessageLoaderHelper(IntentSender si, int requestCode, Intent fillIntent,
- int flagsMask, int flagValues, int extraFlags) {
- 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!");
- }
- }
- };
-
-
- @Override
- public void onViewAttachment(AttachmentViewInfo attachment) {
- currentAttachmentViewInfo = attachment;
- getAttachmentController(attachment).viewAttachment();
- }
-
- @Override
- public void onSaveAttachment(final AttachmentViewInfo attachment) {
- 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);
-
- try {
- startActivityForResult(intent, REQUEST_CODE_CREATE_DOCUMENT);
- } catch (ActivityNotFoundException e) {
- 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 enum FolderOperation {
- COPY, MOVE
- }
-}
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
new file mode 100644
index 0000000000000000000000000000000000000000..9eec05e6f5314cc3ca2bd01237e4f27097609306
--- /dev/null
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt
@@ -0,0 +1,958 @@
+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.Menu
+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.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.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.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
+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 val generalSettingsManager: GeneralSettingsManager 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 var destinationFolderId: Long? = null
+ private lateinit var fragmentListener: MessageViewFragmentListener
+
+ private lateinit var account: Account
+ lateinit var messageReference: MessageReference
+
+ private var currentAttachmentViewInfo: AttachmentViewInfo? = null
+ private var isDeleteMenuItemDisabled: Boolean = false
+ private var wasMessageMarkedAsOpened: Boolean = false
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+
+ fragmentListener = try {
+ activity as MessageViewFragmentListener
+ } catch (e: ClassCastException) {
+ throw ClassCastException("This fragment must be attached to a MessageViewFragmentListener")
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setHasOptionsMenu(true)
+
+ 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,
+ loaderManager = loaderManager,
+ fragmentManager = parentFragmentManager,
+ callback = messageLoaderCallbacks
+ )
+ }
+
+ 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)
+
+ val view = layoutInflater.inflate(R.layout.message, container, false)
+ messageTopView = view.findViewById(R.id.message_view)
+
+ initializeMessageTopView(messageTopView)
+
+ return view
+ }
+
+ private fun initializeMessageTopView(messageTopView: MessageTopView) {
+ messageTopView.setAttachmentCallback(this)
+ messageTopView.setMessageCryptoPresenter(messageCryptoPresenter)
+
+ messageTopView.setOnToggleFlagClickListener {
+ onToggleFlagged()
+ }
+
+ messageTopView.setOnMenuItemClickListener { item ->
+ onReplyMenuItemClicked(item.itemId)
+ }
+
+ messageTopView.setOnDownloadButtonClickListener {
+ onDownloadButtonClicked()
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ loadMessage(messageReference)
+ }
+
+ private fun loadMessage(messageReference: MessageReference) {
+ Timber.d("MessageViewFragment displaying message %s", messageReference)
+
+ account = accountManager.getAccount(messageReference.accountUuid)
+ ?: error("Account ${messageReference.accountUuid} not found")
+
+ messageLoaderHelper.asyncStartOrResumeLoadingMessage(messageReference, null)
+
+ 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()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+
+ if (requireActivity().isChangingConfigurations) {
+ messageLoaderHelper.onDestroyChangingConfigurations()
+ } else {
+ messageLoaderHelper.onDestroy()
+ }
+ }
+
+ override fun onPrepareOptionsMenu(menu: Menu) {
+ menu.findItem(R.id.delete).apply {
+ isVisible = K9.isMessageViewDeleteActionVisible
+ isEnabled = !isDeleteMenuItemDisabled
+ }
+
+ 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)
+ }
+ }
+
+ 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
+ menu.findItem(R.id.compose).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
+ }
+ }
+
+ 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()
+
+ val handledByCryptoPresenter = messageCryptoPresenter.maybeHandleShowMessage(
+ messageTopView, account, messageViewInfo
+ )
+
+ if (!handledByCryptoPresenter) {
+ messageTopView.showMessage(account, messageViewInfo)
+
+ if (account.isOpenPgpProviderConfigured) {
+ messageTopView.messageHeaderView.setCryptoStatusDisabled()
+ } else {
+ messageTopView.messageHeaderView.hideCryptoStatus()
+ }
+ }
+
+ if (messageViewInfo.subject != null) {
+ displaySubject(messageViewInfo.subject)
+ }
+ }
+
+ 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.subject)
+ invalidateMenu()
+ }
+
+ 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")
+ }
+
+ return true
+ }
+
+ private fun onDownloadButtonClicked() {
+ messageTopView.disableDownloadButton()
+ messageLoaderHelper.downloadCompleteMessage()
+ }
+
+ /**
+ * Called from UI thread when user select Delete
+ */
+ fun onDelete() {
+ val message = checkNotNull(message)
+
+ if (K9.isConfirmDelete || K9.isConfirmDeleteStarred && message.isSet(Flag.FLAGGED)) {
+ showDialog(R.id.dialog_confirm_delete)
+ } else {
+ delete()
+ }
+ }
+
+ private fun delete() {
+ disableDeleteMenuItem()
+
+ fragmentListener.showNextMessageOrReturn()
+
+ messagingController.deleteMessage(messageReference)
+ }
+
+ private fun disableDeleteMenuItem() {
+ isDeleteMenuItemDisabled = true
+ invalidateMenu()
+ }
+
+ private fun onRefile(destinationFolderId: Long?) {
+ if (destinationFolderId == null || !messagingController.isMoveCapable(account)) {
+ return
+ }
+
+ if (!messagingController.isMoveCapable(messageReference)) {
+ Toast.makeText(activity, R.string.move_copy_cannot_copy_unsynced_message, Toast.LENGTH_LONG).show()
+ return
+ }
+
+ if (destinationFolderId == account.spamFolderId && K9.isConfirmSpam) {
+ this.destinationFolderId = destinationFolderId
+ showDialog(R.id.dialog_confirm_spam)
+ } else {
+ refileMessage(destinationFolderId)
+ }
+ }
+
+ private fun refileMessage(destinationFolderId: Long) {
+ fragmentListener.showNextMessageOrReturn()
+
+ val sourceFolderId = messageReference.folderId
+ messagingController.moveMessage(account, sourceFolderId, messageReference, destinationFolderId)
+ }
+
+ fun onReply() {
+ val message = this.message ?: return
+
+ fragmentListener.onReply(
+ messageReference = message.makeMessageReference(),
+ decryptionResultForReply = messageCryptoPresenter.decryptionResultForReply
+ )
+ }
+
+ fun onReplyAll() {
+ val message = checkNotNull(this.message)
+
+ fragmentListener.onReplyAll(
+ messageReference = message.makeMessageReference(),
+ decryptionResultForReply = messageCryptoPresenter.decryptionResultForReply
+ )
+ }
+
+ fun onForward() {
+ val message = checkNotNull(this.message)
+
+ fragmentListener.onForward(
+ messageReference = message.makeMessageReference(),
+ decryptionResultForReply = messageCryptoPresenter.decryptionResultForReply
+ )
+ }
+
+ private fun onForwardAsAttachment() {
+ val message = checkNotNull(this.message)
+
+ fragmentListener.onForwardAsAttachment(
+ messageReference = message.makeMessageReference(),
+ decryptionResultForReply = messageCryptoPresenter.decryptionResultForReply
+ )
+ }
+
+ private fun onEditAsNewMessage() {
+ val message = checkNotNull(this.message)
+
+ fragmentListener.onEditAsNewMessage(message.makeMessageReference())
+ }
+
+ fun onMove() {
+ check(messagingController.isMoveCapable(account))
+ checkNotNull(message)
+
+ 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)
+ }
+
+ 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)
+ }
+
+ private fun onMoveToDrafts() {
+ fragmentListener.showNextMessageOrReturn()
+
+ val account = account
+ val folderId = messageReference.folderId
+ val messages = listOf(messageReference)
+ messagingController.moveToDraftsFolder(account, folderId, messages)
+ }
+
+ fun onArchive() {
+ onRefile(account.archiveFolderId)
+ }
+
+ private fun onSpam() {
+ onRefile(account.spamFolderId)
+ }
+
+ private fun startRefileActivity(operation: FolderOperation, requestCode: Int) {
+ val action = if (operation == FolderOperation.MOVE) {
+ ChooseFolderActivity.Action.MOVE
+ } else {
+ ChooseFolderActivity.Action.COPY
+ }
+
+ val intent = ChooseFolderActivity.buildLaunchIntent(
+ context = requireActivity(),
+ action = action,
+ accountUuid = account.uuid,
+ currentFolderId = messageReference.folderId,
+ scrollToFolderId = account.lastSelectedFolderId,
+ showDisplayableOnly = false,
+ messageReference = messageReference
+ )
+
+ startActivityForResult(intent, requestCode)
+ }
+
+ 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)
+ }
+ }
+
+ 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)
+ }
+ }
+
+ private fun onCreateDocumentResult(data: Intent?) {
+ if (data != null && data.data != null) {
+ createAttachmentController(currentAttachmentViewInfo).saveAttachmentTo(data.data)
+ }
+ }
+
+ private fun onChooseFolderMoveResult(data: Intent?) {
+ 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
+
+ account.setLastSelectedFolderId(destinationFolderId)
+
+ fragmentListener.showNextMessageOrReturn()
+
+ moveMessage(messageReference, destinationFolderId)
+ }
+
+ private fun onChooseFolderCopyResult(data: Intent?) {
+ 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
+
+ account.setLastSelectedFolderId(destinationFolderId)
+
+ copyMessage(messageReference, destinationFolderId)
+ }
+
+ private fun onSendAlternate() {
+ val message = checkNotNull(message)
+
+ val shareIntent = shareIntentBuilder.createShareIntent(message)
+ val shareTitle = getString(R.string.send_alternate_chooser_title)
+ val chooserIntent = Intent.createChooser(shareIntent, shareTitle)
+
+ startActivity(chooserIntent)
+ }
+
+ fun onToggleRead() {
+ toggleFlag(Flag.SEEN)
+ }
+
+ fun onToggleFlagged() {
+ toggleFlag(Flag.FLAGGED)
+ }
+
+ private fun toggleFlag(flag: Flag) {
+ check(!isOutbox)
+ val message = checkNotNull(this.message)
+
+ val newState = !message.isSet(flag)
+ messagingController.setFlag(account, message.folder.databaseId, listOf(message), flag, newState)
+
+ messageTopView.setHeaders(message, account, true)
+
+ invalidateMenu()
+ }
+
+ private fun moveMessage(reference: MessageReference?, folderId: Long) {
+ messagingController.moveMessage(account, messageReference.folderId, reference, folderId)
+ }
+
+ private fun copyMessage(reference: MessageReference?, folderId: Long) {
+ messagingController.copyMessage(account, messageReference.folderId, reference, folderId)
+ }
+
+ 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.")
+ }
+ }
+
+ fragment.setTargetFragment(this, dialogId)
+ fragment.show(parentFragmentManager, getDialogTag(dialogId))
+ }
+
+ private fun removeDialog(dialogId: Int) {
+ if (!isAdded) return
+
+ val fragmentManager = parentFragmentManager
+
+ // 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()
+
+ val fragment = fragmentManager.findFragmentByTag(getDialogTag(dialogId)) as DialogFragment?
+ fragment?.dismissAllowingStateLoss()
+ }
+
+ private fun getDialogTag(dialogId: Int): String {
+ return String.format(Locale.US, "dialog-%d", dialogId)
+ }
+
+ 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)
+
+ refileMessage(destinationFolderId)
+ this.destinationFolderId = null
+ }
+ }
+
+ override fun doNegativeClick(dialogId: Int) = Unit
+
+ override fun dialogCancelled(dialogId: Int) = Unit
+
+ private val isOutbox: Boolean
+ get() = messageReference.folderId == account.outboxFolderId
+
+ private val isMessageRead: Boolean
+ get() = message?.isSet(Flag.SEEN) == true
+
+ private val isCopyCapable: Boolean
+ get() = !isOutbox && messagingController.isCopyCapable(account)
+
+ private val isMoveCapable: Boolean
+ get() = !isOutbox && messagingController.isMoveCapable(account)
+
+ private fun canMessageBeArchived(): Boolean {
+ val archiveFolderId = account.archiveFolderId ?: return false
+ return messageReference.folderId != archiveFolderId
+ }
+
+ private fun canMessageBeMovedToSpam(): Boolean {
+ val spamFolderId = account.spamFolderId ?: return false
+ return messageReference.folderId != spamFolderId
+ }
+
+ private fun canMessageBeUnsubscribed(): Boolean {
+ return preferredUnsubscribeUri != null
+ }
+
+ private 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)
+ }
+
+ fun disableAttachmentButtons(attachment: AttachmentViewInfo?) = Unit
+
+ fun enableAttachmentButtons(attachment: AttachmentViewInfo?) = Unit
+
+ fun runOnMainThread(runnable: Runnable) {
+ requireActivity().runOnUiThread(runnable)
+ }
+
+ fun showAttachmentLoadingDialog() {
+ showDialog(R.id.dialog_attachment_progress)
+ }
+
+ fun hideAttachmentLoadingDialogOnMainThread() {
+ runOnMainThread {
+ removeDialog(R.id.dialog_attachment_progress)
+ }
+ }
+
+ fun refreshAttachmentThumbnail(attachment: AttachmentViewInfo?) {
+ 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()
+ }
+
+ @Throws(SendIntentException::class)
+ override fun startPendingIntentForCryptoPresenter(
+ intentSender: IntentSender,
+ requestCode: Int?,
+ fillIntent: Intent?,
+ flagsMask: Int,
+ flagValues: Int,
+ extraFlags: Int
+ ) {
+ if (requestCode == null) {
+ requireActivity().startIntentSender(intentSender, fillIntent, flagsMask, flagValues, extraFlags)
+ return
+ }
+
+ val maskedRequestCode = requestCode or REQUEST_MASK_CRYPTO_PRESENTER
+ requireActivity().startIntentSenderForResult(
+ intentSender, maskedRequestCode, fillIntent, flagsMask, flagValues, extraFlags
+ )
+ }
+
+ 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 fun restartMessageCryptoProcessing() {
+ messageTopView.setToLoadingState()
+ messageLoaderHelper.asyncRestartMessageCryptoProcessing()
+ }
+
+ override fun showCryptoConfigDialog() {
+ AccountSettingsActivity.startCryptoSettings(requireActivity(), account.uuid)
+ }
+ }
+
+ override fun onClickShowSecurityWarning() {
+ messageCryptoPresenter.onClickShowCryptoWarningDetails()
+ }
+
+ override fun onClickSearchKey() {
+ messageCryptoPresenter.onClickSearchKey()
+ }
+
+ override fun onClickShowCryptoKey() {
+ messageCryptoPresenter.onClickShowCryptoKey()
+ }
+
+ interface MessageViewFragmentListener {
+ fun onForward(messageReference: MessageReference, decryptionResultForReply: Parcelable?)
+ fun onForwardAsAttachment(messageReference: MessageReference, decryptionResultForReply: Parcelable?)
+ fun onEditAsNewMessage(messageReference: MessageReference)
+ 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
+
+ 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() {
+ Toast.makeText(activity, R.string.status_loading_error, Toast.LENGTH_LONG).show()
+ showProgressThreshold = null
+ }
+
+ override fun onMessageViewInfoLoadFinished(messageViewInfo: MessageViewInfo) {
+ showMessage(messageViewInfo)
+ preferredUnsubscribeUri = messageViewInfo.preferredUnsubscribeUri
+ showProgressThreshold = null
+ }
+
+ override fun onMessageViewInfoLoadFailed(messageViewInfo: MessageViewInfo) {
+ showMessage(messageViewInfo)
+ preferredUnsubscribeUri = null
+ showProgressThreshold = null
+ }
+
+ 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 fun onDownloadErrorMessageNotFound() {
+ messageTopView.enableDownloadButton()
+ Toast.makeText(requireContext(), R.string.status_invalid_id_error, Toast.LENGTH_LONG).show()
+ }
+
+ override fun onDownloadErrorNetworkError() {
+ messageTopView.enableDownloadButton()
+ Toast.makeText(requireContext(), R.string.status_network_error, Toast.LENGTH_LONG).show()
+ }
+
+ override fun startIntentSenderForMessageLoaderHelper(
+ intentSender: IntentSender,
+ requestCode: Int,
+ fillIntent: Intent?,
+ flagsMask: Int,
+ flagValues: Int,
+ extraFlags: Int
+ ) {
+ showProgressThreshold = null
+ try {
+ 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
+
+ createAttachmentController(attachment).viewAttachment()
+ }
+
+ override fun onSaveAttachment(attachment: AttachmentViewInfo) {
+ currentAttachmentViewInfo = attachment
+
+ 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 (e: ActivityNotFoundException) {
+ Toast.makeText(requireContext(), R.string.error_activity_not_found, Toast.LENGTH_LONG).show()
+ }
+ }
+
+ private fun createAttachmentController(attachment: AttachmentViewInfo?): AttachmentController {
+ return AttachmentController(requireContext(), messagingController, this, attachment)
+ }
+
+ private fun invalidateMenu() {
+ activity?.invalidateMenu()
+ }
+
+ 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 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
+
+ fun newInstance(reference: MessageReference): MessageViewFragment {
+ return MessageViewFragment().withArguments(
+ ARG_REFERENCE to reference.toIdentityString()
+ )
+ }
+ }
+}
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 0000000000000000000000000000000000000000..df6fe2c015c86d6cfed10f81e1b97a1fd0ed7662
--- /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/java/com/fsck/k9/view/MessageWebView.java b/app/ui/legacy/src/main/java/com/fsck/k9/view/MessageWebView.java
deleted file mode 100644
index 206785237fa09824a23419d4176408d0c56d1bb4..0000000000000000000000000000000000000000
--- a/app/ui/legacy/src/main/java/com/fsck/k9/view/MessageWebView.java
+++ /dev/null
@@ -1,156 +0,0 @@
-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.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;
-
-
-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.
- *
- */
- try {
- getSettings().setBlockNetworkLoads(shouldBlockNetworkData);
- } catch (SecurityException e) {
- Timber.e(e, "Failed to unblock network loads. Missing INTERNET permission?");
- }
- }
-
-
- /**
- * 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);
- }
-
- final WebSettings webSettings = this.getSettings();
-
- /* 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);
- }
-
- disableDisplayZoomControls();
-
- webSettings.setJavaScriptEnabled(false);
- webSettings.setLoadsImagesAutomatically(true);
- webSettings.setRenderPriority(RenderPriority.HIGH);
-
- // TODO: Review alternatives. NARROW_COLUMNS is deprecated on KITKAT
- webSettings.setLayoutAlgorithm(LayoutAlgorithm.NARROW_COLUMNS);
-
- setOverScrollMode(OVER_SCROLL_NEVER);
-
- webSettings.setTextZoom(config.getTextZoom());
-
- // 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);
-
- getSettings().setDisplayZoomControls(!supportsMultiTouch);
- }
-
- public void displayHtmlContentWithInlineAttachments(@NonNull String htmlText,
- @Nullable AttachmentResolver attachmentResolver, @Nullable OnPageFinishedListener onPageFinishedListener) {
- setWebViewClient(attachmentResolver, onPageFinishedListener);
- setHtmlContent(htmlText);
- }
-
- private void setWebViewClient(@Nullable AttachmentResolver attachmentResolver,
- @Nullable OnPageFinishedListener onPageFinishedListener) {
- K9WebViewClient webViewClient = K9WebViewClient.newInstance(attachmentResolver);
- if (onPageFinishedListener != null) {
- webViewClient.setOnPageFinishedListener(onPageFinishedListener);
- }
- setWebViewClient(webViewClient);
- }
-
- private void setHtmlContent(@NonNull String htmlText) {
- loadDataWithBaseURL("about:blank", htmlText, "text/html", "utf-8", null);
- 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/java/com/fsck/k9/view/MessageWebView.kt b/app/ui/legacy/src/main/java/com/fsck/k9/view/MessageWebView.kt
new file mode 100644
index 0000000000000000000000000000000000000000..fde796689ff6f096de00cd1ecb29376253de97c9
--- /dev/null
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/view/MessageWebView.kt
@@ -0,0 +1,99 @@
+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 {
+ 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
+
+ if (config.useDarkMode) {
+ setBackgroundColor(0xff000000L.toInt())
+ }
+
+ with(settings) {
+ setSupportZoom(true)
+ builtInZoomControls = true
+ useWideViewPort = true
+
+ if (config.autoFitWidth) {
+ loadWithOverviewMode = true
+ }
+
+ disableDisplayZoomControls()
+
+ javaScriptEnabled = false
+ loadsImagesAutomatically = true
+ setRenderPriority(RenderPriority.HIGH)
+
+ // TODO: Review alternatives. NARROW_COLUMNS is deprecated on KITKAT
+ layoutAlgorithm = LayoutAlgorithm.NARROW_COLUMNS
+
+ overScrollMode = OVER_SCROLL_NEVER
+
+ textZoom = config.textZoom
+ }
+
+ // Disable network images by default. This is overridden by preferences.
+ blockNetworkData(true)
+ }
+
+ private fun disableDisplayZoomControls() {
+ val packageManager = context.packageManager
+ val supportsMultiTouch = packageManager.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH) ||
+ packageManager.hasSystemFeature(PackageManager.FEATURE_FAKETOUCH_MULTITOUCH_DISTINCT)
+
+ settings.displayZoomControls = !supportsMultiTouch
+ }
+
+ fun displayHtmlContentWithInlineAttachments(
+ htmlText: String,
+ attachmentResolver: AttachmentResolver?,
+ onPageFinishedListener: OnPageFinishedListener?
+ ) {
+ setWebViewClient(attachmentResolver, onPageFinishedListener)
+ setHtmlContent(htmlText)
+ }
+
+ private fun setWebViewClient(
+ attachmentResolver: AttachmentResolver?,
+ onPageFinishedListener: OnPageFinishedListener?
+ ) {
+ val webViewClient = K9WebViewClient.newInstance(attachmentResolver)
+ if (onPageFinishedListener != null) {
+ webViewClient.setOnPageFinishedListener(onPageFinishedListener)
+ }
+ setWebViewClient(webViewClient)
+ }
+
+ private fun setHtmlContent(htmlText: String) {
+ loadDataWithBaseURL("about:blank", htmlText, "text/html", "utf-8", null)
+ resumeTimers()
+ }
+
+ fun interface OnPageFinishedListener {
+ fun onPageFinished()
+ }
+}
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 87bba8ff11814a9c90ac2c406909da6a65a9a02d..0000000000000000000000000000000000000000
--- a/app/ui/legacy/src/main/res/drawable/ic_chevron_left.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/app/ui/legacy/src/main/res/layout/message.xml b/app/ui/legacy/src/main/res/layout/message.xml
index 8deb75ec0690c371c4d22a4cc63c27df3034517e..c2e0590901b431d90b35728b305d33b27266b487 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_container.xml b/app/ui/legacy/src/main/res/layout/message_container.xml
index 1e3278c15e62dbb7c7137585ac040aa8058ed8f4..1a5db6d58c0d733d3f6f5f1cbb44b6e7aed24512 100644
--- a/app/ui/legacy/src/main/res/layout/message_container.xml
+++ b/app/ui/legacy/src/main/res/layout/message_container.xml
@@ -65,13 +65,6 @@
android:id="@+id/attachments_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:orientation="vertical">
-
-
-
+ android:orientation="vertical" />
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 0000000000000000000000000000000000000000..99ce488fa26d5d38243b36bcfa660e978035215b
--- /dev/null
+++ b/app/ui/legacy/src/main/res/layout/message_view_container.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
\ No newline at end of file
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 0049b3750304f432e59ec0bdc2b37cdb590dbc26..5aaa60b3daded7668d935996801d0813293d0d6b 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,45 +1,44 @@
-
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 d2e38b39c2e1c833af8c6010180cbbf39753efde..328f4cce3bd66d86abcd97ddaebc20ee615bfe63 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 @@
- Кыргыз
- Қазақ
- Македонски
+ - Русский
- Српски
- Українська
- Հայերեն
diff --git a/app/ui/legacy/src/main/res/values/attrs.xml b/app/ui/legacy/src/main/res/values/attrs.xml
index 0e2a9d7c286a3e6cd1ba1af7ff032f3f9be76f36..9c695e98122e35d8d059777ae94ba996b6982a71 100644
--- a/app/ui/legacy/src/main/res/values/attrs.xml
+++ b/app/ui/legacy/src/main/res/values/attrs.xml
@@ -20,8 +20,6 @@
-
-
diff --git a/app/ui/legacy/src/main/res/values/constants.xml b/app/ui/legacy/src/main/res/values/constants.xml
index c38ff525a075d5e6120d89066f3e0a3fef65b7ad..ce4bd8ea4fc14a29ae3d6269c2c41e745eb657d7 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/
Mail for Android
- https://github.com/k9mail/k-9/graphs/contributors
- https://github.com/k9mail/k-9
+ https://github.com/thundernest/k-9/graphs/contributorshttps://github.com/thundernest/k-9/graphs/contributors
+ https://gitlab.e.foundation/e/os/mail
https://www.apache.org/licenses/LICENSE-2.0
\@k9mail@fosstodon.org
https://fosstodon.org/@k9mail
diff --git a/app/ui/legacy/src/main/res/values/strings.xml b/app/ui/legacy/src/main/res/values/strings.xml
index 0b93288661f907f006583b0ebbacf7722fb8631c..6f5075db60958ef7153ae34087ea8011b49897f0 100644
--- a/app/ui/legacy/src/main/res/values/strings.xml
+++ b/app/ui/legacy/src/main/res/values/strings.xml
@@ -867,8 +867,6 @@
Confirm deletion
Do you want to delete 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
diff --git a/docs/google-play/full_description.txt b/docs/google-play/full_description.txt
index 9571ebdd9db60b52726c53ddd49b14d65275d64b..c51bb041981b74c922ed1988946bd3f2933ad4de 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/changelogs/33000.txt b/fastlane/metadata/android/en-US/changelogs/33000.txt
new file mode 100644
index 0000000000000000000000000000000000000000..8b5c3a67c0ee3a4accbaf35486b51c6f1dd5d3f2
--- /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
diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt
index 732e4f72723d080f64ed725d4848a7107a71cfd2..06bc26e2a8c6b1ad7e74efe8b554a537d52b803c 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/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 b4023de0f4167f8ef70fff7c4e76e55dde3fad4e..51e0505ac7fea2d0a9e71b8fb093ee7777372543 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 9508716b77de824036be59a6ee0f8838701c3304..c67605f645d7f8e0a958517e89cc8f3e88e85508 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 1fea9ae2ca0725c7ecf0a4c97c88b5daf62d548b..5b2251fc57fa44c070c69945673f8a694f22c962 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
}
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 e5b142f9c065a95621bee8e8658ac8f8c6920fd8..b62b3a0458e2dff8a75e8b5ce76bee25723a8a8c 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 {
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 73766b23eeab4e3038671d895aa070c493c69df6..cada3ad040713c065535628b16d18bfa7f72c92a 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"""), "")