diff --git a/app/core/src/main/java/com/fsck/k9/Account.kt b/app/core/src/main/java/com/fsck/k9/Account.kt index b32e628e704dbc9556c63c663db59a001eb5b236..c1e66a281915b85e8c0c3651f0037f17cf2a81a5 100644 --- a/app/core/src/main/java/com/fsck/k9/Account.kt +++ b/app/core/src/main/java/com/fsck/k9/Account.kt @@ -490,11 +490,6 @@ class Account(override val uuid: String) : BaseAccount { (oldSyncMode != FolderMode.NONE && syncMode == FolderMode.NONE) } - @Synchronized - fun incrementMessagesNotificationChannelVersion() { - messagesNotificationChannelVersion++ - } - @Synchronized fun isSortAscending(sortType: SortType): Boolean { return sortAscending.getOrPut(sortType) { sortType.isDefaultAscending } diff --git a/app/core/src/main/java/com/fsck/k9/AccountPreferenceSerializer.kt b/app/core/src/main/java/com/fsck/k9/AccountPreferenceSerializer.kt index 4052fd25482eaafb3c1e7f1cf18c4b4474e15410..51e3d1b793793b0df1034abfdee0e259aa24137d 100644 --- a/app/core/src/main/java/com/fsck/k9/AccountPreferenceSerializer.kt +++ b/app/core/src/main/java/com/fsck/k9/AccountPreferenceSerializer.kt @@ -142,9 +142,11 @@ class AccountPreferenceSerializer( isRingEnabled = storage.getBoolean("$accountUuid.ring", true), ringtone = storage.getString("$accountUuid.ringtone", DEFAULT_RINGTONE_URI), light = getEnumStringPref(storage, "$accountUuid.notificationLight", NotificationLight.Disabled), - isVibrateEnabled = storage.getBoolean("$accountUuid.vibrate", false), - vibratePattern = VibratePattern.deserialize(storage.getInt("$accountUuid.vibratePattern", 0)), - vibrateTimes = storage.getInt("$accountUuid.vibrateTimes", 5) + vibration = NotificationVibration( + isEnabled = storage.getBoolean("$accountUuid.vibrate", false), + pattern = VibratePattern.deserialize(storage.getInt("$accountUuid.vibratePattern", 0)), + repeatCount = storage.getInt("$accountUuid.vibrateTimes", 5) + ) ) } @@ -322,9 +324,9 @@ class AccountPreferenceSerializer( editor.putBoolean("$accountUuid.markMessageAsReadOnDelete", isMarkMessageAsReadOnDelete) editor.putBoolean("$accountUuid.alwaysShowCcBcc", isAlwaysShowCcBcc) - editor.putBoolean("$accountUuid.vibrate", notificationSettings.isVibrateEnabled) - editor.putInt("$accountUuid.vibratePattern", notificationSettings.vibratePattern.serialize()) - editor.putInt("$accountUuid.vibrateTimes", notificationSettings.vibrateTimes) + editor.putBoolean("$accountUuid.vibrate", notificationSettings.vibration.isEnabled) + editor.putInt("$accountUuid.vibratePattern", notificationSettings.vibration.pattern.serialize()) + editor.putInt("$accountUuid.vibrateTimes", notificationSettings.vibration.repeatCount) editor.putBoolean("$accountUuid.ring", notificationSettings.isRingEnabled) editor.putString("$accountUuid.ringtone", notificationSettings.ringtone) editor.putString("$accountUuid.notificationLight", notificationSettings.light.name) @@ -607,9 +609,7 @@ class AccountPreferenceSerializer( isRingEnabled = true, ringtone = DEFAULT_RINGTONE_URI, light = NotificationLight.Disabled, - isVibrateEnabled = false, - vibratePattern = VibratePattern.Default, - vibrateTimes = 5 + vibration = NotificationVibration.DEFAULT ) } diff --git a/app/core/src/main/java/com/fsck/k9/NotificationSettings.kt b/app/core/src/main/java/com/fsck/k9/NotificationSettings.kt index b94eec51b8ecfcad33e70900d2e005533b063876..5852e4b14d07e4d130263f8d358f662f60c5ac42 100644 --- a/app/core/src/main/java/com/fsck/k9/NotificationSettings.kt +++ b/app/core/src/main/java/com/fsck/k9/NotificationSettings.kt @@ -7,60 +7,5 @@ data class NotificationSettings( val isRingEnabled: Boolean = false, val ringtone: String? = null, val light: NotificationLight = NotificationLight.Disabled, - val isVibrateEnabled: Boolean = false, - val vibratePattern: VibratePattern = VibratePattern.Default, - val vibrateTimes: Int = 0 -) { - val vibrationPattern: LongArray - get() = getVibrationPattern(vibratePattern, vibrateTimes) - - companion object { - fun getVibrationPattern(vibratePattern: VibratePattern, times: Int): LongArray { - val selectedPattern = vibratePattern.vibrationPattern - val repeatedPattern = LongArray(selectedPattern.size * times) - for (n in 0 until times) { - System.arraycopy(selectedPattern, 0, repeatedPattern, n * selectedPattern.size, selectedPattern.size) - } - - // Do not wait before starting the vibration pattern. - repeatedPattern[0] = 0 - - return repeatedPattern - } - } -} - -enum class VibratePattern( - /** - * These are "off, on" patterns, specified in milliseconds. - */ - val vibrationPattern: LongArray -) { - Default(vibrationPattern = longArrayOf(300, 200)), - Pattern1(vibrationPattern = longArrayOf(100, 200)), - Pattern2(vibrationPattern = longArrayOf(100, 500)), - Pattern3(vibrationPattern = longArrayOf(200, 200)), - Pattern4(vibrationPattern = longArrayOf(200, 500)), - Pattern5(vibrationPattern = longArrayOf(500, 500)); - - fun serialize(): Int = when (this) { - Default -> 0 - Pattern1 -> 1 - Pattern2 -> 2 - Pattern3 -> 3 - Pattern4 -> 4 - Pattern5 -> 5 - } - - companion object { - fun deserialize(value: Int): VibratePattern = when (value) { - 0 -> Default - 1 -> Pattern1 - 2 -> Pattern2 - 3 -> Pattern3 - 4 -> Pattern4 - 5 -> Pattern5 - else -> error("Unknown VibratePattern value: $value") - } - } -} + val vibration: NotificationVibration = NotificationVibration.DEFAULT +) diff --git a/app/core/src/main/java/com/fsck/k9/NotificationVibration.kt b/app/core/src/main/java/com/fsck/k9/NotificationVibration.kt new file mode 100644 index 0000000000000000000000000000000000000000..0986baeff3c3406db4ef5aa03a91cca4d2ce0068 --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/NotificationVibration.kt @@ -0,0 +1,62 @@ +package com.fsck.k9 + +data class NotificationVibration( + val isEnabled: Boolean, + val pattern: VibratePattern, + val repeatCount: Int +) { + val systemPattern: LongArray + get() = getSystemPattern(pattern, repeatCount) + + companion object { + val DEFAULT = NotificationVibration(isEnabled = false, pattern = VibratePattern.Default, repeatCount = 5) + + fun getSystemPattern(vibratePattern: VibratePattern, repeatCount: Int): LongArray { + val selectedPattern = vibratePattern.vibrationPattern + val repeatedPattern = LongArray(selectedPattern.size * repeatCount) + for (n in 0 until repeatCount) { + System.arraycopy(selectedPattern, 0, repeatedPattern, n * selectedPattern.size, selectedPattern.size) + } + + // Do not wait before starting the vibration pattern. + repeatedPattern[0] = 0 + + return repeatedPattern + } + } +} + +enum class VibratePattern( + /** + * These are "off, on" patterns, specified in milliseconds. + */ + val vibrationPattern: LongArray +) { + Default(vibrationPattern = longArrayOf(300, 200)), + Pattern1(vibrationPattern = longArrayOf(100, 200)), + Pattern2(vibrationPattern = longArrayOf(100, 500)), + Pattern3(vibrationPattern = longArrayOf(200, 200)), + Pattern4(vibrationPattern = longArrayOf(200, 500)), + Pattern5(vibrationPattern = longArrayOf(500, 500)); + + fun serialize(): Int = when (this) { + Default -> 0 + Pattern1 -> 1 + Pattern2 -> 2 + Pattern3 -> 3 + Pattern4 -> 4 + Pattern5 -> 5 + } + + companion object { + fun deserialize(value: Int): VibratePattern = when (value) { + 0 -> Default + 1 -> Pattern1 + 2 -> Pattern2 + 3 -> Pattern3 + 4 -> Pattern4 + 5 -> Pattern5 + else -> error("Unknown VibratePattern value: $value") + } + } +} 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 21633eef31be19422e56321ae303f573805fbd69..8fc4f9c04efd7600cd4d35c76fcd629da4ab6e11 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 @@ -60,7 +60,6 @@ import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.FolderClass; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessageDownloadState; -import com.fsck.k9.mail.MessageRetrievalListener; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Part; import com.fsck.k9.mail.ServerSettings; @@ -401,56 +400,22 @@ public class MessagingController { /** * Find all messages in any local account which match the query 'query' */ - public void searchLocalMessages(final LocalSearch search, final MessagingListener listener) { - threadPool.execute(new Runnable() { - @Override - public void run() { - searchLocalMessagesSynchronous(search, listener); - } - }); - } - - @VisibleForTesting - void searchLocalMessagesSynchronous(final LocalSearch search, final MessagingListener listener) { + public List searchLocalMessages(final LocalSearch search) { List searchAccounts = getAccountsFromLocalSearch(search, preferences); + List messages = new ArrayList<>(); for (final Account account : searchAccounts) { - - // Collecting statistics of the search result - MessageRetrievalListener retrievalListener = new MessageRetrievalListener() { - @Override - public void messageStarted(String message, int number, int ofTotal) { - } - - @Override - public void messagesFinished(int number) { - } - - @Override - public void messageFinished(LocalMessage message, int number, int ofTotal) { - if (!isMessageSuppressed(message)) { - List messages = new ArrayList<>(); - - messages.add(message); - if (listener != null) { - listener.listLocalMessagesAddMessages(account, null, messages); - } - } - } - }; - - // build and do the query in the localstore try { LocalStore localStore = localStoreProvider.getInstance(account); - localStore.searchForMessages(retrievalListener, search); + List localMessages = localStore.searchForMessages(search); + + messages.addAll(localMessages); } catch (Exception e) { Timber.e(e); } } - if (listener != null) { - listener.listLocalMessagesFinished(); - } + return messages; } public Future searchRemoteMessages(String acctUuid, long folderId, String query, Set requiredFlags, @@ -575,7 +540,7 @@ public class MessagingController { if (localFolder.getVisibleLimit() > 0) { localFolder.setVisibleLimit(localFolder.getVisibleLimit() + account.getDisplayCount()); } - synchronizeMailbox(account, folderId, listener); + synchronizeMailbox(account, folderId, false, listener); } catch (MessagingException me) { throw new RuntimeException("Unable to set visible limit on folder", me); } @@ -584,9 +549,9 @@ public class MessagingController { /** * Start background synchronization of the specified folder. */ - public void synchronizeMailbox(Account account, long folderId, MessagingListener listener) { + public void synchronizeMailbox(Account account, long folderId, boolean notify, MessagingListener listener) { putBackground("synchronizeMailbox", listener, () -> - synchronizeMailboxSynchronous(account, folderId, listener, new NotificationState()) + synchronizeMailboxSynchronous(account, folderId, notify, listener, new NotificationState()) ); } @@ -596,7 +561,7 @@ public class MessagingController { final CountDownLatch latch = new CountDownLatch(1); putBackground("synchronizeMailbox", null, () -> { try { - synchronizeMailboxSynchronous(account, folderId, null, new NotificationState()); + synchronizeMailboxSynchronous(account, folderId, true, null, new NotificationState()); } finally { latch.countDown(); } @@ -609,19 +574,12 @@ public class MessagingController { } } - /** - * Start foreground synchronization of the specified folder. This is generally only called - * by synchronizeMailbox. - *

- * TODO Break this method up into smaller chunks. - */ - @VisibleForTesting - void synchronizeMailboxSynchronous(Account account, long folderId, MessagingListener listener, - NotificationState notificationState) { + private void synchronizeMailboxSynchronous(Account account, long folderId, boolean notify, + MessagingListener listener, NotificationState notificationState) { refreshFolderListIfStale(account); Backend backend = getBackend(account); - syncFolder(account, folderId, listener, backend, notificationState); + syncFolder(account, folderId, notify, listener, backend, notificationState); } private void refreshFolderListIfStale(Account account) { @@ -636,7 +594,7 @@ public class MessagingController { } } - private void syncFolder(Account account, long folderId, MessagingListener listener, Backend backend, + private void syncFolder(Account account, long folderId, boolean notify, MessagingListener listener, Backend backend, NotificationState notificationState) { ServerSettings serverSettings = account.getIncomingServerSettings(); if (serverSettings.isMissingCredentials()) { @@ -667,9 +625,14 @@ public class MessagingController { return; } - MessageStore messageStore = messageStoreManager.getMessageStore(account); - Long lastChecked = messageStore.getFolder(folderId, FolderDetailsAccessor::getLastChecked); - boolean suppressNotifications = lastChecked == null; + final boolean suppressNotifications; + if (notify) { + MessageStore messageStore = messageStoreManager.getMessageStore(account); + Long lastChecked = messageStore.getFolder(folderId, FolderDetailsAccessor::getLastChecked); + suppressNotifications = lastChecked == null; + } else { + suppressNotifications = true; + } String folderServerId = localFolder.getServerId(); SyncConfig syncConfig = createSyncConfig(account); @@ -1072,7 +1035,7 @@ public class MessagingController { Timber.i("Marking all messages in %s:%s as read", account, folderServerId); // TODO: Make this one database UPDATE operation - List messages = localFolder.getMessages(null, false); + List messages = localFolder.getMessages(false); for (Message message : messages) { if (!message.isSet(Flag.SEEN)) { message.setFlag(Flag.SEEN, true); @@ -1541,7 +1504,7 @@ public class MessagingController { long outboxFolderId = localFolder.getDatabaseId(); - List localMessages = localFolder.getMessages(null); + List localMessages = localFolder.getMessages(); int progress = 0; int todo = localMessages.size(); for (MessagingListener l : getListeners()) { @@ -2204,7 +2167,7 @@ public class MessagingController { // Remove all messages marked as deleted folder.destroyDeletedMessages(); - compact(account, null); + compact(account); } public void emptyTrash(final Account account, MessagingListener listener) { @@ -2283,7 +2246,7 @@ public class MessagingController { public boolean performPeriodicMailSync(Account account) { final CountDownLatch latch = new CountDownLatch(1); MutableBoolean syncError = new MutableBoolean(false); - checkMail(account, false, false, new SimpleMessagingListener() { + checkMail(account, false, false, true, new SimpleMessagingListener() { @Override public void checkMailFinished(Context context, Account account) { latch.countDown(); @@ -2319,10 +2282,8 @@ public class MessagingController { * Checks mail for one or multiple accounts. If account is null all accounts * are checked. */ - public void checkMail(final Account account, - final boolean ignoreLastCheckedTime, - final boolean useManualWakeLock, - final MessagingListener listener) { + public void checkMail(Account account, boolean ignoreLastCheckedTime, boolean useManualWakeLock, boolean notify, + MessagingListener listener) { final WakeLock wakeLock; if (useManualWakeLock) { @@ -2354,7 +2315,7 @@ public class MessagingController { } for (final Account account : accounts) { - checkMailForAccount(context, account, ignoreLastCheckedTime, listener); + checkMailForAccount(account, ignoreLastCheckedTime, notify, listener); } } catch (Exception e) { @@ -2381,9 +2342,8 @@ public class MessagingController { } - private void checkMailForAccount(final Context context, final Account account, - final boolean ignoreLastCheckedTime, - final MessagingListener listener) { + private void checkMailForAccount(Account account, boolean ignoreLastCheckedTime, boolean notify, + MessagingListener listener) { Timber.i("Synchronizing account %s", account); NotificationState notificationState = new NotificationState(); @@ -2405,28 +2365,14 @@ public class MessagingController { if (LocalFolder.isModeMismatch(aDisplayMode, fDisplayClass)) { // Never sync a folder that isn't displayed - /* - if (K9.DEBUG) { - Log.v(K9.LOG_TAG, "Not syncing folder " + folder.getName() + - " which is in display mode " + fDisplayClass + " while account is in display mode " + aDisplayMode); - } - */ - continue; } if (LocalFolder.isModeMismatch(aSyncMode, fSyncClass)) { // Do not sync folders in the wrong class - /* - if (K9.DEBUG) { - Log.v(K9.LOG_TAG, "Not syncing folder " + folder.getName() + - " which is in sync mode " + fSyncClass + " while account is in sync mode " + aSyncMode); - } - */ - continue; } - synchronizeFolder(account, folder, ignoreLastCheckedTime, listener, notificationState); + synchronizeFolder(account, folder, ignoreLastCheckedTime, notify, listener, notificationState); } } catch (MessagingException e) { Timber.e(e, "Unable to synchronize account %s", account); @@ -2450,14 +2396,14 @@ public class MessagingController { } private void synchronizeFolder(Account account, LocalFolder folder, boolean ignoreLastCheckedTime, - MessagingListener listener, NotificationState notificationState) { + boolean notify, MessagingListener listener, NotificationState notificationState) { putBackground("sync" + folder.getServerId(), null, () -> { - synchronizeFolderInBackground(account, folder, ignoreLastCheckedTime, listener, notificationState); + synchronizeFolderInBackground(account, folder, ignoreLastCheckedTime, notify, listener, notificationState); }); } private void synchronizeFolderInBackground(Account account, LocalFolder folder, boolean ignoreLastCheckedTime, - MessagingListener listener, NotificationState notificationState) { + boolean notify, MessagingListener listener, NotificationState notificationState) { Timber.v("Folder %s was last synced @ %tc", folder.getServerId(), folder.getLastChecked()); if (!ignoreLastCheckedTime) { @@ -2480,7 +2426,7 @@ public class MessagingController { try { showFetchingMailNotificationIfNecessary(account, folder); try { - synchronizeMailboxSynchronous(account, folder.getDatabaseId(), listener, notificationState); + synchronizeMailboxSynchronous(account, folder.getDatabaseId(), notify, listener, notificationState); } finally { showEmptyFetchingMailNotificationIfNecessary(account); } @@ -2505,21 +2451,13 @@ public class MessagingController { notificationController.clearFetchingMailNotification(account); } - public void compact(final Account account, final MessagingListener ml) { - putBackground("compact:" + account, ml, new Runnable() { - @Override - public void run() { - try { - MessageStore messageStore = messageStoreManager.getMessageStore(account); - long oldSize = messageStore.getSize(); - messageStore.compact(); - long newSize = messageStore.getSize(); - for (MessagingListener l : getListeners(ml)) { - l.accountSizeChanged(account, oldSize, newSize); - } - } catch (Exception e) { - Timber.e(e, "Failed to compact account %s", account); - } + public void compact(Account account) { + putBackground("compact:" + account, null, () -> { + try { + MessageStore messageStore = messageStoreManager.getMessageStore(account); + messageStore.compact(); + } catch (Exception e) { + Timber.e(e, "Failed to compact account %s", account); } }); } diff --git a/app/core/src/main/java/com/fsck/k9/controller/MessagingListener.java b/app/core/src/main/java/com/fsck/k9/controller/MessagingListener.java index 299f9e8dbd136289927683687d562bc9b9fe2d85..e0ea6f01f1dffb40f7beddc72e583089a0910cdf 100644 --- a/app/core/src/main/java/com/fsck/k9/controller/MessagingListener.java +++ b/app/core/src/main/java/com/fsck/k9/controller/MessagingListener.java @@ -13,11 +13,6 @@ import com.fsck.k9.mailstore.LocalMessage; public interface MessagingListener { - void accountSizeChanged(Account account, long oldSize, long newSize); - - void listLocalMessagesAddMessages(Account account, String folderServerId, List messages); - void listLocalMessagesFinished(); - void synchronizeMailboxStarted(Account account, long folderId); void synchronizeMailboxHeadersStarted(Account account, String folderServerId); void synchronizeMailboxHeadersProgress(Account account, String folderServerId, int completed, int total); diff --git a/app/core/src/main/java/com/fsck/k9/controller/SimpleMessagingListener.java b/app/core/src/main/java/com/fsck/k9/controller/SimpleMessagingListener.java index 6f2747cf2365e16445658b97dc386ab275761a8d..0f7c6b40981ac2688b9b19b01e7b822a52889585 100644 --- a/app/core/src/main/java/com/fsck/k9/controller/SimpleMessagingListener.java +++ b/app/core/src/main/java/com/fsck/k9/controller/SimpleMessagingListener.java @@ -9,22 +9,9 @@ import android.content.Context; import com.fsck.k9.Account; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.Part; -import com.fsck.k9.mailstore.LocalMessage; public abstract class SimpleMessagingListener implements MessagingListener { - @Override - public void accountSizeChanged(Account account, long oldSize, long newSize) { - } - - @Override - public void listLocalMessagesAddMessages(Account account, String folderServerId, List messages) { - } - - @Override - public void listLocalMessagesFinished() { - } - @Override public void synchronizeMailboxStarted(Account account, long folderId) { } diff --git a/app/core/src/main/java/com/fsck/k9/helper/CallbackFlowHelper.kt b/app/core/src/main/java/com/fsck/k9/helper/CallbackFlowHelper.kt index c04866115888f3788140c839bb129eba414e34a4..85bc379665919d303cf04023fef1d6b22faad516 100644 --- a/app/core/src/main/java/com/fsck/k9/helper/CallbackFlowHelper.kt +++ b/app/core/src/main/java/com/fsck/k9/helper/CallbackFlowHelper.kt @@ -2,15 +2,18 @@ package com.fsck.k9.helper import kotlinx.coroutines.channels.ClosedSendChannelException import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.channels.onClosed +import kotlinx.coroutines.channels.onFailure +import kotlinx.coroutines.channels.onSuccess import kotlinx.coroutines.channels.sendBlocking +import kotlinx.coroutines.channels.trySendBlocking /** * Like [sendBlocking], but ignores [ClosedSendChannelException]. */ fun SendChannel.sendBlockingSilently(element: E) { - try { - sendBlocking(element) - } catch (e: ClosedSendChannelException) { - // Ignore - } + trySendBlocking(element) + .onSuccess { } + .onFailure { } + .onClosed { } } diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/LocalFolder.java b/app/core/src/main/java/com/fsck/k9/mailstore/LocalFolder.java index 02b508d7c403b6cb13783f1ae02974ba51ae00b7..72441da924306714752ce6c6570af9120a3a253a 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/LocalFolder.java +++ b/app/core/src/main/java/com/fsck/k9/mailstore/LocalFolder.java @@ -576,17 +576,16 @@ public class LocalFolder { }); } - public List getMessages(MessageRetrievalListener listener) throws MessagingException { - return getMessages(listener, true); + public List getMessages() throws MessagingException { + return getMessages(true); } - public List getMessages(final MessageRetrievalListener listener, - final boolean includeDeleted) throws MessagingException { + public List getMessages(final boolean includeDeleted) throws MessagingException { return localStore.getDatabase().execute(false, new DbCallback>() { @Override public List doDbWork(final SQLiteDatabase db) throws MessagingException { open(); - return LocalFolder.this.localStore.getMessages(listener, LocalFolder.this, + return LocalFolder.this.localStore.getMessages(LocalFolder.this, "SELECT " + LocalStore.GET_MESSAGES_COLS + "FROM messages " + "LEFT JOIN message_parts ON (message_parts.id = messages.message_part_id) " + @@ -898,7 +897,7 @@ public class LocalFolder { public void setFlags(final Set flags, boolean value) throws MessagingException { open(); - for (LocalMessage message : getMessages(null)) { + for (LocalMessage message : getMessages()) { message.setFlags(flags, value); } } diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java b/app/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java index 773e2926eb6a8b062d9513927862d4d548e66c26..1fb427d6cd0550ac2da71b18d05e7686d2e3560c 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java +++ b/app/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java @@ -43,7 +43,6 @@ import com.fsck.k9.mail.FetchProfile.Item; import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.FolderClass; import com.fsck.k9.mail.FolderType; -import com.fsck.k9.mail.MessageRetrievalListener; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Multipart; import com.fsck.k9.mail.Part; @@ -359,9 +358,7 @@ public class LocalStore { }); } - public List searchForMessages(MessageRetrievalListener retrievalListener, - LocalSearch search) throws MessagingException { - + public List searchForMessages(LocalSearch search) throws MessagingException { StringBuilder query = new StringBuilder(); List queryArgs = new ArrayList<>(); SqlQueryBuilder.buildWhereClause(account, search.getConditions(), query, queryArgs); @@ -382,24 +379,20 @@ public class LocalStore { Timber.d("Query = %s", sqlQuery); - return getMessages(retrievalListener, null, sqlQuery, selectionArgs); + return getMessages(null, sqlQuery, selectionArgs); } /* * Given a query string, actually do the query for the messages and * call the MessageRetrievalListener for each one */ - List getMessages( - final MessageRetrievalListener listener, - final LocalFolder folder, - final String queryString, final String[] placeHolders - ) throws MessagingException { + List getMessages(LocalFolder folder, String queryString, String[] placeHolders) + throws MessagingException { final List messages = new ArrayList<>(); - final int j = database.execute(false, new DbCallback() { + database.execute(false, new DbCallback() { @Override - public Integer doDbWork(final SQLiteDatabase db) { + public Void doDbWork(final SQLiteDatabase db) { Cursor cursor = null; - int i = 0; try { cursor = db.rawQuery(queryString + " LIMIT 10", placeHolders); @@ -408,10 +401,6 @@ public class LocalStore { message.populateFromGetMessageCursor(cursor); messages.add(message); - if (listener != null) { - listener.messageFinished(message, i, -1); - } - i++; } cursor.close(); cursor = db.rawQuery(queryString + " LIMIT -1 OFFSET 10", placeHolders); @@ -421,22 +410,16 @@ public class LocalStore { message.populateFromGetMessageCursor(cursor); messages.add(message); - if (listener != null) { - listener.messageFinished(message, i, -1); - } - i++; } } catch (Exception e) { Timber.d(e, "Got an exception"); } finally { Utility.closeQuietly(cursor); } - return i; + + return null; } }); - if (listener != null) { - listener.messagesFinished(j); - } return Collections.unmodifiableList(messages); @@ -448,7 +431,7 @@ public class LocalStore { LocalSearch search = new LocalSearch(); search.and(SearchField.THREAD_ID, rootIdString, Attribute.EQUALS); - return searchForMessages(null, search); + return searchForMessages(search); } public AttachmentInfo getAttachmentInfo(final String attachmentId) throws MessagingException { diff --git a/app/core/src/main/java/com/fsck/k9/notification/BaseNotificationDataCreator.kt b/app/core/src/main/java/com/fsck/k9/notification/BaseNotificationDataCreator.kt index b07b78276b323e270c935681741d16e9bdaec600..9fe65d367cad4d34c8855699a536049d8970509a 100644 --- a/app/core/src/main/java/com/fsck/k9/notification/BaseNotificationDataCreator.kt +++ b/app/core/src/main/java/com/fsck/k9/notification/BaseNotificationDataCreator.kt @@ -41,7 +41,7 @@ internal class BaseNotificationDataCreator { private fun createNotificationAppearance(account: Account): NotificationAppearance { return with(account.notificationSettings) { - val vibrationPattern = if (isVibrateEnabled) vibrationPattern else null + val vibrationPattern = vibration.systemPattern.takeIf { vibration.isEnabled } NotificationAppearance(ringtone, vibrationPattern, account.notificationSettings.light.toColor(account)) } } diff --git a/app/core/src/main/java/com/fsck/k9/notification/CoreKoinModule.kt b/app/core/src/main/java/com/fsck/k9/notification/CoreKoinModule.kt index 970d3515b31efd6cbf95e1f2e47d1325617e743c..de6797b498682e2119d74ea859eac95eb604aee7 100644 --- a/app/core/src/main/java/com/fsck/k9/notification/CoreKoinModule.kt +++ b/app/core/src/main/java/com/fsck/k9/notification/CoreKoinModule.kt @@ -114,4 +114,13 @@ val coreNotificationModule = module { ) } factory { NotificationLightDecoder() } + factory { NotificationVibrationDecoder() } + factory { + NotificationConfigurationConverter(notificationLightDecoder = get(), notificationVibrationDecoder = get()) + } + factory { + NotificationSettingsUpdater( + preferences = get(), notificationChannelManager = get(), notificationConfigurationConverter = get() + ) + } } diff --git a/app/core/src/main/java/com/fsck/k9/notification/NewMailNotificationController.kt b/app/core/src/main/java/com/fsck/k9/notification/NewMailNotificationController.kt index 190144862113200f25529bceef710962cc23c44e..c29c7791f533c08c9c248d1d339111ba16b611df 100644 --- a/app/core/src/main/java/com/fsck/k9/notification/NewMailNotificationController.kt +++ b/app/core/src/main/java/com/fsck/k9/notification/NewMailNotificationController.kt @@ -27,7 +27,9 @@ internal class NewMailNotificationController( fun addNewMailNotification(account: Account, message: LocalMessage, silent: Boolean) { val notificationData = newMailNotificationManager.addNewMailNotification(account, message, silent) - processNewMailNotificationData(notificationData) + if (notificationData != null) { + processNewMailNotificationData(notificationData) + } } fun removeNewMailNotifications( diff --git a/app/core/src/main/java/com/fsck/k9/notification/NewMailNotificationManager.kt b/app/core/src/main/java/com/fsck/k9/notification/NewMailNotificationManager.kt index 8e1a8c3fe2645aacaaf5b48b7d083b458f862ffa..3edfdb6abb4a052246776bf94fc56dff03f8c613 100644 --- a/app/core/src/main/java/com/fsck/k9/notification/NewMailNotificationManager.kt +++ b/app/core/src/main/java/com/fsck/k9/notification/NewMailNotificationManager.kt @@ -38,10 +38,10 @@ internal class NewMailNotificationManager( ) } - fun addNewMailNotification(account: Account, message: LocalMessage, silent: Boolean): NewMailNotificationData { + fun addNewMailNotification(account: Account, message: LocalMessage, silent: Boolean): NewMailNotificationData? { val content = contentCreator.createFromMessage(account, message) - val result = notificationRepository.addNotification(account, content, timestamp = now()) + val result = notificationRepository.addNotification(account, content, timestamp = now()) ?: return null val singleNotificationData = createSingleNotificationData( account = account, diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationChannelManager.kt b/app/core/src/main/java/com/fsck/k9/notification/NotificationChannelManager.kt index 55b12338013d16fae44d0092d8704dba826ba4b2..ad33d10d281f1a7916b666967a7464e16f2ceb63 100644 --- a/app/core/src/main/java/com/fsck/k9/notification/NotificationChannelManager.kt +++ b/app/core/src/main/java/com/fsck/k9/notification/NotificationChannelManager.kt @@ -58,7 +58,7 @@ class NotificationChannelManager( accounts: List ) { for (account in accounts) { - val groupId = account.uuid + val groupId = account.notificationChannelGroupId val group = NotificationChannelGroup(groupId, account.displayName) val channelMessages = getChannelMessages(account) @@ -75,25 +75,13 @@ class NotificationChannelManager( notificationManager: NotificationManager, accounts: List ) { - val existingAccounts = HashMap() - for (account in accounts) { - existingAccounts[account.uuid] = account - } + val accountUuids = accounts.map { it.uuid }.toSet() val groups = notificationManager.notificationChannelGroups for (group in groups) { - val groupId = group.id - - var shouldDelete = false - if (!existingAccounts.containsKey(groupId)) { - shouldDelete = true - } else if (existingAccounts[groupId]?.displayName != group.name.toString()) { - // There is no way to change group names. Deleting group, so it is re-generated. - shouldDelete = true - } - - if (shouldDelete) { - notificationManager.deleteNotificationChannelGroup(groupId) + val accountUuid = group.id.toAccountUuid() + if (accountUuid !in accountUuids) { + notificationManager.deleteNotificationChannelGroup(group.id) } } } @@ -116,16 +104,15 @@ class NotificationChannelManager( @RequiresApi(api = Build.VERSION_CODES.O) private fun getChannelMessages(account: Account): NotificationChannel { val channelName = resourceProvider.messagesChannelName - val channelDescription = resourceProvider.messagesChannelDescription val channelId = getChannelIdFor(account, ChannelType.MESSAGES) val importance = NotificationManager.IMPORTANCE_DEFAULT - val channelGroupId = account.uuid - val messagesChannel = NotificationChannel(channelId, channelName, importance) - messagesChannel.description = channelDescription - messagesChannel.group = channelGroupId + return NotificationChannel(channelId, channelName, importance).apply { + description = resourceProvider.messagesChannelDescription + group = account.uuid - return messagesChannel + setPropertiesFrom(account) + } } @RequiresApi(api = Build.VERSION_CODES.O) @@ -145,12 +132,16 @@ class NotificationChannelManager( fun getChannelIdFor(account: Account, channelType: ChannelType): String { return if (channelType == ChannelType.MESSAGES) { - "messages_channel_${account.uuid}${account.messagesNotificationChannelSuffix}" + getMessagesChannelId(account, account.messagesNotificationChannelSuffix) } else { "miscellaneous_channel_${account.uuid}" } } + private fun getMessagesChannelId(account: Account, suffix: String): String { + return "messages_channel_${account.uuid}$suffix" + } + @RequiresApi(Build.VERSION_CODES.O) fun getNotificationConfiguration(account: Account): NotificationConfiguration { val channelId = getChannelIdFor(account, ChannelType.MESSAGES) @@ -176,11 +167,8 @@ class NotificationChannelManager( return } - notificationManager.deleteNotificationChannel(oldChannelId) - - account.incrementMessagesNotificationChannelVersion() - - val newChannelId = getChannelIdFor(account, ChannelType.MESSAGES) + val newChannelVersion = account.messagesNotificationChannelVersion + 1 + val newChannelId = getMessagesChannelId(account, "_$newChannelVersion") val channelName = resourceProvider.messagesChannelName val importance = oldNotificationChannel.importance @@ -189,13 +177,18 @@ class NotificationChannelManager( group = account.uuid copyPropertiesFrom(oldNotificationChannel) - copyPropertiesFrom(account) + setPropertiesFrom(account) } Timber.v("Recreating NotificationChannel(%s => %s)", oldChannelId, newChannelId) Timber.v("Old NotificationChannel: %s", oldNotificationChannel) Timber.v("New NotificationChannel: %s", newNotificationChannel) notificationManager.createNotificationChannel(newNotificationChannel) + + // To avoid a race condition we first create the new NotificationChannel, point the Account to it, + // then delete the old one. + account.messagesNotificationChannelVersion = newChannelVersion + notificationManager.deleteNotificationChannel(oldChannelId) } @RequiresApi(Build.VERSION_CODES.O) @@ -208,8 +201,8 @@ class NotificationChannelManager( val notificationSettings = account.notificationSettings return sound == notificationSettings.ringtoneUri && systemLight == notificationSettings.light && - shouldVibrate() == notificationSettings.isVibrateEnabled && - vibrationPattern.contentEquals(notificationSettings.vibrationPattern) + shouldVibrate() == notificationSettings.vibration.isEnabled && + vibrationPattern.contentEquals(notificationSettings.vibration.systemPattern) } @RequiresApi(Build.VERSION_CODES.O) @@ -226,7 +219,7 @@ class NotificationChannelManager( } @RequiresApi(Build.VERSION_CODES.O) - private fun NotificationChannel.copyPropertiesFrom(account: Account) { + private fun NotificationChannel.setPropertiesFrom(account: Account) { val notificationSettings = account.notificationSettings if (notificationSettings.isRingEnabled) { @@ -239,10 +232,15 @@ class NotificationChannelManager( val isLightEnabled = notificationSettings.light != NotificationLight.Disabled enableLights(isLightEnabled) - vibrationPattern = notificationSettings.vibrationPattern - enableVibration(notificationSettings.isVibrateEnabled) + vibrationPattern = notificationSettings.vibration.systemPattern + enableVibration(notificationSettings.vibration.isEnabled) } + private val Account.notificationChannelGroupId: String + get() = uuid + + private fun String.toAccountUuid(): String = this + private val Account.messagesNotificationChannelSuffix: String get() = messagesNotificationChannelVersion.let { version -> if (version == 0) "" else "_$version" } diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationConfigurationConverter.kt b/app/core/src/main/java/com/fsck/k9/notification/NotificationConfigurationConverter.kt new file mode 100644 index 0000000000000000000000000000000000000000..3f40015f9d6a324509d080ddb2f865530d310497 --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/notification/NotificationConfigurationConverter.kt @@ -0,0 +1,32 @@ +package com.fsck.k9.notification + +import com.fsck.k9.Account +import com.fsck.k9.NotificationSettings + +/** + * Converts the [NotificationConfiguration] read from a `NotificationChannel` into a [NotificationSettings] instance. + */ +class NotificationConfigurationConverter( + private val notificationLightDecoder: NotificationLightDecoder, + private val notificationVibrationDecoder: NotificationVibrationDecoder +) { + fun convert(account: Account, notificationConfiguration: NotificationConfiguration): NotificationSettings { + val light = notificationLightDecoder.decode( + isBlinkLightsEnabled = notificationConfiguration.isBlinkLightsEnabled, + lightColor = notificationConfiguration.lightColor, + accountColor = account.chipColor + ) + + val vibration = notificationVibrationDecoder.decode( + isVibrationEnabled = notificationConfiguration.isVibrationEnabled, + systemPattern = notificationConfiguration.vibrationPattern + ) + + return NotificationSettings( + isRingEnabled = notificationConfiguration.sound != null, + ringtone = notificationConfiguration.sound?.toString(), + light = light, + vibration = vibration + ) + } +} diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationData.kt b/app/core/src/main/java/com/fsck/k9/notification/NotificationData.kt index c50985022b2d6637b5d28276eb9dc1e0d1823a02..ab6f36399580c4ec57b0b7616f579bdebe50d17c 100644 --- a/app/core/src/main/java/com/fsck/k9/notification/NotificationData.kt +++ b/app/core/src/main/java/com/fsck/k9/notification/NotificationData.kt @@ -17,7 +17,6 @@ internal data class NotificationData( val isSingleMessageNotification: Boolean get() = activeNotifications.size == 1 - @OptIn(ExperimentalStdlibApi::class) val messageReferences: List get() { return buildList(capacity = newMessagesCount) { 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 50cd1be99e1d0301032b7cca8a0f4f803f4c71a2..db99f40340ec52f42ee475fa4ad315094611ecda 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 @@ -2,7 +2,6 @@ package com.fsck.k9.notification import com.fsck.k9.Account import com.fsck.k9.controller.MessageReference -import com.fsck.k9.core.BuildConfig internal const val MAX_NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS = 8 @@ -30,15 +29,45 @@ internal class NotificationDataStore { } @Synchronized - fun addNotification(account: Account, content: NotificationContent, timestamp: Long): AddNotificationResult { + fun addNotification(account: Account, content: NotificationContent, timestamp: Long): AddNotificationResult? { val notificationData = getNotificationData(account) val messageReference = content.messageReference - if (BuildConfig.DEBUG && notificationData.contains(messageReference)) { - throw AssertionError("Notification for message $messageReference already exists") + val activeNotification = notificationData.activeNotifications.firstOrNull { notificationHolder -> + notificationHolder.content.messageReference == messageReference } + val inactiveNotification = notificationData.inactiveNotifications.firstOrNull { inactiveNotificationHolder -> + inactiveNotificationHolder.content.messageReference == messageReference + } + + return if (activeNotification != null) { + val newActiveNotification = activeNotification.copy(content = content) + val notificationHolder = activeNotification.copy( + content = content + ) + + val operations = emptyList() + + val newActiveNotifications = notificationData.activeNotifications + .replace(activeNotification, newActiveNotification) + val newNotificationData = notificationData.copy( + activeNotifications = newActiveNotifications + ) + notificationDataMap[account.uuid] = newNotificationData + + AddNotificationResult.newNotification(newNotificationData, operations, notificationHolder) + } else if (inactiveNotification != null) { + val newInactiveNotification = inactiveNotification.copy(content = content) + val newInactiveNotifications = notificationData.inactiveNotifications + .replace(inactiveNotification, newInactiveNotification) - return if (notificationData.isMaxNumberOfActiveNotificationsReached) { + val newNotificationData = notificationData.copy( + inactiveNotifications = newInactiveNotifications + ) + notificationDataMap[account.uuid] = newNotificationData + + null + } else if (notificationData.isMaxNumberOfActiveNotificationsReached) { val lastNotificationHolder = notificationData.activeNotifications.last() val inactiveNotificationHolder = lastNotificationHolder.toInactiveNotificationHolder() @@ -175,14 +204,15 @@ internal class NotificationDataStore { throw AssertionError("getNewNotificationId() called with no free notification ID") } - private fun NotificationData.contains(messageReference: MessageReference): Boolean { - return activeNotifications.any { it.content.messageReference == messageReference } || - inactiveNotifications.any { it.content.messageReference == messageReference } - } - private fun NotificationHolder.toInactiveNotificationHolder() = InactiveNotificationHolder(timestamp, content) private fun InactiveNotificationHolder.toNotificationHolder(notificationId: Int): NotificationHolder { return NotificationHolder(notificationId, timestamp, content) } + + private fun List.replace(old: T, new: T): List { + return map { element -> + if (element === old) new else element + } + } } diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationRepository.kt b/app/core/src/main/java/com/fsck/k9/notification/NotificationRepository.kt index f624810c990bb45730f96bbe4d294d6c480f973e..ddad7bb965d3be14e3cb9ad18d20eb618c926172 100644 --- a/app/core/src/main/java/com/fsck/k9/notification/NotificationRepository.kt +++ b/app/core/src/main/java/com/fsck/k9/notification/NotificationRepository.kt @@ -37,8 +37,8 @@ internal class NotificationRepository( } @Synchronized - fun addNotification(account: Account, content: NotificationContent, timestamp: Long): AddNotificationResult { - return notificationDataStore.addNotification(account, content, timestamp).also { result -> + fun addNotification(account: Account, content: NotificationContent, timestamp: Long): AddNotificationResult? { + return notificationDataStore.addNotification(account, content, timestamp)?.also { result -> persistNotificationDataStoreChanges( account = account, operations = result.notificationStoreOperations, diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationSettingsUpdater.kt b/app/core/src/main/java/com/fsck/k9/notification/NotificationSettingsUpdater.kt new file mode 100644 index 0000000000000000000000000000000000000000..193e99c8724599df62230a9890cadc0b7cdb72cb --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/notification/NotificationSettingsUpdater.kt @@ -0,0 +1,34 @@ +package com.fsck.k9.notification + +import android.os.Build +import androidx.annotation.RequiresApi +import com.fsck.k9.Account +import com.fsck.k9.Preferences + +/** + * Update accounts with notification settings read from their "Messages" `NotificationChannel`. + */ +class NotificationSettingsUpdater( + private val preferences: Preferences, + private val notificationChannelManager: NotificationChannelManager, + private val notificationConfigurationConverter: NotificationConfigurationConverter +) { + fun updateNotificationSettings(accountUuids: Collection) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return + + accountUuids + .mapNotNull { accountUuid -> preferences.getAccount(accountUuid) } + .forEach { account -> updateNotificationSettings(account) } + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun updateNotificationSettings(account: Account) { + val notificationConfiguration = notificationChannelManager.getNotificationConfiguration(account) + val notificationSettings = notificationConfigurationConverter.convert(account, notificationConfiguration) + + if (notificationSettings != account.notificationSettings) { + account.updateNotificationSettings { notificationSettings } + preferences.saveAccount(account) + } + } +} diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationVibrationDecoder.kt b/app/core/src/main/java/com/fsck/k9/notification/NotificationVibrationDecoder.kt new file mode 100644 index 0000000000000000000000000000000000000000..554479b1f6de619eb6ee3403c7b9d57e1e508c57 --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/notification/NotificationVibrationDecoder.kt @@ -0,0 +1,26 @@ +package com.fsck.k9.notification + +import com.fsck.k9.NotificationVibration +import com.fsck.k9.VibratePattern + +/** + * Converts the vibration values read from a `NotificationChannel` into [NotificationVibration]. + */ +class NotificationVibrationDecoder { + fun decode(isVibrationEnabled: Boolean, systemPattern: List?): NotificationVibration { + if (systemPattern == null || systemPattern.size < 2 || systemPattern.size % 2 != 0) { + return NotificationVibration.DEFAULT + } + + val systemPatternArray = systemPattern.toLongArray() + val repeatCount = systemPattern.size / 2 + val pattern = VibratePattern.values() + .firstOrNull { vibratePattern -> + val testPattern = NotificationVibration.getSystemPattern(vibratePattern, repeatCount) + + testPattern.contentEquals(systemPatternArray) + } ?: VibratePattern.Default + + return NotificationVibration(isVibrationEnabled, pattern, repeatCount) + } +} diff --git a/app/core/src/main/java/com/fsck/k9/notification/SingleMessageNotificationDataCreator.kt b/app/core/src/main/java/com/fsck/k9/notification/SingleMessageNotificationDataCreator.kt index c9012752fbca9f0dfc46a3341371ff14e311d22c..d095ef00b63958aaf099083576d2c7638b74ec35 100644 --- a/app/core/src/main/java/com/fsck/k9/notification/SingleMessageNotificationDataCreator.kt +++ b/app/core/src/main/java/com/fsck/k9/notification/SingleMessageNotificationDataCreator.kt @@ -41,7 +41,6 @@ internal class SingleMessageNotificationDataCreator { ) } - @OptIn(ExperimentalStdlibApi::class) private fun createSingleNotificationActions(): List { return buildList { add(NotificationAction.Reply) @@ -53,7 +52,6 @@ internal class SingleMessageNotificationDataCreator { } } - @OptIn(ExperimentalStdlibApi::class) private fun createSingleNotificationWearActions(account: Account): List { return buildList { add(WearNotificationAction.Reply) diff --git a/app/core/src/main/java/com/fsck/k9/notification/SummaryNotificationDataCreator.kt b/app/core/src/main/java/com/fsck/k9/notification/SummaryNotificationDataCreator.kt index 77e1272826515e46aaccca190d9bc1e62f82b56d..6decadf15f468ea72143216274497d1991ab397f 100644 --- a/app/core/src/main/java/com/fsck/k9/notification/SummaryNotificationDataCreator.kt +++ b/app/core/src/main/java/com/fsck/k9/notification/SummaryNotificationDataCreator.kt @@ -43,7 +43,6 @@ internal class SummaryNotificationDataCreator( ) } - @OptIn(ExperimentalStdlibApi::class) private fun createSummaryNotificationActions(): List { return buildList { add(SummaryNotificationAction.MarkAsRead) @@ -54,7 +53,6 @@ internal class SummaryNotificationDataCreator( } } - @OptIn(ExperimentalStdlibApi::class) private fun createSummaryWearNotificationActions(account: Account): List { return buildList { add(SummaryWearNotificationAction.MarkAsRead) diff --git a/app/core/src/main/java/com/fsck/k9/preferences/KoinModule.kt b/app/core/src/main/java/com/fsck/k9/preferences/KoinModule.kt index 8d1ca4c88a633ac335888327a37734678521a2b3..0d92a2ad2c2491c9e45156bc512487e405f53f16 100644 --- a/app/core/src/main/java/com/fsck/k9/preferences/KoinModule.kt +++ b/app/core/src/main/java/com/fsck/k9/preferences/KoinModule.kt @@ -11,7 +11,8 @@ val preferencesModule = module { contentResolver = get(), preferences = get(), folderSettingsProvider = get(), - folderRepository = get() + folderRepository = get(), + notificationSettingsUpdater = get() ) } factory { FolderSettingsProvider(folderRepository = get()) } diff --git a/app/core/src/main/java/com/fsck/k9/preferences/SettingsExporter.kt b/app/core/src/main/java/com/fsck/k9/preferences/SettingsExporter.kt index 7362d9d7312acf88b30797575f2b084ff2ac641f..39983b13918858999acb1173a9d59011af00dff6 100644 --- a/app/core/src/main/java/com/fsck/k9/preferences/SettingsExporter.kt +++ b/app/core/src/main/java/com/fsck/k9/preferences/SettingsExporter.kt @@ -10,6 +10,7 @@ import com.fsck.k9.AccountPreferenceSerializer.Companion.IDENTITY_EMAIL_KEY import com.fsck.k9.AccountPreferenceSerializer.Companion.IDENTITY_NAME_KEY import com.fsck.k9.Preferences import com.fsck.k9.mailstore.FolderRepository +import com.fsck.k9.notification.NotificationSettingsUpdater import com.fsck.k9.preferences.ServerTypeConverter.fromServerSettingsType import com.fsck.k9.preferences.Settings.InvalidSettingValueException import com.fsck.k9.preferences.Settings.SettingsDescription @@ -24,10 +25,13 @@ class SettingsExporter( private val contentResolver: ContentResolver, private val preferences: Preferences, private val folderSettingsProvider: FolderSettingsProvider, - private val folderRepository: FolderRepository + private val folderRepository: FolderRepository, + private val notificationSettingsUpdater: NotificationSettingsUpdater ) { @Throws(SettingsImportExportException::class) fun exportToUri(includeGlobals: Boolean, accountUuids: Set, uri: Uri) { + updateNotificationSettings(accountUuids) + try { contentResolver.openOutputStream(uri)!!.use { outputStream -> exportPreferences(outputStream, includeGlobals, accountUuids) @@ -37,6 +41,16 @@ class SettingsExporter( } } + private fun updateNotificationSettings(accountUuids: Set) { + try { + notificationSettingsUpdater.updateNotificationSettings(accountUuids) + } catch (e: Exception) { + // An error here could mean we export notification settings that don't reflect the current configuration + // of the notification channels. But we prefer stale data over failing the export. + Timber.w(e, "Error while updating accounts with notification configuration from system") + } + } + @Throws(SettingsImportExportException::class) fun exportPreferences(outputStream: OutputStream, includeGlobals: Boolean, accountUuids: Set) { try { diff --git a/app/core/src/test/java/com/fsck/k9/controller/MessagingControllerTest.java b/app/core/src/test/java/com/fsck/k9/controller/MessagingControllerTest.java index 6fad0313b17fe185e8cf8da05f7f0f52c366047a..978788b78a074b5b5e173683530bebebb3c4578e 100644 --- a/app/core/src/test/java/com/fsck/k9/controller/MessagingControllerTest.java +++ b/app/core/src/test/java/com/fsck/k9/controller/MessagingControllerTest.java @@ -52,6 +52,7 @@ import org.mockito.stubbing.Answer; import org.robolectric.RuntimeEnvironment; import org.robolectric.shadows.ShadowLog; +import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.ArgumentMatchers.eq; @@ -178,32 +179,17 @@ public class MessagingControllerTest extends K9RobolectricTest { } @Test - public void searchLocalMessagesSynchronous_shouldCallSearchForMessagesOnLocalStore() - throws Exception { - when(search.searchAllAccounts()).thenReturn(true); - when(search.getAccountUuids()).thenReturn(new String[0]); - - controller.searchLocalMessagesSynchronous(search, listener); - - verify(localStore).searchForMessages(nullable(MessageRetrievalListener.class), eq(search)); - } - - @Test - public void searchLocalMessagesSynchronous_shouldNotifyWhenStoreFinishesRetrievingAMessage() + public void searchLocalMessages_shouldIgnoreExceptions() throws Exception { LocalMessage localMessage = mock(LocalMessage.class); when(localMessage.getFolder()).thenReturn(localFolder); when(search.searchAllAccounts()).thenReturn(true); when(search.getAccountUuids()).thenReturn(new String[0]); - when(localStore.searchForMessages(nullable(MessageRetrievalListener.class), eq(search))) - .thenThrow(new MessagingException("Test")); + when(localStore.searchForMessages(search)).thenThrow(new MessagingException("Test")); - controller.searchLocalMessagesSynchronous(search, listener); + List messages = controller.searchLocalMessages(search); - verify(localStore).searchForMessages(messageRetrievalListenerCaptor.capture(), eq(search)); - messageRetrievalListenerCaptor.getValue().messageFinished(localMessage, 1, 1); - verify(listener).listLocalMessagesAddMessages(eq(account), - eq((String) null), eq(Collections.singletonList(localMessage))); + assertThat(messages).isEmpty(); } private void setupRemoteSearch() throws Exception { @@ -419,7 +405,7 @@ public class MessagingControllerTest extends K9RobolectricTest { when(localStore.getFolder(SENT_FOLDER_ID)).thenReturn(sentFolder); when(sentFolder.getDatabaseId()).thenReturn(SENT_FOLDER_ID); when(localFolder.exists()).thenReturn(true); - when(localFolder.getMessages(null)).thenReturn(Collections.singletonList(localMessageToSend1)); + when(localFolder.getMessages()).thenReturn(Collections.singletonList(localMessageToSend1)); when(localMessageToSend1.getUid()).thenReturn("localMessageToSend1"); when(localMessageToSend1.getDatabaseId()).thenReturn(42L); when(localMessageToSend1.getHeader(K9.IDENTITY_HEADER)).thenReturn(new String[]{}); diff --git a/app/core/src/test/java/com/fsck/k9/message/html/HtmlHelper.kt b/app/core/src/test/java/com/fsck/k9/message/html/HtmlHelper.kt index d0725a4d6289c2710d946ebaf8d0341e50a795d7..2a31f49d3e4ce82ee783a2b32003ec430502a4c7 100644 --- a/app/core/src/test/java/com/fsck/k9/message/html/HtmlHelper.kt +++ b/app/core/src/test/java/com/fsck/k9/message/html/HtmlHelper.kt @@ -1,11 +1,11 @@ package com.fsck.k9.message.html import org.jsoup.Jsoup -import org.jsoup.safety.Whitelist as AllowList +import org.jsoup.safety.Safelist object HtmlHelper { @JvmStatic fun extractText(html: String): String { - return Jsoup.clean(html, AllowList.none()) + return Jsoup.clean(html, Safelist.none()) } } diff --git a/app/core/src/test/java/com/fsck/k9/notification/BaseNotificationDataCreatorTest.kt b/app/core/src/test/java/com/fsck/k9/notification/BaseNotificationDataCreatorTest.kt index f1520fc4970a8357af51e7cf2328c7e39aaa0002..65dea9c1e26d2565aed94c0f917a8821b2d38386 100644 --- a/app/core/src/test/java/com/fsck/k9/notification/BaseNotificationDataCreatorTest.kt +++ b/app/core/src/test/java/com/fsck/k9/notification/BaseNotificationDataCreatorTest.kt @@ -5,7 +5,7 @@ import com.fsck.k9.Identity import com.fsck.k9.K9 import com.fsck.k9.K9.LockScreenNotificationVisibility import com.fsck.k9.NotificationLight -import com.fsck.k9.NotificationSettings +import com.fsck.k9.NotificationVibration import com.fsck.k9.VibratePattern import com.google.common.truth.Truth.assertThat import org.junit.Test @@ -151,14 +151,20 @@ class BaseNotificationDataCreatorTest { @Test fun `vibration pattern`() { account.updateNotificationSettings { - it.copy(isVibrateEnabled = true, vibratePattern = VibratePattern.Pattern3, vibrateTimes = 2) + it.copy( + vibration = NotificationVibration( + isEnabled = true, + pattern = VibratePattern.Pattern3, + repeatCount = 2 + ) + ) } val notificationData = createNotificationData() val result = notificationDataCreator.createBaseNotificationData(notificationData) assertThat(result.appearance.vibrationPattern).isEqualTo( - NotificationSettings.getVibrationPattern(VibratePattern.Pattern3, 2) + NotificationVibration.getSystemPattern(VibratePattern.Pattern3, 2) ) } diff --git a/app/core/src/test/java/com/fsck/k9/notification/NewMailNotificationManagerTest.kt b/app/core/src/test/java/com/fsck/k9/notification/NewMailNotificationManagerTest.kt index 72e8a193d582b6c9ecb6cdc08c845efaea478352..105f49649b5f5d3e1a747370884a176683d5d4c3 100644 --- a/app/core/src/test/java/com/fsck/k9/notification/NewMailNotificationManagerTest.kt +++ b/app/core/src/test/java/com/fsck/k9/notification/NewMailNotificationManagerTest.kt @@ -49,6 +49,7 @@ class NewMailNotificationManagerTest { val result = manager.addNewMailNotification(account, message, silent = false) + assertNotNull(result) assertThat(result.singleNotificationData.first().content).isEqualTo( NotificationContent( messageReference = createMessageReference("msg-1"), @@ -85,6 +86,7 @@ class NewMailNotificationManagerTest { val result = manager.addNewMailNotification(account, messageTwo, silent = false) + assertNotNull(result) assertThat(result.singleNotificationData.first().content).isEqualTo( NotificationContent( messageReference = createMessageReference("msg-2"), @@ -121,6 +123,7 @@ class NewMailNotificationManagerTest { val result = manager.addNewMailNotification(account, message, silent = false) + assertNotNull(result) val notificationId = NotificationIds.getSingleMessageNotificationId(account, index = 0) assertThat(result.cancelNotificationIds).isEqualTo(listOf(notificationId)) assertThat(result.singleNotificationData.first().notificationId).isEqualTo(notificationId) @@ -196,6 +199,7 @@ class NewMailNotificationManagerTest { messageUid = "msg-2" ) val dataTwo = manager.addNewMailNotification(account, messageTwo, silent = true) + assertNotNull(dataTwo) val notificationIdTwo = dataTwo.singleNotificationData.first().notificationId val messageThree = addMessageToNotificationContentCreator( sender = "Alice", 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 a07463909649277ee11aa582f9191a90e1259d64..57ffc8aba4fa3cff55554af60b40bd6c73e343df 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 @@ -3,12 +3,8 @@ package com.fsck.k9.notification import com.fsck.k9.Account import com.fsck.k9.RobolectricTest import com.fsck.k9.controller.MessageReference -import com.fsck.k9.core.BuildConfig import com.google.common.truth.Truth.assertThat import kotlin.test.assertNotNull -import org.junit.Assert.fail -import org.junit.Assume.assumeFalse -import org.junit.Assume.assumeTrue import org.junit.Test private const val ACCOUNT_UUID = "1-2-3" @@ -26,6 +22,7 @@ class NotificationDataStoreTest : RobolectricTest() { val result = notificationDataStore.addNotification(account, content, TIMESTAMP) + assertNotNull(result) assertThat(result.shouldCancelNotification).isFalse() val holder = result.notificationHolder @@ -48,6 +45,7 @@ class NotificationDataStoreTest : RobolectricTest() { val result = notificationDataStore.addNotification(account, createNotificationContent("9"), TIMESTAMP) + assertNotNull(result) assertThat(result.shouldCancelNotification).isTrue() assertThat(result.cancelNotificationId).isEqualTo(NotificationIds.getSingleMessageNotificationId(account, 0)) } @@ -107,19 +105,23 @@ class NotificationDataStoreTest : RobolectricTest() { fun testNewMessagesCount() { val contentOne = createNotificationContent("1") val resultOne = notificationDataStore.addNotification(account, contentOne, TIMESTAMP) + assertNotNull(resultOne) assertThat(resultOne.notificationData.newMessagesCount).isEqualTo(1) val contentTwo = createNotificationContent("2") val resultTwo = notificationDataStore.addNotification(account, contentTwo, TIMESTAMP) + assertNotNull(resultTwo) assertThat(resultTwo.notificationData.newMessagesCount).isEqualTo(2) } @Test fun testIsSingleMessageNotification() { val resultOne = notificationDataStore.addNotification(account, createNotificationContent("1"), TIMESTAMP) + assertNotNull(resultOne) assertThat(resultOne.notificationData.isSingleMessageNotification).isTrue() val resultTwo = notificationDataStore.addNotification(account, createNotificationContent("2"), TIMESTAMP) + assertNotNull(resultTwo) assertThat(resultTwo.notificationData.isSingleMessageNotification).isFalse() } @@ -128,37 +130,58 @@ class NotificationDataStoreTest : RobolectricTest() { val content = createNotificationContent("1") val addResult = notificationDataStore.addNotification(account, content, TIMESTAMP) + assertNotNull(addResult) assertThat(addResult.notificationData.activeNotifications.first()).isEqualTo(addResult.notificationHolder) } @Test - fun `adding notification for same message twice should throw in debug build`() { - assumeTrue(BuildConfig.DEBUG) - + fun `adding notification for message with active notification should update notification`() { val content1 = createNotificationContent("1") val content2 = createNotificationContent("1") - notificationDataStore.addNotification(account, content1, TIMESTAMP) - try { - notificationDataStore.addNotification(account, content2, TIMESTAMP) - fail("Exception expected") - } catch (e: AssertionError) { - assertThat(e).hasMessageThat().matches("Notification for message .+ already exists") + val resultOne = notificationDataStore.addNotification(account, content1, TIMESTAMP) + val resultTwo = notificationDataStore.addNotification(account, content2, TIMESTAMP) + + assertNotNull(resultOne) + assertNotNull(resultTwo) + assertThat(resultTwo.notificationData.activeNotifications).hasSize(1) + assertThat(resultTwo.notificationData.activeNotifications.first().content).isSameInstanceAs(content2) + assertThat(resultTwo.notificationStoreOperations).isEmpty() + with(resultTwo.notificationHolder) { + assertThat(notificationId).isEqualTo(resultOne.notificationHolder.notificationId) + assertThat(timestamp).isEqualTo(resultOne.notificationHolder.timestamp) + assertThat(content).isSameInstanceAs(content2) } + assertThat(resultTwo.shouldCancelNotification).isFalse() } @Test - fun `adding notification for same message twice should add another notification in release build`() { - assumeFalse(BuildConfig.DEBUG) + fun `adding notification for message with inactive notification should update notificationData`() { + notificationDataStore.addNotification(account, createNotificationContent("1"), TIMESTAMP) + notificationDataStore.addNotification(account, createNotificationContent("2"), TIMESTAMP) + notificationDataStore.addNotification(account, createNotificationContent("3"), TIMESTAMP) + notificationDataStore.addNotification(account, createNotificationContent("4"), TIMESTAMP) + notificationDataStore.addNotification(account, createNotificationContent("5"), TIMESTAMP) + notificationDataStore.addNotification(account, createNotificationContent("6"), TIMESTAMP) + notificationDataStore.addNotification(account, createNotificationContent("7"), TIMESTAMP) + notificationDataStore.addNotification(account, createNotificationContent("8"), TIMESTAMP) + val latestNotificationContent = createNotificationContent("9") + notificationDataStore.addNotification(account, latestNotificationContent, TIMESTAMP) + val content = createNotificationContent("1") - val content1 = createNotificationContent("1") - val content2 = createNotificationContent("1") + val resultOne = notificationDataStore.addNotification(account, content, TIMESTAMP) - val addResult1 = notificationDataStore.addNotification(account, content1, TIMESTAMP) - val addResult2 = notificationDataStore.addNotification(account, content2, TIMESTAMP) + assertThat(resultOne).isNull() - assertThat(addResult1.notificationHolder.notificationId) - .isNotEqualTo(addResult2.notificationHolder.notificationId) + val resultTwo = notificationDataStore.removeNotifications(account) { + listOf(latestNotificationContent.messageReference) + } + + assertNotNull(resultTwo) + val notificationHolder = resultTwo.notificationData.activeNotifications.first { notificationHolder -> + notificationHolder.content.messageReference == content.messageReference + } + assertThat(notificationHolder.content).isSameInstanceAs(content) } private fun createAccount(): Account { diff --git a/app/core/src/test/java/com/fsck/k9/notification/SummaryNotificationDataCreatorTest.kt b/app/core/src/test/java/com/fsck/k9/notification/SummaryNotificationDataCreatorTest.kt index 216d20c1ae959926152dbfe2ea6482fef51f8ae5..906e6a00980201e02269a5b453e95bdf6c2bbef7 100644 --- a/app/core/src/test/java/com/fsck/k9/notification/SummaryNotificationDataCreatorTest.kt +++ b/app/core/src/test/java/com/fsck/k9/notification/SummaryNotificationDataCreatorTest.kt @@ -266,7 +266,6 @@ class SummaryNotificationDataCreatorTest { return NotificationData(account, activeNotifications, inactiveNotifications = emptyList()) } - @OptIn(ExperimentalStdlibApi::class) private fun createNotificationDataWithMultipleMessages(times: Int = 2): NotificationData { val contentList = buildList { repeat(times) { diff --git a/app/core/src/test/java/com/fsck/k9/preferences/SettingsExporterTest.kt b/app/core/src/test/java/com/fsck/k9/preferences/SettingsExporterTest.kt index b92e017ba347e0ca4f0e637474d8893e2b3d7fe2..e3177f825640fd79e8592cb4c355efb9550c16d4 100644 --- a/app/core/src/test/java/com/fsck/k9/preferences/SettingsExporterTest.kt +++ b/app/core/src/test/java/com/fsck/k9/preferences/SettingsExporterTest.kt @@ -11,6 +11,7 @@ import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Test import org.koin.core.component.inject +import org.mockito.kotlin.mock import org.robolectric.RuntimeEnvironment class SettingsExporterTest : K9RobolectricTest() { @@ -22,7 +23,8 @@ class SettingsExporterTest : K9RobolectricTest() { contentResolver, preferences, folderSettingsProvider, - folderRepository + folderRepository, + notificationSettingsUpdater = mock() ) @Test diff --git a/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt index 0720118d385f4e428fcabe9bd4b9426c0ba39a19..327d7f8061355dba52792b4f7de2d536bcdaeb32 100644 --- a/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt +++ b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt @@ -2,7 +2,7 @@ package app.k9mail.html.cleaner import org.jsoup.nodes.Document import org.jsoup.safety.Cleaner -import org.jsoup.safety.Whitelist as AllowList +import org.jsoup.safety.Safelist internal class BodyCleaner { private val cleaner: Cleaner @@ -12,7 +12,7 @@ internal class BodyCleaner { ) init { - val allowList = AllowList.relaxed() + val allowList = Safelist.relaxed() .addTags("font", "hr", "ins", "del", "center", "map", "area", "title") .addAttributes("font", "color", "face", "size") .addAttributes( diff --git a/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HeadCleaner.kt b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HeadCleaner.kt index 54b9920d16d52ac1a24d3b0f6e9ced24c287e445..d9ac70f810c7b9b7ac3dc705a5444b4ff682ea22 100644 --- a/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HeadCleaner.kt +++ b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HeadCleaner.kt @@ -54,12 +54,12 @@ internal class CleaningVisitor( if (source === elementToSkip) { elementToSkip = null } else if (source is Element && isSafeTag(source)) { - destination = destination.parent() + destination = destination.parent() ?: error("Missing parent") } } - private fun isSafeTag(node: Node): Boolean { - if (isMetaRefresh(node)) return false + private fun isSafeTag(node: Node?): Boolean { + if (node == null || isMetaRefresh(node)) return false val tag = node.nodeName().lowercase() return tag in ALLOWED_TAGS diff --git a/app/k9mail/build.gradle b/app/k9mail/build.gradle index 14375bd268a2452afb9ecc8f7845eca8e3ea9b03..940597d77a774a051f9c0e0e142259442d92472b 100644 --- a/app/k9mail/build.gradle +++ b/app/k9mail/build.gradle @@ -48,8 +48,8 @@ android { applicationId "foundation.e.mail" testApplicationId "foundation.e.mail.tests" - versionCode 29012 - versionName '5.912' + versionCode 29013 + versionName '5.913' // Keep in sync with the resource string array 'supported_languages' resConfigs "in", "br", "ca", "cs", "cy", "da", "de", "et", "en", "en_GB", "es", "eo", "eu", "fr", "gd", "gl", diff --git a/app/k9mail/src/main/java/com/fsck/k9/external/MessageProvider.java b/app/k9mail/src/main/java/com/fsck/k9/external/MessageProvider.java index b05091dc68d2f87d39a0d78a81872d15b0220733..8ef22f8e80a2fcf211383b46012c7bc7642df9f4 100644 --- a/app/k9mail/src/main/java/com/fsck/k9/external/MessageProvider.java +++ b/app/k9mail/src/main/java/com/fsck/k9/external/MessageProvider.java @@ -478,16 +478,12 @@ public class MessageProvider extends ContentProvider { } protected MatrixCursor getMessages(String[] projection) throws InterruptedException { - BlockingQueue> queue = new SynchronousQueue<>(); - // new code for integrated inbox, only execute this once as it will be processed afterwards via the listener SearchAccount integratedInboxAccount = SearchAccount.createUnifiedInboxAccount(); MessagingController msgController = MessagingController.getInstance(getContext()); - msgController.searchLocalMessages(integratedInboxAccount.getRelatedSearch(), - new MessageInfoHolderRetrieverListener(queue)); - - List holders = queue.take(); + List messages = msgController.searchLocalMessages(integratedInboxAccount.getRelatedSearch()); + List holders = convertToMessageInfoHolder(messages); // TODO add sort order parameter Collections.sort(holders, new ReverseDateComparator()); @@ -521,6 +517,20 @@ public class MessageProvider extends ContentProvider { return cursor; } + private List convertToMessageInfoHolder(List messages) { + List holders = new ArrayList<>(); + + Context context = getContext(); + for (LocalMessage message : messages) { + Account messageAccount = message.getAccount(); + MessageInfoHolder messageInfoHolder = MessageInfoHolder.create(context, message, messageAccount); + + holders.add(messageInfoHolder); + } + + return holders; + } + protected LinkedHashMap> resolveMessageExtractors( String[] projection, int count) { LinkedHashMap> extractors = new LinkedHashMap<>(); @@ -1033,38 +1043,4 @@ public class MessageProvider extends ContentProvider { return wrapped; } } - - /** - * Synchronized listener used to retrieve {@link MessageInfoHolder}s using a given {@link BlockingQueue}. - */ - protected class MessageInfoHolderRetrieverListener extends SimpleMessagingListener { - private final BlockingQueue> queue; - private List holders = new ArrayList<>(); - - - public MessageInfoHolderRetrieverListener(BlockingQueue> queue) { - this.queue = queue; - } - - @Override - public void listLocalMessagesAddMessages(Account account, String folderServerId, List messages) { - Context context = getContext(); - - for (LocalMessage message : messages) { - Account messageAccount = message.getAccount(); - MessageInfoHolder messageInfoHolder = MessageInfoHolder.create(context, message, messageAccount); - - holders.add(messageInfoHolder); - } - } - - @Override - public void listLocalMessagesFinished() { - try { - queue.put(holders); - } catch (InterruptedException e) { - Timber.e(e, "Unable to return message list back to caller"); - } - } - } } diff --git a/app/k9mail/src/main/res/values/themes.xml b/app/k9mail/src/main/res/values/themes.xml index 2f8e2d73ff7ffb25f2719f157ade7a8e5888460c..1951ae4cc9ace6dcbd430b08c53789e6549bc30c 100644 --- a/app/k9mail/src/main/res/values/themes.xml +++ b/app/k9mail/src/main/res/values/themes.xml @@ -33,7 +33,7 @@ @drawable/ic_send @drawable/ic_alert_octagon @drawable/ic_trash_can - @lineageos.platform:drawable/ic_attachment + @drawable/ic_messagelist_attachment @drawable/ic_archive @drawable/ic_pencil @drawable/ic_trash_can @@ -95,7 +95,7 @@ @color/color_default_secondary_text @color/color_default_divider @color/default_icon_color - @lineageos.platform:drawable/ic_attachment + @drawable/ic_messagelist_attachment @drawable/ic_messagelist_answered @drawable/ic_messagelist_forwarded @drawable/ic_messagelist_answered_forwarded @@ -159,7 +159,7 @@ @drawable/ic_send @drawable/ic_alert_octagon @drawable/ic_trash_can - @lineageos.platform:drawable/ic_attachment + @drawable/ic_messagelist_attachment @drawable/ic_archive @drawable/ic_pencil @drawable/ic_trash_can @@ -221,7 +221,7 @@ @color/color_default_secondary_text @color/color_default_divider #bbbbbb - @lineageos.platform:drawable/ic_attachment + @drawable/ic_messagelist_attachment @drawable/ic_messagelist_answered @drawable/ic_messagelist_forwarded @drawable/ic_messagelist_answered_forwarded diff --git a/app/ui/base/build.gradle b/app/ui/base/build.gradle index 6b94ff2b65c822963c0c1a44d42e06b049ef0c0a..97dc1f2bd12a2df1c5ad9b5323be47a0f3f65697 100644 --- a/app/ui/base/build.gradle +++ b/app/ui/base/build.gradle @@ -7,8 +7,8 @@ dependencies { api "androidx.appcompat:appcompat:${versions.androidxAppCompat}" api "androidx.activity:activity:${versions.androidxActivity}" api "com.google.android.material:material:${versions.materialComponents}" - api "androidx.navigation:navigation-fragment-ktx:${versions.androidxNavigation}" - api "androidx.navigation:navigation-ui-ktx:${versions.androidxNavigation}" + api "androidx.navigation:navigation-fragment:${versions.androidxNavigation}" + api "androidx.navigation:navigation-ui:${versions.androidxNavigation}" api "androidx.lifecycle:lifecycle-livedata-ktx:${versions.androidxLifecycle}" implementation "androidx.core:core-ktx:${versions.androidxCore}" diff --git a/app/ui/legacy/build.gradle b/app/ui/legacy/build.gradle index 57aec23a9c5ab3998847587cf2997e4fdf8553a7..a7504055da8a62c47ffe699dd9206f85558fae0e 100644 --- a/app/ui/legacy/build.gradle +++ b/app/ui/legacy/build.gradle @@ -32,10 +32,10 @@ dependencies { implementation "de.cketti.library.changelog:ckchangelog-core:2.0.0-beta02" implementation "com.splitwise:tokenautocomplete:3.0.2" implementation "de.cketti.safecontentresolver:safe-content-resolver-v21:1.0.0" - implementation 'com.mikepenz:materialdrawer:8.4.1' - implementation 'com.mikepenz:materialdrawer-iconics:8.3.3' + implementation 'com.mikepenz:materialdrawer:8.4.5' + implementation 'com.mikepenz:materialdrawer-iconics:8.4.5' implementation 'com.mikepenz:fontawesome-typeface:5.9.0.0-kotlin@aar' - implementation 'com.github.ByteHamster:SearchPreference:v2.0.0' + implementation 'com.github.ByteHamster:SearchPreference:v2.1.0' implementation "com.mikepenz:fastadapter:${versions.fastAdapter}" implementation "com.mikepenz:fastadapter-extensions-drag:${versions.fastAdapter}" implementation "com.mikepenz:fastadapter-extensions-utils:${versions.fastAdapter}" diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupCheckSettings.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupCheckSettings.java index 5980705a57f814ea993370ba4426150af9992e18..b8e2b1c173c9856b38fd988c26703392afb5b4b9 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupCheckSettings.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupCheckSettings.java @@ -542,7 +542,7 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList messagingController.refreshFolderListSynchronous(account); Long inboxFolderId = account.getInboxFolderId(); if (inboxFolderId != null) { - messagingController.synchronizeMailbox(account, inboxFolderId, null); + messagingController.synchronizeMailbox(account, inboxFolderId, false, null); } } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt index e84b4a9cefb0fd0cde4b49d51efe5b586ea138ec..246f6d9ef0286808eb25f3b95e5e99ebdf1bc616 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 @@ -1184,14 +1184,14 @@ class MessageListFragment : private fun checkMail() { if (isSingleAccountMode && isSingleFolderMode) { val folderId = currentFolder!!.databaseId - messagingController.synchronizeMailbox(account, folderId, activityListener) + messagingController.synchronizeMailbox(account, folderId, false, activityListener) messagingController.sendPendingMessages(account, activityListener) } else if (allAccounts) { - messagingController.checkMail(null, true, true, activityListener) + messagingController.checkMail(null, true, true, false, activityListener) } else { for (accountUuid in accountUuids) { val account = preferences.getAccount(accountUuid) - messagingController.checkMail(account, true, true, activityListener) + messagingController.checkMail(account, true, true, false, activityListener) } } } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/FlowExtensions.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/FlowExtensions.kt index 61689a8e5de118020667f655eae0e9ad4a33025e..d1c6e6267b40a3f79d1e7dcfa0af09fb8c71547f 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/FlowExtensions.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/FlowExtensions.kt @@ -5,13 +5,13 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.launch -fun Flow.observe(lifecycleOwner: LifecycleOwner, action: suspend (T) -> Unit) { +fun Flow.observe(lifecycleOwner: LifecycleOwner, collector: FlowCollector) { lifecycleOwner.lifecycleScope.launch { lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - collect(action) + collect(collector) } } } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/K9Drawer.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/K9Drawer.kt index 212a88aaa6717b723d99c0184e9042ea678c822f..35982a999c9d6984267f6fe177115508f63a194e 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/K9Drawer.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/K9Drawer.kt @@ -325,7 +325,7 @@ class K9Drawer(private val parent: MessageList, savedInstanceState: Bundle?) : K swipeRefreshLayout.setOnRefreshListener { val accountToRefresh = if (headerView.selectionListShown) null else account messagingController.checkMail( - accountToRefresh, true, true, + accountToRefresh, true, true, true, object : SimpleMessagingListener() { override fun checkMailFinished(context: Context?, account: Account?) { swipeRefreshLayout.post { diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/AccountSettingsDataStore.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/AccountSettingsDataStore.kt index 250964d0a8ecb2640e0bc2c970c3338eca400f34..9672c89366f77b4988225ef703094aab322eb576 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/AccountSettingsDataStore.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/AccountSettingsDataStore.kt @@ -4,6 +4,7 @@ import androidx.preference.PreferenceDataStore import com.fsck.k9.Account import com.fsck.k9.Account.SpecialFolderSelection import com.fsck.k9.NotificationLight +import com.fsck.k9.NotificationVibration import com.fsck.k9.Preferences import com.fsck.k9.job.K9JobManager import com.fsck.k9.notification.NotificationChannelManager @@ -263,20 +264,24 @@ class AccountSettingsDataStore( } private fun getCombinedVibrationValue(): String { - return VibrationPreference.encode( - isVibrationEnabled = account.notificationSettings.isVibrateEnabled, - vibratePattern = account.notificationSettings.vibratePattern, - vibrationTimes = account.notificationSettings.vibrateTimes - ) + return with(account.notificationSettings.vibration) { + VibrationPreference.encode( + isVibrationEnabled = isEnabled, + vibratePattern = pattern, + vibrationTimes = repeatCount + ) + } } private fun setCombinedVibrationValue(value: String) { val (isVibrationEnabled, vibrationPattern, vibrationTimes) = VibrationPreference.decode(value) account.updateNotificationSettings { notificationSettings -> notificationSettings.copy( - isVibrateEnabled = isVibrationEnabled, - vibratePattern = vibrationPattern, - vibrateTimes = vibrationTimes, + vibration = NotificationVibration( + isEnabled = isVibrationEnabled, + pattern = vibrationPattern, + repeatCount = vibrationTimes, + ) ) } notificationSettingsChanged = true diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/AccountSettingsFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/AccountSettingsFragment.kt index 8aceb476e1f71a4bc719f655cde4b36ce7a98b91..5c9ad9f150a49402803a30dcc1212f22822b0531 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/AccountSettingsFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/AccountSettingsFragment.kt @@ -29,6 +29,7 @@ import com.fsck.k9.mailstore.RemoteFolder import com.fsck.k9.notification.NotificationChannelManager import com.fsck.k9.notification.NotificationChannelManager.ChannelType import com.fsck.k9.notification.NotificationLightDecoder +import com.fsck.k9.notification.NotificationVibrationDecoder import com.fsck.k9.ui.R import com.fsck.k9.ui.endtoend.AutocryptKeyTransferActivity import com.fsck.k9.ui.settings.onClick @@ -52,6 +53,7 @@ class AccountSettingsFragment : PreferenceFragmentCompat(), ConfirmationDialogFr private val accountRemover: BackgroundAccountRemover by inject() private val notificationChannelManager: NotificationChannelManager by inject() private val notificationLightDecoder: NotificationLightDecoder by inject() + private val notificationVibrationDecoder: NotificationVibrationDecoder by inject() private val vibrator by lazy { requireContext().getSystemService() } private lateinit var dataStore: AccountSettingsDataStore @@ -101,7 +103,16 @@ class AccountSettingsFragment : PreferenceFragmentCompat(), ConfirmationDialogFr val account = getAccount() initializeCryptoSettings(account) - maybeUpdateNotificationPreferences(account) + // Don't update the notification preferences when resuming after the user has selected a new notification sound + // via NotificationSoundPreference. Otherwise we race the background thread and might read data from the old + // NotificationChannel, overwriting the notification sound with the previous value. + notificationSoundPreference?.let { notificationSoundPreference -> + if (notificationSoundPreference.receivedActivityResultJustNow) { + notificationSoundPreference.receivedActivityResultJustNow = false + } else { + maybeUpdateNotificationPreferences(account) + } + } } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -258,9 +269,14 @@ class AccountSettingsFragment : PreferenceFragmentCompat(), ConfirmationDialogFr } notificationVibrationPreference?.let { preference -> - preference.setVibrationFromSystem( + val notificationVibration = notificationVibrationDecoder.decode( isVibrationEnabled = notificationConfiguration.isVibrationEnabled, - combinedPattern = notificationConfiguration.vibrationPattern + systemPattern = notificationConfiguration.vibrationPattern + ) + preference.setVibration( + isVibrationEnabled = notificationVibration.isEnabled, + vibratePattern = notificationVibration.pattern, + vibrationTimes = notificationVibration.repeatCount ) preference.isEnabled = true } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/FolderListPreference.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/FolderListPreference.kt index c6ec7a0bffc08137cd0c53ccc9a302eede931790..8f6677ac5ce4366e12598f972b0734bbbad4fe03 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/FolderListPreference.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/FolderListPreference.kt @@ -65,7 +65,7 @@ constructor( isEnabled = true } - override fun getSummary(): CharSequence { + override fun getSummary(): CharSequence? { // While folders are being loaded the summary returned by ListPreference will be empty. This leads to the // summary view being hidden. Once folders are loaded the summary updates and the list height changes. This // adds quite a bit of visual clutter. We avoid that by returning a placeholder summary value. diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/NotificationSoundPreference.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/NotificationSoundPreference.kt index 7546f862af32c58883f84905e2604a9e5751d647..44050ee66997487f858b08c7105d40d4e562e415 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/NotificationSoundPreference.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/NotificationSoundPreference.kt @@ -27,6 +27,7 @@ constructor( ), defStyleRes: Int = 0 ) : Preference(context, attrs, defStyleAttr, defStyleRes), PreferenceActivityResultListener { + var receivedActivityResultJustNow = false fun setNotificationSound(sound: Uri?) { persistRingtone(sound) @@ -42,6 +43,7 @@ constructor( val uri = data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) if (callChangeListener(uri?.toString().orEmpty())) { + receivedActivityResultJustNow = true persistRingtone(uri) } } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/VibrationDialogFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/VibrationDialogFragment.kt index d978d2c3b0279fb017fe754c6eb2d67d91002db1..4ecedb8baaa0e043ded25745e86c0137bc251607 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/VibrationDialogFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/VibrationDialogFragment.kt @@ -16,7 +16,7 @@ import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.SwitchCompat import androidx.core.content.getSystemService import androidx.preference.PreferenceDialogFragmentCompat -import com.fsck.k9.NotificationSettings +import com.fsck.k9.NotificationVibration import com.fsck.k9.VibratePattern import com.fsck.k9.ui.R import com.fsck.k9.ui.getEnum @@ -81,7 +81,7 @@ class VibrationDialogFragment : PreferenceDialogFragmentCompat() { private fun playVibration() { val vibratePattern = adapter.vibratePattern val vibrationTimes = adapter.vibrationTimes - val vibrationPattern = NotificationSettings.getVibrationPattern(vibratePattern, vibrationTimes) + val vibrationPattern = NotificationVibration.getSystemPattern(vibratePattern, vibrationTimes) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val vibrationEffect = VibrationEffect.createWaveform(vibrationPattern, -1) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/VibrationPreference.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/VibrationPreference.kt index 825f75a853a302a5ee3ee6bcb571d1b55238196c..d8ea240f18131054c697bde44b3f17b904729eee 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/VibrationPreference.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/VibrationPreference.kt @@ -5,7 +5,6 @@ import android.content.Context import android.util.AttributeSet import androidx.core.content.res.TypedArrayUtils import androidx.preference.ListPreference -import com.fsck.k9.NotificationSettings import com.fsck.k9.VibratePattern import com.fsck.k9.ui.R import com.takisoft.preferencex.PreferenceFragmentCompat @@ -61,28 +60,6 @@ constructor( updateSummary() } - fun setVibrationFromSystem(isVibrationEnabled: Boolean, combinedPattern: List?) { - if (combinedPattern == null || combinedPattern.size < 2 || combinedPattern.size % 2 != 0) { - setVibration(isVibrationEnabled, DEFAULT_VIBRATE_PATTERN, DEFAULT_VIBRATION_TIMES) - return - } - - val combinedPatternArray = combinedPattern.toLongArray() - val vibrationTimes = combinedPattern.size / 2 - val vibrationPattern = entryValues.asSequence() - .map { entryValue -> - val serializedVibratePattern = entryValue.toString().toInt() - VibratePattern.deserialize(serializedVibratePattern) - } - .firstOrNull { vibratePattern -> - val testPattern = NotificationSettings.getVibrationPattern(vibratePattern, vibrationTimes) - - testPattern.contentEquals(combinedPatternArray) - } ?: DEFAULT_VIBRATE_PATTERN - - setVibration(isVibrationEnabled, vibrationPattern, vibrationTimes) - } - private fun updateSummary() { summary = if (isVibrationEnabled) { val index = entryValues.indexOf(vibratePattern.serialize().toString()) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsActivity.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsActivity.kt index 3aec73cd44b286eb55368f52cbc2e5c3f16416e7..35c4bfd22ed65df67b9aa755f7b610ed85d6a531 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsActivity.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsActivity.kt @@ -16,7 +16,6 @@ import com.fsck.k9.ui.R import com.fsck.k9.ui.base.K9Activity import com.fsck.k9.ui.fragmentTransaction import com.fsck.k9.ui.fragmentTransactionWithBackStack -import com.fsck.k9.ui.resolveColorAttribute class GeneralSettingsActivity : K9Activity(), OnPreferenceStartScreenCallback, SearchPreferenceResultListener { private lateinit var searchPreferenceActionView: SearchPreferenceActionView @@ -63,8 +62,7 @@ class GeneralSettingsActivity : K9Activity(), OnPreferenceStartScreenCallback, S replace(R.id.generalSettingsContainer, fragment) } - val accentColor = theme.resolveColorAttribute(R.attr.colorAccent) - result.highlight(fragment as PreferenceFragmentCompat, accentColor) + result.highlight(fragment as PreferenceFragmentCompat) } override fun onCreateOptionsMenu(menu: Menu): Boolean { diff --git a/app/ui/legacy/src/main/res/drawable/ic_messagelist_attachment.xml b/app/ui/legacy/src/main/res/drawable/ic_messagelist_attachment.xml index cd291b768487f4a83e45c654c9e666fafaa5e176..2b7070cde12e73ce41c83a60f4453b8fb2c478bc 100644 --- a/app/ui/legacy/src/main/res/drawable/ic_messagelist_attachment.xml +++ b/app/ui/legacy/src/main/res/drawable/ic_messagelist_attachment.xml @@ -1,10 +1,9 @@ + android:fillColor="@color/default_icon_color" + android:pathData="M18.5,16H7c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4h12.5c1.38,0 2.5,1.12 2.5,2.5S20.88,13 19.5,13H9c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h9.5V9.5H9c-1.38,0 -2.5,1.12 -2.5,2.5s1.12,2.5 2.5,2.5h10.5c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4H7c-3.04,0 -5.5,2.46 -5.5,5.5s2.46,5.5 5.5,5.5h11.5V16z" /> diff --git a/app/ui/legacy/src/main/res/drawable/ic_opened_envelope.xml b/app/ui/legacy/src/main/res/drawable/ic_opened_envelope.xml index 2d2b1557d1b39bdfdd1e8b99e17e6247da0eec49..6013d5ada9c5bcad0ce5572815ec871907c0a5d2 100644 --- a/app/ui/legacy/src/main/res/drawable/ic_opened_envelope.xml +++ b/app/ui/legacy/src/main/res/drawable/ic_opened_envelope.xml @@ -2,8 +2,7 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24" - android:viewportHeight="24" - android:tint="?android:attr/colorControlNormal"> + android:viewportHeight="24"> diff --git a/app/ui/legacy/src/main/res/raw/changelog_master.xml b/app/ui/legacy/src/main/res/raw/changelog_master.xml index e724cf1d0b8df5a39fe4826c754ee76fbaf6e905..0dc11a4e17f786080407bda3ee564e165275f474 100644 --- a/app/ui/legacy/src/main/res/raw/changelog_master.xml +++ b/app/ui/legacy/src/main/res/raw/changelog_master.xml @@ -5,6 +5,15 @@ Locale-specific versions are kept in res/raw-/changelog.xml. --> + + Don't create notifications when manually refreshing the message list + Fixed import and export of notification settings + Fixed bug that could lead to multiple notifications being created for the same message + Fixed bug that sometimes crashed the app after changing the notification sound + Added support for messages that specified the character set in multiple ways + A lot of internal improvements + Updated translations + Fixed bugs where notifications weren't removed when they should have been Added a setting to configure the notification sound on Android 8+ (because some vendor-specific Android versions removed this feature from their user interface) diff --git a/app/ui/legacy/src/main/res/values-fa/strings.xml b/app/ui/legacy/src/main/res/values-fa/strings.xml index df47d4acacde5e2fab2e1768814e5c4d6baced9e..cea4b18f07b90cab9931324e8208af87fcac5086 100644 --- a/app/ui/legacy/src/main/res/values-fa/strings.xml +++ b/app/ui/legacy/src/main/res/values-fa/strings.xml @@ -213,6 +213,7 @@ در صورت وجود، نام گیرنده را از لیست مخاطبان نمایش بده رنگی‌کردن مخاطبان نام‌های موجود در لیست مخاطبان رنگی شود + رنگ نام مخاطب قلم‌های تک‌عرض هنگام نمایش پیام‌های متنی از قلم تک‌عرض استفاده کن جاگیری خودکار پیام‌ها diff --git a/backend/imap/src/main/java/com/fsck/k9/backend/imap/CommandFetchMessage.kt b/backend/imap/src/main/java/com/fsck/k9/backend/imap/CommandFetchMessage.kt index 059fcd4a5cb59fd7ddeadd9430278de4bda8060f..9feb47492d1f4394e41f97c14966f5769de7bdb8 100644 --- a/backend/imap/src/main/java/com/fsck/k9/backend/imap/CommandFetchMessage.kt +++ b/backend/imap/src/main/java/com/fsck/k9/backend/imap/CommandFetchMessage.kt @@ -13,7 +13,7 @@ internal class CommandFetchMessage(private val imapStore: ImapStore) { folder.open(OpenMode.READ_WRITE) val message = folder.getMessage(messageServerId) - folder.fetchPart(message, part, null, bodyFactory, -1) + folder.fetchPart(message, part, bodyFactory, -1) } finally { folder.close() } diff --git a/backend/imap/src/main/java/com/fsck/k9/backend/imap/ImapSync.kt b/backend/imap/src/main/java/com/fsck/k9/backend/imap/ImapSync.kt index 05c40802dd7e5c0b74b438cea914f2bd04f1a412..8b67a6ea0dbd148f259e97aa752aba65fd72ff3d 100644 --- a/backend/imap/src/main/java/com/fsck/k9/backend/imap/ImapSync.kt +++ b/backend/imap/src/main/java/com/fsck/k9/backend/imap/ImapSync.kt @@ -13,8 +13,8 @@ import com.fsck.k9.mail.DefaultBodyFactory import com.fsck.k9.mail.FetchProfile import com.fsck.k9.mail.Flag import com.fsck.k9.mail.MessageDownloadState -import com.fsck.k9.mail.MessageRetrievalListener import com.fsck.k9.mail.internet.MessageExtractor +import com.fsck.k9.mail.store.imap.FetchListener import com.fsck.k9.mail.store.imap.ImapFolder import com.fsck.k9.mail.store.imap.ImapMessage import com.fsck.k9.mail.store.imap.ImapStore @@ -195,12 +195,11 @@ internal class ImapSync( /* * Now we download the actual content of messages. */ - val newMessages = downloadMessages( + downloadMessages( syncConfig, remoteFolder, backendFolder, remoteMessages, - false, highestKnownUid, listener ) @@ -212,13 +211,7 @@ internal class ImapSync( backendFolder.setLastChecked(System.currentTimeMillis()) backendFolder.setStatus(null) - Timber.d( - "Done synchronizing folder %s:%s @ %tc with %d new messages", - accountName, - folder, - System.currentTimeMillis(), - newMessages - ) + Timber.d("Done synchronizing folder %s:%s @ %tc", accountName, folder, System.currentTimeMillis()) listener.syncFinished(folder) @@ -266,7 +259,6 @@ internal class ImapSync( remoteFolder, backendFolder, listOf(remoteMessage), - false, null, SimpleSyncListener() ) @@ -284,34 +276,28 @@ internal class ImapSync( * The [BackendFolder] instance corresponding to the remote folder. * @param inputMessages * A list of messages objects that store the UIDs of which messages to download. - * @param flagSyncOnly - * Only flags will be fetched from the remote store if this is `true`. - * @return The number of downloaded messages that are not flagged as [Flag.SEEN]. */ private fun downloadMessages( syncConfig: SyncConfig, remoteFolder: ImapFolder, backendFolder: BackendFolder, inputMessages: List, - flagSyncOnly: Boolean, highestKnownUid: Long?, listener: SyncListener - ): Int { + ) { val folder = remoteFolder.serverId val syncFlagMessages = mutableListOf() var unsyncedMessages = mutableListOf() - val newMessages = AtomicInteger(0) + val downloadedMessageCount = AtomicInteger(0) val messages = inputMessages.toMutableList() for (message in messages) { evaluateMessageForDownload( message, backendFolder, - remoteFolder, unsyncedMessages, - syncFlagMessages, - flagSyncOnly + syncFlagMessages ) } @@ -333,10 +319,6 @@ internal class ImapSync( unsyncedMessages = unsyncedMessages.subList(0, visibleLimit) } - val fp = FetchProfile() - fp.add(FetchProfile.Item.FLAGS) - fp.add(FetchProfile.Item.ENVELOPE) - Timber.d("SYNC: About to fetch %d unsynced messages for folder %s", unsyncedMessages.size, folder) fetchUnsyncedMessages( @@ -347,7 +329,6 @@ internal class ImapSync( largeMessages, progress, todo, - fp, listener ) @@ -367,19 +348,14 @@ internal class ImapSync( * download of 625k. */ val maxDownloadSize = syncConfig.maximumAutoDownloadMessageSize - var fp = FetchProfile() // TODO: Only fetch small and large messages if we have some - fp.add(FetchProfile.Item.BODY) - // fp.add(FetchProfile.Item.FLAGS); - // fp.add(FetchProfile.Item.ENVELOPE); downloadSmallMessages( remoteFolder, backendFolder, smallMessages, progress, - newMessages, + downloadedMessageCount, todo, - fp, highestKnownUid, listener ) @@ -388,16 +364,13 @@ internal class ImapSync( /* * Now do the large messages that require more round trips. */ - fp = FetchProfile() - fp.add(FetchProfile.Item.STRUCTURE) downloadLargeMessages( remoteFolder, backendFolder, largeMessages, progress, - newMessages, + downloadedMessageCount, todo, - fp, highestKnownUid, listener, maxDownloadSize @@ -410,18 +383,14 @@ internal class ImapSync( */ refreshLocalMessageFlags(syncConfig, remoteFolder, backendFolder, syncFlagMessages, progress, todo, listener) - Timber.d("SYNC: Synced remote messages for folder %s, %d new messages", folder, newMessages.get()) - - return newMessages.get() + Timber.d("SYNC: Synced remote messages for folder %s, %d new messages", folder, downloadedMessageCount.get()) } private fun evaluateMessageForDownload( message: ImapMessage, backendFolder: BackendFolder, - remoteFolder: ImapFolder, unsyncedMessages: MutableList, - syncFlagMessages: MutableList, - flagSyncOnly: Boolean + syncFlagMessages: MutableList ) { val messageServerId = message.uid if (message.isSet(Flag.DELETED)) { @@ -432,10 +401,8 @@ internal class ImapSync( val messagePresentLocally = backendFolder.isMessagePresent(messageServerId) if (!messagePresentLocally) { - if (!flagSyncOnly) { - Timber.v("Message with uid %s has not yet been downloaded", messageServerId) - unsyncedMessages.add(message) - } + Timber.v("Message with uid %s has not yet been downloaded", messageServerId) + unsyncedMessages.add(message) return } @@ -474,23 +441,29 @@ internal class ImapSync( largeMessages: MutableList, progress: AtomicInteger, todo: Int, - fetchProfile: FetchProfile, listener: SyncListener ) { val folder = remoteFolder.serverId + val fetchProfile = FetchProfile().apply { + add(FetchProfile.Item.FLAGS) + add(FetchProfile.Item.ENVELOPE) + } remoteFolder.fetch( unsyncedMessages, fetchProfile, - object : MessageRetrievalListener { - override fun messageFinished(message: ImapMessage, number: Int, ofTotal: Int) { + object : FetchListener { + override fun onFetchResponse(message: ImapMessage, isFirstResponse: Boolean) { try { if (message.isSet(Flag.DELETED)) { Timber.v( "Newly downloaded message %s:%s:%s was marked deleted on server, skipping", accountName, folder, message.uid ) - progress.incrementAndGet() + + if (isFirstResponse) { + progress.incrementAndGet() + } // TODO: This might be the source of poll count errors in the UI. Is todo always the same as ofTotal listener.syncProgress(folder, progress.get(), todo) @@ -509,9 +482,6 @@ internal class ImapSync( Timber.e(e, "Error while storing downloaded message.") } } - - override fun messageStarted(uid: String, number: Int, ofTotal: Int) = Unit - override fun messagesFinished(total: Int) = Unit }, syncConfig.maximumAutoDownloadMessageSize ) @@ -522,29 +492,30 @@ internal class ImapSync( backendFolder: BackendFolder, smallMessages: List, progress: AtomicInteger, - newMessages: AtomicInteger, + downloadedMessageCount: AtomicInteger, todo: Int, - fetchProfile: FetchProfile, highestKnownUid: Long?, listener: SyncListener ) { val folder = remoteFolder.serverId + val fetchProfile = FetchProfile().apply { + add(FetchProfile.Item.BODY) + } Timber.d("SYNC: Fetching %d small messages for folder %s", smallMessages.size, folder) remoteFolder.fetch( smallMessages, fetchProfile, - object : MessageRetrievalListener { - override fun messageFinished(message: ImapMessage, number: Int, ofTotal: Int) { + object : FetchListener { + override fun onFetchResponse(message: ImapMessage, isFirstResponse: Boolean) { try { // Store the updated message locally backendFolder.saveMessage(message, MessageDownloadState.FULL) - progress.incrementAndGet() - // Increment the number of "new messages" if the newly downloaded message is not marked as read. - if (!message.isSet(Flag.SEEN)) { - newMessages.incrementAndGet() + if (isFirstResponse) { + progress.incrementAndGet() + downloadedMessageCount.incrementAndGet() } val messageServerId = message.uid @@ -562,9 +533,6 @@ internal class ImapSync( Timber.e(e, "SYNC: fetch small messages") } } - - override fun messageStarted(uid: String, number: Int, ofTotal: Int) = Unit - override fun messagesFinished(total: Int) = Unit }, -1 ) @@ -577,14 +545,17 @@ internal class ImapSync( backendFolder: BackendFolder, largeMessages: List, progress: AtomicInteger, - newMessages: AtomicInteger, + downloadedMessageCount: AtomicInteger, todo: Int, - fetchProfile: FetchProfile, highestKnownUid: Long?, listener: SyncListener, maxDownloadSize: Int ) { val folder = remoteFolder.serverId + val fetchProfile = FetchProfile().apply { + add(FetchProfile.Item.STRUCTURE) + } + Timber.d("SYNC: Fetching large messages for folder %s", folder) remoteFolder.fetch(largeMessages, fetchProfile, null, maxDownloadSize) @@ -603,14 +574,7 @@ internal class ImapSync( // Update the listener with what we've found progress.incrementAndGet() - - // TODO do we need to re-fetch this here? - val flags = backendFolder.getMessageFlags(messageServerId) - // Increment the number of "new messages" if the newly downloaded message is - // not marked as read. - if (!flags.contains(Flag.SEEN)) { - newMessages.incrementAndGet() - } + downloadedMessageCount.incrementAndGet() listener.syncProgress(folder, progress.get(), todo) @@ -699,7 +663,7 @@ internal class ImapSync( */ val bodyFactory: BodyFactory = DefaultBodyFactory() for (part in viewables) { - remoteFolder.fetchPart(message, part, null, bodyFactory, maxDownloadSize) + remoteFolder.fetchPart(message, part, bodyFactory, maxDownloadSize) } // Store the updated message locally diff --git a/backend/imap/src/test/java/com/fsck/k9/backend/imap/ImapSyncTest.kt b/backend/imap/src/test/java/com/fsck/k9/backend/imap/ImapSyncTest.kt index cb990fbb90ccc12e5d69c39c126f89b782bb6ce3..273de02a110300e3656515d095f3f996684ee186 100644 --- a/backend/imap/src/test/java/com/fsck/k9/backend/imap/ImapSyncTest.kt +++ b/backend/imap/src/test/java/com/fsck/k9/backend/imap/ImapSyncTest.kt @@ -5,11 +5,14 @@ import com.fsck.k9.backend.api.FolderInfo import com.fsck.k9.backend.api.SyncConfig import com.fsck.k9.backend.api.SyncConfig.ExpungePolicy import com.fsck.k9.backend.api.SyncListener +import com.fsck.k9.mail.FetchProfile import com.fsck.k9.mail.Flag import com.fsck.k9.mail.FolderType import com.fsck.k9.mail.Message import com.fsck.k9.mail.MessageDownloadState import com.fsck.k9.mail.buildMessage +import com.fsck.k9.mail.store.imap.FetchListener +import com.fsck.k9.mail.store.imap.ImapMessage import com.google.common.truth.Truth.assertThat import java.util.Date import org.apache.james.mime4j.dom.field.DateTimeField @@ -17,6 +20,7 @@ import org.apache.james.mime4j.field.DefaultFieldParser import org.junit.Test import org.mockito.Mockito.verify import org.mockito.kotlin.any +import org.mockito.kotlin.atLeast import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never @@ -208,6 +212,35 @@ class ImapSyncTest { verify(syncListener).syncNewMessage(FOLDER_SERVER_ID, messageServerId = "1", isOldMessage = false) } + @Test + fun `sync with multiple FETCH responses when downloading small message should report correct progress`() { + val folderServerId = "FOLDER_TWO" + backendStorage.createBackendFolder(folderServerId) + val specialImapFolder = object : TestImapFolder(folderServerId) { + override fun fetch( + messages: List, + fetchProfile: FetchProfile, + listener: FetchListener?, + maxDownloadSize: Int + ) { + super.fetch(messages, fetchProfile, listener, maxDownloadSize) + + // When fetching the body simulate an additional FETCH response + if (FetchProfile.Item.BODY in fetchProfile) { + val message = messages.first() + listener?.onFetchResponse(message, isFirstResponse = false) + } + } + } + specialImapFolder.addMessage(42) + imapStore.addFolder(specialImapFolder) + + imapSync.sync(folderServerId, defaultSyncConfig, syncListener) + + verify(syncListener, atLeast(1)).syncProgress(folderServerId, completed = 1, total = 1) + verify(syncListener, never()).syncProgress(folderServerId, completed = 2, total = 1) + } + private fun addMessageToBackendFolder(uid: Long, date: String = DEFAULT_MESSAGE_DATE) { val messageServerId = uid.toString() val message = createSimpleMessage(messageServerId, date).apply { @@ -222,13 +255,21 @@ class ImapSyncTest { } private fun addMessageToImapFolder(uid: Long, flags: Set = emptySet(), date: String = DEFAULT_MESSAGE_DATE) { + imapFolder.addMessage(uid, flags, date) + } + + private fun TestImapFolder.addMessage( + uid: Long, + flags: Set = emptySet(), + date: String = DEFAULT_MESSAGE_DATE + ) { val messageServerId = uid.toString() val message = createSimpleMessage(messageServerId, date) - imapFolder.addMessage(uid, message) + addMessage(uid, message) if (flags.isNotEmpty()) { - val imapMessage = imapFolder.getMessage(messageServerId) - imapFolder.setFlags(listOf(imapMessage), flags, true) + val imapMessage = getMessage(messageServerId) + setFlags(listOf(imapMessage), flags, true) } } @@ -239,14 +280,18 @@ class ImapSyncTest { private fun createBackendStorage(): InMemoryBackendStorage { return InMemoryBackendStorage().apply { - createFolderUpdater().use { updater -> - val folderInfo = FolderInfo( - serverId = FOLDER_SERVER_ID, - name = "irrelevant", - type = FolderType.REGULAR - ) - updater.createFolders(listOf(folderInfo)) - } + createBackendFolder(FOLDER_SERVER_ID) + } + } + + private fun InMemoryBackendStorage.createBackendFolder(serverId: String) { + createFolderUpdater().use { updater -> + val folderInfo = FolderInfo( + serverId = serverId, + name = "irrelevant", + type = FolderType.REGULAR + ) + updater.createFolders(listOf(folderInfo)) } } diff --git a/backend/imap/src/test/java/com/fsck/k9/backend/imap/TestImapFolder.kt b/backend/imap/src/test/java/com/fsck/k9/backend/imap/TestImapFolder.kt index 2ac34ede9488b592b181120bb0f2b54840febb54..729a909f919102fa1e64807bf6fecf73ba0761bc 100644 --- a/backend/imap/src/test/java/com/fsck/k9/backend/imap/TestImapFolder.kt +++ b/backend/imap/src/test/java/com/fsck/k9/backend/imap/TestImapFolder.kt @@ -6,15 +6,16 @@ import com.fsck.k9.mail.Flag import com.fsck.k9.mail.Message import com.fsck.k9.mail.MessageRetrievalListener import com.fsck.k9.mail.Part +import com.fsck.k9.mail.store.imap.FetchListener import com.fsck.k9.mail.store.imap.ImapFolder import com.fsck.k9.mail.store.imap.ImapMessage import com.fsck.k9.mail.store.imap.OpenMode import com.fsck.k9.mail.store.imap.createImapMessage import java.util.Date -class TestImapFolder(override val serverId: String) : ImapFolder { +open class TestImapFolder(override val serverId: String) : ImapFolder { override var mode: OpenMode? = null - private set + protected set override var messageCount: Int = 0 @@ -92,12 +93,12 @@ class TestImapFolder(override val serverId: String) : ImapFolder { override fun fetch( messages: List, fetchProfile: FetchProfile, - listener: MessageRetrievalListener?, + listener: FetchListener?, maxDownloadSize: Int ) { if (messages.isEmpty()) return - messages.forEachIndexed { index, imapMessage -> + for (imapMessage in messages) { val uid = imapMessage.uid.toLong() val flags = messageFlags[uid].orEmpty().toSet() @@ -109,14 +110,13 @@ class TestImapFolder(override val serverId: String) : ImapFolder { } imapMessage.body = storedMessage.body - listener?.messageFinished(imapMessage, index, messages.size) + listener?.onFetchResponse(imapMessage, isFirstResponse = true) } } override fun fetchPart( message: ImapMessage, part: Part, - listener: MessageRetrievalListener?, bodyFactory: BodyFactory, maxDownloadSize: Int ) { diff --git a/backend/imap/src/test/java/com/fsck/k9/backend/imap/TestImapStore.kt b/backend/imap/src/test/java/com/fsck/k9/backend/imap/TestImapStore.kt index 0c7168779eb07e6f5a61c2923c97d74d466815d6..db698d52efbcaf36a7bb5528a9cc4efc8e458656 100644 --- a/backend/imap/src/test/java/com/fsck/k9/backend/imap/TestImapStore.kt +++ b/backend/imap/src/test/java/com/fsck/k9/backend/imap/TestImapStore.kt @@ -6,16 +6,23 @@ import com.fsck.k9.mail.store.imap.ImapFolder import com.fsck.k9.mail.store.imap.ImapStore class TestImapStore : ImapStore { - private val folders = mutableMapOf() + private val folders = mutableMapOf() - fun addFolder(name: String): TestImapFolder { - require(!folders.containsKey(name)) { "Folder '$name' already exists" } + fun addFolder(serverId: String): TestImapFolder { + require(!folders.containsKey(serverId)) { "Folder '$serverId' already exists" } - return TestImapFolder(name).also { folder -> - folders[name] = folder + return TestImapFolder(serverId).also { folder -> + folders[serverId] = folder } } + fun addFolder(folder: ImapFolder) { + val serverId = folder.serverId + require(!folders.containsKey(serverId)) { "Folder '$serverId' already exists" } + + folders[serverId] = folder + } + override fun getFolder(name: String): ImapFolder { return folders[name] ?: error("Folder '$name' not found") } diff --git a/backend/pop3/src/main/java/com/fsck/k9/backend/pop3/Pop3Sync.java b/backend/pop3/src/main/java/com/fsck/k9/backend/pop3/Pop3Sync.java index 84f50108506768f98a369db221238f8b485dbbfd..136364eba244dee4d7da886a33605a05dccd258e 100644 --- a/backend/pop3/src/main/java/com/fsck/k9/backend/pop3/Pop3Sync.java +++ b/backend/pop3/src/main/java/com/fsck/k9/backend/pop3/Pop3Sync.java @@ -419,7 +419,7 @@ class Pop3Sync { remoteFolder.fetch(unsyncedMessages, fp, new MessageRetrievalListener() { @Override - public void messageFinished(Pop3Message message, int number, int ofTotal) { + public void messageFinished(Pop3Message message) { try { if (message.isSet(Flag.DELETED) || message.olderThan(earliestDate)) { if (message.isSet(Flag.DELETED)) { @@ -447,16 +447,6 @@ class Pop3Sync { Timber.e(e, "Error while storing downloaded message."); } } - - @Override - public void messageStarted(String uid, int number, int ofTotal) { - } - - @Override - public void messagesFinished(int total) { - // FIXME this method is almost never invoked by various Stores! Don't rely on it unless fixed!! - } - }, syncConfig.getMaximumAutoDownloadMessageSize()); } @@ -477,7 +467,7 @@ class Pop3Sync { remoteFolder.fetch(smallMessages, fp, new MessageRetrievalListener() { @Override - public void messageFinished(final Pop3Message message, int number, int ofTotal) { + public void messageFinished(final Pop3Message message) { try { // Store the updated message locally @@ -503,14 +493,6 @@ class Pop3Sync { Timber.e(e, "SYNC: fetch small messages"); } } - - @Override - public void messageStarted(String uid, int number, int ofTotal) { - } - - @Override - public void messagesFinished(int total) { - } }, -1); diff --git a/backend/webdav/src/main/java/com/fsck/k9/backend/webdav/WebDavSync.java b/backend/webdav/src/main/java/com/fsck/k9/backend/webdav/WebDavSync.java index 1b0c627e8d6daa1d3d412c29513debfa020c951d..ca400431f3d7842e5c93e81d5ef87e6a23ba7c66 100644 --- a/backend/webdav/src/main/java/com/fsck/k9/backend/webdav/WebDavSync.java +++ b/backend/webdav/src/main/java/com/fsck/k9/backend/webdav/WebDavSync.java @@ -405,7 +405,7 @@ class WebDavSync { remoteFolder.fetch(unsyncedMessages, fp, new MessageRetrievalListener() { @Override - public void messageFinished(WebDavMessage message, int number, int ofTotal) { + public void messageFinished(WebDavMessage message) { try { if (message.isSet(Flag.DELETED) || message.olderThan(earliestDate)) { if (message.isSet(Flag.DELETED)) { @@ -433,16 +433,6 @@ class WebDavSync { Timber.e(e, "Error while storing downloaded message."); } } - - @Override - public void messageStarted(String uid, int number, int ofTotal) { - } - - @Override - public void messagesFinished(int total) { - // FIXME this method is almost never invoked by various Stores! Don't rely on it unless fixed!! - } - }, syncConfig.getMaximumAutoDownloadMessageSize()); } @@ -463,7 +453,7 @@ class WebDavSync { remoteFolder.fetch(smallMessages, fp, new MessageRetrievalListener() { @Override - public void messageFinished(final WebDavMessage message, int number, int ofTotal) { + public void messageFinished(final WebDavMessage message) { try { // Store the updated message locally @@ -488,14 +478,6 @@ class WebDavSync { Timber.e(e, "SYNC: fetch small messages"); } } - - @Override - public void messageStarted(String uid, int number, int ofTotal) { - } - - @Override - public void messagesFinished(int total) { - } }, -1); diff --git a/build.gradle b/build.gradle index 741da0b80c71c258f3270101ceb20162661745c2..0f209cbcd2929d0eec7091f44b43f388155b05b8 100644 --- a/build.gradle +++ b/build.gradle @@ -6,47 +6,46 @@ buildscript { 'compileSdk': 31, 'targetSdk': 31, 'minSdk': 21, - 'buildTools': '30.0.3', + 'buildTools': '32.0.0', 'robolectricSdk': 31 ] versions = [ - 'kotlin': '1.5.31', - 'kotlinCoroutines': '1.5.2', - 'androidxAppCompat': '1.3.1', + 'kotlin': '1.6.10', + 'kotlinCoroutines': '1.6.0', + 'androidxAppCompat': '1.4.1', 'androidxActivity': '1.4.0', 'androidxRecyclerView': '1.2.1', - 'androidxLifecycle': '2.4.0', - 'androidxAnnotation': '1.2.0', + 'androidxLifecycle': '2.4.1', + 'androidxAnnotation': '1.3.0', 'androidxBiometric': '1.1.0', - 'androidxNavigation': '2.3.5', - 'androidxConstraintLayout': '2.1.1', - 'androidxWorkManager': '2.7.0', - 'androidxFragment': '1.3.6', - 'androidxLocalBroadcastManager': '1.0.0', + 'androidxNavigation': '2.4.1', + 'androidxConstraintLayout': '2.1.3', + 'androidxWorkManager': '2.7.1', + 'androidxFragment': '1.4.1', + 'androidxLocalBroadcastManager': '1.1.0', 'androidxCore': '1.7.0', 'androidxCardView': '1.0.0', - 'androidxPreference': '1.1.1', + 'androidxPreference': '1.2.0', 'androidxTestCore': '1.4.0', - 'materialComponents': '1.4.0', - 'fastAdapter': '5.5.1', + 'materialComponents': '1.5.0', + 'fastAdapter': '5.6.0', 'preferencesFix': '1.1.0', - 'okio': '2.10.0', - 'moshi': '1.12.0', + 'okio': '3.0.0', + 'moshi': '1.13.0', 'timber': '5.0.1', 'koin': '3.1.5', 'commonsIo': '2.6', 'mime4j': '0.8.6', - 'okhttp': '4.9.2', - 'minidns': '1.0.0', - 'glide': '4.12.0', - 'jsoup': '1.13.1', - 'retrofit': '2.7.2', - + 'okhttp': '4.9.3', + 'minidns': '1.0.3', + 'glide': '4.13.1', + 'jsoup': '1.14.3', + 'retrofit': '2.9.0', 'androidxTestRunner': '1.4.0', 'junit': '4.13.2', 'robolectric': '4.7.3', - 'mockito': '4.0.0', + 'mockito': '4.3.1', 'mockitoKotlin': '4.0.0', 'truth': '1.1.3', 'turbine': '0.7.0', @@ -66,7 +65,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.3' + classpath 'com.android.tools.build:gradle:7.1.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}" classpath "org.jlleitschuh.gradle:ktlint-gradle:10.0.0" } diff --git a/fastlane/metadata/android/en-US/changelogs/29013.txt b/fastlane/metadata/android/en-US/changelogs/29013.txt new file mode 100644 index 0000000000000000000000000000000000000000..38ce474597f64b47667958cf33f756645785792f --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/29013.txt @@ -0,0 +1,7 @@ +- Don't create notifications when manually refreshing the message list +- Fixed import and export of notification settings +- Fixed bug that could lead to multiple notifications being created for the same message +- Fixed bug that sometimes crashed the app after changing the notification sound +- Added support for messages that specified the character set in multiple ways +- A lot of internal improvements +- Updated translations diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180f2ae8848c63b8b4dea2cb829da983f2fa..41d9927a4d4fb3f96a785543079b8df6723c946b 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a0f7639f7d360d6ed38045a936b289c3f88690c1..b1159fc54f39b3b208f767fd65a460b6016e04ad 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/mail/common/src/main/java/com/fsck/k9/mail/MessageRetrievalListener.java b/mail/common/src/main/java/com/fsck/k9/mail/MessageRetrievalListener.java index 273898acc4d4904a96c58fe1ad0ae02103d959a7..0503aa4be1db647c22f0c8819a42efc6152e361d 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/MessageRetrievalListener.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/MessageRetrievalListener.java @@ -3,12 +3,5 @@ package com.fsck.k9.mail; public interface MessageRetrievalListener { - void messageStarted(String uid, int number, int ofTotal); - - void messageFinished(T message, int number, int ofTotal); - - /** - * FIXME this method is almost never invoked by various Stores! Don't rely on it unless fixed!! - */ - void messagesFinished(int total); + void messageFinished(T message); } diff --git a/mail/common/src/main/java/com/fsck/k9/mail/internet/MessageExtractor.java b/mail/common/src/main/java/com/fsck/k9/mail/internet/MessageExtractor.java index e45aa18e34a4900448a9033a570649b704643604..c54f469be8ab60bb8d113770321658f99a9b4058 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/internet/MessageExtractor.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/internet/MessageExtractor.java @@ -23,7 +23,6 @@ import org.apache.commons.io.input.BoundedInputStream; import timber.log.Timber; import static com.fsck.k9.mail.internet.CharsetSupport.fixupCharset; -import static com.fsck.k9.mail.internet.MimeUtility.getHeaderParameter; import static com.fsck.k9.mail.internet.MimeUtility.isSameMimeType; import static com.fsck.k9.mail.internet.Viewable.Alternative; import static com.fsck.k9.mail.internet.Viewable.Html; @@ -77,7 +76,7 @@ public class MessageExtractor { /* * We've got a text part, so let's see if it needs to be processed further. */ - String charset = getHeaderParameter(part.getContentType(), "charset"); + String charset = PartExtensions.getCharset(part); /* * determine the charset from HTML message. */ diff --git a/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeParameterDecoder.kt b/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeParameterDecoder.kt index 7dcf29a6d6a35cfe3097cd7716c2373a38b52d12..5f4fc4d4468ce432a17e5024515a56071dc1bd0e 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeParameterDecoder.kt +++ b/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeParameterDecoder.kt @@ -54,6 +54,26 @@ object MimeParameterDecoder { ) } + fun decodeBasic(headerBody: String): MimeValue { + val parser = MimeHeaderParser(headerBody) + + val value = parser.readHeaderValue() + parser.skipCFWS() + if (parser.endReached()) { + return MimeValue(value) + } + + val (basicParameters, duplicateParameters, parserErrorIndex) = readBasicParameters(parser) + val parameters = basicParameters.mapValues { (_, parameterValue) -> parameterValue.value } + + return MimeValue( + value = value, + parameters = parameters, + ignoredParameters = duplicateParameters, + parserErrorIndex = parserErrorIndex + ) + } + @JvmStatic fun extractHeaderValue(headerBody: String): String { val parser = MimeHeaderParser(headerBody) diff --git a/mail/common/src/main/java/com/fsck/k9/mail/internet/PartExtensions.kt b/mail/common/src/main/java/com/fsck/k9/mail/internet/PartExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..a91d7db833cb672eab84cf8f92517c00f9535fc3 --- /dev/null +++ b/mail/common/src/main/java/com/fsck/k9/mail/internet/PartExtensions.kt @@ -0,0 +1,24 @@ +@file:JvmName("PartExtensions") +package com.fsck.k9.mail.internet + +import com.fsck.k9.mail.Part + +/** + * Return the `charset` parameter value of this [Part]'s `Content-Type` header. + */ +val Part.charset: String? + get() { + val contentTypeHeader = this.contentType ?: return null + val (_, parameters, duplicateParameters) = MimeParameterDecoder.decodeBasic(contentTypeHeader) + return parameters["charset"] ?: extractNonConflictingCharsetValue(duplicateParameters) + } + +// If there are multiple "charset" parameters, but they all agree on the value, we use that value. +private fun extractNonConflictingCharsetValue(duplicateParameters: List>): String? { + val charsets = duplicateParameters.asSequence() + .filter { (parameterName, _) -> parameterName == "charset" } + .map { (_, charset) -> charset.lowercase() } + .toSet() + + return if (charsets.size == 1) charsets.first() else null +} diff --git a/mail/common/src/test/java/com/fsck/k9/mail/internet/PartExtensionsTest.kt b/mail/common/src/test/java/com/fsck/k9/mail/internet/PartExtensionsTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..f5afbdec56874c7d7f8ca627b231072c1f4b15aa --- /dev/null +++ b/mail/common/src/test/java/com/fsck/k9/mail/internet/PartExtensionsTest.kt @@ -0,0 +1,54 @@ +package com.fsck.k9.mail.internet + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class PartExtensionsTest { + @Test + fun `get charset without charset parameter`() { + assertGetCharset(headerValue = "text/plain", expectedCharset = null) + } + + @Test + fun `get charset with single charset parameter`() { + assertGetCharset(headerValue = "text/plain; charset=UTF-8", expectedCharset = "utf-8") + } + + @Test + fun `get charset with single quoted charset parameter`() { + assertGetCharset(headerValue = "text/plain; charset=\"iso-8859-1\"", expectedCharset = "ISO-8859-1") + } + + @Test + fun `get charset with two charset parameters where values match exactly`() { + assertGetCharset(headerValue = "text/plain; charset=utf-8; charset=utf-8", expectedCharset = "utf-8") + } + + @Test + fun `get charset with two charset parameters where values differ in case`() { + assertGetCharset(headerValue = "text/plain; charset=utf-8; charset=UTF-8", expectedCharset = "utf-8") + } + + @Test + fun `get charset with two charset parameters where values differ in quoting`() { + assertGetCharset(headerValue = "text/plain; charset=utf-8; charset=\"utf-8\"", expectedCharset = "utf-8") + } + + @Test + fun `get charset with two charset parameters with conflicting values`() { + assertGetCharset(headerValue = "text/plain; charset=utf-8; charset=iso-8859-1", expectedCharset = null) + } + + @Test + fun `get charset with extended parameter syntax`() { + assertGetCharset(headerValue = "text/plain; charset*=us-ascii'en-us'utf-8", expectedCharset = null) + } + + private fun assertGetCharset(headerValue: String, expectedCharset: String?) { + val part = MimeBodyPart.create(null, headerValue) + + val charset = part.charset + + assertThat(charset).ignoringCase().isEqualTo(expectedCharset) + } +} diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapFolder.kt b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapFolder.kt index ffd4733185d4fd22a98aadb9e41677e622e60d3c..821522bba77ef7950a3283ca6e73ebee71e84116 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapFolder.kt +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapFolder.kt @@ -45,7 +45,7 @@ interface ImapFolder { fun fetch( messages: List, fetchProfile: FetchProfile, - listener: MessageRetrievalListener?, + listener: FetchListener?, maxDownloadSize: Int ) @@ -53,7 +53,6 @@ interface ImapFolder { fun fetchPart( message: ImapMessage, part: Part, - listener: MessageRetrievalListener?, bodyFactory: BodyFactory, maxDownloadSize: Int ) @@ -87,3 +86,7 @@ interface ImapFolder { @Throws(MessagingException::class) fun expungeUids(uids: List) } + +interface FetchListener { + fun onFetchResponse(message: ImapMessage, isFirstResponse: Boolean) +} 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 959240f39cfc13f24f3097129989aaab14a0b9f7..f037aadd8410cea0c5e4fb07fc8a249b0280e8ef 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 @@ -502,12 +502,10 @@ internal class RealImapFolder( // crazy adding stuff at the top. val uids = searchResponse.numbers.sortedDescending() - val count = uids.size - return uids.mapIndexed { index, uidLong -> + return uids.map { uidLong -> val uid = uidLong.toString() - listener?.messageStarted(uid, index, count) val message = ImapMessage(uid) - listener?.messageFinished(message, index, count) + listener?.messageFinished(message) message } @@ -517,7 +515,7 @@ internal class RealImapFolder( override fun fetch( messages: List, fetchProfile: FetchProfile, - listener: MessageRetrievalListener?, + listener: FetchListener?, maxDownloadSize: Int ) { if (messages.isEmpty()) { @@ -563,6 +561,7 @@ internal class RealImapFolder( val spaceSeparatedFetchFields = ImapUtility.join(" ", fetchFields) var windowStart = 0 + val processedUids = mutableSetOf() while (windowStart < messages.size) { val windowEnd = min(windowStart + FETCH_WINDOW_SIZE, messages.size) val uidWindow = uids.subList(windowStart, windowEnd) @@ -572,7 +571,6 @@ internal class RealImapFolder( val command = String.format("UID FETCH %s (%s)", commaSeparatedUids, spaceSeparatedFetchFields) connection!!.sendCommand(command, false) - var messageNumber = 0 var callback: ImapResponseCallback? = null if (fetchProfile.contains(FetchProfile.Item.BODY) || fetchProfile.contains(FetchProfile.Item.BODY_SANE) @@ -596,8 +594,6 @@ internal class RealImapFolder( continue } - listener?.messageStarted(uid, messageNumber++, messageMap.size) - val literal = handleFetchResponse(message, fetchList) if (literal != null) { when (literal) { @@ -615,7 +611,10 @@ internal class RealImapFolder( } } - listener?.messageFinished(message, messageNumber, messageMap.size) + val isFirstResponse = uid !in processedUids + processedUids.add(uid) + + listener?.onFetchResponse(message, isFirstResponse) } else { handleUntaggedResponse(response) } @@ -632,7 +631,6 @@ internal class RealImapFolder( override fun fetchPart( message: ImapMessage, part: Part, - listener: MessageRetrievalListener?, bodyFactory: BodyFactory, maxDownloadSize: Int ) { @@ -650,7 +648,6 @@ internal class RealImapFolder( val command = String.format("UID FETCH %s (UID %s)", message.uid, fetch) connection!!.sendCommand(command, false) - var messageNumber = 0 val callback: ImapResponseCallback = FetchPartCallback(part, bodyFactory) var response: ImapResponse @@ -668,8 +665,6 @@ internal class RealImapFolder( continue } - listener?.messageStarted(uid, messageNumber++, 1) - val literal = handleFetchResponse(message, fetchList) if (literal != null) { when (literal) { @@ -691,8 +686,6 @@ internal class RealImapFolder( } } } - - listener?.messageFinished(message, messageNumber, 1) } else { handleUntaggedResponse(response) } 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 96c7555e3fccfd7c84535b50ec45ff1cec3a3ca0..9e5866a50b01b91a9ae307f74ac5ac86a4355a9d 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 @@ -475,8 +475,7 @@ class RealImapFolderTest { val messages = folder.getMessages(1, 10, null, listener) - verify(listener).messageStarted("99", 0, 1) - verify(listener).messageFinished(messages[0], 0, 1) + verify(listener).messageFinished(messages[0]) verifyNoMoreInteractions(listener) } @@ -563,8 +562,7 @@ class RealImapFolderTest { val messages = folder.getMessages(setOf(1L), true, listener) - verify(listener).messageStarted("99", 0, 1) - verify(listener).messageFinished(messages[0], 0, 1) + verify(listener).messageFinished(messages[0]) verifyNoMoreInteractions(listener) } @@ -796,7 +794,7 @@ class RealImapFolderTest { val part = createPart("TEXT") whenever(imapConnection.readResponse(anyOrNull())).thenReturn(createImapResponse("x OK")) - folder.fetchPart(message, part, null, mock(), 4096) + folder.fetchPart(message, part, mock(), 4096) verify(imapConnection).sendCommand("UID FETCH 1 (UID BODY.PEEK[TEXT]<0.4096>)", false) } @@ -810,7 +808,7 @@ class RealImapFolderTest { val part = createPart("1.1") whenever(imapConnection.readResponse(anyOrNull())).thenReturn(createImapResponse("x OK")) - folder.fetchPart(message, part, null, mock(), MAX_DOWNLOAD_SIZE) + folder.fetchPart(message, part, mock(), MAX_DOWNLOAD_SIZE) verify(imapConnection).sendCommand("UID FETCH 1 (UID BODY.PEEK[1.1])", false) } @@ -824,7 +822,7 @@ class RealImapFolderTest { val part = createPlainTextPart("1.1") setupSingleFetchResponseToCallback() - folder.fetchPart(message, part, null, DefaultBodyFactory(), MAX_DOWNLOAD_SIZE) + folder.fetchPart(message, part, DefaultBodyFactory(), MAX_DOWNLOAD_SIZE) val bodyArgumentCaptor = argumentCaptor() verify(part).body = bodyArgumentCaptor.capture() diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/TestImapFolder.kt b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/TestImapFolder.kt index 75da635116bcb8efb21aa97c7504b08a35813020..495280a7868783f5b04e9a53dbe5bc4347d65054 100644 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/TestImapFolder.kt +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/TestImapFolder.kt @@ -68,7 +68,7 @@ internal open class TestImapFolder( override fun fetch( messages: List, fetchProfile: FetchProfile, - listener: MessageRetrievalListener?, + listener: FetchListener?, maxDownloadSize: Int ) { throw UnsupportedOperationException("not implemented") @@ -77,7 +77,6 @@ internal open class TestImapFolder( override fun fetchPart( message: ImapMessage, part: Part, - listener: MessageRetrievalListener?, bodyFactory: BodyFactory, maxDownloadSize: Int ) { diff --git a/mail/protocols/pop3/src/main/java/com/fsck/k9/mail/store/pop3/Pop3Folder.java b/mail/protocols/pop3/src/main/java/com/fsck/k9/mail/store/pop3/Pop3Folder.java index 260ce705c15cf31ae1b37e42800858934ff856f1..a5c526e93a846bba4cd5d004b9a68fb3aaeb602e 100644 --- a/mail/protocols/pop3/src/main/java/com/fsck/k9/mail/store/pop3/Pop3Folder.java +++ b/mail/protocols/pop3/src/main/java/com/fsck/k9/mail/store/pop3/Pop3Folder.java @@ -120,7 +120,6 @@ public class Pop3Folder { throw new MessagingException("getMessages", ioe); } List messages = new ArrayList<>(); - int i = 0; for (int msgNum = start; msgNum <= end; msgNum++) { Pop3Message message = msgNumToMsgMap.get(msgNum); if (message == null) { @@ -133,12 +132,9 @@ public class Pop3Folder { continue; } - if (listener != null) { - listener.messageStarted(message.getUid(), i++, (end - start) + 1); - } messages.add(message); if (listener != null) { - listener.messageFinished(message, i++, (end - start) + 1); + listener.messageFinished(message); } } return messages; @@ -317,12 +313,8 @@ public class Pop3Folder { } catch (IOException ioe) { throw new MessagingException("fetch", ioe); } - for (int i = 0, count = messages.size(); i < count; i++) { - Pop3Message pop3Message = messages.get(i); + for (Pop3Message pop3Message : messages) { try { - if (listener != null && !fp.contains(FetchProfile.Item.ENVELOPE)) { - listener.messageStarted(pop3Message.getUid(), i, count); - } if (fp.contains(FetchProfile.Item.BODY)) { fetchBody(pop3Message, -1); } else if (fp.contains(FetchProfile.Item.BODY_SANE)) { @@ -343,7 +335,7 @@ public class Pop3Folder { pop3Message.setBody(null); } if (listener != null && !(fp.contains(FetchProfile.Item.ENVELOPE) && fp.size() == 1)) { - listener.messageFinished(pop3Message, i, count); + listener.messageFinished(pop3Message); } } catch (IOException ioe) { throw new MessagingException("Unable to fetch message", ioe); @@ -367,11 +359,7 @@ public class Pop3Folder { * In extreme cases we'll do a command per message instead of a bulk request * to hopefully save some time and bandwidth. */ - for (int i = 0, count = messages.size(); i < count; i++) { - Pop3Message message = messages.get(i); - if (listener != null) { - listener.messageStarted(message.getUid(), i, count); - } + for (Pop3Message message : messages) { String response = connection.executeSimpleCommand( String.format(Locale.US, LIST_COMMAND + " %d", uidToMsgNumMap.get(message.getUid()))); @@ -380,7 +368,7 @@ public class Pop3Folder { int msgSize = Integer.parseInt(listParts[2]); message.setSize(msgSize); if (listener != null) { - listener.messageFinished(message, i, count); + listener.messageFinished(message); } } } else { @@ -400,12 +388,9 @@ public class Pop3Folder { int msgSize = Integer.parseInt(listParts[1]); Pop3Message pop3Message = msgNumToMsgMap.get(msgNum); if (pop3Message != null && msgUidIndex.contains(pop3Message.getUid())) { - if (listener != null) { - listener.messageStarted(pop3Message.getUid(), i, count); - } pop3Message.setSize(msgSize); if (listener != null) { - listener.messageFinished(pop3Message, i, count); + listener.messageFinished(pop3Message); } i++; } diff --git a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavFolder.java b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavFolder.java index ead75aaeb7be1f5e6f1d7027a544d8439183dc71..ddf5adf28d4b2edeb169c7fa41c6ae623b711b6a 100644 --- a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavFolder.java +++ b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavFolder.java @@ -207,7 +207,6 @@ public class WebDavFolder { List messages = new ArrayList<>(); String[] uids; Map headers = new HashMap<>(); - int uidsLength; String messageBody; int prevStart = start; @@ -232,18 +231,14 @@ public class WebDavFolder { DataSet dataset = store.processRequest(this.mFolderUrl, "SEARCH", messageBody, headers); uids = dataset.getUids(); Map uidToUrl = dataset.getUidToUrl(); - uidsLength = uids.length; - for (int i = 0; i < uidsLength; i++) { - if (listener != null) { - listener.messageStarted(uids[i], i, uidsLength); - } - WebDavMessage message = new WebDavMessage(uids[i], this); - message.setUrl(uidToUrl.get(uids[i])); + for (String uid : uids) { + WebDavMessage message = new WebDavMessage(uid, this); + message.setUrl(uidToUrl.get(uid)); messages.add(message); if (listener != null) { - listener.messageFinished(message, i, uidsLength); + listener.messageFinished(message); } } @@ -309,14 +304,9 @@ public class WebDavFolder { /** * We can't hand off to processRequest() since we need the stream to parse. */ - for (int i = 0, count = messages.size(); i < count; i++) { - WebDavMessage wdMessage = messages.get(i); + for (WebDavMessage wdMessage : messages) { int statusCode = 0; - if (listener != null) { - listener.messageStarted(wdMessage.getUid(), i, count); - } - /** * If fetch is called outside of the initial list (ie, a locally stored message), it may not have a URL * associated. Verify and fix that @@ -405,7 +395,7 @@ public class WebDavFolder { } if (listener != null) { - listener.messageFinished(wdMessage, i, count); + listener.messageFinished(wdMessage); } } } @@ -511,13 +501,7 @@ public class WebDavFolder { Map envelopes = dataset.getMessageEnvelopes(); - int count = messages.size(); - for (int i = messages.size() - 1; i >= 0; i--) { - WebDavMessage message = messages.get(i); - if (listener != null) { - listener.messageStarted(messages.get(i).getUid(), i, count); - } - + for (WebDavMessage message : messages) { ParsedMessageEnvelope envelope = envelopes.get(message.getUid()); if (envelope != null) { message.setNewHeaders(envelope); @@ -527,7 +511,7 @@ public class WebDavFolder { } if (listener != null) { - listener.messageFinished(messages.get(i), i, count); + listener.messageFinished(message); } } } diff --git a/mail/protocols/webdav/src/test/java/com/fsck/k9/mail/store/webdav/WebDavFolderTest.java b/mail/protocols/webdav/src/test/java/com/fsck/k9/mail/store/webdav/WebDavFolderTest.java index 8fcf00a20dde3a5dc2935c4e6b8163c647a828d9..79f3809f29c60547c9fc2640c9985bca037a151f 100644 --- a/mail/protocols/webdav/src/test/java/com/fsck/k9/mail/store/webdav/WebDavFolderTest.java +++ b/mail/protocols/webdav/src/test/java/com/fsck/k9/mail/store/webdav/WebDavFolderTest.java @@ -221,8 +221,7 @@ public class WebDavFolderTest { FetchProfile profile = new FetchProfile(); profile.add(FetchProfile.Item.BODY_SANE); folder.fetch(messages, profile, listener, MAX_DOWNLOAD_SIZE); - verify(listener, times(25)).messageStarted(any(String.class), anyInt(), eq(25)); - verify(listener, times(25)).messageFinished(any(WebDavMessage.class), anyInt(), eq(25)); + verify(listener, times(25)).messageFinished(any(WebDavMessage.class)); } @Test @@ -252,8 +251,7 @@ public class WebDavFolderTest { profile.add(FetchProfile.Item.FLAGS); profile.add(FetchProfile.Item.BODY); folder.fetch(messages, profile, listener, MAX_DOWNLOAD_SIZE); - verify(listener, times(25)).messageStarted(any(String.class), anyInt(), anyInt()); - verify(listener, times(25)).messageFinished(any(WebDavMessage.class), anyInt(), anyInt()); + verify(listener, times(25)).messageFinished(any(WebDavMessage.class)); } private void setupStoreForMessageFetching() { @@ -293,8 +291,7 @@ public class WebDavFolderTest { FetchProfile profile = new FetchProfile(); profile.add(FetchProfile.Item.BODY_SANE); folder.fetch(messages, profile, listener, MAX_DOWNLOAD_SIZE); - verify(listener, times(25)).messageStarted(any(String.class), anyInt(), eq(25)); - verify(listener, times(25)).messageFinished(any(WebDavMessage.class), anyInt(), eq(25)); + verify(listener, times(25)).messageFinished(any(WebDavMessage.class)); } @Test @@ -324,8 +321,7 @@ public class WebDavFolderTest { FetchProfile profile = new FetchProfile(); profile.add(FetchProfile.Item.BODY_SANE); folder.fetch(messages, profile, listener, MAX_DOWNLOAD_SIZE); - verify(listener, times(25)).messageStarted(any(String.class), anyInt(), eq(25)); - verify(listener, times(25)).messageFinished(any(WebDavMessage.class), anyInt(), eq(25)); + verify(listener, times(25)).messageFinished(any(WebDavMessage.class)); } @Test @@ -391,8 +387,7 @@ public class WebDavFolderTest { folder.getMessages(messageStart, messageEnd, listener); - verify(listener, times(5)).messageStarted(anyString(), anyInt(), eq(5)); - verify(listener, times(5)).messageFinished(any(WebDavMessage.class), anyInt(), eq(5)); + verify(listener, times(5)).messageFinished(any(WebDavMessage.class)); } @Test